diff --git a/ExtendWebAITokenChart.js b/ExtendWebAITokenChart.js index 456b5a0..5433169 100644 --- a/ExtendWebAITokenChart.js +++ b/ExtendWebAITokenChart.js @@ -28,6 +28,7 @@ let historyByAssignment = {}; let assignmentNames = {}; let selectedAssignmentId = ''; + let selectedRange = 'today'; let myChart = null; let timerId = null; let isProcessing = false; @@ -60,18 +61,43 @@ }; } + function getDayKey(timestamp) { + const date = new Date(Number(timestamp)); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + function normalizeHistory(raw) { const normalized = {}; if (!raw || typeof raw !== 'object') return normalized; - Object.entries(raw).forEach(([assignmentId, points]) => { - if (!assignmentId || !Array.isArray(points)) return; - const unique = new Map(); - points.map(normalizePoint).filter(Boolean).forEach(point => unique.set(point.timestamp, point)); - normalized[assignmentId] = Array.from(unique.values()).sort((a, b) => a.timestamp - b.timestamp); + Object.entries(raw).forEach(([assignmentId, storedDays]) => { + if (!assignmentId || !storedDays || typeof storedDays !== 'object') return; + const days = {}; + const sourceDays = Array.isArray(storedDays) + ? storedDays.reduce((result, point) => { + const dayKey = point && Number.isFinite(Number(point.timestamp)) ? getDayKey(point.timestamp) : ''; + if (dayKey) (result[dayKey] ||= []).push(point); + return result; + }, {}) + : storedDays; + Object.entries(sourceDays).forEach(([dayKey, points]) => { + if (!Array.isArray(points)) return; + const unique = new Map(); + points.map(normalizePoint).filter(Boolean).forEach(point => unique.set(point.timestamp, point)); + if (unique.size) days[dayKey] = Array.from(unique.values()).sort((a, b) => a.timestamp - b.timestamp); + }); + if (Object.keys(days).length) normalized[assignmentId] = days; }); return normalized; } + function getAssignmentPoints(assignmentId) { + const days = historyByAssignment[assignmentId] || {}; + return Object.values(days).flat().sort((a, b) => a.timestamp - b.timestamp); + } + async function loadHistory() { historyByAssignment = normalizeHistory(await GM_getValue(STORAGE_KEY, {})); } @@ -120,13 +146,31 @@ backgroundColor: palette.surface, color: palette.text, padding: '15px', borderRadius: '10px', boxShadow: palette.shadow, zIndex: '9999', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', - fontSize: '14px', border: `1px solid ${palette.border}`, display: 'flex', flexDirection: 'column', overflow: 'hidden' + fontSize: '14px', border: `1px solid ${palette.border}`, boxSizing: 'border-box', display: 'flex', flexDirection: 'column', overflow: 'hidden' }); const header = document.createElement('div'); header.style.cssText = `display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-shrink:0;color:${palette.text}`; - header.innerHTML = `用量监控`; + header.innerHTML = `用量监控`; container.appendChild(header); + const rangeBar = document.createElement('div'); + rangeBar.id = 'chart-range-selector'; + rangeBar.style.cssText = 'display:flex;gap:6px;margin-bottom:7px;flex-shrink:0'; + [['today', '今天'], ['7d', '最近 7 天'], ['30d', '最近 30 天']].forEach(([value, label]) => { + const button = document.createElement('button'); + button.type = 'button'; + button.dataset.range = value; + button.textContent = label; + button.style.cssText = `background:${palette.control};color:${palette.text};border:1px solid ${palette.border};border-radius:5px;padding:3px 7px;font-size:12px;cursor:pointer`; + button.addEventListener('click', () => { + selectedRange = value; + updateRangeButtons(); + updateChart(); + }); + rangeBar.appendChild(button); + }); + container.appendChild(rangeBar); + const status = document.createElement('div'); status.id = 'monitor-status'; status.textContent = '初始化...'; @@ -155,6 +199,7 @@ }); refreshAssignmentOptions(); initChart(); + updateRangeButtons(); } function makeDraggable(container, handle) { @@ -203,10 +248,13 @@ }); window.addEventListener('mousemove', event => { if (!resizing) return; - const maxWidth = Math.max(PANEL_MIN_WIDTH, window.innerWidth - container.getBoundingClientRect().left); - const maxHeight = Math.max(PANEL_MIN_HEIGHT, window.innerHeight - container.getBoundingClientRect().top); - const width = Math.min(Math.max(PANEL_MIN_WIDTH, startWidth + event.clientX - startX), maxWidth); - const height = Math.min(Math.max(PANEL_MIN_HEIGHT, startHeight + event.clientY - startY), maxHeight); + const rect = container.getBoundingClientRect(); + const minWidth = Math.min(PANEL_MIN_WIDTH, window.innerWidth); + const minHeight = Math.min(PANEL_MIN_HEIGHT, window.innerHeight); + const maxWidth = Math.max(minWidth, window.innerWidth - rect.left); + const maxHeight = Math.max(minHeight, window.innerHeight - rect.top); + const width = Math.min(Math.max(minWidth, startWidth + event.clientX - startX), maxWidth); + const height = Math.min(Math.max(minHeight, startHeight + event.clientY - startY), maxHeight); container.style.width = `${width}px`; container.style.height = `${height}px`; myChart?.resize(); @@ -261,11 +309,45 @@ }); } + function startOfToday() { + const date = new Date(); + date.setHours(0, 0, 0, 0); + return date.getTime(); + } + + function aggregatePoints(points, intervalMs) { + const buckets = new Map(); + points.forEach(point => { + const bucket = Math.floor(point.timestamp / intervalMs) * intervalMs; + // 额度为累计值,取每个时间桶最后一次采样,保留该时间段结束时的真实状态。 + const previous = buckets.get(bucket); + if (!previous || point.timestamp > previous.timestamp) buckets.set(bucket, point); + }); + return Array.from(buckets.values()).sort((a, b) => a.timestamp - b.timestamp); + } + + function getDisplayedPoints(assignmentId) { + const allPoints = getAssignmentPoints(assignmentId); + const now = Date.now(); + if (selectedRange === 'today') return allPoints.filter(point => point.timestamp >= startOfToday()); + if (selectedRange === '7d') return aggregatePoints(allPoints.filter(point => point.timestamp >= now - 7 * 86400000), 3600000); + return aggregatePoints(allPoints.filter(point => point.timestamp >= now - 30 * 86400000), 6 * 3600000); + } + function getPointsWithDelta(assignmentId) { - const points = historyByAssignment[assignmentId] || []; + const points = getDisplayedPoints(assignmentId); return points.map((point, index) => ({ ...point, delta: index ? point.used - points[index - 1].used : 0 })); } + function updateRangeButtons() { + document.querySelectorAll('#chart-range-selector button').forEach(button => { + const active = button.dataset.range === selectedRange; + button.style.background = active ? '#0ea5e9' : ''; + button.style.borderColor = active ? '#7dd3fc' : ''; + button.style.fontWeight = active ? '700' : '400'; + }); + } + function updateChart() { if (!myChart) return; const points = getPointsWithDelta(selectedAssignmentId); @@ -292,9 +374,11 @@ const used = numberOrNull(quota && quota.used); if (!assignmentId || used === null) return false; const point = normalizePoint({ timestamp: Date.now(), used, total: quota.total, remaining: quota.remaining, unlimited: quota.unlimited, unit: quota.unit, quotaUpdatedAt: quota.updatedAt }); - historyByAssignment[assignmentId] = historyByAssignment[assignmentId] || []; - historyByAssignment[assignmentId].push(point); - historyByAssignment[assignmentId].sort((a, b) => a.timestamp - b.timestamp); + const dayKey = getDayKey(point.timestamp); + historyByAssignment[assignmentId] = historyByAssignment[assignmentId] || {}; + historyByAssignment[assignmentId][dayKey] = historyByAssignment[assignmentId][dayKey] || []; + historyByAssignment[assignmentId][dayKey].push(point); + historyByAssignment[assignmentId][dayKey].sort((a, b) => a.timestamp - b.timestamp); assignmentNames[assignmentId] = assignmentNames[assignmentId] || assignmentId; if (!selectedAssignmentId) selectedAssignmentId = assignmentId; refreshAssignmentOptions(); @@ -402,8 +486,15 @@ } function buildExportPayload() { - const assignments = Object.entries(historyByAssignment).map(([id, points]) => [id, assignmentNames[id] || '', points.map(point => [point.timestamp, point.used, point.total, point.remaining, point.unlimited ? 1 : 0, point.unit, point.quotaUpdatedAt])]); - return { v: 1, a: assignments }; + const assignments = Object.entries(historyByAssignment).map(([id, days]) => [ + id, + assignmentNames[id] || '', + Object.entries(days).map(([dayKey, points]) => [ + dayKey, + points.map(point => [point.timestamp, point.used, point.total, point.remaining, point.unlimited ? 1 : 0, point.unit, point.quotaUpdatedAt]) + ]) + ]); + return { v: 2, a: assignments }; } async function writeClipboard(text) { @@ -419,7 +510,7 @@ } async function exportHistory() { - const count = Object.values(historyByAssignment).reduce((sum, points) => sum + points.length, 0); + const count = Object.keys(historyByAssignment).reduce((sum, assignmentId) => sum + getAssignmentPoints(assignmentId).length, 0); if (!count) throw new Error('没有可导出的历史记录'); const compressed = await gzipText(JSON.stringify(buildExportPayload())); const text = EXPORT_PREFIX + bytesToBase64(compressed); @@ -428,18 +519,27 @@ } function mergeImportPayload(payload) { - if (!payload || payload.v !== 1 || !Array.isArray(payload.a)) throw new Error('历史数据格式无效'); + if (!payload || ![1, 2].includes(payload.v) || !Array.isArray(payload.a)) throw new Error('历史数据格式无效'); let imported = 0; payload.a.forEach(entry => { if (!Array.isArray(entry) || typeof entry[0] !== 'string' || !Array.isArray(entry[2])) return; - const [assignmentId, name, rows] = entry; - const existing = new Map((historyByAssignment[assignmentId] || []).map(point => [point.timestamp, point])); + const [assignmentId, name, source] = entry; + const rows = payload.v === 1 + ? source + : source.flatMap(day => Array.isArray(day) && Array.isArray(day[1]) ? day[1] : []); + const existing = new Set(getAssignmentPoints(assignmentId).map(point => point.timestamp)); + historyByAssignment[assignmentId] = historyByAssignment[assignmentId] || {}; rows.forEach(row => { if (!Array.isArray(row)) return; const point = normalizePoint({ timestamp: row[0], used: row[1], total: row[2], remaining: row[3], unlimited: row[4], unit: row[5], quotaUpdatedAt: row[6] }); - if (point && !existing.has(point.timestamp)) { existing.set(point.timestamp, point); imported += 1; } + if (!point || existing.has(point.timestamp)) return; + const dayKey = getDayKey(point.timestamp); + historyByAssignment[assignmentId][dayKey] = historyByAssignment[assignmentId][dayKey] || []; + historyByAssignment[assignmentId][dayKey].push(point); + existing.add(point.timestamp); + imported += 1; }); - historyByAssignment[assignmentId] = Array.from(existing.values()).sort((a, b) => a.timestamp - b.timestamp); + Object.values(historyByAssignment[assignmentId]).forEach(points => points.sort((a, b) => a.timestamp - b.timestamp)); assignmentNames[assignmentId] = assignmentNames[assignmentId] || name || assignmentId; }); return imported; @@ -492,11 +592,18 @@ window.addEventListener('resize', () => { const container = document.getElementById('usage-monitor-container'); if (!container) return; + const maxWidth = Math.max(PANEL_MIN_WIDTH, window.innerWidth - 20); + const maxHeight = Math.max(PANEL_MIN_HEIGHT, window.innerHeight - 20); + const width = Math.min(container.getBoundingClientRect().width, maxWidth); + const height = Math.min(container.getBoundingClientRect().height, maxHeight); + container.style.width = `${width}px`; + container.style.height = `${height}px`; const rect = container.getBoundingClientRect(); container.style.left = `${Math.min(Math.max(0, rect.left), Math.max(0, window.innerWidth - rect.width))}px`; container.style.top = `${Math.min(Math.max(0, rect.top), Math.max(0, window.innerHeight - rect.height))}px`; container.style.right = 'auto'; myChart?.resize(); + myChart?.update('none'); }); window.addEventListener('beforeunload', () => clearTimeout(timerId)); console.log('[用量监控] 脚本 V5.0 已启动。');