Initial commit

This commit is contained in:
2026-03-08 01:34:54 +08:00
commit 1f104f73c8
441 changed files with 64911 additions and 0 deletions

37
shared/constants/api.ts Normal file
View File

@@ -0,0 +1,37 @@
export const API_ENDPOINTS = {
FILES: '/api/files',
FILES_CONTENT: '/api/files/content',
FILES_MOVE: '/api/files/move',
FILES_RECYCLE: '/api/files/recycle',
FILES_EXISTS: '/api/files/exists',
FILES_RENAME: '/api/files/rename',
FILES_CREATE: '/api/files/create',
FILES_MKDIR: '/api/files/mkdir',
FILES_RESTORE: '/api/files/restore',
FILES_PERMANENT_DELETE: '/api/files/permanent-delete',
FILES_EMPTY_RECYCLE_BIN: '/api/files/empty-recycle-bin',
TIME: '/api/time',
TIME_EVENT: '/api/time/event',
TIME_STATS: '/api/time/stats',
TIME_DAY: '/api/time/day',
TIME_MONTH: '/api/time/month',
TIME_YEAR: '/api/time/year',
TODO: '/api/todo',
TODO_ADD: '/api/todo/add',
TODO_TOGGLE: '/api/todo/toggle',
TODO_UPDATE: '/api/todo/update',
TODO_DELETE: '/api/todo/delete',
TODO_MIGRATE: '/api/todo/migrate',
SEARCH: '/api/search',
EVENTS: '/api/events',
SYNC: '/api/sync',
SYNC_UPLOAD: '/api/sync/upload',
SYNC_DOWNLOAD: '/api/sync/download',
SYNC_STATUS: '/api/sync/status',
PYDEMOS: '/api/pydemos',
PYDEMOS_CREATE: '/api/pydemos/create',
PYDEMOS_RENAME: '/api/pydemos/rename',
PYDEMOS_DELETE: '/api/pydemos/delete',
} as const
export type ApiEndpoint = typeof API_ENDPOINTS[keyof typeof API_ENDPOINTS]

View File

@@ -0,0 +1,14 @@
export const ASYNC_IMPORT_STATUS = {
PDF_PARSING_TITLE: '# 正在解析 PDF 内容...',
HTML_PARSING_TITLE: '# 正在解析 HTML 内容...',
PDF_PARSING_CONTENT: '# 正在解析 PDF 内容...\n\n> 请稍候,系统正在后台解析您的 PDF 文件。解析完成后,本文档将自动更新。\n> \n> 您可以继续进行其他操作。',
HTML_PARSING_CONTENT: '# 正在解析 HTML 内容...\n\n> 请稍候,系统正在解析您的 HTML 文件。解析完成后,本文档将自动更新。',
} as const
export const isAsyncImportProcessingContent = (text: string): boolean => {
const trimmed = text.trimStart()
return (
trimmed.startsWith(ASYNC_IMPORT_STATUS.PDF_PARSING_TITLE) ||
trimmed.startsWith(ASYNC_IMPORT_STATUS.HTML_PARSING_TITLE)
)
}

View File

@@ -0,0 +1 @@
export const WEEK_DAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] as const

View File

@@ -0,0 +1,22 @@
export const ERROR_CODES = {
PATH_NOT_FOUND: 'PATH_NOT_FOUND',
NOT_A_DIRECTORY: 'NOT_A_DIRECTORY',
ACCESS_DENIED: 'ACCESS_DENIED',
FILE_EXISTS: 'FILE_EXISTS',
INVALID_PATH: 'INVALID_PATH',
VALIDATION_ERROR: 'VALIDATION_ERROR',
INTERNAL_ERROR: 'INTERNAL_ERROR',
NOT_FOUND: 'NOT_FOUND',
BAD_REQUEST: 'BAD_REQUEST',
NAME_GENERATION_FAILED: 'NAME_GENERATION_FAILED',
SSE_UNSUPPORTED: 'SSE_UNSUPPORTED',
ALREADY_EXISTS: 'ALREADY_EXISTS',
NOT_A_FILE: 'NOT_A_FILE',
FORBIDDEN: 'FORBIDDEN',
UNSUPPORTED_MEDIA_TYPE: 'UNSUPPORTED_MEDIA_TYPE',
PAYLOAD_TOO_LARGE: 'PAYLOAD_TOO_LARGE',
RESOURCE_LOCKED: 'RESOURCE_LOCKED',
INVALID_NAME: 'INVALID_NAME',
} as const
export type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES]

