workbuddy的积分使用情况没有在客户端展示,网站上( https://www.workbuddy.cn/profile/plans-usage )展示的也很差劲,所以就做了一个Tampermonkey 脚本,展示效果如下: // ==UserScript== // @name WorkBuddy 积分统计仪表盘 // @namespace https://www.workbuddy.cn/ // @version 1.2.0 // @description 使用 ECharts 将套餐与用量中的积分明细汇总为趋势、时段和模型统计。 // @author Local // @match https://www.workbuddy.cn/profile/ // @icon https://www.workbuddy.cn/favicon.ico // @require https://cdn.jsdelivr.net/npm/echarts@6.0.0/dist/echarts.min.js // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const API_PATH = '/billing/meter/get-user-request-usage'; const TIMEZONE = 'Asia/Shanghai'; const AVAILABLE_SINCE = '2025-12-01'; const MAX_RANGE_DAYS = 31; const PAGE_SIZE = 1000; const MAX_PAGES = 100; const UNKNOWN = '未标记'; function toNumber(value) { if (typeof value === 'number') return Number.isFinite(value) ? value : 0; const parsed = Number.parseFloat(String(value ?? '').replace(/[^\d.+-]/g, '')); return Number.isFinite(parsed) ? parsed : 0; } function pad2(value) { return String(value).padStart(2, '0'); } function formatLocalDate(date) { const parts = new Intl.DateTimeFormat('en-CA', { timeZone: TIMEZONE, year: 'numeric', month: '2-digit', day: '2-digit', }).formatToParts(date); const map = Object.fromEntries(parts.map((part) => [part.type, part.value])); return ${map.year}-${map.month}-${map.day}; } function addDays(dateText, delta) { const [year, month, day] = dateText.split('-').map(Number); const value = new Date(Date.UTC(year, month - 1, day + delta, 12)); return ${value.getUTCFullYear()}-${pad2(value.getUTCMonth() + 1)}-${pad2(value.getUTCDate())}; } function daysBetween(start, end) { const startTime = Date.parse(${start}T00:00:00Z); const endTime = Date.parse(${end}T00:00:00Z); return Math.round((endTime - startTime) / 86400000); } function dateRange(start, end) { const result = []; for (let value = start; value <= end; value = addDays(value, 1)) result.push(value); return result; } function getDateParts(value) { const text = String(value ?? '').trim(); const plain = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2})/); if (plain && !/(?:Z|[+-]\d{2}:?\d{2})$/i.test(text)) { return { date: plain[1], hour: Number(plain[2]) }; } const parsed = new Date(text); if (Number.isNaN(parsed.getTime())) return { date: '', hour: -1 }; const parts = new Intl.DateTimeFormat('en-CA', { timeZone: TIMEZONE, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', hourCycle: 'h23', }).formatToParts(parsed); const map = Object.fromEntries(parts.map((part) => [part.type, part.value])); return { date: ${map.year}-${map.month}-${map.day}, hour: Number(map.hour) }; } function toEpochMs(value) { const text = String(value ?? '').trim(); const plain = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?/); if (plain && !/(?:Z|[+-]\d{2}:?\d{2})$/i.test(text)) { return Date.parse(${plain[1]}T${plain[2]}:${plain[3]}:${plain[4] || '00'}+08:00); } const parsed = Date.parse(text); return Number.isFinite(parsed) ? parsed : Number.NaN; } function buildRecent24Hours(records, end, now = new Date()) { const nowParts = getDateParts(now.toISOString()); const anchorDate = end >= nowParts.date ? nowParts.date : end; const anchorHour = end >= nowParts.date ? nowParts.hour : 23; const anchorMs = Date.parse(${anchorDate}T${pad2(anchorHour)}:00:00+08:00); const firstMs = anchorMs - 23 3600000; const buckets = Array.from({ length: 24 }, (_, index) => { const startMs = firstMs + index 3600000; const startParts = getDateParts(new Date(startMs).toISOString()); const endParts = getDateParts(new Date(startMs + 3600000).toISOString()); return { startMs, date: startParts.date, hour: startParts.hour, label: ${startParts.date.slice(5)} ${pad2(startParts.hour)}:00, endLabel: ${endParts.date.slice(5)} ${pad2(endParts.hour)}:00, credits: 0, calls: 0, }; }); for (const record of records) { const epochMs = record.epochMs; if (!Number.isFinite(epochMs) || epochMs < firstMs || epochMs >= anchorMs + 3600000) continue; const index = Math.floor((epochMs - firstMs) / 3600000); buckets[index].credits += record.credit; buckets[index].calls += 1; } return buckets; } function normalizeRecords(records) { return (Array.isArray(records) ? records : []).map((record) => { const time = record?.requestTime ?? record?.request_time ?? record?.time ?? record?.createdAt ?? ''; const parts = getDateParts(time); return { credit: toNumber(record?.credit ?? record?.credits ?? record?.usage), model: String(record?.model ?? record?.modelName ?? UNKNOWN).trim() || UNKNOWN, client: String(record?.client ?? record?.clientName ?? UNKNOWN).trim() || UNKNOWN, requestTime: String(time), epochMs: toEpochMs(time), date: parts.date, hour: parts.hour, }; }); } function rankedGroups(map, totalCredits) { return [...map.entries()] .map(([name, item]) => ({ name, credits: item.credits, calls: item.calls, average: item.calls ? item.credits / item.calls : 0, percent: totalCredits ? (item.credits / totalCredits) 100 : 0, })) .sort((left, right) => right.credits - left.credits || right.calls - left.calls); } function buildStats(records, start, end, now = new Date()) { const normalized = normalizeRecords(records).filter((record) => record.date >= start && record.date <= end); const dailyMap = new Map(dateRange(start, end).map((date) => [date, { date, credits: 0, calls: 0 }])); const modelMap = new Map(); const clientMap = new Map(); const hourly = Array.from({ length: 24 }, (_, hour) => ({ hour, credits: 0, calls: 0 })); let totalCredits = 0; for (const record of normalized) { totalCredits += record.credit; const day = dailyMap.get(record.date); if (day) { day.credits += record.credit; day.calls += 1; } for (const [map, key] of [ [modelMap, record.model], [clientMap, record.client], ]) { const item = map.get(key) || { credits: 0, calls: 0 }; item.credits += record.credit; item.calls += 1; map.set(key, item); } if (record.hour >= 0 &a


  • 情报分类:开源项目与落地
  • 分类依据:分享自制 Tampermonkey 脚本用于积分统计,属个人项目与工具落地。
  • 信息来源:服务器 / LINUX DO - 最新话题
  • 发布时间:2026/9/14 18:05:02