// ==UserScript== // @name ChatGPT用量统计 // @namespace https://github.com/tizee/tampermonkey-chatgpt-model-usage-monitor // @version 4.0.0 // @description 优雅的 ChatGPT 模型调用量实时统计,界面简洁清爽(中文版),支持导入导出、一周分析报告、快捷键切换最小化(Ctrl/Cmd+I) // @author tizee (original), schweigen (modified) // @match https://chatgpt.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @license MIT // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/533399/ChatGPT%E7%94%A8%E9%87%8F%E7%BB%9F%E8%AE%A1.user.js // @updateURL https://update.greasyfork.icu/scripts/533399/ChatGPT%E7%94%A8%E9%87%8F%E7%BB%9F%E8%AE%A1.meta.js // ==/UserScript== (() => { // src/config.js var COLORS = { primary: "#5E9EFF", background: "#1A1B1E", surface: "#2A2B2E", border: "#363636", text: "#E5E7EB", secondaryText: "#9CA3AF", success: "#10B981", warning: "#F59E0B", danger: "#EF4444", disabled: "#4B5563", white: "oklch(.928 .006 264.531)", gray: "oklch(.92 .004 286.32)", yellow: "oklch(.905 .182 98.111)", green: "oklch(.845 .143 164.978)", progressLow: "#EF4444", progressMed: "#F59E0B", progressHigh: "#10B981", progressExceed: "#4B5563", hourModel: "#61DAFB", dailyModel: "#9F7AEA", weeklyModel: "#10B981", monthlyModel: "#F472B6" }; var STYLE = { borderRadius: "12px", boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.1)", spacing: { xs: "4px", sm: "8px", md: "16px", lg: "24px" }, textSize: { xs: "0.75rem", sm: "0.875rem", md: "1rem" }, lineHeight: { xs: "calc(1/.75)", sm: "calc(1.25/.875)", md: "1.5" } }; var TIME_WINDOWS = { hour3: 3 * 60 * 60 * 1e3, hour5: 5 * 60 * 60 * 1e3, daily: 24 * 60 * 60 * 1e3, weekly: 7 * 24 * 60 * 60 * 1e3, monthly: 30 * 24 * 60 * 60 * 1e3 }; var defaultUsageData = { position: { x: null, y: null }, size: { width: 400, height: 500 }, minimized: false, silentMode: false, progressType: "bar", planType: "team", showWindowResetTime: false, sharedQuotaGroups: {}, models: { "gpt-5-2-pro": { requests: [], quota: 15, windowType: "monthly" }, "gpt-5-1-pro": { requests: [], quota: 15, windowType: "monthly" }, "gpt-5-pro": { requests: [], quota: 15, windowType: "monthly" }, "o3-pro": { requests: [], quota: 0, windowType: "monthly" }, "gpt-4-5": { requests: [], quota: 0, windowType: "daily" }, "gpt-5-2-thinking": { requests: [], quota: 3e3, windowType: "weekly" }, "gpt-5-1-thinking": { requests: [], quota: 3e3, windowType: "weekly" }, "gpt-5-thinking": { requests: [], quota: 3e3, windowType: "weekly" }, o3: { requests: [], quota: 100, windowType: "weekly" }, "gpt-5-2-instant": { requests: [], quota: 1e4, windowType: "hour3" }, "gpt-5-1": { requests: [], quota: 1e4, windowType: "hour3" }, "gpt-5": { requests: [], quota: 1e4, windowType: "hour3" }, "gpt-5-t-mini": { requests: [], quota: 1e4, windowType: "hour3" }, "o4-mini": { requests: [], quota: 300, windowType: "daily" }, "gpt-4o": { requests: [], quota: 80, windowType: "hour3" }, "gpt-4-1": { requests: [], quota: 500, windowType: "hour3" }, "gpt-5-mini": { requests: [], quota: 1e4, windowType: "hour3" } } }; var SHARED_GROUP_COLORS = { "pro-premium-shared": "#facc15", "pro-thinking-shared": "#34d399", "pro-instant-shared": "#60a5fa", "team-premium-shared": "#facc15", "edu-premium-shared": "#facc15", "enterprise-premium-shared": "#facc15", "go-thinking-shared": "#34d399", "k12-thinking-shared": "#34d399", "plus-thinking-shared": "#34d399", "team-thinking-shared": "#34d399", "edu-thinking-shared": "#34d399", "enterprise-thinking-shared": "#34d399", "free-instant-shared": "#60a5fa", "go-instant-shared": "#60a5fa", "k12-instant-shared": "#60a5fa", "plus-instant-shared": "#60a5fa", "team-instant-shared": "#60a5fa", "edu-instant-shared": "#60a5fa", "enterprise-instant-shared": "#60a5fa" }; var MODEL_DISPLAY_ORDER = [ "gpt-5-2-pro", "gpt-5-1-pro", "gpt-5-pro", "o3-pro", "gpt-4-5", "gpt-5-2-thinking", "gpt-5-1-thinking", "gpt-5-thinking", "o3", "gpt-5-2-instant", "gpt-5-1", "gpt-5-t-mini", "o4-mini", "gpt-4o", "gpt-4-1", "gpt-5-mini", "gpt-5", "alpha" ]; var MODEL_DISPLAY_NAME_OVERRIDES = { "gpt-5": "gpt-5-instant", "gpt-5-1": "gpt-5-1-instant" }; function displayModelName(modelKey) { return MODEL_DISPLAY_NAME_OVERRIDES[modelKey] || modelKey; } var PLAN_DISPLAY_ORDER = [ "free", "go", "k12_teacher", "plus", "team", "edu", "enterprise", "pro" ]; var PLAN_CONFIGS = { free: { name: "Free", sharedQuotaGroups: { "free-instant-shared": { quota: 10, windowType: "hour5", displayName: "Free即时共用池" } }, models: { "gpt-5-2-pro": { quota: 0, windowType: "monthly" }, "gpt-5-1-pro": { quota: 0, windowType: "monthly" }, "gpt-5-pro": { quota: 0, windowType: "monthly" }, "o3-pro": { quota: 0, windowType: "monthly" }, "gpt-4-5": { quota: 0, windowType: "daily" }, "gpt-5-2-thinking": { quota: 1, windowType: "hour5" }, "gpt-5-1-thinking": { quota: 1, windowType: "hour5" }, "gpt-5-thinking": { quota: 1, windowType: "hour5" }, o3: { quota: 0, windowType: "weekly" }, "gpt-5-2-instant": { sharedGroup: "free-instant-shared" }, "gpt-5-1": { sharedGroup: "free-instant-shared" }, "gpt-5": { sharedGroup: "free-instant-shared" }, "gpt-5-t-mini": { quota: 10, windowType: "daily" }, "o4-mini": { quota: 0, windowType: "daily" }, "gpt-4o": { quota: 0, windowType: "hour3" }, "gpt-4-1": { quota: 0, windowType: "hour3" }, "gpt-5-mini": { quota: 1e4, windowType: "hour3" } } }, go: { name: "Go", sharedQuotaGroups: { "go-thinking-shared": { quota: 10, windowType: "hour5", displayName: "Go思考共用池" }, "go-instant-shared": { quota: 100, windowType: "hour5", displayName: "Go即时共用池" } }, models: { "gpt-5-2-pro": { quota: 0, windowType: "monthly" }, "gpt-5-1-pro": { quota: 0, windowType: "monthly" }, "gpt-5-pro": { quota: 0, windowType: "monthly" }, "o3-pro": { quota: 0, windowType: "monthly" }, "gpt-4-5": { quota: 0, windowType: "daily" }, "gpt-5-2-thinking": { sharedGroup: "go-thinking-shared" }, "gpt-5-1-thinking": { sharedGroup: "go-thinking-shared" }, "gpt-5-thinking": { sharedGroup: "go-thinking-shared" }, o3: { quota: 0, windowType: "weekly" }, "gpt-5-2-instant": { sharedGroup: "go-instant-shared" }, "gpt-5-1": { sharedGroup: "go-instant-shared" }, "gpt-5": { sharedGroup: "go-instant-shared" }, "gpt-5-t-mini": { quota: 100, windowType: "daily" }, "o4-mini": { quota: 0, windowType: "daily" }, "gpt-4o": { quota: 0, windowType: "hour3" }, "gpt-4-1": { quota: 0, windowType: "hour3" }, "gpt-5-mini": { quota: 1e4, windowType: "hour3" } } }, k12_teacher: { name: "K12 Teacher", sharedQuotaGroups: { "k12-thinking-shared": { quota: 160, windowType: "hour3", displayName: "K12思考共用池" }, "k12-instant-shared": { quota: 1e4, windowType: "hour3", displayName: "K12即时共用池" } }, models: { "gpt-5-2-pro": { quota: 0, windowType: "monthly" }, "gpt-5-1-pro": { quota: 0, windowType: "monthly" }, "gpt-5-pro": { quota: 0, windowType: "monthly" }, "o3-pro": { quota: 0, windowType: "monthly" }, "gpt-4-5": { quota: 0, windowType: "daily" }, "gpt-5-2-thinking": { sharedGroup: "k12-thinking-shared" }, "gpt-5-1-thinking": { sharedGroup: "k12-thinking-shared" }, "gpt-5-thinking": { sharedGroup: "k12-thinking-shared" }, o3: { quota: 0, windowType: "weekly" }, "gpt-5-2-instant": { sharedGroup: "k12-instant-shared" }, "gpt-5-1": { sharedGroup: "k12-instant-shared" }, "gpt-5": { sharedGroup: "k12-instant-shared" }, "gpt-5-t-mini": { quota: 0, windowType: "daily" }, "o4-mini": { quota: 0, windowType: "daily" }, "gpt-4o": { quota: 0, windowType: "hour3" }, "gpt-4-1": { quota: 0, windowType: "hour3" }, "gpt-5-mini": { quota: 1e4, windowType: "hour3" } } }, plus: { name: "Plus", sharedQuotaGroups: { "plus-thinking-shared": { quota: 160, windowType: "hour3", displayName: "Plus思考共用池" }, "plus-instant-shared": { quota: 1e4, windowType: "hour3", displayName: "Plus即时共用池" } }, models: { "gpt-5-2-pro": { quota: 0, windowType: "monthly" }, "gpt-5-1-pro": { quota: 0, windowType: "monthly" }, "gpt-5-pro": { quota: 0, windowType: "monthly" }, "o3-pro": { quota: 0, windowType: "monthly" }, "gpt-4-5": { quota: 0, windowType: "daily" }, "gpt-5-2-thinking": { sharedGroup: "plus-thinking-shared" }, "gpt-5-1-thinking": { sharedGroup: "plus-thinking-shared" }, "gpt-5-thinking": { sharedGroup: "plus-thinking-shared" }, o3: { quota: 100, windowType: "weekly" }, "gpt-5-2-instant": { sharedGroup: "plus-instant-shared" }, "gpt-5-1": { sharedGroup: "plus-instant-shared" }, "gpt-5": { sharedGroup: "plus-instant-shared" }, "gpt-5-t-mini": { quota: 1e4, windowType: "hour3" }, "o4-mini": { quota: 300, windowType: "daily" }, "gpt-4o": { quota: 80, windowType: "hour3" }, "gpt-4-1": { quota: 80, windowType: "hour3" }, "gpt-5-mini": { quota: 1e4, windowType: "hour3" } } }, team: { name: "Team", sharedQuotaGroups: { "team-premium-shared": { quota: 15, windowType: "monthly", displayName: "Team高级共用池" }, "team-thinking-shared": { quota: 3e3, windowType: "weekly", displayName: "Team思考共用池" }, "team-instant-shared": { quota: 1e4, windowType: "hour3", displayName: "Team即时共用池" } }, models: { "gpt-5-2-pro": { sharedGroup: "team-premium-shared" }, "gpt-5-1-pro": { sharedGroup: "team-premium-shared" }, "gpt-5-pro": { sharedGroup: "team-premium-shared" }, "o3-pro": { quota: 0, windowType: "monthly" }, "gpt-4-5": { quota: 0, windowType: "daily" }, "gpt-5-2-thinking": { sharedGroup: "team-thinking-shared" }, "gpt-5-1-thinking": { sharedGroup: "team-thinking-shared" }, "gpt-5-thinking": { sharedGroup: "team-thinking-shared" }, o3: { quota: 100, windowType: "weekly" }, "gpt-5-2-instant": { sharedGroup: "team-instant-shared" }, "gpt-5-1": { sharedGroup: "team-instant-shared" }, "gpt-5": { sharedGroup: "team-instant-shared" }, "gpt-5-t-mini": { quota: 1e4, windowType: "hour3" }, "o4-mini": { quota: 300, windowType: "daily" }, "gpt-4o": { quota: 80, windowType: "hour3" }, "gpt-4-1": { quota: 500, windowType: "hour3" }, "gpt-5-mini": { quota: 1e4, windowType: "hour3" } } }, edu: { name: "Edu", sharedQuotaGroups: { "edu-premium-shared": { quota: 15, windowType: "monthly", displayName: "Edu高级共用池" }, "edu-thinking-shared": { quota: 3e3, windowType: "weekly", displayName: "Edu思考共用池" }, "edu-instant-shared": { quota: 1e4, windowType: "hour3", displayName: "Edu即时共用池" } }, models: { "gpt-5-2-pro": { sharedGroup: "edu-premium-shared" }, "gpt-5-1-pro": { sharedGroup: "edu-premium-shared" }, "gpt-5-pro": { sharedGroup: "edu-premium-shared" }, "o3-pro": { quota: 0, windowType: "monthly" }, "gpt-4-5": { quota: 0, windowType: "daily" }, "gpt-5-2-thinking": { sharedGroup: "edu-thinking-shared" }, "gpt-5-1-thinking": { sharedGroup: "edu-thinking-shared" }, "gpt-5-thinking": { sharedGroup: "edu-thinking-shared" }, o3: { quota: 100, windowType: "weekly" }, "gpt-5-2-instant": { sharedGroup: "edu-instant-shared" }, "gpt-5-1": { sharedGroup: "edu-instant-shared" }, "gpt-5": { sharedGroup: "edu-instant-shared" }, "gpt-5-t-mini": { quota: 1e4, windowType: "hour3" }, "o4-mini": { quota: 300, windowType: "daily" }, "gpt-4o": { quota: 80, windowType: "hour3" }, "gpt-4-1": { quota: 500, windowType: "hour3" }, "gpt-5-mini": { quota: 1e4, windowType: "hour3" } } }, enterprise: { name: "Enterprise", sharedQuotaGroups: { "enterprise-premium-shared": { quota: 15, windowType: "monthly", displayName: "Enterprise高级共用池" }, "enterprise-thinking-shared": { quota: 3e3, windowType: "weekly", displayName: "Enterprise思考共用池" }, "enterprise-instant-shared": { quota: 1e4, windowType: "hour3", displayName: "Enterprise即时共用池" } }, models: { "gpt-5-2-pro": { sharedGroup: "enterprise-premium-shared" }, "gpt-5-1-pro": { sharedGroup: "enterprise-premium-shared" }, "gpt-5-pro": { sharedGroup: "enterprise-premium-shared" }, "o3-pro": { quota: 0, windowType: "monthly" }, "gpt-4-5": { quota: 0, windowType: "daily" }, "gpt-5-2-thinking": { sharedGroup: "enterprise-thinking-shared" }, "gpt-5-1-thinking": { sharedGroup: "enterprise-thinking-shared" }, "gpt-5-thinking": { sharedGroup: "enterprise-thinking-shared" }, o3: { quota: 100, windowType: "weekly" }, "gpt-5-2-instant": { sharedGroup: "enterprise-instant-shared" }, "gpt-5-1": { sharedGroup: "enterprise-instant-shared" }, "gpt-5": { sharedGroup: "enterprise-instant-shared" }, "gpt-5-t-mini": { quota: 1e4, windowType: "hour3" }, "o4-mini": { quota: 300, windowType: "daily" }, "gpt-4o": { quota: 80, windowType: "hour3" }, "gpt-4-1": { quota: 500, windowType: "hour3" }, "gpt-5-mini": { quota: 1e4, windowType: "hour3" } } }, pro: { name: "Pro", sharedQuotaGroups: { "pro-premium-shared": { quota: 100, windowType: "daily", displayName: "Pro高级共用池" }, "pro-thinking-shared": { quota: 1e4, windowType: "hour3", displayName: "Pro思考共用池" }, "pro-instant-shared": { quota: 1e4, windowType: "hour3", displayName: "Pro即时共用池" } }, models: { "gpt-5-2-pro": { sharedGroup: "pro-premium-shared" }, "gpt-5-1-pro": { sharedGroup: "pro-premium-shared" }, "gpt-5-pro": { sharedGroup: "pro-premium-shared" }, "o3-pro": { sharedGroup: "pro-premium-shared" }, "gpt-4-5": { quota: 100, windowType: "daily" }, "gpt-5-2-thinking": { sharedGroup: "pro-thinking-shared" }, "gpt-5-1-thinking": { sharedGroup: "pro-thinking-shared" }, "gpt-5-thinking": { sharedGroup: "pro-thinking-shared" }, o3: { quota: 1e4, windowType: "hour3" }, "gpt-5-2-instant": { sharedGroup: "pro-instant-shared" }, "gpt-5-1": { sharedGroup: "pro-instant-shared" }, "gpt-5": { sharedGroup: "pro-instant-shared" }, "gpt-5-t-mini": { quota: 1e4, windowType: "hour3" }, "o4-mini": { quota: 1e4, windowType: "hour3" }, "gpt-4o": { quota: 1e4, windowType: "hour3" }, "gpt-4-1": { quota: 1e4, windowType: "hour3" }, "gpt-5-mini": { quota: 1e4, windowType: "hour3" } } } }; // src/events.js var EVENT_DATA_CHANGED = "chatgpt-usage-monitor:data-changed"; function emitDataChanged() { try { window.dispatchEvent(new CustomEvent(EVENT_DATA_CHANGED)); } catch { } } function onDataChanged(handler) { window.addEventListener(EVENT_DATA_CHANGED, handler); return () => window.removeEventListener(EVENT_DATA_CHANGED, handler); } // src/state.js var usageData = null; function setUsageData(next) { usageData = next; } // src/userConfig.js var SILENT_MODE = false; // src/utils.js function formatTimeAgo(timestamp) { const now = Date.now(); const seconds = Math.floor((now - timestamp) / 1e3); if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } function formatTimestampForFilename(date = /* @__PURE__ */ new Date()) { const pad = (n) => String(n).padStart(2, "0"); const y = date.getFullYear(); const m = pad(date.getMonth() + 1); const d = pad(date.getDate()); const hh = pad(date.getHours()); const mm = pad(date.getMinutes()); const ss = pad(date.getSeconds()); return `${y}-${m}-${d}_${hh}-${mm}-${ss}`; } function tsOf(req) { if (typeof req === "number") return req; if (req && typeof req.t === "number") return req.t; if (req && typeof req.timestamp === "number") return req.timestamp; return NaN; } function formatTimeLeft(windowEnd) { const now = Date.now(); const timeLeft = windowEnd - now; if (timeLeft <= 0) return "0h 0m"; const hours = Math.floor(timeLeft / (60 * 60 * 1e3)); const minutes = Math.floor(timeLeft % (60 * 60 * 1e3) / (60 * 1e3)); return `${hours}h ${minutes}m`; } function getWindowEnd(timestamp, windowType) { return timestamp + TIME_WINDOWS[windowType]; } // src/storage.js function deepClone(value) { return JSON.parse(JSON.stringify(value)); } var Storage = { key: "usageData", get() { let data = GM_getValue(this.key); if (!data || typeof data !== "object") { data = deepClone(defaultUsageData); } if (!data.models || typeof data.models !== "object") { data.models = {}; } if (!data.position) { data.position = { x: null, y: null }; } if (!data.size) { data.size = { width: 400, height: 500 }; } if (data.minimized === void 0) { data.minimized = false; } if (data.silentMode === void 0) { data.silentMode = false; } if (typeof SILENT_MODE === "boolean") { data.silentMode = SILENT_MODE; } if (!data.progressType) { data.progressType = "bar"; } if (!data.planType) { data.planType = "team"; } if (!PLAN_CONFIGS[data.planType]) { data.planType = "team"; } if (!data.sharedQuotaGroups) { data.sharedQuotaGroups = {}; } if (data.showWindowResetTime === void 0) { data.showWindowResetTime = false; } if (data.models["gpt-4-1-mini"]) delete data.models["gpt-4-1-mini"]; if (data.models["o4-mini-high"]) delete data.models["o4-mini-high"]; const gpt5ProAllowedPlans = ["team", "edu", "enterprise", "pro"]; const isGpt5ProAllowed = gpt5ProAllowedPlans.includes(data.planType); if (!isGpt5ProAllowed) { ["gpt-5-2-pro", "gpt-5-1-pro"].forEach((key) => { if (data.models[key]) delete data.models[key]; }); } if (data.deepResearch) delete data.deepResearch; const newModels = [ "gpt-5", "gpt-5-thinking", "gpt-5-2-instant", "gpt-5-2-thinking", "gpt-5-2-pro", "gpt-5-1", "gpt-5-1-thinking", "gpt-5-pro", "gpt-5-1-pro", "o3", "o3-pro", "gpt-4-5", "o4-mini", "gpt-4o", "gpt-4-1", "gpt-5-t-mini", "gpt-5-mini" ]; if (!isGpt5ProAllowed) { ["gpt-5-2-pro", "gpt-5-1-pro"].forEach((m) => { const idx = newModels.indexOf(m); if (idx !== -1) newModels.splice(idx, 1); }); } newModels.forEach((modelId) => { if (data.models[modelId]) return; if (modelId === "gpt-5") { data.models[modelId] = { requests: [], quota: 1e4, windowType: "hour3" }; } else if (modelId === "gpt-5-thinking") { data.models[modelId] = { requests: [], quota: 3e3, windowType: "weekly" }; } else if (modelId === "gpt-5-2-instant") { data.models[modelId] = { requests: [], quota: 1e4, windowType: "hour3" }; } else if (modelId === "gpt-5-2-thinking") { data.models[modelId] = { requests: [], quota: 3e3, windowType: "weekly" }; } else if (modelId === "gpt-5-1") { data.models[modelId] = { requests: [], quota: 1e4, windowType: "hour3" }; } else if (modelId === "gpt-5-1-thinking") { data.models[modelId] = { requests: [], quota: 3e3, windowType: "weekly" }; } else if (modelId === "gpt-5-pro") { data.models[modelId] = { requests: [], quota: 15, windowType: "monthly" }; } else if (modelId === "gpt-5-2-pro") { data.models[modelId] = { requests: [], quota: 15, windowType: "monthly" }; } else if (modelId === "gpt-5-1-pro") { data.models[modelId] = { requests: [], quota: 15, windowType: "monthly" }; } else if (modelId === "o3") { data.models[modelId] = { requests: [], quota: 100, windowType: "weekly" }; } else if (modelId === "o3-pro") { data.models[modelId] = { requests: [], quota: 0, windowType: "monthly" }; } else if (modelId === "gpt-4-5") { data.models[modelId] = { requests: [], quota: 0, windowType: "daily" }; } else if (modelId === "o4-mini") { data.models[modelId] = { requests: [], quota: 300, windowType: "daily" }; } else if (modelId === "gpt-4o") { data.models[modelId] = { requests: [], quota: 80, windowType: "hour3" }; } else if (modelId === "gpt-4-1") { data.models[modelId] = { requests: [], quota: 500, windowType: "hour3" }; } else if (modelId === "gpt-5-t-mini") { data.models[modelId] = { requests: [], quota: 1e4, windowType: "hour3" }; } else if (modelId === "gpt-5-mini") { data.models[modelId] = { requests: [], quota: 1e4, windowType: "hour3" }; } }); if (data.models["gpt-4"]) delete data.models["gpt-4"]; Object.entries(data.models).forEach(([key, model]) => { if (!model || typeof model !== "object") { data.models[key] = { requests: [], quota: 50, windowType: "daily" }; return; } if (!Array.isArray(model.requests)) { model.requests = []; if (typeof model.count === "number" && model.count > 0) { const now = Date.now(); for (let i = 0; i < model.count; i++) { model.requests.push(now - i * 6e4); } } delete model.count; delete model.lastUpdate; } if (model.dailyLimit !== void 0 && model.quota === void 0) { model.quota = model.dailyLimit; delete model.dailyLimit; } if (model.resetFrequency !== void 0 && model.windowType === void 0) { model.windowType = model.resetFrequency; delete model.resetFrequency; } if (!["hour3", "hour5", "daily", "weekly", "monthly"].includes(model.windowType)) { model.windowType = "daily"; } if (Array.isArray(model.requests)) { model.requests = model.requests.map((r) => tsOf(r)).filter((ts) => typeof ts === "number" && !Number.isNaN(ts)); } }); if (data.sharedQuotaGroups && typeof data.sharedQuotaGroups === "object") { Object.values(data.sharedQuotaGroups).forEach((group) => { if (group && Array.isArray(group.requests)) { group.requests = group.requests.map((r) => { if (typeof r === "number") return { t: r }; if (r && typeof r === "object") { const t = tsOf(r); if (typeof r.modelId === "string") return { t, modelId: r.modelId }; const copy = { ...r }; if (t && typeof copy.t !== "number") copy.t = t; return copy; } return r; }); group.requests = []; } }); } delete data.lastDailyReset; delete data.lastWeeklyReset; delete data.lastReset; this.set(data); return data; }, set(newData) { GM_setValue(this.key, newData); }, update(callback) { const data = this.get(); callback(data); this.set(data); } }; function refreshUsageData() { const data = Storage.get(); setUsageData(data); return usageData; } function updateUsageData(mutator) { Storage.update((data) => { mutator(data); }); return refreshUsageData(); } // src/ui/toast.js function showToast(message, type = "success") { const container = document.getElementById("chatUsageMonitor"); if (!container) return; const existingToast = container.querySelector(".toast"); if (existingToast) existingToast.remove(); const toast = document.createElement("div"); toast.className = "toast"; toast.textContent = message; if (type === "error") { toast.style.color = COLORS.danger; toast.style.borderColor = COLORS.danger; } else if (type === "warning") { toast.style.color = COLORS.warning; toast.style.borderColor = COLORS.warning; } container.appendChild(toast); setTimeout(() => { toast.classList.add("show"); }, 10); setTimeout(() => { toast.classList.remove("show"); setTimeout(() => toast.remove(), 300); }, 3e3); } // src/features/importExport.js function exportUsageData() { const data = Storage.get(); const exportData = { ...data }; const jsonData = JSON.stringify(exportData, null, 2); const blob = new Blob([jsonData], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `chatgpt-usage-${formatTimestampForFilename()}.json`; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); showToast("用量统计数据已导出"); }, 100); } function importUsageData() { if (!confirm("导入将合并现有记录与导入文件中的记录。继续吗?")) return; const input = document.createElement("input"); input.type = "file"; input.accept = "application/json"; input.style.display = "none"; input.onchange = function(e) { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(event) { try { const importedData = JSON.parse(event.target.result); if (!validateImportedData(importedData)) { showToast("导入失败:数据格式不正确", "error"); return; } const importSummary = generateImportSummary(importedData); if (!confirm(`导入记录摘要:\\n${importSummary}\\n\\n确认导入这些数据吗?`)) return; const currentData = Storage.get(); const mergedData = mergeUsageData(currentData, importedData); Storage.set(mergedData); refreshUsageData(); emitDataChanged(); showToast("用量记录已成功导入", "success"); } catch (error) { console.error("[monitor] Import error:", error); showToast("导入失败:" + error.message, "error"); } finally { document.body.removeChild(input); } }; reader.readAsText(file); }; document.body.appendChild(input); input.click(); } function validateImportedData(data) { if (!data || typeof data !== "object") return false; if (!("models" in data) || typeof data.models !== "object") return false; for (const modelKey in data.models) { const model = data.models[modelKey]; if (!model || typeof model !== "object") return false; if (!Array.isArray(model.requests)) return false; if (typeof model.quota !== "number" && typeof model.sharedGroup !== "string") return false; if (model.windowType && !["hour3", "hour5", "daily", "weekly", "monthly"].includes(model.windowType)) return false; } return true; } function generateImportSummary(importedData) { let summary = ""; const modelCount = Object.keys(importedData.models || {}).length; let totalRequests = 0; const modelDetails = []; Object.entries(importedData.models || {}).forEach(([key, model]) => { const count = (model.requests || []).length; totalRequests += count; if (count > 0) modelDetails.push(`${key}: ${count}条记录`); }); summary += `共 ${modelCount} 个模型,${totalRequests} 条请求记录\\n`; if (modelDetails.length <= 5) { summary += `\\n模型详情:\\n${modelDetails.join("\\n")}`; } if (importedData.legacyMiniCount !== void 0 && importedData.legacyMiniCount > 0) { summary += `\\n\\n遗留特殊模型计数: ${importedData.legacyMiniCount} (已不再使用)`; } return summary; } function mergeUsageData(currentData, importedData) { const result = JSON.parse(JSON.stringify(currentData)); Object.entries(importedData.models || {}).forEach(([modelKey, importedModel]) => { if (!result.models[modelKey]) { result.models[modelKey] = { requests: [], quota: importedModel.quota || 50, windowType: importedModel.windowType || "daily" }; if (importedModel.sharedGroup) result.models[modelKey].sharedGroup = importedModel.sharedGroup; } const currentRequests = result.models[modelKey].requests || []; const now = Date.now(); const windowType = result.models[modelKey].windowType || "daily"; const windowDuration = TIME_WINDOWS[windowType] || TIME_WINDOWS.daily; const oldestRelevantTime = now - windowDuration; const relevantImportedRequests = (importedModel.requests || []).map((req) => tsOf(req)).filter((ts) => ts > oldestRelevantTime); const existingTimeMap = /* @__PURE__ */ new Map(); currentRequests.forEach((req) => { const roundedTime = Math.floor(tsOf(req) / 1e3) * 1e3; existingTimeMap.set(roundedTime, true); }); const newRequests = relevantImportedRequests.filter((ts) => { const roundedTime = Math.floor(ts / 1e3) * 1e3; return !existingTimeMap.has(roundedTime); }); result.models[modelKey].requests = [...currentRequests.map(tsOf), ...newRequests].filter((ts) => typeof ts === "number" && !Number.isNaN(ts)).sort((a, b) => b - a); }); return result; } // src/styles.js function injectStyles() { GM_addStyle(` #chatUsageMonitor { position: fixed; bottom: 100px; /* 往下移动一点点 */ left: ${STYLE.spacing.lg}; /* 改为左侧 */ width: 400px; height: 500px; max-height: 80vh; overflow: auto; background: ${COLORS.background}; color: ${COLORS.text}; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; border-radius: ${STYLE.borderRadius}; box-shadow: ${STYLE.boxShadow}; z-index: 9999; border: 1px solid ${COLORS.border}; user-select: none; resize: both; transition: all 0.3s ease; transform-origin: top left; /* 改为左侧 */ } #chatUsageMonitor.hidden { display: none !important; } #chatUsageMonitor::after { content: ""; position: absolute; bottom: 0; right: 0; width: 15px; height: 15px; background: transparent; border-bottom: 2px solid ${COLORS.yellow}; border-right: 2px solid ${COLORS.yellow}; opacity: 0.5; pointer-events: none; } #chatUsageMonitor:hover::after { opacity: 1; } #chatUsageMonitor.minimized { width: 30px !important; height: 30px !important; border-radius: 50%; overflow: hidden; resize: none; opacity: 0.8; cursor: pointer; background-color: ${COLORS.primary}; bottom: auto; top: 100px; /* 往下移动一点点 */ left: ${STYLE.spacing.lg}; /* 改为左侧 */ z-index: 9999; } #chatUsageMonitor.minimized:hover { opacity: 1; } #chatUsageMonitor.minimized > * { display: none !important; } #chatUsageMonitor.minimized::before { content: "次"; color: white; position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: bold; } #chatUsageMonitor header { padding: 0 ${STYLE.spacing.md}; display: flex; border-radius: ${STYLE.borderRadius} ${STYLE.borderRadius} 0 0; background: ${COLORS.background}; flex-direction: row; position: relative; align-items: center; height: 36px; cursor: move; /* 指示整个头部可拖动 */ } #chatUsageMonitor .minimize-btn { position: absolute; left: 8px; top: 0; height: 36px; width: 24px; display: flex; align-items: center; justify-content: center; color: ${COLORS.secondaryText}; cursor: pointer; font-size: 18px; transition: color 0.2s ease; z-index: 10; } #chatUsageMonitor .minimize-btn:hover { color: ${COLORS.yellow}; } #chatUsageMonitor header button { border: none; background: none; color: ${COLORS.secondaryText}; cursor: pointer; font-weight: 500; transition: color 0.2s ease; display: flex; align-items: center; justify-content: center; margin-left: 30px; /* Move buttons to the right to avoid overlap with minimize button */ padding-top: ${STYLE.spacing.sm}; } #chatUsageMonitor header button.active { color: ${COLORS.yellow}; } #chatUsageMonitor .content { padding: ${STYLE.spacing.xs} ${STYLE.spacing.md}; overflow-y: auto; } #chatUsageMonitor .reset-info { font-size: ${STYLE.textSize.xs}; color: ${COLORS.secondaryText}; margin: ${STYLE.spacing.xs} 0; } #chatUsageMonitor input { width: 80px; padding: ${STYLE.spacing.xs} ${STYLE.spacing.sm}; margin: 0; border: none; border-radius: 0; background: transparent; color: ${COLORS.secondaryText}; font-family: monospace; font-size: ${STYLE.textSize.xs}; line-height: ${STYLE.lineHeight.xs}; transition: color 0.2s ease; } #chatUsageMonitor input:focus { outline: none; color: ${COLORS.yellow}; background: transparent; } #chatUsageMonitor input:hover { color: ${COLORS.yellow}; } #chatUsageMonitor .btn { padding: ${STYLE.spacing.sm} ${STYLE.spacing.md}; border: none; cursor: pointer; color: ${COLORS.white}; font-weight: 500; font-size: ${STYLE.textSize.sm}; transition: all 0.2s ease; text-decoration: underline; } #chatUsageMonitor .btn:hover { color: ${COLORS.yellow}; } #chatUsageMonitor .delete-btn { padding: ${STYLE.spacing.xs} ${STYLE.spacing.sm}; margin-left: ${STYLE.spacing.sm}; } #chatUsageMonitor .delete-btn.btn:hover { color: ${COLORS.danger}; } #chatUsageMonitor::-webkit-scrollbar { width: 8px; } #chatUsageMonitor::-webkit-scrollbar-track { background: ${COLORS.surface}; border-radius: 4px; } #chatUsageMonitor::-webkit-scrollbar-thumb { background: ${COLORS.border}; border-radius: 4px; } #chatUsageMonitor::-webkit-scrollbar-thumb:hover { background: ${COLORS.secondaryText}; } #chatUsageMonitor .progress-container { width: 100%; background: ${COLORS.surface}; margin-top: ${STYLE.spacing.xs}; border-radius: 6px; overflow: hidden; height: 8px; position: relative; } #chatUsageMonitor .progress-bar { height: 100%; transition: width 0.3s ease; border-radius: 6px; background: linear-gradient( 90deg, ${COLORS.progressLow} 0%, ${COLORS.progressMed} 50%, ${COLORS.progressHigh} 100% ); background-size: 200% 100%; animation: gradientShift 2s linear infinite; } #chatUsageMonitor .progress-bar.low-usage { animation: pulse 1.5s ease-in-out infinite; } #chatUsageMonitor .progress-bar.exceeded { background: ${COLORS.progressExceed}; animation: none; } #chatUsageMonitor .window-badge { display: inline-block; font-size: 10px; padding: 2px 4px; border-radius: 4px; margin-left: 4px; color: ${COLORS.background}; font-weight: bold; } #chatUsageMonitor .window-badge.hour3 { background-color: ${COLORS.hourModel}; } #chatUsageMonitor .window-badge.hour5 { background-color: ${COLORS.hourModel}; } #chatUsageMonitor .window-badge.daily { background-color: ${COLORS.dailyModel}; } #chatUsageMonitor .window-badge.weekly { background-color: ${COLORS.weeklyModel}; } #chatUsageMonitor .window-badge.monthly { background-color: ${COLORS.monthlyModel}; } #chatUsageMonitor .request-time { color: ${COLORS.secondaryText}; font-size: ${STYLE.textSize.xs}; } #chatUsageMonitor .window-info { color: ${COLORS.secondaryText}; font-size: ${STYLE.textSize.xs}; margin-top: 2px; } #chatUsageMonitor .active-window { font-weight: bold; } #chatUsageMonitor .unknown-quota { color: ${COLORS.warning}; font-style: italic; } /* 为特殊模型添加样式 */ #chatUsageMonitor .special-model-row { border-top: 1px dashed ${COLORS.border}; margin-top: 8px; padding-top: 8px; opacity: 0.8; } #chatUsageMonitor .special-model-name { color: ${COLORS.disabled}; font-style: italic; } /* 周分析报告样式 */ #chatUsageMonitor .weekly-report { background: ${COLORS.surface}; border-radius: 8px; padding: ${STYLE.spacing.md}; margin-top: ${STYLE.spacing.md}; } #chatUsageMonitor .weekly-report h3 { color: ${COLORS.yellow}; margin-bottom: ${STYLE.spacing.sm}; font-size: ${STYLE.textSize.md}; } #chatUsageMonitor .weekly-report .stat-row { display: flex; justify-content: space-between; padding: ${STYLE.spacing.xs} 0; font-size: ${STYLE.textSize.sm}; border-bottom: 1px solid ${COLORS.border}; } #chatUsageMonitor .weekly-report .stat-row:last-child { border-bottom: none; } #chatUsageMonitor .weekly-report .stat-label { color: ${COLORS.secondaryText}; } #chatUsageMonitor .weekly-report .stat-value { color: ${COLORS.text}; font-weight: 500; } #chatUsageMonitor .weekly-report .model-breakdown { margin-top: ${STYLE.spacing.sm}; } #chatUsageMonitor .weekly-report .model-item { display: flex; justify-content: space-between; padding: ${STYLE.spacing.xs} ${STYLE.spacing.sm}; font-size: ${STYLE.textSize.xs}; background: ${COLORS.background}; border-radius: 4px; margin: 2px 0; } @keyframes gradientShift { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } 70% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } } /* Dot-based progression system */ #chatUsageMonitor .dot-progress { display: flex; gap: 4px; align-items: center; height: 8px; } #chatUsageMonitor .dot { width: 8px; height: 8px; border-radius: 50%; transition: all 0.3s ease; } #chatUsageMonitor .dot-empty { background: rgba(239, 68, 68, 0.3); border: 1px solid ${COLORS.progressLow}; } #chatUsageMonitor .dot-partial { background: ${COLORS.progressMed}; } #chatUsageMonitor .dot-full { background: ${COLORS.progressHigh}; } #chatUsageMonitor .dot-exceeded { background: ${COLORS.progressExceed}; position: relative; } #chatUsageMonitor .dot-exceeded::before { content: ''; position: absolute; top: 50%; left: -2px; right: -2px; height: 2px; background: ${COLORS.surface}; transform: rotate(45deg); } #chatUsageMonitor .table-header { font-family: monospace; color: ${COLORS.white}; font-size: ${STYLE.textSize.xs}; line-height: ${STYLE.lineHeight.xs}; display : grid; align-items: center; grid-template-columns: 2fr 1.5fr 1.5fr 2fr; } #chatUsageMonitor .model-row { font-family: monospace; color: ${COLORS.secondaryText}; transition: color 0.2s ease; font-size: ${STYLE.textSize.xs}; line-height: ${STYLE.lineHeight.xs}; display : grid; grid-template-columns: 2fr 1.5fr 1.5fr 2fr; align-items: center; } #chatUsageMonitor .model-row:hover { color: ${COLORS.yellow}; text-decoration-line: underline; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } /* DeepResearch 分割线与区域样式 */ #chatUsageMonitor .section-divider { margin: 8px 0 10px 0; border-top: 1px dashed ${COLORS.border}; opacity: 0.8; } #chatUsageMonitor .deepresearch-title { font-family: monospace; font-weight: bold; color: ${COLORS.white}; font-size: ${STYLE.textSize.xs}; margin: 6px 0; } /* Container to help position the arrow (pseudo-element) */ #chatUsageMonitor .custom-select { position: relative; display: inline-block; margin-right: 8px; } /* Hide the native select arrow and style the dropdown */ #chatUsageMonitor .custom-select select { -webkit-appearance: none; /* Safari and Chrome */ -moz-appearance: none; /* Firefox */ appearance: none; /* Standard modern browsers */ background-color: transparent; color: #ffffff; border: none; cursor: pointer; color: ${COLORS.white}; font-size: ${STYLE.textSize.sm}; line-height: ${STYLE.lineHeight.sm}; padding: 2px 5px; } /* Style the list of options (when the dropdown is open) */ .custom-select select option { background: ${COLORS.background}; color: ${COLORS.white}; } /* Optional: highlight the hovered option in some browsers */ .custom-select select option:hover { background: ${COLORS.background}; color: ${COLORS.yellow}; text-decoration-line: underline; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } #chatUsageMonitor input { width: 90%; padding: ${STYLE.spacing.xs} ${STYLE.spacing.sm}; margin: 0; border: 1px solid ${COLORS.border}; border-radius: 4px; background: ${COLORS.surface}; color: ${COLORS.secondaryText}; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: ${STYLE.textSize.xs}; line-height: ${STYLE.lineHeight.xs}; transition: all 0.2s ease; } #chatUsageMonitor input:focus { outline: none; border-color: ${COLORS.yellow}; color: ${COLORS.yellow}; background: rgba(245, 158, 11, 0.1); } #chatUsageMonitor input:hover { border-color: ${COLORS.yellow}; color: ${COLORS.yellow}; } /* Toast notification for feedback */ #chatUsageMonitor .toast { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background: ${COLORS.background}; color: ${COLORS.success}; padding: ${STYLE.spacing.sm} ${STYLE.spacing.md}; border-radius: ${STYLE.borderRadius}; border: 1px solid ${COLORS.success}; opacity: 0; transition: opacity 0.3s ease; z-index: 10000; } #chatUsageMonitor .toast.show { opacity: 1; } `); } // src/tracking/fetchInterceptor.js function installFetchInterceptor(modelRouting) { const targetWindow = typeof unsafeWindow === "undefined" ? window : unsafeWindow; const originalFetch = targetWindow.fetch; if (originalFetch?.__chatgptUsagePatched) return; const wrapped = new Proxy(originalFetch, { apply: async function(target, thisArg, args) { let autoRequestId = null; try { const [requestInfo, requestInit] = args; const fetchUrl = typeof requestInfo === "string" ? requestInfo : requestInfo?.href || requestInfo?.url || ""; const requestMethod = typeof requestInfo === "object" && requestInfo?.method ? requestInfo.method : requestInit?.method || "GET"; if (requestMethod === "PATCH" && fetchUrl?.includes("/backend-api/settings/user_last_used_model_config")) { modelRouting.updateLastSelectedModelConfigFromUrl(fetchUrl); } if (requestMethod === "POST" && /\/conversation(?:\?|$)/.test(fetchUrl || "")) { const bodyText = requestInit?.body; if (typeof bodyText === "string") { const bodyObj = JSON.parse(bodyText); if (bodyObj?.model) { autoRequestId = modelRouting.handleConversationRequest(bodyObj.model); } } } } catch { } const response = await target.apply(thisArg, args); if (autoRequestId) { modelRouting.attachAutoSseParser(autoRequestId, response); } return response; } }); wrapped.__chatgptUsagePatched = true; targetWindow.fetch = wrapped; } // src/usage.js function cleanupExpiredRequests() { const now = Date.now(); const maxWindow = TIME_WINDOWS.monthly; Object.values(usageData.models || {}).forEach((model) => { if (!Array.isArray(model.requests)) return; model.requests = model.requests.map((req) => tsOf(req)).filter((ts) => now - ts < maxWindow); }); if (usageData.sharedQuotaGroups && typeof usageData.sharedQuotaGroups === "object") { Object.values(usageData.sharedQuotaGroups).forEach((group) => { if (!Array.isArray(group.requests)) return; group.requests = group.requests.filter((req) => now - tsOf(req) < maxWindow); }); } } function recordModelUsageByModelId(modelId) { refreshUsageData(); cleanupExpiredRequests(); if (!usageData.models[modelId]) { usageData.models[modelId] = { requests: [], quota: 50, windowType: "daily" }; } usageData.models[modelId].requests.push(Date.now()); Storage.set(usageData); refreshUsageData(); emitDataChanged(); } function applyPlanConfig(planType) { const planConfig = PLAN_CONFIGS[planType]; if (!planConfig) return; updateUsageData((data) => { const existingUsageByModel = {}; Object.entries(data.models || {}).forEach(([modelKey, model]) => { if (model?.requests?.length) existingUsageByModel[modelKey] = [...model.requests]; }); data.sharedQuotaGroups = {}; if (planConfig.sharedQuotaGroups) { Object.entries(planConfig.sharedQuotaGroups).forEach(([groupId, groupConfig]) => { data.sharedQuotaGroups[groupId] = { quota: groupConfig.quota, windowType: groupConfig.windowType, models: groupConfig.models, displayName: groupConfig.displayName, requests: [] }; }); } const nextModels = {}; Object.entries(planConfig.models).forEach(([modelKey, cfg]) => { nextModels[modelKey] = { requests: existingUsageByModel[modelKey] ? [...existingUsageByModel[modelKey]] : [] }; if (cfg.sharedGroup) { nextModels[modelKey].sharedGroup = cfg.sharedGroup; } else { nextModels[modelKey].quota = cfg.quota; nextModels[modelKey].windowType = cfg.windowType; } }); data.models = nextModels; }); emitDataChanged(); } function collectSharedGroupUsage(groupId, now = Date.now()) { const group = usageData.sharedQuotaGroups?.[groupId]; if (!group) return null; const windowType = group.windowType || "daily"; const windowDuration = TIME_WINDOWS[windowType]; const activeRequests = []; Object.entries(usageData.models || {}).forEach(([key, model]) => { if (model.sharedGroup !== groupId) return; if (!Array.isArray(model.requests)) return; model.requests.map((req) => tsOf(req)).filter((ts) => typeof ts === "number" && !Number.isNaN(ts) && now - ts < windowDuration).forEach((ts) => activeRequests.push({ ts, modelKey: key })); }); activeRequests.sort((a, b) => a.ts - b.ts); return { group, windowType, windowDuration, activeRequests, windowEnd: activeRequests.length > 0 ? getWindowEnd(activeRequests[0].ts, windowType) : null }; } // src/tracking/modelRouting.js function resolveRedirectedModelId(originalModelId) { if (originalModelId === "chatgpt_alpha_model_external_access_reserved_gate_13") { return "alpha"; } if (originalModelId === "auto") { return "gpt-5-2"; } try { const plan = usageData && usageData.planType || "team"; if (originalModelId === "gpt-4-5" && plan !== "pro") return "gpt-5-2-instant"; if (originalModelId === "o3-pro" && plan !== "pro") return "gpt-5-2-instant"; } catch { } return originalModelId; } function createModelRouting() { const pendingAutoRequests = /* @__PURE__ */ new Map(); const seenAssistantMessageIds = /* @__PURE__ */ new Set(); let assistantObserverStarted = false; let lastSelectedModelConfig = { modelSlug: null, thinkingEffort: null, updatedAt: 0 }; function parseUrl(urlLike) { try { return new URL(urlLike, location.origin); } catch { return null; } } function updateLastSelectedModelConfigFromUrl(urlLike) { const url = parseUrl(urlLike); if (!url) return; const modelSlug = url.searchParams.get("model_slug") || url.searchParams.get("model"); const thinkingEffort = url.searchParams.get("thinking_effort") || url.searchParams.get("effort"); if (!modelSlug && !thinkingEffort) return; lastSelectedModelConfig = { modelSlug: modelSlug ?? lastSelectedModelConfig.modelSlug, thinkingEffort: thinkingEffort ?? lastSelectedModelConfig.thinkingEffort, updatedAt: Date.now() }; } function getOldestUnresolvedAutoRequest() { let oldest = null; for (const [id, req] of pendingAutoRequests.entries()) { if (req.resolved) continue; if (!oldest || req.startedAt < oldest.startedAt) oldest = { id, ...req }; } return oldest; } function mapRoutedSlugToModelKey(baseModelKey, routedModelSlug, didAutoSwitchToReasoning) { const slug = (routedModelSlug || "").toLowerCase(); if (baseModelKey === "gpt-5-2") { if (slug.includes("pro")) return "gpt-5-2-pro"; const looksReasoning = didAutoSwitchToReasoning === true || slug.includes("thinking") || slug.includes("reasoning"); return looksReasoning ? "gpt-5-2-thinking" : "gpt-5-2-instant"; } return routedModelSlug || baseModelKey; } function resolveAutoRequest(requestId, routed) { const req = pendingAutoRequests.get(requestId); if (!req || req.resolved) return; const modelKey = mapRoutedSlugToModelKey( req.baseModelKey, routed?.modelSlug, routed?.didAutoSwitchToReasoning ); req.resolved = true; req.resolvedAt = Date.now(); req.routed = routed; recordModelUsageByModelId(modelKey); } async function parseSseFromResponse(response, onJson) { const body = response?.body; if (!body || typeof body.getReader !== "function") return; const reader = body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); buffer = buffer.replace(/\r\n/g, "\n"); let sepIdx; while ((sepIdx = buffer.indexOf("\n\n")) !== -1) { const rawEvent = buffer.slice(0, sepIdx); buffer = buffer.slice(sepIdx + 2); const dataLines = rawEvent.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()); if (!dataLines.length) continue; const data = dataLines.join("\n"); if (!data || data === "[DONE]") continue; try { onJson(JSON.parse(data)); } catch { } } } } function extractRoutingInfo(json) { if (!json || typeof json !== "object") return null; const ste = json.server_ste_metadata && typeof json.server_ste_metadata === "object" ? json.server_ste_metadata : json; if (ste && (ste.type === "server_ste_metadata" || json.type === "server_ste_metadata")) { const modelSlug2 = ste.model_slug || ste.model || ste.modelSlug; const didAutoSwitchToReasoning = ste.did_auto_switch_to_reasoning ?? ste.didAutoSwitchToReasoning; const thinkingEffort2 = ste.thinking_effort ?? ste.thinkingEffort ?? ste.effort; if (modelSlug2 || didAutoSwitchToReasoning !== void 0 || thinkingEffort2) { return { source: "sse", modelSlug: modelSlug2, didAutoSwitchToReasoning, thinkingEffort: thinkingEffort2 }; } } const message = json.message && typeof json.message === "object" ? json.message : null; const metadata = message?.metadata && typeof message.metadata === "object" ? message.metadata : null; const modelSlug = metadata?.model_slug || metadata?.modelSlug; const thinkingEffort = metadata?.thinking_effort || metadata?.thinkingEffort; if (modelSlug || thinkingEffort) { return { source: "sse-message", modelSlug, thinkingEffort }; } return null; } function attachAutoSseParser(requestId, response) { try { const clone = response.clone(); parseSseFromResponse(clone, (json) => { const info = extractRoutingInfo(json); if (info) resolveAutoRequest(requestId, info); }).catch(() => { }); } catch { } } function startAssistantMessageObserver() { if (assistantObserverStarted) return; assistantObserverStarted = true; const start = () => { if (!document?.body) { setTimeout(start, 300); return; } const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!(node instanceof Element)) continue; const candidates = []; if (node.matches?.('[data-message-author-role="assistant"]')) candidates.push(node); node.querySelectorAll?.('[data-message-author-role="assistant"]').forEach((el) => candidates.push(el)); for (const el of candidates) { const msgId = el.getAttribute("data-message-id") || el.id || null; if (msgId && seenAssistantMessageIds.has(msgId)) continue; if (msgId) seenAssistantMessageIds.add(msgId); const modelSlug = el.getAttribute("data-message-model-slug"); if (!modelSlug) continue; const oldest = getOldestUnresolvedAutoRequest(); if (!oldest) continue; const ageMs = Date.now() - oldest.startedAt; if (ageMs < 0 || ageMs > 2 * 60 * 1e3) continue; resolveAutoRequest(oldest.id, { source: "dom", modelSlug }); } } } }); observer.observe(document.body, { childList: true, subtree: true }); }; start(); } function beginAutoRequest(requestedModelId, baseModelKey = "gpt-5-2") { startAssistantMessageObserver(); const requestId = crypto?.randomUUID && crypto.randomUUID() || `auto_${Date.now()}_${Math.random().toString(16).slice(2)}`; pendingAutoRequests.set(requestId, { baseModelKey, requestedModel: requestedModelId, startedAt: Date.now(), resolved: false, lastSelectedModelConfig: { ...lastSelectedModelConfig } }); setTimeout(() => { const req = pendingAutoRequests.get(requestId); if (!req || req.resolved) return; resolveAutoRequest(requestId, { source: "timeout", modelSlug: req.baseModelKey, didAutoSwitchToReasoning: false }); }, 60 * 1e3); return requestId; } function handleConversationRequest(modelId) { const effectiveModelId = resolveRedirectedModelId(modelId); if (effectiveModelId === "gpt-5-instant") { recordModelUsageByModelId("gpt-5"); return null; } if (effectiveModelId === "gpt-5-1-instant") { recordModelUsageByModelId("gpt-5-1"); return null; } if (effectiveModelId === "gpt-5-2") { return beginAutoRequest(modelId, "gpt-5-2"); } recordModelUsageByModelId(resolveRedirectedModelId(effectiveModelId)); return null; } return { handleConversationRequest, attachAutoSseParser, updateLastSelectedModelConfigFromUrl }; } // src/textScrambler.js function installTextScrambler() { (() => { var TextScrambler = (() => { var l = Object.defineProperty; var c = Object.getOwnPropertyDescriptor; var u = Object.getOwnPropertyNames; var m = Object.prototype.hasOwnProperty; var d = (n, t) => { for (var e in t) l(n, e, { get: t[e], enumerable: true }); }, f = (n, t, e, s) => { if (t && typeof t == "object" || typeof t == "function") for (let i of u(t)) !m.call(n, i) && i !== e && l(n, i, { get: () => t[i], enumerable: !(s = c(t, i)) || s.enumerable }); return n; }; var g = (n) => f(l({}, "__esModule", { value: true }), n); var T = {}; d(T, { default: () => r }); function _(n) { let t = document.createTreeWalker(n, NodeFilter.SHOW_TEXT, { acceptNode: (s) => s.nodeValue.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP }), e = []; for (; t.nextNode(); ) t.currentNode.nodeValue = t.currentNode.nodeValue.replace(/(\n|\r|\t)/gm, ""), e.push(t.currentNode); return e; } function p(n, t, e) { return t < 0 || t >= n.length ? n : n.substring(0, t) + e + n.substring(t + 1); } function M(n, t) { return n ? "x" : t[Math.floor(Math.random() * t.length)]; } var r = class { constructor(t, e = {}) { this.el = t; let s = { duration: 1e3, delay: 0, reverse: false, absolute: false, pointerEvents: true, scrambleSymbols: "—~±§|[].+$^@*()•x%!?#", randomThreshold: null }; this.config = Object.assign({}, s, e), this.config.randomThreshold === null && (this.config.randomThreshold = this.config.reverse ? 0.1 : 0.8), this.textNodes = _(this.el), this.nodeLengths = this.textNodes.map((i) => i.nodeValue.length), this.originalText = this.textNodes.map((i) => i.nodeValue).join(""), this.mask = this.originalText.split(" ").map((i) => " ".repeat(i.length)).join(" "), this.currentMask = this.mask, this.totalChars = this.originalText.length, this.scrambleRange = Math.floor(this.totalChars * (this.config.reverse ? 0.25 : 1.5)), this.direction = this.config.reverse ? -1 : 1, this.config.absolute && (this.el.style.position = "absolute", this.el.style.top = "0"), this.config.pointerEvents || (this.el.style.pointerEvents = "none"), this._animationFrame = null, this._startTime = null, this._running = false; } initialize() { return this.currentMask = this.mask, this; } _getEased(t) { let e = -(Math.cos(Math.PI * t) - 1) / 2; return e = Math.pow(e, 2), this.config.reverse ? 1 - e : e; } _updateScramble(t, e, s) { if (Math.random() < 0.5 && t > 0 && t < 1) for (let i = 0; i < 20; i++) { let o = i / 20, a; if (this.config.reverse) a = e - Math.floor((1 - Math.random()) * this.scrambleRange * o); else a = e + Math.floor((1 - Math.random()) * this.scrambleRange * o); if (!(a < 0 || a >= this.totalChars) && this.currentMask[a] !== " ") { let h = Math.random() > this.config.randomThreshold ? this.originalText[a] : M(this.config.reverse, this.config.scrambleSymbols); this.currentMask = p(this.currentMask, a, h); } } } _composeOutput(t, e, s) { let i = ""; if (this.config.reverse) { let o = Math.max(e - s, 0); i = this.mask.slice(0, o) + this.currentMask.slice(o, e) + this.originalText.slice(e); } else i = this.originalText.slice(0, e) + this.currentMask.slice(e, e + s) + this.mask.slice(e + s); return i; } _updateTextNodes(t) { let e = 0; for (let s = 0; s < this.textNodes.length; s++) { let i = this.nodeLengths[s]; this.textNodes[s].nodeValue = t.slice(e, e + i), e += i; } } _tick = (t) => { this._startTime || (this._startTime = t); let e = t - this._startTime, s = Math.min(e / this.config.duration, 1), i = this._getEased(s), o = Math.floor(this.totalChars * s), a = Math.floor(2 * (0.5 - Math.abs(s - 0.5)) * this.scrambleRange); this._updateScramble(s, o, a); let h = this._composeOutput(s, o, a); this._updateTextNodes(h), s < 1 ? this._animationFrame = requestAnimationFrame(this._tick) : this._running = false; }; start() { this._running = true, this._startTime = null, this.config.delay ? setTimeout(() => { this._animationFrame = requestAnimationFrame(this._tick); }, this.config.delay) : this._animationFrame = requestAnimationFrame(this._tick); } stop() { this._animationFrame && (cancelAnimationFrame(this._animationFrame), this._animationFrame = null), this._running = false; } }; return g(T); })(); window.TextScrambler = TextScrambler.default || TextScrambler; })(); } // src/features/reports.js function generateWeeklyReport() { const now = /* @__PURE__ */ new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); const sevenDaysAgoStart = todayStart - 6 * TIME_WINDOWS.daily; const report = { totalRequests: 0, modelBreakdown: {}, dailyData: [], // 最近7天的数据 peakDay: "", averageDaily: 0, generatedAt: (/* @__PURE__ */ new Date()).toISOString() }; for (let i = 0; i < 7; i++) { const dayStart = todayStart - (6 - i) * TIME_WINDOWS.daily; const date = new Date(dayStart); report.dailyData.push({ date: date.toLocaleDateString("zh-CN"), dayOfWeek: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][date.getDay()], models: {}, total: 0, dayStart, dayEnd: dayStart + TIME_WINDOWS.daily - 1 }); } const currentPlanForWeekly = usageData && usageData.planType || "team"; const sortedModelEntries = MODEL_DISPLAY_ORDER.filter((modelKey) => usageData.models[modelKey]).filter((modelKey) => !(modelKey === "o3-pro" && currentPlanForWeekly !== "pro")).map((modelKey) => [modelKey, usageData.models[modelKey]]); Object.entries(usageData.models).forEach(([modelKey, model]) => { if (!MODEL_DISPLAY_ORDER.includes(modelKey)) { if (modelKey === "o3-pro" && currentPlanForWeekly !== "pro") return; sortedModelEntries.push([modelKey, model]); } }); sortedModelEntries.forEach(([modelKey, model]) => { const validRequests = model.requests.map((req) => tsOf(req)).filter((ts) => ts >= sevenDaysAgoStart && ts < todayStart + TIME_WINDOWS.daily); if (validRequests.length > 0) { if (!report.modelBreakdown[modelKey]) { report.modelBreakdown[modelKey] = 0; } validRequests.forEach((ts) => { const dayData = report.dailyData.find((day) => ts >= day.dayStart && ts <= day.dayEnd); if (dayData) { dayData.total++; dayData.models[modelKey] = (dayData.models[modelKey] || 0) + 1; report.modelBreakdown[modelKey]++; report.totalRequests++; } }); } }); const activeDays = report.dailyData.filter((d) => d.total > 0).length || 1; report.averageDaily = Math.round(report.totalRequests / activeDays); const maxDayUsage = Math.max(...report.dailyData.map((d) => d.total), 0); const peakDayData = report.dailyData.find((d) => d.total === maxDayUsage); if (peakDayData) { report.peakDay = `${peakDayData.date} ${peakDayData.dayOfWeek}`; } return report; } function generateMonthlyReport() { const now = /* @__PURE__ */ new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); const thirtyDaysAgoStart = todayStart - 29 * TIME_WINDOWS.daily; const report = { totalRequests: 0, modelBreakdown: {}, dailyData: [], // 最近30天的数据 peakDay: "", averageDaily: 0, generatedAt: (/* @__PURE__ */ new Date()).toISOString() }; for (let i = 0; i < 30; i++) { const dayStart = todayStart - (29 - i) * TIME_WINDOWS.daily; const date = new Date(dayStart); report.dailyData.push({ date: date.toLocaleDateString("zh-CN"), dayOfWeek: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][date.getDay()], models: {}, total: 0, dayStart, dayEnd: dayStart + TIME_WINDOWS.daily - 1 }); } const currentPlanForMonthly = usageData && usageData.planType || "team"; const sortedModelEntries = MODEL_DISPLAY_ORDER.filter((modelKey) => usageData.models[modelKey]).filter((modelKey) => !(modelKey === "o3-pro" && currentPlanForMonthly !== "pro")).map((modelKey) => [modelKey, usageData.models[modelKey]]); Object.entries(usageData.models).forEach(([modelKey, model]) => { if (!MODEL_DISPLAY_ORDER.includes(modelKey)) { if (modelKey === "o3-pro" && currentPlanForMonthly !== "pro") return; sortedModelEntries.push([modelKey, model]); } }); sortedModelEntries.forEach(([modelKey, model]) => { const validRequests = model.requests.map((req) => tsOf(req)).filter((ts) => ts >= thirtyDaysAgoStart && ts < todayStart + TIME_WINDOWS.daily); if (validRequests.length > 0) { if (!report.modelBreakdown[modelKey]) { report.modelBreakdown[modelKey] = 0; } validRequests.forEach((ts) => { const dayData = report.dailyData.find((day) => ts >= day.dayStart && ts <= day.dayEnd); if (dayData) { dayData.total++; dayData.models[modelKey] = (dayData.models[modelKey] || 0) + 1; report.modelBreakdown[modelKey]++; report.totalRequests++; } }); } }); const activeDays = report.dailyData.filter((d) => d.total > 0).length || 1; report.averageDaily = Math.round(report.totalRequests / activeDays); const maxDayUsage = Math.max(...report.dailyData.map((d) => d.total), 0); const peakDayData = report.dailyData.find((d) => d.total === maxDayUsage); if (peakDayData) { report.peakDay = `${peakDayData.date} ${peakDayData.dayOfWeek}`; } return report; } function mergeUnknownModelsForHtml(report) { try { const KNOWN = /* @__PURE__ */ new Set([ // 采用固定显示顺序中的模型 ...MODEL_DISPLAY_ORDER, // 再补充兼容显示顺序外但“已知”的模型键 "gpt-5", "gpt-5-thinking", "gpt-5-2-instant", "gpt-5-2-thinking", "gpt-5-2-pro", "gpt-5-1", "gpt-5-1-thinking", "gpt-5-1-instant", "alpha" ]); const targetKey = "gpt-5-2-instant"; if (!report.modelBreakdown[targetKey]) report.modelBreakdown[targetKey] = 0; const unknownKeys = Object.keys(report.modelBreakdown).filter((k) => !KNOWN.has(k)); if (unknownKeys.length === 0) return report; for (const key of unknownKeys) { report.modelBreakdown[targetKey] += report.modelBreakdown[key] || 0; delete report.modelBreakdown[key]; } for (const day of report.dailyData) { let add = 0; for (const key of unknownKeys) { if (day.models[key]) { add += day.models[key]; delete day.models[key]; } } if (add > 0) { day.models[targetKey] = (day.models[targetKey] || 0) + add; } } return report; } catch (e) { console.warn("[monitor] Failed to merge unknown models for HTML:", e); return report; } } function exportWeeklyAnalysis() { const report = mergeUnknownModelsForHtml(generateWeeklyReport()); const sortedModelKeys = MODEL_DISPLAY_ORDER.filter((modelKey) => report.modelBreakdown[modelKey]).concat(Object.keys(report.modelBreakdown).filter((key) => !MODEL_DISPLAY_ORDER.includes(key))); const htmlContent = `