View File

@@ -0,0 +1,5 @@
export { API_ENDPOINTS, type ApiEndpoint } from './api.js'
export { ERROR_CODES, type ErrorCode } from './errors.js'
export { ERROR_MESSAGES, getErrorMessage } from './messages.js'
export { ASYNC_IMPORT_STATUS, isAsyncImportProcessingContent } from './asyncImport.js'
export { WEEK_DAYS } from './dates.js'

View File

@@ -0,0 +1,26 @@
import { ERROR_CODES } from './errors'
export const ERROR_MESSAGES: Record<string, string> = {
[ERROR_CODES.PATH_NOT_FOUND]: '路径不存在',
[ERROR_CODES.NOT_A_DIRECTORY]: '不是目录',
[ERROR_CODES.ACCESS_DENIED]: '访问被拒绝',
[ERROR_CODES.FILE_EXISTS]: '文件已存在',
[ERROR_CODES.INVALID_PATH]: '无效路径',
[ERROR_CODES.VALIDATION_ERROR]: '验证失败',
[ERROR_CODES.INTERNAL_ERROR]: '内部错误',
[ERROR_CODES.NOT_FOUND]: '资源不存在',
[ERROR_CODES.BAD_REQUEST]: '请求错误',
[ERROR_CODES.NAME_GENERATION_FAILED]: '名称生成失败',
[ERROR_CODES.SSE_UNSUPPORTED]: 'SSE 不支持',
[ERROR_CODES.ALREADY_EXISTS]: '已存在',
[ERROR_CODES.NOT_A_FILE]: '不是文件',
[ERROR_CODES.FORBIDDEN]: '禁止访问',
[ERROR_CODES.UNSUPPORTED_MEDIA_TYPE]: '不支持的媒体类型',
[ERROR_CODES.PAYLOAD_TOO_LARGE]: '请求体过大',
[ERROR_CODES.RESOURCE_LOCKED]: '资源已锁定',
[ERROR_CODES.INVALID_NAME]: '无效名称',
}
export const getErrorMessage = (code: string): string => {
return ERROR_MESSAGES[code] || '未知错误'
}

171
shared/errors/index.ts Normal file
View File

