Compare commits
No commits in common. "f7f0e2fb738157139d374d2f4a90d83d4ef10bc7" and "d1c5b818d9a249a2516374e80873473eec2de191" have entirely different histories.
f7f0e2fb73
...
d1c5b818d9
@ -141,6 +141,9 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
|||||||
<h2 className="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
<h2 className="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||||
{periodLabel}
|
{periodLabel}
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
切换日期时会自动取消上一条请求,图表与排行保持同步刷新。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||||
@ -293,10 +296,6 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
|||||||
<span>{app.percentage}%</span>
|
<span>{app.percentage}%</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 truncate text-xs text-gray-500 dark:text-gray-400" title={app.processPath || '路径未知'}>
|
|
||||||
路径: {app.processPath || '路径未知'}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
|
<div className="mt-3 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full bg-blue-600 dark:bg-blue-500"
|
className="h-full rounded-full bg-blue-600 dark:bg-blue-500"
|
||||||
|
|||||||
@ -28,7 +28,6 @@ interface TitleUsage {
|
|||||||
interface MutableAppUsage {
|
interface MutableAppUsage {
|
||||||
processId: string
|
processId: string
|
||||||
rawProcessName: string
|
rawProcessName: string
|
||||||
rawProcessPath: string
|
|
||||||
durationMs: number
|
durationMs: number
|
||||||
titleDurations: Map<string, number>
|
titleDurations: Map<string, number>
|
||||||
}
|
}
|
||||||
@ -44,29 +43,6 @@ interface ScreenTimeRecordRow {
|
|||||||
path: string | null
|
path: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROCESS_DISPLAY_NAME_PRESETS = [
|
|
||||||
{ match: 'microsoft vs code\\code.exe', displayName: 'Visual Studio Code' },
|
|
||||||
{ match: 'msedge.exe', displayName: 'Microsoft Edge' },
|
|
||||||
{ match: 'windowsTerminal.exe', displayName: '终端' },
|
|
||||||
{ match: 'windowsterminal.exe', displayName: '终端' },
|
|
||||||
{ match: 'blender.exe', displayName: 'Blender' },
|
|
||||||
{ match: 'chrome.exe', displayName: 'Google Chrome' },
|
|
||||||
{ match: 'steamwebhelper.exe', displayName: 'Steam' },
|
|
||||||
{ match: 'steam.exe', displayName: 'Steam' },
|
|
||||||
{ match: 'bandizip.exe', displayName: 'Bandizip' },
|
|
||||||
{ match: 'explorer.exe', displayName: '文件资源管理器' },
|
|
||||||
{ match: 'devenv.exe', displayName: 'Visual Studio' },
|
|
||||||
{ match: 'idea64.exe', displayName: 'IntelliJ IDEA' },
|
|
||||||
{ match: 'pycharm64.exe', displayName: 'PyCharm' },
|
|
||||||
{ match: 'cursor.exe', displayName: 'Cursor' },
|
|
||||||
{ match: 'obsidian.exe', displayName: 'Obsidian' },
|
|
||||||
{ match: 'notion.exe', displayName: 'Notion' },
|
|
||||||
{ match: 'wechat.exe', displayName: '微信' },
|
|
||||||
{ match: 'qq.exe', displayName: 'QQ' },
|
|
||||||
{ match: 'teams.exe', displayName: 'Microsoft Teams' },
|
|
||||||
{ match: 'telegram.exe', displayName: 'Telegram' }
|
|
||||||
] as const
|
|
||||||
|
|
||||||
const cleanTitle = (title: string) => title.replace(/\s+/g, ' ').trim()
|
const cleanTitle = (title: string) => title.replace(/\s+/g, ' ').trim()
|
||||||
|
|
||||||
const trimLabelSeparators = (label: string) => label
|
const trimLabelSeparators = (label: string) => label
|
||||||
@ -77,8 +53,6 @@ const trimLabelSeparators = (label: string) => label
|
|||||||
|
|
||||||
const stripExtension = (value: string) => value.replace(/\.[^.]+$/, '')
|
const stripExtension = (value: string) => value.replace(/\.[^.]+$/, '')
|
||||||
|
|
||||||
const normalizeProcessPathForMatch = (value: string) => value.replace(/\//g, '\\').toLowerCase()
|
|
||||||
|
|
||||||
const toDateKey = ({ year, month, day }: DateParts) => `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
const toDateKey = ({ year, month, day }: DateParts) => `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
||||||
|
|
||||||
const parseDateParts = (value: string | null): DateParts | null => {
|
const parseDateParts = (value: string | null): DateParts | null => {
|
||||||
@ -137,14 +111,12 @@ const getLocalDateUtcMs = (parts: DateParts, tzOffsetMinutes: number) => {
|
|||||||
|
|
||||||
const normalizeProcess = (processPath: string, title: string) => {
|
const normalizeProcess = (processPath: string, title: string) => {
|
||||||
const rawProcessName = win32.basename(processPath || '').trim()
|
const rawProcessName = win32.basename(processPath || '').trim()
|
||||||
const rawProcessPath = processPath.trim()
|
|
||||||
const normalizedTitle = cleanTitle(title)
|
const normalizedTitle = cleanTitle(title)
|
||||||
const fallbackId = normalizedTitle ? `title:${normalizedTitle.toLowerCase()}` : 'unknown'
|
const fallbackId = normalizedTitle ? `title:${normalizedTitle.toLowerCase()}` : 'unknown'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
processId: rawProcessName ? rawProcessName.toLowerCase() : fallbackId,
|
processId: rawProcessName ? rawProcessName.toLowerCase() : fallbackId,
|
||||||
rawProcessName,
|
rawProcessName
|
||||||
rawProcessPath
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,24 +202,7 @@ const buildCommonSubstringLabel = (titles: TitleUsage[]) => {
|
|||||||
return trimLabelSeparators(shared)
|
return trimLabelSeparators(shared)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvePresetProcessDisplayName = (rawProcessName: string, rawProcessPath: string) => {
|
const resolveProcessDisplayName = (rawProcessName: string, titles: TitleUsage[]) => {
|
||||||
const normalizedProcessName = rawProcessName.toLowerCase()
|
|
||||||
const normalizedProcessPath = normalizeProcessPathForMatch(rawProcessPath)
|
|
||||||
|
|
||||||
const matchedPreset = PROCESS_DISPLAY_NAME_PRESETS.find(({ match }) => {
|
|
||||||
const normalizedMatch = match.toLowerCase()
|
|
||||||
return normalizedProcessName === normalizedMatch || normalizedProcessPath.includes(normalizedMatch)
|
|
||||||
})
|
|
||||||
|
|
||||||
return matchedPreset?.displayName ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveProcessDisplayName = (rawProcessName: string, rawProcessPath: string, titles: TitleUsage[]) => {
|
|
||||||
const presetDisplayName = resolvePresetProcessDisplayName(rawProcessName, rawProcessPath)
|
|
||||||
if (presetDisplayName) {
|
|
||||||
return presetDisplayName
|
|
||||||
}
|
|
||||||
|
|
||||||
const processBaseName = stripExtension(rawProcessName).toLowerCase()
|
const processBaseName = stripExtension(rawProcessName).toLowerCase()
|
||||||
const totalDurationMs = titles.reduce((total, item) => total + item.durationMs, 0)
|
const totalDurationMs = titles.reduce((total, item) => total + item.durationMs, 0)
|
||||||
const weightedTokenLabel = trimLabelSeparators(buildWeightedTokenLabel(titles, totalDurationMs))
|
const weightedTokenLabel = trimLabelSeparators(buildWeightedTokenLabel(titles, totalDurationMs))
|
||||||
@ -273,7 +228,6 @@ const addDurationToUsageMap = (
|
|||||||
usageMap: Map<string, MutableAppUsage>,
|
usageMap: Map<string, MutableAppUsage>,
|
||||||
processId: string,
|
processId: string,
|
||||||
rawProcessName: string,
|
rawProcessName: string,
|
||||||
rawProcessPath: string,
|
|
||||||
title: string,
|
title: string,
|
||||||
durationMs: number
|
durationMs: number
|
||||||
) => {
|
) => {
|
||||||
@ -286,16 +240,12 @@ const addDurationToUsageMap = (
|
|||||||
if (!existing.rawProcessName && rawProcessName) {
|
if (!existing.rawProcessName && rawProcessName) {
|
||||||
existing.rawProcessName = rawProcessName
|
existing.rawProcessName = rawProcessName
|
||||||
}
|
}
|
||||||
if (!existing.rawProcessPath && rawProcessPath) {
|
|
||||||
existing.rawProcessPath = rawProcessPath
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
usageMap.set(processId, {
|
usageMap.set(processId, {
|
||||||
processId,
|
processId,
|
||||||
rawProcessName,
|
rawProcessName,
|
||||||
rawProcessPath,
|
|
||||||
durationMs,
|
durationMs,
|
||||||
titleDurations: new Map([[title, durationMs]])
|
titleDurations: new Map([[title, durationMs]])
|
||||||
})
|
})
|
||||||
@ -314,9 +264,8 @@ const buildAppList = (usageMap: Map<string, MutableAppUsage>) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
processId: app.processId,
|
processId: app.processId,
|
||||||
processName: resolveProcessDisplayName(app.rawProcessName, app.rawProcessPath, sortedTitles),
|
processName: resolveProcessDisplayName(app.rawProcessName, sortedTitles),
|
||||||
rawProcessName: app.rawProcessName,
|
rawProcessName: app.rawProcessName,
|
||||||
processPath: app.rawProcessPath,
|
|
||||||
durationSeconds: Math.round(app.durationMs / 1000),
|
durationSeconds: Math.round(app.durationMs / 1000),
|
||||||
percentage: totalDurationMs > 0 ? Number(((app.durationMs / totalDurationMs) * 100).toFixed(2)) : 0,
|
percentage: totalDurationMs > 0 ? Number(((app.durationMs / totalDurationMs) * 100).toFixed(2)) : 0,
|
||||||
titleCount: sortedTitles.length,
|
titleCount: sortedTitles.length,
|
||||||
@ -435,7 +384,6 @@ async function handleScreenTime(req: NextRequest) {
|
|||||||
aggregateUsageMap,
|
aggregateUsageMap,
|
||||||
process.processId,
|
process.processId,
|
||||||
process.rawProcessName,
|
process.rawProcessName,
|
||||||
process.rawProcessPath,
|
|
||||||
title,
|
title,
|
||||||
aggregateContributionMs
|
aggregateContributionMs
|
||||||
)
|
)
|
||||||
@ -445,7 +393,6 @@ async function handleScreenTime(req: NextRequest) {
|
|||||||
selectedDayUsageMap,
|
selectedDayUsageMap,
|
||||||
process.processId,
|
process.processId,
|
||||||
process.rawProcessName,
|
process.rawProcessName,
|
||||||
process.rawProcessPath,
|
|
||||||
title,
|
title,
|
||||||
selectedDayContributionMs
|
selectedDayContributionMs
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { prisma } from '@/lib/prisma'
|
|||||||
import { storeFile } from '@/lib/fileStorage'
|
import { storeFile } from '@/lib/fileStorage'
|
||||||
import { push } from '@/lib/push'
|
import { push } from '@/lib/push'
|
||||||
import { withCors } from '@/lib/middleware'
|
import { withCors } from '@/lib/middleware'
|
||||||
import { filterIgnoredWindows } from '@/lib/windowFilters'
|
|
||||||
import ffmpeg from 'fluent-ffmpeg'
|
import ffmpeg from 'fluent-ffmpeg'
|
||||||
import { writeFileSync, unlinkSync, mkdtempSync, readFileSync, rmSync } from 'fs'
|
import { writeFileSync, unlinkSync, mkdtempSync, readFileSync, rmSync } from 'fs'
|
||||||
import { tmpdir } from 'os'
|
import { tmpdir } from 'os'
|
||||||
@ -29,8 +28,7 @@ async function handleScreenshotUpload(req: NextRequest) {
|
|||||||
|
|
||||||
const formData = await req.formData()
|
const formData = await req.formData()
|
||||||
const files: File[] = []
|
const files: File[] = []
|
||||||
const rawWindowsInfo: WindowInfo[] = JSON.parse(formData.get('windows_info') as string || '[]')
|
const windowsInfo: WindowInfo[] = JSON.parse(formData.get('windows_info') as string || '[]')
|
||||||
const windowsInfo = filterIgnoredWindows(rawWindowsInfo)
|
|
||||||
|
|
||||||
// Extract files from formData
|
// Extract files from formData
|
||||||
for (const [key, value] of formData.entries()) {
|
for (const [key, value] of formData.entries()) {
|
||||||
|
|||||||
@ -33,7 +33,6 @@ export interface ScreenTimeAppUsage {
|
|||||||
processId: string;
|
processId: string;
|
||||||
processName: string;
|
processName: string;
|
||||||
rawProcessName: string;
|
rawProcessName: string;
|
||||||
processPath: string;
|
|
||||||
durationSeconds: number;
|
durationSeconds: number;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
titleCount: number;
|
titleCount: number;
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
import { prisma } from '../lib/prisma'
|
|
||||||
import { IGNORED_WINDOW_KEYWORDS } from '../lib/windowFilters'
|
|
||||||
|
|
||||||
async function cleanupIgnoredWindows() {
|
|
||||||
console.log(`开始清理数据库中的忽略窗口记录: ${IGNORED_WINDOW_KEYWORDS.join(', ')}`)
|
|
||||||
|
|
||||||
const whereClause = {
|
|
||||||
OR: [
|
|
||||||
...IGNORED_WINDOW_KEYWORDS.flatMap(keyword => ([
|
|
||||||
{
|
|
||||||
title: {
|
|
||||||
contains: keyword,
|
|
||||||
mode: 'insensitive' as const
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: {
|
|
||||||
contains: keyword,
|
|
||||||
mode: 'insensitive' as const
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]))
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const [matchedCount, affectedHosts] = await Promise.all([
|
|
||||||
prisma.window.count({ where: whereClause }),
|
|
||||||
prisma.window.findMany({
|
|
||||||
where: whereClause,
|
|
||||||
select: {
|
|
||||||
record: {
|
|
||||||
select: {
|
|
||||||
hostname: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
distinct: ['recordId']
|
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
if (matchedCount === 0) {
|
|
||||||
console.log('没有找到需要清理的忽略窗口记录。')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostnames = Array.from(new Set(affectedHosts.map(item => item.record.hostname))).sort()
|
|
||||||
console.log(`匹配到 ${matchedCount} 条窗口记录,涉及 ${hostnames.length} 台主机。`)
|
|
||||||
console.log(`主机列表: ${hostnames.join(', ')}`)
|
|
||||||
|
|
||||||
const deleteResult = await prisma.window.deleteMany({
|
|
||||||
where: whereClause
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`已删除 ${deleteResult.count} 条忽略窗口记录。`)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupIgnoredWindows()
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('清理忽略窗口失败:', error)
|
|
||||||
process.exitCode = 1
|
|
||||||
})
|
|
||||||
.finally(async () => {
|
|
||||||
await prisma.$disconnect()
|
|
||||||
})
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
export const IGNORED_WINDOW_KEYWORDS = [
|
|
||||||
'NVIDIA Overlay',
|
|
||||||
'Twinkle Tray Panel'
|
|
||||||
] as const
|
|
||||||
|
|
||||||
const normalizedIgnoredWindowKeywords = IGNORED_WINDOW_KEYWORDS.map(keyword => keyword.toLowerCase())
|
|
||||||
|
|
||||||
interface WindowLike {
|
|
||||||
title?: string | null
|
|
||||||
path?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isIgnoredWindow = ({ title, path }: WindowLike) => {
|
|
||||||
const normalizedValue = `${title ?? ''}\n${path ?? ''}`.toLowerCase()
|
|
||||||
return normalizedIgnoredWindowKeywords.some(keyword => normalizedValue.includes(keyword))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const filterIgnoredWindows = <T extends WindowLike>(windows: T[]) => {
|
|
||||||
return windows.filter(window => !isIgnoredWindow(window))
|
|
||||||
}
|
|
||||||
@ -10,9 +10,7 @@
|
|||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
"db:reset": "prisma migrate reset",
|
"db:reset": "prisma migrate reset",
|
||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio"
|
||||||
"db:cleanup:ignored-windows": "bun envtests/cleanup-nvidia-overlay.ts",
|
|
||||||
"db:cleanup:nvidia-overlay": "bun envtests/cleanup-nvidia-overlay.ts"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.10.1",
|
"@prisma/client": "^6.10.1",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user