@@ -0,0 +1,171 @@
import type { ErrorCode } from '../constants/errors.js'
export type { ErrorCode } from '../constants/errors.js'
export interface ErrorDetails {
[key: string]: unknown
}
export class AppError extends Error {
public readonly statusCode: number
public readonly details?: ErrorDetails
constructor(
public readonly code: ErrorCode,
message: string,
statusCode: number = 500,
details?: ErrorDetails
) {
super(message)
this.name = 'AppError'
this.statusCode = statusCode
this.details = details
}
toJSON() {
return {
name: this.name,
code: this.code,
message: this.message,
statusCode: this.statusCode,
details: this.details
}
}
}
export class ValidationError extends AppError {
constructor(message: string, details?: ErrorDetails) {
super('VALIDATION_ERROR', message, 400, details)
this.name = 'ValidationError'
}
}
export class NotFoundError extends AppError {
constructor(message: string = 'Resource not found', details?: ErrorDetails) {
super('NOT_FOUND', message, 404, details)
this.name = 'NotFoundError'
}
}
export class AccessDeniedError extends AppError {
constructor(message: string = 'Access denied', details?: ErrorDetails) {
super('ACCESS_DENIED', message, 403, details)
this.name = 'AccessDeniedError'
}
}
export class BadRequestError extends AppError {
constructor(message: string, details?: ErrorDetails) {
super('BAD_REQUEST', message, 400, details)
this.name = 'BadRequestError'
}
}
export class PathNotFoundError extends AppError {
constructor(message: string = 'Path not found', details?: ErrorDetails) {
super('PATH_NOT_FOUND', message, 404, details)
this.name = 'PathNotFoundError'
}
}
export class NotADirectoryError extends AppError {
constructor(message: string = '不是目录', details?: ErrorDetails) {
super('NOT_A_DIRECTORY', message, 400, details)
this.name = 'NotADirectoryError'
}
}
export class FileExistsError extends AppError {
constructor(message: string = 'File already exists', details?: ErrorDetails) {
super('FILE_EXISTS', message, 409, details)
this.name = 'FileExistsError'
}
}
export class InvalidPathError extends AppError {
constructor(message: string = '无效路径', details?: ErrorDetails) {
super('INVALID_PATH', message, 400, details)
this.name = 'InvalidPathError'
}
}
export class AlreadyExistsError extends AppError {
constructor(message: string = 'Resource already exists', details?: ErrorDetails) {
super('ALREADY_EXISTS', message, 409, details)
this.name = 'AlreadyExistsError'
}
}
export class ForbiddenError extends AppError {
constructor(message: string = '禁止访问', details?: ErrorDetails) {
super('FORBIDDEN', message, 403, details)
this.name = 'ForbiddenError'
}
}
export class UnsupportedMediaTypeError extends AppError {
constructor(message: string = '不支持的媒体类型', details?: ErrorDetails) {
super('UNSUPPORTED_MEDIA_TYPE', message, 415, details)
this.name = 'UnsupportedMediaTypeError'
}
}
export class PayloadTooLargeError extends AppError {
constructor(message: string = 'Payload too large', details?: ErrorDetails) {
super('PAYLOAD_TOO_LARGE', message, 413, details)
this.name = 'PayloadTooLargeError'
}
}
export class ResourceLockedError extends AppError {
constructor(message: string = '资源已锁定', details?: ErrorDetails) {
super('RESOURCE_LOCKED', message, 423, details)
this.name = 'ResourceLockedError'
}
}
export class InternalError extends AppError {
constructor(message: string = '服务器内部错误', details?: ErrorDetails) {
super('INTERNAL_ERROR', message, 500, details)
this.name = 'InternalError'
}
}
export function isAppError(error: unknown): error is AppError {
return error instanceof AppError
}
export function isNodeError(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error && 'code' in error
}
export function createErrorFromCode(code: ErrorCode, message: string, details?: ErrorDetails): AppError {
switch (code) {
case 'VALIDATION_ERROR':
return new ValidationError(message, details)
case 'NOT_FOUND':
case 'PATH_NOT_FOUND':
return new NotFoundError(message, details)
case 'ACCESS_DENIED':
case 'FORBIDDEN':
return new AccessDeniedError(message, details)
case 'BAD_REQUEST':
case 'NOT_A_DIRECTORY':
case 'INVALID_PATH':
case 'INVALID_NAME':
return new BadRequestError(message, details)
case 'FILE_EXISTS':
case 'ALREADY_EXISTS':
return new FileExistsError(message, details)
case 'UNSUPPORTED_MEDIA_TYPE':
return new UnsupportedMediaTypeError(message, details)
case 'PAYLOAD_TOO_LARGE':
return new PayloadTooLargeError(message, details)
case 'RESOURCE_LOCKED':
return new ResourceLockedError(message, details)
case 'INTERNAL_ERROR':
return new InternalError(message, details)
default:
return new AppError(code, message, 500, details)
}
}

4
shared/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './types/index.js'
export * from './constants'
export * from './errors'
export * from './utils'

View File

@@ -0,0 +1,15 @@
import { defineApiModule } from '../types.js'
export const AI_MODULE = defineApiModule({
id: 'ai',
name: 'AI',
basePath: '/ai',
order: 70,
version: '1.0.0',
frontend: {
enabled: false,
},
backend: {
enabled: true,
},
})

View File

@@ -0,0 +1,15 @@
import { defineApiModule } from '../types.js'
export const DOCUMENT_PARSER_MODULE = defineApiModule({
id: 'document-parser',
name: 'Document Parser',
basePath: '/document-parser',
order: 60,
version: '1.0.0',
frontend: {
enabled: false,
},
backend: {
enabled: true,
},
})

View File

@@ -0,0 +1,12 @@
import { defineModule } from '../types.js'
export const HOME_MODULE = defineModule({
id: 'home',
name: '首页',
basePath: '/home',
order: 0,
version: '1.0.0',
backend: {
enabled: false,
},
})

1
shared/modules/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './types.js'

View File

@@ -0,0 +1,10 @@
import { defineEndpoints } from '../types.js'
export const PYDEMOS_ENDPOINTS = defineEndpoints({
list: { path: '/', method: 'GET' },
create: { path: '/create', method: 'POST' },
delete: { path: '/delete', method: 'DELETE' },
rename: { path: '/rename', method: 'POST' },
})
export type PyDemosEndpoints = typeof PYDEMOS_ENDPOINTS

View File

@@ -0,0 +1,15 @@
import { defineApiModule } from '../types.js'
import { PYDEMOS_ENDPOINTS } from './api.js'
export * from './api.js'
export const PYDEMOS_MODULE = defineApiModule({
id: 'pydemos',
name: 'Python Demos',
basePath: '/pydemos',
order: 50,
version: '1.0.0',
endpoints: PYDEMOS_ENDPOINTS,
})
export type { PyDemosEndpoints } from './api.js'

View File

@@ -0,0 +1,10 @@
import { defineEndpoints } from '../types.js'
export const RECYCLE_BIN_ENDPOINTS = defineEndpoints({
list: { path: '/', method: 'GET' },
restore: { path: '/restore', method: 'POST' },
permanent: { path: '/permanent', method: 'DELETE' },
empty: { path: '/empty', method: 'DELETE' },
})
export type RecycleBinEndpoints = typeof RECYCLE_BIN_ENDPOINTS

View File

@@ -0,0 +1,15 @@
import { defineApiModule } from '../types.js'
import { RECYCLE_BIN_ENDPOINTS } from './api.js'
export * from './api.js'
export const RECYCLE_BIN_MODULE = defineApiModule({
id: 'recycle-bin',
name: '回收站',
basePath: '/recycle-bin',
order: 40,
version: '1.0.0',
endpoints: RECYCLE_BIN_ENDPOINTS,
})
export type { RecycleBinEndpoints } from './api.js'

View File

@@ -0,0 +1,12 @@
import { defineEndpoints } from '../types.js'
export const REMOTE_ENDPOINTS = defineEndpoints({
getConfig: { path: '/config', method: 'GET' },
saveConfig: { path: '/config', method: 'POST' },
getScreenshot: { path: '/screenshot', method: 'GET' },
saveScreenshot: { path: '/screenshot', method: 'POST' },
getData: { path: '/data', method: 'GET' },
saveData: { path: '/data', method: 'POST' },
})
export type RemoteEndpoints = typeof REMOTE_ENDPOINTS

View File

@@ -0,0 +1,16 @@
import { defineApiModule } from '../types.js'
import { REMOTE_ENDPOINTS } from './api.js'
export * from './types.js'
export * from './api.js'
export const REMOTE_MODULE = defineApiModule({
id: 'remote',
name: '远程',
basePath: '/remote',
order: 25,
version: '1.0.0',
endpoints: REMOTE_ENDPOINTS,
})
export type { RemoteEndpoints } from './api.js'

View File

@@ -0,0 +1,11 @@
export interface RemoteDevice {
id: string
deviceName: string
serverHost: string
desktopPort: number
gitPort: number
}
export interface RemoteConfig {
devices: RemoteDevice[]
}

View File

@@ -0,0 +1,12 @@
import { defineModule } from '../types.js'
export const SEARCH_MODULE = defineModule({
id: 'search',
name: '搜索',
basePath: '/search',
order: 10,
version: '1.0.0',
backend: {
enabled: false,
},
})

View File

@@ -0,0 +1,12 @@
import { defineModule } from '../types.js'
export const SETTINGS_MODULE = defineModule({
id: 'settings',
name: '设置',
basePath: '/settings',
order: 100,
version: '1.0.0',
backend: {
enabled: false,
},
})

View File

@@ -0,0 +1,13 @@
import { defineEndpoints } from '../types.js'
export const 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' },
})
export type TimeTrackingEndpoints = typeof TIME_TRACKING_ENDPOINTS

View File

@@ -0,0 +1,15 @@
import { defineApiModule } from '../types.js'
import { TIME_TRACKING_ENDPOINTS } from './api.js'
export * from './api.js'
export const TIME_TRACKING_MODULE = defineApiModule({
id: 'time-tracking',
name: '时间统计',
basePath: '/time',
order: 20,
version: '1.0.0',
endpoints: TIME_TRACKING_ENDPOINTS,
})
export type { TimeTrackingEndpoints } from './api.js'

View File

@@ -0,0 +1,12 @@
import { defineEndpoints } from '../types.js'
export const TODO_ENDPOINTS = defineEndpoints({
list: { path: '/', method: 'GET' },
save: { path: '/save', method: 'POST' },
add: { path: '/add', method: 'POST' },
toggle: { path: '/toggle', method: 'POST' },
update: { path: '/update', method: 'POST' },
delete: { path: '/delete', method: 'DELETE' },
})
export type TodoEndpoints = typeof TODO_ENDPOINTS

View File

@@ -0,0 +1,16 @@
import { defineApiModule } from '../types.js'
import { TODO_ENDPOINTS } from './api.js'
export * from './types.js'
export * from './api.js'
export const TODO_MODULE = defineApiModule({
id: 'todo',
name: 'TODO',
basePath: '/todo',
order: 30,
version: '1.0.0',
endpoints: TODO_ENDPOINTS,
})
export type { TodoEndpoints } from './api.js'

View File

@@ -0,0 +1,22 @@
import type { DayTodo } from '../../types/todo.js'
export interface TodoFilePath {
relPath: string
fullPath: string
}
export interface ParsedTodoFile {
fullPath: string
dayTodos: DayTodo[]
}
export interface GetTodoResult {
dayTodos: DayTodo[]
year: number
month: number
}
export interface MigrationContext {
todayStr: string
yesterdayStr: string
}

83
shared/modules/types.ts Normal file
View File

@@ -0,0 +1,83 @@
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
export interface EndpointDefinition {
path: string
method: HttpMethod
description?: string
}
export interface ModuleEndpoints {
[key: string]: EndpointDefinition
}
export interface ModuleFrontendConfig {
enabled?: boolean
icon?: string
component?: string
}
export interface ModuleBackendConfig {
enabled?: boolean
createModule?: string
}
export interface ModuleDefinition<
TId extends string = string,
TEndpoints extends ModuleEndpoints = ModuleEndpoints
> {
id: TId
name: string
basePath: string
order: number
version?: string
endpoints?: TEndpoints
dependencies?: string[]
icon?: string
frontend?: ModuleFrontendConfig
backend?: ModuleBackendConfig
}
export interface ApiModuleConfig<
TId extends string = string,
TEndpoints extends ModuleEndpoints = ModuleEndpoints
> extends ModuleDefinition<TId, TEndpoints> {
version: string
}
export function defineModule<
TId extends string,
TEndpoints extends ModuleEndpoints
>(
config: ModuleDefinition<TId, TEndpoints>
): ModuleDefinition<TId, TEndpoints> {
return config
}
export function defineApiModule<
TId extends string,
TEndpoints extends ModuleEndpoints
>(
config: ApiModuleConfig<TId, TEndpoints>
): ApiModuleConfig<TId, TEndpoints> {
return config
}
export function defineEndpoints<TEndpoints extends ModuleEndpoints>(
endpoints: TEndpoints
): TEndpoints {
return endpoints
}
export type ExtractEndpointPaths<TEndpoints extends ModuleEndpoints> = {
[K in keyof TEndpoints]: TEndpoints[K]['path']
}
export type ExtractEndpointMethods<TEndpoints extends ModuleEndpoints> = {
[K in keyof TEndpoints]: TEndpoints[K]['method']
}
export type EndpointConfig = EndpointDefinition
export interface ModuleApiConfig {
endpoints: ModuleEndpoints
}

View File

@@ -0,0 +1,12 @@
import { defineModule } from '../types.js'
export const WEREAD_MODULE = defineModule({
id: 'weread',
name: '微信读书',
basePath: '/weread',
order: 20,
version: '1.0.0',
backend: {
enabled: false,
},
})

1
shared/types.ts Normal file
View File

@@ -0,0 +1 @@
export * from './types/index.js'

9
shared/types/api.ts Normal file
View File

@@ -0,0 +1,9 @@
export type ApiErrorDTO = {
code: string
message: string
details?: unknown
}
export type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: ApiErrorDTO }

22
shared/types/file.ts Normal file
View File

@@ -0,0 +1,22 @@
export type FileKind = 'file' | 'dir'
export interface FileItemDTO {
name: string
type: FileKind
size: number
modified: string
path: string
}
export interface FileContentDTO {
content: string
metadata: {
size: number
modified: string
}
}
export type PathExistsDTO = {
exists: boolean
type: FileKind | null
}

9
shared/types/index.ts Normal file
View File

@@ -0,0 +1,9 @@
export * from './file.js'
export * from './module.js'
export * from './tab.js'
export * from './time.js'
export * from './todo.js'
export * from './pydemos.js'
export * from './recycle-bin.js'
export * from './settings.js'
export * from './api.js'

34
shared/types/module.ts Normal file
View File

@@ -0,0 +1,34 @@
import type { LucideIcon } from 'lucide-react'
import type { FileItemDTO as FileItem } from './file.js'
export type {
HttpMethod,
EndpointConfig,
EndpointDefinition,
ModuleEndpoints,
ModuleApiConfig,
ModuleDefinition,
ApiModuleConfig,
} from '../modules/types.js'
export type Brand<T, TBrand extends string> = T & { __brand: TBrand }
export type ModuleId = Brand<string, 'ModuleId'>
import type { ModuleDefinition, ModuleEndpoints } from '../modules/types.js'
export interface FrontendModuleConfig<
TEndpoints extends ModuleEndpoints = ModuleEndpoints
> extends Omit<ModuleDefinition<string, TEndpoints>, 'icon' | 'basePath'> {
basePath?: string
icon: LucideIcon
component: React.ComponentType
}
export interface InternalModuleConfig extends FrontendModuleConfig {
tabId: string
fileItem: FileItem
}
export const createModuleId = (id: string): ModuleId => {
return id as ModuleId
}

11
shared/types/pydemos.ts Normal file
View File

@@ -0,0 +1,11 @@
export interface PyDemoItem {
name: string
path: string
created: string
fileCount: number
}
export interface PyDemoMonth {
month: number
demos: PyDemoItem[]
}

View File

@@ -0,0 +1,14 @@
import type { FileKind } from './file.js'
export interface RecycleBinItemDTO {
name: string
originalName: string
type: FileKind
deletedDate: string
path: string
}
export interface RecycleBinGroupDTO {
date: string
items: RecycleBinItemDTO[]
}

7
shared/types/settings.ts Normal file
View File

@@ -0,0 +1,7 @@
export type ThemeMode = 'light' | 'dark'
export interface SettingsDTO {
theme?: 'light' | 'dark'
wallpaperOpacity?: number
markdownFontSize?: number
}

1
shared/types/tab.ts Normal file
View File

@@ -0,0 +1 @@
export type TabType = 'markdown' | 'todo' | 'settings' | 'search' | 'recycle-bin' | 'weread' | 'time-tracking' | 'pydemos' | 'remote' | 'remote-desktop' | 'remote-git' | 'other'

101
shared/types/time.ts Normal file
View File

@@ -0,0 +1,101 @@
export type { TabType } from './tab.js'
export interface TimePeriod {
start: string
end: string
}
export interface TabRecord {
tabId: string
filePath: string | null
fileName: string
tabType: import('./tab.js').TabType
duration: number
focusedPeriods: TimePeriod[]
}
export interface TimingSession {
id: string
startTime: string
endTime?: string
duration: number
status: 'active' | 'paused' | 'ended'
tabRecords: TabRecord[]
}
export interface TabSummary {
fileName: string
tabType: import('./tab.js').TabType
totalDuration: number
focusCount: number
}
export interface DayTimeData {
date: string
totalDuration: number
sessions: TimingSession[]
tabSummary: Record<string, TabSummary>
lastUpdated: string
}
export interface DaySummary {
totalDuration: number
sessions: number
topTabs: Array<{ fileName: string; duration: number }>
}
export interface MonthTimeData {
year: number
month: number
days: Record<string, DaySummary>
monthlyTotal: number
averageDaily: number
activeDays: number
lastUpdated: string
}
export interface YearTimeData {
year: number
months: Record<string, { totalDuration: number; activeDays: number }>
yearlyTotal: number
averageMonthly: number
averageDaily: number
totalActiveDays: number
}
export interface CurrentTimerState {
isRunning: boolean
isPaused: boolean
currentSession: {
id: string
startTime: string
duration: number
currentTab: {
tabId: string
fileName: string
tabType: import('./tab.js').TabType
} | null
} | null
todayDuration: number
}
export interface TimeStats {
totalDuration: number
activeDays: number
averageDaily: number
longestDay: { date: string; duration: number } | null
longestSession: { date: string; duration: number } | null
topTabs: Array<{ fileName: string; duration: number; percentage: number }>
tabTypeDistribution: Array<{ tabType: import('./tab.js').TabType; duration: number; percentage: number }>
}
export interface TimeTrackingEvent {
type: 'tab-switch' | 'tab-open' | 'tab-close' | 'window-focus' | 'window-blur' | 'app-quit' | 'heartbeat'
timestamp: string
tabInfo?: {
tabId: string
filePath: string | null
fileName: string
tabType: import('./tab.js').TabType
}
}

10
shared/types/todo.ts Normal file
View File

@@ -0,0 +1,10 @@
export interface TodoItem {
id: string
content: string
completed: boolean
}
export interface DayTodo {
date: string
items: TodoItem[]
}

159
shared/utils/date.ts Normal file
View File

@@ -0,0 +1,159 @@
export const pad2 = (n: number) => String(n).padStart(2, '0')
export const pad3 = (n: number) => String(n).padStart(3, '0')
export const formatTimestamp = (d: Date) => {
const yyyy = d.getFullYear()
const mm = pad2(d.getMonth() + 1)
const dd = pad2(d.getDate())
const hh = pad2(d.getHours())
const mi = pad2(d.getMinutes())
const ss = pad2(d.getSeconds())
const ms = pad3(d.getMilliseconds())
return `${yyyy}${mm}${dd}_${hh}${mi}${ss}_${ms}`
}
export const formatDate = (date: Date) => {
const yyyy = date.getFullYear()
const mm = pad2(date.getMonth() + 1)
const dd = pad2(date.getDate())
return `${yyyy}-${mm}-${dd}`
}
export const formatTime = (date: Date) => {
const hh = pad2(date.getHours())
const mm = pad2(date.getMinutes())
const ss = pad2(date.getSeconds())
return `${hh}:${mm}:${ss}`
}
export const formatDateTime = (date: Date) => {
return `${formatDate(date)} ${formatTime(date)}`
}
export const getTodayDate = (): string => {
return formatDate(new Date())
}
export const getTomorrowDate = (): string => {
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
return formatDate(tomorrow)
}
export const formatDateDisplay = (dateStr: string): string => {
const today = getTodayDate()
const tomorrow = getTomorrowDate()
if (dateStr === today) return '今天'
if (dateStr === tomorrow) return '明天'
const [year, month, day] = dateStr.split('-')
const currentYear = new Date().getFullYear()
if (parseInt(year) === currentYear) {
return `${parseInt(month)}${parseInt(day)}`
}
return `${year}${parseInt(month)}${parseInt(day)}`
}
export const formatDuration = (ms: number): string => {
const seconds = Math.floor(ms / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
if (hours > 0) {
const remainingMinutes = minutes % 60
return `${hours}小时${remainingMinutes > 0 ? `${remainingMinutes}分钟` : ''}`
}
if (minutes > 0) {
return `${minutes}分钟`
}
return `${seconds}`
}
export const formatDurationShort = (ms: number): string => {
const seconds = Math.floor(ms / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
if (hours > 0) {
const remainingMinutes = minutes % 60
return remainingMinutes > 0 ? `${hours}h${remainingMinutes}m` : `${hours}h`
}
if (minutes > 0) {
return `${minutes}m`
}
return `${seconds}s`
}
export const getWeekStart = (date: Date): Date => {
const d = new Date(date)
const day = d.getDay()
const diff = d.getDate() - day + (day === 0 ? -6 : 1)
d.setDate(diff)
d.setHours(0, 0, 0, 0)
return d
}
export const getWeekEnd = (date: Date): Date => {
const weekStart = getWeekStart(date)
const weekEnd = new Date(weekStart)
weekEnd.setDate(weekStart.getDate() + 6)
return weekEnd
}
export const getMonthStart = (date: Date): Date => {
return new Date(date.getFullYear(), date.getMonth(), 1)
}
export const getMonthEnd = (date: Date): Date => {
return new Date(date.getFullYear(), date.getMonth() + 1, 0)
}
export const getYearStart = (date: Date): Date => {
return new Date(date.getFullYear(), 0, 1)
}
export const getYearEnd = (date: Date): Date => {
return new Date(date.getFullYear(), 11, 31)
}
export const isSameDay = (date1: Date, date2: Date): boolean => {
return formatDate(date1) === formatDate(date2)
}
export const isToday = (date: Date): boolean => {
return isSameDay(date, new Date())
}
export const addDays = (date: Date, days: number): Date => {
const result = new Date(date)
result.setDate(result.getDate() + days)
return result
}
export const addMonths = (date: Date, months: number): Date => {
const result = new Date(date)
result.setMonth(result.getMonth() + months)
return result
}
export const addYears = (date: Date, years: number): Date => {
const result = new Date(date)
result.setFullYear(result.getFullYear() + years)
return result
}
export const parseDate = (dateStr: string): Date | null => {
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/)
if (!match) return null
const [, year, month, day] = match
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day))
}
export const getDaysInMonth = (year: number, month: number): number => {
return new Date(year, month, 0).getDate()
}
export const getDayOfWeek = (date: Date): string => {
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return days[date.getDay()]
}

3
shared/utils/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './path'
export * from './tabType'
export * from './date'

54
shared/utils/path.ts Normal file
View File

@@ -0,0 +1,54 @@
export const toPosixPath = (p: string) => p.replace(/\\/g, '/')
export const toWindowsPath = (p: string) => p.replace(/\//g, '\\')
export const normalizePath = (p: string) => {
const parts = p.replace(/\\/g, '/').split('/')
const stack: string[] = []
for (const part of parts) {
if (!part || part === '.') continue
if (part === '..') {
stack.pop()
continue
}
stack.push(part)
}
return stack.join('/')
}
export const joinPaths = (...paths: string[]) => {
return paths.map(normalizePath).join('/').replace(/\/+/g, '/')
}
export const getDirectoryName = (filePath: string) => {
const normalized = normalizePath(filePath)
const lastSlash = normalized.lastIndexOf('/')
return lastSlash === -1 ? '' : normalized.slice(0, lastSlash)
}
export const getFileName = (filePath: string) => {
const normalized = normalizePath(filePath)
const lastSlash = normalized.lastIndexOf('/')
return lastSlash === -1 ? normalized : normalized.slice(lastSlash + 1)
}
export const getFileExtension = (filePath: string) => {
const fileName = getFileName(filePath)
const lastDot = fileName.lastIndexOf('.')
return lastDot === -1 ? '' : fileName.slice(lastDot + 1)
}
export const removeExtension = (filePath: string) => {
const ext = getFileExtension(filePath)
if (!ext) return filePath
return filePath.slice(0, -(ext.length + 1))
}
export const isAbsolutePath = (p: string) => {
return p.startsWith('/') || /^[A-Za-z]:/.test(p)
}
export const isHiddenPath = (p: string) => {
const normalized = normalizePath(p)
return normalized.split('/').some(part => part.startsWith('.'))
}

65
shared/utils/tabType.ts Normal file
View File

@@ -0,0 +1,65 @@
import type { TabType } from '../types/tab.js'
const KNOWN_MODULE_IDS = [
'home', 'settings', 'search', 'weread',
'recycle-bin', 'todo', 'time-tracking', 'pydemos'
] as const
export function getTabTypeFromPath(filePath: string | null): TabType {
if (!filePath) return 'other'
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 as TabType
}
}
if (filePath.endsWith('.md')) {
return 'markdown'
}
return 'other'
}
export function getFileNameFromPath(filePath: string | null): string {
if (!filePath) return '未知'
for (const moduleId of KNOWN_MODULE_IDS) {
if (filePath === `${moduleId}-tab` || filePath === moduleId) {
const names: Record<string, string> = {
'home': '首页',
'settings': '设置',
'search': '搜索',
'weread': '微信读书',
'recycle-bin': '回收站',
'todo': 'TODO',
'time-tracking': '时间统计',
'pydemos': 'Python Demo',
'remote': '远程桌面',
}
return names[moduleId] ?? moduleId
}
}
const parts = filePath.split('/')
return parts[parts.length - 1] || filePath
}