// ==UserScript== // @name Team 助手 // @namespace https://github.com/zjm54321/chatgpt-scripts // @version v2026.09.08-1 // @description 官方下期账单读取、用量额度统计与席位历史提醒;不修改额度或计费设置。 // @author zjm54321 // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @match https://pay.openai.com/* // @grant none // @run-at document-start // @noframes // @homepageURL https://github.com/zjm54321/chatgpt-scripts // @supportURL https://github.com/zjm54321/chatgpt-scripts/issues // @downloadURL https://raw.githubusercontent.com/zjm54321/chatgpt-scripts/main/scripts/team-assistant.user.js // @updateURL https://raw.githubusercontent.com/zjm54321/chatgpt-scripts/main/scripts/team-assistant.user.js // ==/UserScript== (() => { "use strict"; if(location.origin==='https://pay.openai.com') { if(window.top!==window.self || window.__CHATGPT_TEAM_BILLING_READER__) return; window.__CHATGPT_TEAM_BILLING_READER__=true; runOfficialBillingReader(); return; } if(location.origin!=='https://chatgpt.com' && location.origin!=='https://chat.openai.com') return; if(window.__CHATGPT_TEAM_ASSISTANT__)return;window.__CHATGPT_TEAM_ASSISTANT__=true; function createSeatHistoryMonitor(onChange, onPolicyNotice) { 'use strict'; var HISTORY_KEY = 'codex:chatgpt-vacancy-monitor:v1'; var MEMBERS_KEY = 'codex:chatgpt-vacancy-monitor:members:v1'; var LOCK_NAME = 'codex:chatgpt-vacancy-monitor:write'; var own = Object.prototype.hasOwnProperty; var win = typeof window === 'object' ? window : null; var doc = win && win.document; var destroyed = false; var storageError = null; var fallbackAccountId = null; var activeAccountId = null; var writeQueue = Promise.resolve(); var latestMembersRequest = 0; var intervalId = null; var pendingXhrs = new Set(); var xhrStates = new WeakMap(); var nativeFetch = null; var fetchWrapper = null; var xhrPrototype = null; var nativeXhrOpen = null; var nativeXhrSend = null; var xhrOpenWrapper = null; var xhrSendWrapper = null; function has(object, key) { return object !== null && typeof object === 'object' && own.call(object, key); } function isRecord(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } function defineOwn(object, key, value) { Object.defineProperty(object, key, { configurable: true, enumerable: true, value: value, writable: true }); } function safePrimitive(value) { if (value === null) return null; var type = typeof value; return type === 'string' || type === 'number' || type === 'boolean' ? value : null; } function ownPrimitive(object, key) { return has(object, key) ? safePrimitive(object[key]) : null; } function noteReadFailure() { if (storageError === null) storageError = '本地历史不可用'; } function noteSaveFailure() { storageError = '本地历史未能保存'; } function readStoredObject(key) { try { var raw = win.localStorage.getItem(key); if (raw === null) return { available: true, value: {} }; var parsed = JSON.parse(raw); if (isRecord(parsed)) return { available: true, value: parsed }; noteReadFailure(); return { available: false, value: {} }; } catch (_) { noteReadFailure(); return { available: false, value: {} }; } } function saveStoredObject(key, value) { try { win.localStorage.setItem(key, JSON.stringify(value)); storageError = null; return true; } catch (_) { noteSaveFailure(); return false; } } function stripQuotes(value) { if (value.length >= 2 && ((value[0] === '"' && value[value.length - 1] === '"') || (value[0] === "'" && value[value.length - 1] === "'"))) { return value.slice(1, -1); } return value; } // Deliberately inspect only the _account cookie's value. function accountFromCookie() { if (!doc) return null; var cookies; try { cookies = doc.cookie; } catch (_) { return null; } var parts = cookies.split(';'); for (var index = 0; index < parts.length; index += 1) { var part = parts[index]; var equals = part.indexOf('='); var name = (equals < 0 ? part : part.slice(0, equals)).trim(); if (name !== '_account') continue; var value = (equals < 0 ? '' : part.slice(equals + 1)).trim(); value = stripQuotes(value); try { value = decodeURIComponent(value); } catch (_) { return null; } value = stripQuotes(value).trim(); return value ? value : null; } return null; } function resolvedAccountId() { return accountFromCookie() || fallbackAccountId || null; } function refreshAccountId() { var next = resolvedAccountId(); var changed = next !== activeAccountId; activeAccountId = next; return changed; } function cleanHistory(history) { var result = []; if (!Array.isArray(history)) return result; for (var index = 0; index < history.length; index += 1) { var item = history[index]; if (!isRecord(item) || !has(item, 'capturedAt') || typeof item.capturedAt !== 'string') continue; result.push({ capturedAt: item.capturedAt, vacancyOrdinal: ownPrimitive(item, 'vacancyOrdinal'), freeVacancyThreshold: ownPrimitive(item, 'freeVacancyThreshold'), billingStartsAt: ownPrimitive(item, 'billingStartsAt'), expiresAt: ownPrimitive(item, 'expiresAt') }); } return result; } function snapshot() { refreshAccountId(); var accountId = activeAccountId; var records = readStoredObject(HISTORY_KEY).value; var aliases = readStoredObject(MEMBERS_KEY).value; var accountAliases = accountId && has(aliases, accountId) && isRecord(aliases[accountId]) ? aliases[accountId] : null; var entries = []; if (accountId) { var keys = Object.keys(records); for (var index = 0; index < keys.length; index += 1) { var record = records[keys[index]]; if (!isRecord(record) || !has(record, 'accountId') || !has(record, 'userId') || !has(record, 'history') || record.accountId !== accountId || typeof record.userId !== 'string' || !Array.isArray(record.history)) continue; var email = accountAliases && has(accountAliases, record.userId) && typeof accountAliases[record.userId] === 'string' ? accountAliases[record.userId] : null; var history = cleanHistory(record.history); if (!history.length) continue; entries.push({ accountId: accountId, userId: record.userId, email: email, history: history }); } } entries.sort(function (left, right) { var leftHistory = left.history; var rightHistory = right.history; var leftAt = leftHistory.length ? leftHistory[leftHistory.length - 1].capturedAt : ''; var rightAt = rightHistory.length ? rightHistory[rightHistory.length - 1].capturedAt : ''; if (leftAt !== rightAt) return leftAt < rightAt ? 1 : -1; return left.userId < right.userId ? -1 : left.userId > right.userId ? 1 : 0; }); return { accountId: accountId, entries: entries, storageError: storageError }; } function notify() { if (destroyed || typeof onChange !== 'function') return; try { onChange(snapshot()); } catch (_) { // The host callback is never allowed to affect page behavior. } } function notifyPolicy(accountId, userId, capturedAt, values) { if (destroyed || typeof onPolicyNotice !== 'function') return; refreshAccountId(); if (activeAccountId !== accountId) return; try { onPolicyNotice({ accountId: accountId, userId: userId, capturedAt: capturedAt, policy: { vacancyOrdinal: values.vacancyOrdinal, freeVacancyThreshold: values.freeVacancyThreshold, billingStartsAt: values.billingStartsAt, expiresAt: values.expiresAt }, saved: true }); } catch (_) { // The host callback is never allowed to affect page behavior. } } function serialize(task) { function run() { var locks = win && win.navigator && win.navigator.locks; if (!locks || typeof locks.request !== 'function') return task(); try { return locks.request(LOCK_NAME, { mode: 'exclusive' }, task); } catch (_) { return task(); } } var next = writeQueue.then(run, run); writeQueue = next.catch(function () {}); return next; } function samePolicy(left, right) { return left && Object.is(left.vacancyOrdinal, right.vacancyOrdinal) && Object.is(left.freeVacancyThreshold, right.freeVacancyThreshold) && Object.is(left.billingStartsAt, right.billingStartsAt) && Object.is(left.expiresAt, right.expiresAt); } function appendPolicy(accountId, userId, values, shouldNotifyPolicy) { return serialize(function () { if (destroyed) return { ok: false, changed: false }; var stored = readStoredObject(HISTORY_KEY); if (!stored.available) { noteSaveFailure(); return { ok: false, changed: false }; } var records = stored.value; var key = accountId + '/' + userId; var previous = has(records, key) && isRecord(records[key]) ? records[key] : null; var history = previous && has(previous, 'history') && Array.isArray(previous.history) ? previous.history.slice() : []; var last = history.length && isRecord(history[history.length - 1]) ? history[history.length - 1] : null; var lastValues = last && { vacancyOrdinal: ownPrimitive(last, 'vacancyOrdinal'), freeVacancyThreshold: ownPrimitive(last, 'freeVacancyThreshold'), billingStartsAt: ownPrimitive(last, 'billingStartsAt'), expiresAt: ownPrimitive(last, 'expiresAt') }; if (samePolicy(lastValues, values)) return { ok: true, changed: false }; var capturedAt = new Date().toISOString(); history.push({ capturedAt: capturedAt, vacancyOrdinal: values.vacancyOrdinal, freeVacancyThreshold: values.freeVacancyThreshold, billingStartsAt: values.billingStartsAt, expiresAt: values.expiresAt }); defineOwn(records, key, { accountId: accountId, userId: userId, history: history }); return { ok: saveStoredObject(HISTORY_KEY, records), changed: true, capturedAt: capturedAt }; }).then(function (result) { if (!result.ok) notify(); else if (result.changed) { notify(); if (shouldNotifyPolicy && !destroyed) notifyPolicy(accountId, userId, result.capturedAt, values); } return result.ok; }, function () { noteSaveFailure(); notify(); return false; }); } function updateAliases(accountId, pairs) { if (!pairs.length) return Promise.resolve(true); return serialize(function () { if (destroyed) return { ok: false, changed: false }; var stored = readStoredObject(MEMBERS_KEY); if (!stored.available) { noteSaveFailure(); return { ok: false, changed: false }; } var members = stored.value; var aliases = has(members, accountId) && isRecord(members[accountId]) ? members[accountId] : {}; var changed = !has(members, accountId) || members[accountId] !== aliases; defineOwn(members, accountId, aliases); for (var index = 0; index < pairs.length; index += 1) { var pair = pairs[index]; if (!has(aliases, pair.userId) || !Object.is(aliases[pair.userId], pair.email)) { defineOwn(aliases, pair.userId, pair.email); changed = true; } } if (!changed) return { ok: true, changed: false }; return { ok: saveStoredObject(MEMBERS_KEY, members), changed: true }; }).then(function (result) { if (!result.ok || result.changed) notify(); return result.ok; }, function () { noteSaveFailure(); notify(); return false; }); } function clearHistoryFor(accountId) { return serialize(function () { if (destroyed) return { ok: false, changed: false }; var stored = readStoredObject(HISTORY_KEY); if (!stored.available) { noteSaveFailure(); return { ok: false, changed: false }; } var records = stored.value; var prefix = accountId + '/'; var changed = false; Object.keys(records).forEach(function (key) { var record = records[key]; if (key.indexOf(prefix) === 0 && isRecord(record) && has(record, 'accountId') && record.accountId === accountId) { delete records[key]; changed = true; } }); if (!changed) return { ok: true, changed: false }; return { ok: saveStoredObject(HISTORY_KEY, records), changed: true }; }).then(function (result) { if (!result.ok || result.changed) notify(); return result.ok; }, function () { noteSaveFailure(); notify(); return false; }); } function policyValues(payload) { if (!isRecord(payload) || !has(payload, 'policy_notice')) return null; var notice = payload.policy_notice; if (notice === null) { return { vacancyOrdinal: null, freeVacancyThreshold: null, billingStartsAt: null, expiresAt: null }; } if (!isRecord(notice) || !has(notice, 'vacancy_ordinal') || !has(notice, 'free_vacancy_threshold')) return null; var vacancyOrdinal = policyNumber(notice.vacancy_ordinal); var freeVacancyThreshold = policyNumber(notice.free_vacancy_threshold); var billingStartsAt = has(notice, 'billing_starts_at') ? policyDate(notice.billing_starts_at) : { value: null }; var expiresAt = has(notice, 'expires_at') ? policyDate(notice.expires_at) : { value: null }; if (!vacancyOrdinal || !freeVacancyThreshold || !billingStartsAt || !expiresAt) return null; return { vacancyOrdinal: vacancyOrdinal.value, freeVacancyThreshold: freeVacancyThreshold.value, billingStartsAt: billingStartsAt.value, expiresAt: expiresAt.value }; } function policyNumber(value) { if (value === null || (typeof value === 'number' && isFinite(value))) return { value: value }; if (typeof value === 'string' && value.trim() !== '' && isFinite(Number(value))) { return { value: value }; } return null; } function policyDate(value) { if (value === null || typeof value === 'string' || (typeof value === 'number' && isFinite(value))) return { value: value }; return null; } function decodeSegment(value) { try { var decoded = decodeURIComponent(value); return decoded && decoded.indexOf('/') < 0 ? decoded : null; } catch (_) { return null; } } function makeTarget(method, inputUrl) { if (!win || (method !== 'GET' && method !== 'DELETE')) return null; try { var url = new win.URL(inputUrl, win.location.href); if (url.origin !== win.location.origin) return null; var membersMatch = /^\/backend-api\/accounts\/([^/]+)\/users\/?$/.exec(url.pathname); if (method === 'GET' && membersMatch) { var membersAccount = decodeSegment(membersMatch[1]); if (!membersAccount) return null; return { accountId: membersAccount, kind: 'members' }; } var deleteMatch = /^\/backend-api\/accounts\/([^/]+)\/users\/([^/]+)\/?$/.exec(url.pathname); if (method === 'DELETE' && deleteMatch) { var accountId = decodeSegment(deleteMatch[1]); var userId = decodeSegment(deleteMatch[2]); if (!accountId || !userId || userId.indexOf('user-') !== 0) return null; return { accountId: accountId, kind: 'delete', userId: userId }; } } catch (_) { // URL inspection is best-effort and must not affect the request. } return null; } function beginMembersRequest(target) { if (!target || target.kind !== 'members') return target; latestMembersRequest += 1; var cookieAccountId = accountFromCookie(); var started = { accountId: target.accountId, accountAtStart: cookieAccountId, kind: 'members', requestNumber: latestMembersRequest }; if (!cookieAccountId && fallbackAccountId !== started.accountId) { fallbackAccountId = started.accountId; if (refreshAccountId()) notify(); } return started; } function memberPairs(payload) { if (!isRecord(payload) || !Array.isArray(payload.items)) return null; var pairs = []; payload.items.forEach(function (item) { if (!isRecord(item) || !has(item, 'email') || typeof item.email !== 'string') return; var email = item.email.trim(); if (!email) return; [has(item, 'id') ? item.id : null, has(item, 'account_user_id') ? item.account_user_id : null] .forEach(function (candidate) { if (typeof candidate !== 'string' || !candidate) return; pairs.push({ email: email, userId: candidate }); }); }); return pairs; } function consumeMembers(target, payload) { var pairs = memberPairs(payload); if (pairs === null) return; if (refreshAccountId()) notify(); updateAliases(target.accountId, pairs); } function consumePayload(target, payload) { if (destroyed || !target) return; if (target.kind === 'members') { consumeMembers(target, payload); return; } var values = policyValues(payload); if (values) appendPolicy(target.accountId, target.userId, values, payload.policy_notice !== null); } function fetchTarget(args) { var input = args[0]; var init = args[1]; var url = null; if (typeof input === 'string') url = input; else if (win.URL && input instanceof win.URL) url = input.href; else if (input && typeof input.url === 'string') url = input.url; if (url === null) return null; var method = 'GET'; if (init !== null && (typeof init === 'object' || typeof init === 'function') && init.method !== null && init.method !== undefined) { method = String(init.method); } else if (input && typeof input.method === 'string') { method = input.method; } return makeTarget(method.toUpperCase(), url); } function observeFetch(target, response) { if (destroyed) return; try { if (!response || response.status < 200 || response.status >= 300) return; var cloned = response.clone(); Promise.resolve(cloned.json()).then(function (payload) { consumePayload(target, payload); }, function () {}).catch(function () {}); } catch (_) { // A response clone or JSON parse failure is intentionally invisible to the page. } } function installFetchHook() { if (!win || typeof win.fetch !== 'function') return; nativeFetch = win.fetch; fetchWrapper = function () { if (destroyed) return nativeFetch.apply(this, arguments); var target = null; try { target = fetchTarget(arguments); } catch (_) {} var returned = nativeFetch.apply(this, arguments); try { target = beginMembersRequest(target); } catch (_) { target = null; } if (target) { try { Promise.resolve(returned).then(function (response) { observeFetch(target, response); }, function () {}).catch(function () {}); } catch (_) {} } return returned; }; try { win.fetch = fetchWrapper; } catch (_) { fetchWrapper = null; } } function removeXhrState(xhr, state) { var actual = state || xhrStates.get(xhr); if (!actual) return; if (actual.listener) { try { xhr.removeEventListener('loadend', actual.listener); } catch (_) {} } if (xhrStates.get(xhr) === actual) xhrStates.delete(xhr); pendingXhrs.delete(xhr); } function observeXhr(xhr, state) { removeXhrState(xhr, state); if (destroyed) return; try { if (xhr.status < 200 || xhr.status >= 300) return; var responseType = xhr.responseType || ''; if (responseType !== '' && responseType !== 'text' && responseType !== 'json') return; var payload = responseType === 'json' ? xhr.response : JSON.parse(xhr.responseText); consumePayload(state.target, payload); } catch (_) { // XHR response access and parsing are best-effort only. } } function installXhrHook() { if (!win || !win.XMLHttpRequest || !win.XMLHttpRequest.prototype) return; xhrPrototype = win.XMLHttpRequest.prototype; nativeXhrOpen = xhrPrototype.open; nativeXhrSend = xhrPrototype.send; if (typeof nativeXhrOpen !== 'function' || typeof nativeXhrSend !== 'function') return; xhrOpenWrapper = function () { var xhr = this; if (destroyed) return nativeXhrOpen.apply(xhr, arguments); removeXhrState(xhr); var target = null; try { target = makeTarget(String(arguments[0]).toUpperCase(), arguments[1]); } catch (_) {} var returned = nativeXhrOpen.apply(xhr, arguments); if (target && !destroyed) { try { var state = { listener: null, target: target }; xhrStates.set(xhr, state); } catch (_) {} } return returned; }; xhrSendWrapper = function () { var xhr = this; var state = xhrStates.get(xhr); if (!state || destroyed) return nativeXhrSend.apply(xhr, arguments); removeXhrState(xhr, state); xhrStates.set(xhr, state); try { state.target = beginMembersRequest(state.target); } catch (_) { removeXhrState(xhr, state); return nativeXhrSend.apply(xhr, arguments); } state.listener = function () { observeXhr(xhr, state); }; try { xhr.addEventListener('loadend', state.listener); pendingXhrs.add(xhr); } catch (_) { state.listener = null; } try { return nativeXhrSend.apply(xhr, arguments); } catch (error) { removeXhrState(xhr, state); throw error; } }; var openInstalled = false; var sendInstalled = false; try { xhrPrototype.open = xhrOpenWrapper; openInstalled = xhrPrototype.open === xhrOpenWrapper; } catch (_) {} try { xhrPrototype.send = xhrSendWrapper; sendInstalled = xhrPrototype.send === xhrSendWrapper; } catch (_) {} if (!openInstalled || !sendInstalled) { if (openInstalled && xhrPrototype.open === xhrOpenWrapper) { try { xhrPrototype.open = nativeXhrOpen; } catch (_) {} } if (sendInstalled && xhrPrototype.send === xhrSendWrapper) { try { xhrPrototype.send = nativeXhrSend; } catch (_) {} } xhrOpenWrapper = null; xhrSendWrapper = null; } } function isMembersPath() { try { return /(?:^|\/)members(?:\/|$)/i.test(win.location.pathname); } catch (_) { return false; } } function refreshOnAccountEvent() { if (refreshAccountId()) notify(); } function onStorage(event) { if (!event || event.key === null || event.key === HISTORY_KEY || event.key === MEMBERS_KEY) { refreshAccountId(); notify(); } } if (!win || !doc) { return { getSnapshot: function () { return { accountId: null, entries: [], storageError: '本地历史不可用' }; }, clearCurrentHistory: function () { return Promise.resolve(false); }, destroy: function () {} }; } refreshAccountId(); installFetchHook(); installXhrHook(); win.addEventListener('focus', refreshOnAccountEvent); win.addEventListener('pageshow', refreshOnAccountEvent); win.addEventListener('popstate', refreshOnAccountEvent); win.addEventListener('storage', onStorage); intervalId = win.setInterval(function () { if (isMembersPath()) refreshOnAccountEvent(); }, 1000); notify(); return { getSnapshot: function () { return snapshot(); }, clearCurrentHistory: function () { var accountId = snapshot().accountId; return accountId ? clearHistoryFor(accountId) : Promise.resolve(false); }, destroy: function () { if (destroyed) return; destroyed = true; win.removeEventListener('focus', refreshOnAccountEvent); win.removeEventListener('pageshow', refreshOnAccountEvent); win.removeEventListener('popstate', refreshOnAccountEvent); win.removeEventListener('storage', onStorage); if (intervalId !== null) win.clearInterval(intervalId); pendingXhrs.forEach(function (xhr) { removeXhrState(xhr); }); if (fetchWrapper && win.fetch === fetchWrapper) { try { win.fetch = nativeFetch; } catch (_) {} } if (xhrPrototype && xhrOpenWrapper && xhrPrototype.open === xhrOpenWrapper) { try { xhrPrototype.open = nativeXhrOpen; } catch (_) {} } if (xhrPrototype && xhrSendWrapper && xhrPrototype.send === xhrSendWrapper) { try { xhrPrototype.send = nativeXhrSend; } catch (_) {} } } }; } function mountSeatHistoryPanel(options) { 'use strict'; options = options || {}; var currentSnapshot = { accountId: null, entries: [], storageError: null }; var isOpen = false; var isClearing = false; var clearError = null; var destroyed = false; var lastTop = -1; var lastRight = -1; var rafId = null; var lastBodySignature = null; var lastRenderedAccountId = null; var bodyDirty = true; // Clean up stale instances if any exist var oldHosts = document.querySelectorAll('[data-seat-history-btn]'); for (var h = 0; h < oldHosts.length; h++) { oldHosts[h].remove(); } var oldPanelHosts = document.querySelectorAll('[data-seat-history-panel]'); for (var p = 0; p < oldPanelHosts.length; p++) { oldPanelHosts[p].remove(); } var ICONS = { contacts: '', close: '' }; var THEME_CSS = ` :host { --cg-sh-bg-popover: #ffffff; --cg-sh-bg-secondary: #f7f7f8; --cg-sh-bg-hover: rgba(0, 0, 0, 0.05); --cg-sh-bg-active: rgba(0, 0, 0, 0.08); --cg-sh-border: rgba(0, 0, 0, 0.12); --cg-sh-border-secondary: rgba(0, 0, 0, 0.06); --cg-sh-text-primary: #0d0d0d; --cg-sh-text-secondary: #5d5d5d; --cg-sh-text-muted: #8e8e8e; --cg-sh-focus-ring: #0d0d0d; --cg-sh-tag-bg: rgba(0, 0, 0, 0.04); --cg-sh-shadow: 0 10px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.06); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } :host([data-theme="dark"]) { --cg-sh-bg-popover: #212121; --cg-sh-bg-secondary: #2f2f2f; --cg-sh-bg-hover: rgba(255, 255, 255, 0.08); --cg-sh-bg-active: rgba(255, 255, 255, 0.12); --cg-sh-border: rgba(255, 255, 255, 0.15); --cg-sh-border-secondary: rgba(255, 255, 255, 0.08); --cg-sh-text-primary: #ececec; --cg-sh-text-secondary: #b4b4b4; --cg-sh-text-muted: #737373; --cg-sh-focus-ring: #ececec; --cg-sh-tag-bg: rgba(255, 255, 255, 0.06); --cg-sh-shadow: 0 10px 25px -5px rgba(0,0,0,0.5), 0 8px 10px -6px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.1); } `; function el(tag, cls, text, parent, attrs) { var node = document.createElement(tag); if (cls) node.className = cls; if (text !== undefined && text !== null) node.textContent = text; if (attrs) { for (var k in attrs) { if (k === 'hidden') node.hidden = Boolean(attrs[k]); else if (k === 'disabled') node.disabled = Boolean(attrs[k]); else node.setAttribute(k, attrs[k]); } } if (parent) parent.appendChild(node); return node; } // 1. Button Host inside native header action group var btnHost = document.createElement('div'); btnHost.id = '__chatgpt_team_seat_history_btn_host__'; btnHost.setAttribute('data-seat-history-btn', ''); btnHost.style.display = 'none'; var btnShadow = btnHost.attachShadow({ mode: 'open' }); var btnStyle = document.createElement('style'); btnStyle.textContent = THEME_CSS + ` :host { display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; position: relative; box-sizing: border-box; flex-shrink: 0; line-height: 0; margin: 0; padding: 0; } .cg-sh-btn { width: 36px; height: 36px; min-width: 36px; min-height: 36px; padding: 0; margin: 0; box-sizing: border-box; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; border: 1px solid var(--border-light, var(--cg-sh-border)); background: transparent; color: var(--text-secondary, var(--cg-sh-text-secondary)); cursor: pointer; outline: none; user-select: none; -webkit-user-select: none; transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.1s ease; } .cg-sh-btn:hover { background-color: var(--main-surface-secondary, var(--cg-sh-bg-hover)); color: var(--text-primary, var(--cg-sh-text-primary)); border-color: var(--border-light, var(--cg-sh-border)); } .cg-sh-btn:active { background-color: var(--cg-sh-bg-active); transform: scale(0.96); } .cg-sh-btn:focus-visible { outline: 2px solid var(--cg-sh-focus-ring); outline-offset: 2px; } .cg-sh-btn[data-open="true"] { background-color: var(--main-surface-secondary, var(--cg-sh-bg-hover)); color: var(--text-primary, var(--cg-sh-text-primary)); border-color: var(--border-light, var(--cg-sh-border)); } .cg-sh-btn[hidden] { display: none !important; } .cg-sh-icon-slot { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; line-height: 0; color: currentColor; } .cg-sh-icon-slot svg { display: block; width: 18px; height: 18px; } @media (pointer: coarse) { .cg-sh-btn { min-width: 44px; min-height: 44px; } } @media (prefers-reduced-motion: reduce) { .cg-sh-btn { transition: none !important; } }`; btnShadow.appendChild(btnStyle); var btn = el('button', 'cg-sh-btn', null, btnShadow, { type: 'button', id: 'cg-sh-trigger-btn', 'aria-label': '席位阈值历史', title: '席位阈值历史', 'aria-haspopup': 'dialog', 'aria-expanded': 'false', hidden: true }); var iconSlot = el('span', 'cg-sh-icon-slot', null, btn); iconSlot.innerHTML = ICONS.contacts; // 2. Portalled Popover Host in document.body var panelHost = document.createElement('div'); panelHost.id = '__chatgpt_team_seat_history_panel_host__'; panelHost.setAttribute('data-seat-history-panel', ''); (document.body || document.documentElement).appendChild(panelHost); var panelShadow = panelHost.attachShadow({ mode: 'open' }); var panelStyle = document.createElement('style'); panelStyle.textContent = THEME_CSS + ` :host { position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; pointer-events: none; } .cg-sh-popover { position: fixed; pointer-events: auto; width: 440px; max-width: calc(100vw - 24px); box-sizing: border-box; background: var(--main-surface-primary, var(--cg-sh-bg-popover)); border: 1px solid var(--border-light, var(--cg-sh-border)); border-radius: 12px; box-shadow: var(--cg-sh-shadow); color: var(--text-primary, var(--cg-sh-text-primary)); display: flex; flex-direction: column; user-select: none; overflow: hidden; } .cg-sh-popover[hidden] { display: none !important; } @keyframes cg-sh-enter { from { opacity: 0; transform: translateY(-4px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } .cg-sh-popover:not([hidden]) { animation: cg-sh-enter 0.15s cubic-bezier(0.16, 1, 0.3, 1) forwards; } .cg-sh-header { padding: 12px 14px 10px; border-bottom: 1px solid var(--border-light, var(--cg-sh-border-secondary)); display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; } .cg-sh-header-main { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .cg-sh-title-group { display: flex; align-items: center; gap: 8px; } .cg-sh-title { font-size: 14px; font-weight: 600; color: var(--text-primary, var(--cg-sh-text-primary)); line-height: 1.2; } .cg-sh-count-badge { font-size: 11px; font-weight: 500; padding: 2px 6px; border-radius: 4px; background: var(--main-surface-secondary, var(--cg-sh-bg-secondary)); color: var(--text-secondary, var(--cg-sh-text-secondary)); border: 1px solid var(--border-light, var(--cg-sh-border-secondary)); font-variant-numeric: tabular-nums; } .cg-sh-actions { display: flex; align-items: center; gap: 6px; } .cg-sh-clear-btn { font-size: 11px; padding: 3px 8px; border-radius: 6px; border: 1px solid var(--border-light, var(--cg-sh-border-secondary)); background: transparent; color: var(--text-secondary, var(--cg-sh-text-secondary)); cursor: pointer; outline: none; user-select: none; transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; } .cg-sh-clear-btn:hover:not(:disabled) { background-color: rgba(239, 68, 68, 0.08); color: #ef4444; border-color: rgba(239, 68, 68, 0.2); } .cg-sh-clear-btn:active:not(:disabled) { transform: scale(0.96); } .cg-sh-clear-btn:focus-visible { outline: 2px solid var(--cg-sh-focus-ring); outline-offset: 1px; } .cg-sh-clear-btn:disabled { opacity: 0.45; cursor: not-allowed; } .cg-sh-close-btn { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 6px; border: none; background: transparent; color: var(--text-secondary, var(--cg-sh-text-muted)); cursor: pointer; padding: 0; outline: none; transition: background-color 0.15s ease, color 0.15s ease; } .cg-sh-close-btn:hover { background-color: var(--main-surface-secondary, var(--cg-sh-bg-hover)); color: var(--text-primary, var(--cg-sh-text-primary)); } .cg-sh-close-btn:focus-visible { outline: 2px solid var(--cg-sh-focus-ring); outline-offset: 1px; } .cg-sh-workspace-bar { display: flex; align-items: center; gap: 6px; font-size: 11px; } .cg-sh-ws-label { color: var(--text-secondary, var(--cg-sh-text-muted)); flex-shrink: 0; } .cg-sh-ws-id { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 11px; color: var(--text-primary, var(--cg-sh-text-secondary)); background: var(--main-surface-secondary, var(--cg-sh-bg-secondary)); padding: 1px 6px; border-radius: 4px; border: 1px solid var(--border-light, var(--cg-sh-border-secondary)); user-select: text; -webkit-user-select: text; cursor: text; max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cg-sh-error-bar { font-size: 11px; color: #ef4444; background: rgba(239, 68, 68, 0.08); border: 1px solid rgba(239, 68, 68, 0.2); border-radius: 6px; padding: 4px 8px; line-height: 1.3; } .cg-sh-body { padding: 12px 14px; flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; } .cg-sh-body::-webkit-scrollbar { width: 6px; } .cg-sh-body::-webkit-scrollbar-thumb { background: rgba(128, 128, 128, 0.25); border-radius: 3px; } .cg-sh-body::-webkit-scrollbar-thumb:hover { background: rgba(128, 128, 128, 0.4); } .cg-sh-empty { padding: 24px 12px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 6px; } .cg-sh-empty-title { font-size: 13px; font-weight: 500; color: var(--text-primary, var(--cg-sh-text-primary)); } .cg-sh-empty-sub { font-size: 11px; color: var(--text-secondary, var(--cg-sh-text-muted)); line-height: 1.4; } .cg-sh-member-card { background: var(--main-surface-secondary, var(--cg-sh-bg-secondary)); border: 1px solid var(--border-light, var(--cg-sh-border-secondary)); border-radius: 8px; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; } .cg-sh-member-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; } .cg-sh-member-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; } .cg-sh-member-email { font-size: 13px; font-weight: 600; color: var(--text-primary, var(--cg-sh-text-primary)); word-break: break-all; user-select: text; -webkit-user-select: text; cursor: text; } .cg-sh-member-email.pending { color: var(--text-secondary, var(--cg-sh-text-muted)); font-weight: normal; } .cg-sh-member-id { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 11px; color: var(--text-secondary, var(--cg-sh-text-muted)); word-break: break-all; user-select: text; -webkit-user-select: text; cursor: text; } .cg-sh-history-badge { font-size: 10.5px; color: var(--text-secondary, var(--cg-sh-text-muted)); flex-shrink: 0; padding: 1px 5px; border-radius: 4px; background: var(--cg-sh-tag-bg); font-variant-numeric: tabular-nums; } .cg-sh-history-list { display: flex; flex-direction: column; gap: 6px; } .cg-sh-history-item { background: var(--main-surface-primary, var(--cg-sh-bg-popover)); border: 1px solid var(--border-light, var(--cg-sh-border-secondary)); border-radius: 6px; padding: 8px 10px; display: flex; flex-direction: column; gap: 6px; } .cg-sh-item-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 11px; } .cg-sh-item-time { color: var(--text-secondary, var(--cg-sh-text-muted)); font-variant-numeric: tabular-nums; } .cg-sh-null-tag { font-size: 10px; padding: 1px 5px; border-radius: 3px; background: var(--cg-sh-tag-bg); color: var(--text-secondary, var(--cg-sh-text-muted)); } .cg-sh-null-desc { font-size: 11px; color: var(--text-secondary, var(--cg-sh-text-muted)); line-height: 1.4; } .cg-sh-fields-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px 12px; } .cg-sh-field { display: flex; flex-direction: column; gap: 1px; } .cg-sh-field-label { font-size: 10px; color: var(--text-secondary, var(--cg-sh-text-muted)); } .cg-sh-field-val { font-size: 12px; font-weight: 500; color: var(--text-primary, var(--cg-sh-text-primary)); font-variant-numeric: tabular-nums; word-break: break-all; user-select: text; -webkit-user-select: text; } .cg-sh-field-val.null-val { color: var(--text-secondary, var(--cg-sh-text-muted)); font-weight: normal; } .cg-sh-footer { padding: 8px 14px; font-size: 10.5px; color: var(--text-secondary, var(--cg-sh-text-muted)); border-top: 1px solid var(--border-light, var(--cg-sh-border-secondary)); background: var(--main-surface-primary, var(--cg-sh-bg-popover)); line-height: 1.4; flex-shrink: 0; } @media (prefers-reduced-motion: reduce) { .cg-sh-popover:not([hidden]) { animation: none !important; } .cg-sh-clear-btn, .cg-sh-close-btn { transition: none !important; } }`; panelShadow.appendChild(panelStyle); // Build popover DOM var popover = el('div', 'cg-sh-popover', null, panelShadow, { id: 'cg-sh-popover', role: 'dialog', 'aria-labelledby': 'cg-sh-title', 'aria-modal': 'false', hidden: true }); if ('ariaControlsElements' in btn) { try { btn.ariaControlsElements = [popover]; } catch (_) {} } var headerEl = el('div', 'cg-sh-header', null, popover); var headerMain = el('div', 'cg-sh-header-main', null, headerEl); var titleGroup = el('div', 'cg-sh-title-group', null, headerMain); var titleEl = el('span', 'cg-sh-title', '席位阈值历史', titleGroup, { id: 'cg-sh-title' }); var countBadge = el('span', 'cg-sh-count-badge', '0 位成员', titleGroup); var actionsEl = el('div', 'cg-sh-actions', null, headerMain); var clearBtn = el('button', 'cg-sh-clear-btn', '清空当前记录', actionsEl, { type: 'button', title: '清空当前工作空间的历史记录', disabled: true }); var closeBtn = el('button', 'cg-sh-close-btn', null, actionsEl, { type: 'button', 'aria-label': '关闭', title: '关闭' }); closeBtn.innerHTML = ICONS.close; var wsBar = el('div', 'cg-sh-workspace-bar', null, headerEl); el('span', 'cg-sh-ws-label', '工作空间', wsBar); var wsIdEl = el('span', 'cg-sh-ws-id', '未识别', wsBar); var errorBar = el('div', 'cg-sh-error-bar', null, headerEl, { role: 'alert', 'aria-live': 'polite', hidden: true }); var bodyEl = el('div', 'cg-sh-body', null, popover); el('div', 'cg-sh-footer', '仅记录页面操作返回的变化,历史保存在本浏览器。', popover); // Helpers function isMembersRoute() { try { return /^\/admin\/members(?:\/|$)/i.test(window.location.pathname || ''); } catch (_) { return false; } } function isElementVisible(node) { if (!node || !node.isConnected) return false; if (typeof node.checkVisibility === 'function') { try { if (!node.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) return false; } catch (_) { if (!node.checkVisibility()) return false; } } var r = node.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) return false; try { var s = window.getComputedStyle(node); if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return false; } catch (_) {} return r.bottom > 0 && r.top < window.innerHeight && r.right > 0 && r.left < window.innerWidth; } function abbreviateId(id) { if (!id || typeof id !== 'string') return '未识别'; return id.length <= 16 ? id : id.slice(0, 8) + '…' + id.slice(-6); } function formatDateTime(val) { if (val === undefined || val === null || val === '') return '--'; try { var d = new Date(val); if (!isNaN(d.getTime())) { var p = function (n) { return String(n).padStart(2, '0'); }; return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()); } } catch (_) {} return String(val); } function formatFieldValue(val, isDate) { if (val === undefined || val === '') return '--'; if (val === null) return 'null'; return isDate ? formatDateTime(val) : String(val); } // Theme Management var darkMedia = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; function getExplicitTheme(node) { if (!node) return null; var dt = node.getAttribute('data-theme'); if (dt === 'dark' || dt === 'light') return dt; if (node.classList.contains('dark')) return 'dark'; if (node.classList.contains('light')) return 'light'; return null; } function updateTheme() { var theme = getExplicitTheme(document.documentElement) || getExplicitTheme(document.body) || (darkMedia && darkMedia.matches ? 'dark' : 'light'); btnHost.setAttribute('data-theme', theme); panelHost.setAttribute('data-theme', theme); } updateTheme(); if (darkMedia) darkMedia.addEventListener('change', updateTheme); var themeObserver = new MutationObserver(updateTheme); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] }); if (document.body) { themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] }); } // Toolbar anchor search: locate visible native invite button in members view function locateAnchor() { if (!isMembersRoute()) return null; var buttons = document.querySelectorAll('button'); var fallbackMatch = null; for (var i = 0; i < buttons.length; i++) { var b = buttons[i]; if (b === btn) continue; if (btnHost.contains(b) || panelHost.contains(b)) continue; if (b.closest('nav, aside, table, article, dialog, [role="dialog"]')) continue; if (!isElementVisible(b)) continue; var text = (b.textContent || '').trim().replace(/\s+/g, ' '); var isInvite = (text.indexOf('邀请') >= 0 && text.indexOf('成员') >= 0) || (text.indexOf('邀請') >= 0 && text.indexOf('成員') >= 0) || /invite\s+members?/i.test(text); if (!isInvite) continue; var parent = b.parentElement; if (!parent || parent === document.body || parent === document.documentElement) continue; var pStyle = null; try { pStyle = window.getComputedStyle(parent); } catch (_) {} var isFlex = (pStyle && (pStyle.display === 'flex' || pStyle.display === 'inline-flex')) || (parent.className && parent.className.indexOf('flex') >= 0); if (!isFlex) continue; var hasSiblingAction = false; for (var s = 0; s < parent.children.length; s++) { var sib = parent.children[s]; if (sib === b || sib === btnHost) continue; if (sib.tagName === 'BUTTON' || (sib.getAttribute && sib.getAttribute('role') === 'button')) { if (isElementVisible(sib)) { hasSiblingAction = true; break; } } } var match = { inviteButton: b, container: parent }; if (hasSiblingAction) { return match; } if (!fallbackMatch) { fallbackMatch = match; } } return fallbackMatch; } // Toggle Popover function togglePopover(force, returnFocus) { if (destroyed) return; var next = typeof force === 'boolean' ? force : !isOpen; if (isOpen === next) return; isOpen = next; btn.setAttribute('aria-expanded', String(next)); btn.setAttribute('data-open', String(next)); iconSlot.innerHTML = ICONS.contacts; btn.setAttribute('aria-label', next ? '关闭席位阈值历史' : '席位阈值历史'); btn.setAttribute('title', next ? '关闭席位阈值历史' : '席位阈值历史'); if (next) { if (typeof options.getSnapshot === 'function') { try { var fresh = options.getSnapshot(); if (fresh && typeof fresh === 'object') { currentSnapshot = fresh; } } catch (_) {} } popover.hidden = false; render(); updatePosition(); try { closeBtn.focus(); } catch (_) {} } else { popover.hidden = true; if (returnFocus && !btn.hidden && btn.isConnected) { try { btn.focus(); } catch (_) {} } } } // Event Handlers function onBtnClick(e) { e.stopPropagation(); togglePopover(); } function onCloseClick(e) { e.stopPropagation(); togglePopover(false, true); } function onPointerDown(e) { if (!isOpen) return; var path = e.composedPath ? e.composedPath() : []; if (!path.includes(btnHost) && !path.includes(panelHost)) togglePopover(false, false); } function onKeyDown(e) { if (e.key === 'Escape' && isOpen) { e.stopPropagation(); togglePopover(false, true); } } function onClearClick(e) { e.stopPropagation(); if (destroyed || isClearing) return; var targetAccountId = currentSnapshot.accountId; if (!targetAccountId) return; var confirmed = window.confirm('确定要清空当前工作空间的历史记录吗?\n其他工作空间的历史和邮箱映射将保留。'); if (!confirmed || destroyed) return; // Verify fresh snapshot.accountId unchanged before clearing if (typeof options.getSnapshot === 'function') { try { var freshBefore = options.getSnapshot(); if (freshBefore && typeof freshBefore === 'object') { currentSnapshot = freshBefore; } } catch (_) {} } if (currentSnapshot.accountId !== targetAccountId) { render(); return; } isClearing = true; clearError = null; clearBtn.disabled = true; clearBtn.textContent = '清空中…'; Promise.resolve().then(function () { if (typeof options.onClearCurrent === 'function') { return options.onClearCurrent(); } return false; }).then(function (res) { if (destroyed) return; if (res === false) { clearError = '清空失败,请稍后重试'; return; } if (typeof options.getSnapshot === 'function') { try { var fresh = options.getSnapshot(); if (fresh && typeof fresh === 'object') currentSnapshot = fresh; } catch (_) {} } }).catch(function (err) { if (destroyed) return; clearError = (err && err.message) ? err.message : '清空失败,请稍后重试'; }).finally(function () { if (destroyed) return; isClearing = false; clearBtn.textContent = '清空当前记录'; render(); }); } btn.addEventListener('click', onBtnClick); closeBtn.addEventListener('click', onCloseClick); clearBtn.addEventListener('click', onClearClick); document.addEventListener('pointerdown', onPointerDown, { passive: true }); window.addEventListener('keydown', onKeyDown); // Positioning function updatePosition() { if (destroyed) return; var anchor = locateAnchor(); if (!anchor) { btnHost.style.display = 'none'; btn.hidden = true; if (isOpen) togglePopover(false, false); lastTop = -1; lastRight = -1; return; } var inviteButton = anchor.inviteButton; var container = anchor.container; if (inviteButton.previousElementSibling !== btnHost) { container.insertBefore(btnHost, inviteButton); } btnHost.style.display = ''; btn.hidden = false; if (isOpen) { var btnRect = btn.getBoundingClientRect(); if (!isElementVisible(btnHost) || btnRect.width <= 0 || btnRect.height <= 0 || btnRect.bottom <= 0 || btnRect.top >= window.innerHeight || btnRect.right <= 0 || btnRect.left >= window.innerWidth) { togglePopover(false, false); return; } var popWidth = Math.min(440, Math.max(0, window.innerWidth - 24)); var right = window.innerWidth - btnRect.right; var maxRight = Math.max(12, window.innerWidth - popWidth - 12); var clampedRight = Math.min(Math.max(right, 12), maxRight); var spaceBelow = window.innerHeight - btnRect.bottom - 16; var spaceAbove = btnRect.top - 16; var flipAbove = spaceBelow < 180 && spaceAbove > spaceBelow; var top = -1; var bottom = -1; var maxPopHeight = 0; if (flipAbove) { bottom = window.innerHeight - btnRect.top + 8; maxPopHeight = Math.min(560, Math.max(80, spaceAbove - 8)); popover.style.top = 'auto'; popover.style.bottom = bottom + 'px'; } else { top = btnRect.bottom + 8; maxPopHeight = Math.min(560, Math.max(80, spaceBelow - 8)); popover.style.bottom = 'auto'; popover.style.top = top + 'px'; } popover.style.right = clampedRight + 'px'; popover.style.maxHeight = maxPopHeight + 'px'; lastTop = flipAbove ? -bottom : top; lastRight = clampedRight; } } function schedulePositionUpdate() { if (destroyed || rafId) return; rafId = requestAnimationFrame(function () { rafId = null; if (destroyed) return; updatePosition(); }); } window.addEventListener('resize', schedulePositionUpdate, { passive: true }); window.addEventListener('scroll', schedulePositionUpdate, { capture: true, passive: true }); window.addEventListener('popstate', schedulePositionUpdate, { passive: true }); var domObserver = new MutationObserver(function (mutations) { for (var i = 0; i < mutations.length; i++) { var m = mutations[i]; if (m.target === btnHost || m.target === panelHost || btnHost.contains(m.target) || panelHost.contains(m.target)) { continue; } schedulePositionUpdate(); break; } }); domObserver.observe(document.body || document.documentElement, { childList: true, subtree: true }); var pollIntervalId = setInterval(schedulePositionUpdate, 1200); // Rendering function renderField(label, value, isNull, parent, title) { var field = el('div', 'cg-sh-field', null, parent); var labelEl = el('span', 'cg-sh-field-label', label, field); if (title) { labelEl.title = title; } el('span', 'cg-sh-field-val' + (isNull ? ' null-val' : ''), value, field); return field; } function render() { if (destroyed) return; var accountId = currentSnapshot.accountId; var entries = Array.isArray(currentSnapshot.entries) ? currentSnapshot.entries : []; var storageError = currentSnapshot.storageError; countBadge.textContent = entries.length + ' 位成员'; wsIdEl.textContent = abbreviateId(accountId); wsIdEl.title = accountId || '等待识别工作空间'; clearBtn.disabled = isClearing || !accountId || entries.length === 0; var displayError = clearError || storageError; if (displayError) { errorBar.textContent = displayError; errorBar.hidden = false; } else { errorBar.textContent = ''; errorBar.hidden = true; } var currentSignature = ''; try { currentSignature = JSON.stringify({ a: accountId, e: entries }); } catch (_) { currentSignature = String(accountId) + ':' + entries.length; } if (currentSignature !== lastBodySignature) { bodyDirty = true; } if (!isOpen) { return; } if (!bodyDirty && currentSignature === lastBodySignature) { return; } var prevScroll = (accountId && accountId === lastRenderedAccountId) ? bodyEl.scrollTop : 0; while (bodyEl.firstChild) { bodyEl.removeChild(bodyEl.firstChild); } if (!accountId) { var emptyWaited = el('div', 'cg-sh-empty', null, bodyEl); el('span', 'cg-sh-empty-title', '等待识别工作空间', emptyWaited); el('span', 'cg-sh-empty-sub', '仅记录页面操作返回的变化,历史保存在本浏览器。', emptyWaited); lastBodySignature = currentSignature; lastRenderedAccountId = accountId; bodyDirty = false; return; } if (entries.length === 0) { var emptyNone = el('div', 'cg-sh-empty', null, bodyEl); el('span', 'cg-sh-empty-title', '当前工作空间暂无记录', emptyNone); el('span', 'cg-sh-empty-sub', '仅记录页面操作返回的变化,历史保存在本浏览器。', emptyNone); lastBodySignature = currentSignature; lastRenderedAccountId = accountId; bodyDirty = false; return; } for (var eIndex = 0; eIndex < entries.length; eIndex++) { var entry = entries[eIndex]; var card = el('div', 'cg-sh-member-card', null, bodyEl); var cardHeader = el('div', 'cg-sh-member-header', null, card); var meta = el('div', 'cg-sh-member-meta', null, cardHeader); el('div', entry.email ? 'cg-sh-member-email' : 'cg-sh-member-email pending', entry.email || '邮箱待匹配', meta); el('div', 'cg-sh-member-id', entry.userId || '--', meta); var historyItems = Array.isArray(entry.history) ? entry.history : []; el('span', 'cg-sh-history-badge', historyItems.length + ' 条记录', cardHeader); var listEl = el('div', 'cg-sh-history-list', null, card); // entry history is oldest -> newest, display newest first for (var hIndex = historyItems.length - 1; hIndex >= 0; hIndex--) { var item = historyItems[hIndex]; var itemCard = el('div', 'cg-sh-history-item', null, listEl); var itemHdr = el('div', 'cg-sh-item-header', null, itemCard); el('span', 'cg-sh-item-time', '记录时间:' + formatDateTime(item.capturedAt), itemHdr); var isAllNoticeNull = (item.vacancyOrdinal == null) && (item.freeVacancyThreshold == null) && (item.billingStartsAt == null) && (item.expiresAt == null); if (isAllNoticeNull) { el('span', 'cg-sh-null-tag', '空策略字段 (null)', itemHdr); el('div', 'cg-sh-null-desc', '接口返回空策略字段(null),无法据此判断计费情况。', itemCard); } var grid = el('div', 'cg-sh-fields-grid', null, itemCard); renderField('vacancy_ordinal', formatFieldValue(item.vacancyOrdinal, false), item.vacancyOrdinal === null, grid); renderField('free_vacancy_threshold', formatFieldValue(item.freeVacancyThreshold, false), item.freeVacancyThreshold === null, grid); renderField('开始时间', formatFieldValue(item.billingStartsAt, true), item.billingStartsAt === null, grid, 'billing_starts_at'); renderField('结束时间', formatFieldValue(item.expiresAt, true), item.expiresAt === null, grid, 'expires_at'); } } lastBodySignature = currentSignature; lastRenderedAccountId = accountId; bodyDirty = false; if (prevScroll > 0) { bodyEl.scrollTop = prevScroll; } } render(); schedulePositionUpdate(); return { update: function (nextSnapshot) { if (destroyed || !nextSnapshot || typeof nextSnapshot !== 'object') return; var accountChanged = nextSnapshot.accountId !== currentSnapshot.accountId; currentSnapshot = nextSnapshot; clearError = null; if (accountChanged && isOpen) { togglePopover(false, false); } render(); schedulePositionUpdate(); }, destroy: function () { if (destroyed) return; destroyed = true; if (pollIntervalId) clearInterval(pollIntervalId); if (rafId) cancelAnimationFrame(rafId); domObserver.disconnect(); themeObserver.disconnect(); if (darkMedia) darkMedia.removeEventListener('change', updateTheme); btn.removeEventListener('click', onBtnClick); closeBtn.removeEventListener('click', onCloseClick); clearBtn.removeEventListener('click', onClearClick); document.removeEventListener('pointerdown', onPointerDown); window.removeEventListener('keydown', onKeyDown); window.removeEventListener('resize', schedulePositionUpdate); window.removeEventListener('scroll', schedulePositionUpdate, { capture: true }); window.removeEventListener('popstate', schedulePositionUpdate); btnHost.remove(); panelHost.remove(); } }; } function mountSeatPolicyToast(options) { 'use strict'; options = options || {}; var destroyed = false; var isVisible = false; var isHovered = false; var isFocused = false; var burstCount = 0; var hideTimeoutId = null; var hideFallbackTimer = null; var seenKeys = []; var HIDE_DELAY = 6000; // Clean up any existing stale toast host instances var oldHosts = document.querySelectorAll('[data-seat-toast]'); for (var h = 0; h < oldHosts.length; h++) { oldHosts[h].remove(); } var ICONS = { close: '' }; var THEME_CSS = ` :host { --cg-st-bg: #ffffff; --cg-st-bg-hover: rgba(0, 0, 0, 0.05); --cg-st-border: rgba(0, 0, 0, 0.12); --cg-st-text-primary: #0d0d0d; --cg-st-text-secondary: #5d5d5d; --cg-st-text-muted: #8e8e8e; --cg-st-tag-bg: rgba(0, 0, 0, 0.05); --cg-st-shadow: 0 10px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.06); --cg-st-focus-ring: #0d0d0d; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: fixed; bottom: 24px; right: 24px; z-index: 2147483646; pointer-events: none; } :host([data-theme="dark"]) { --cg-st-bg: #212121; --cg-st-bg-hover: rgba(255, 255, 255, 0.08); --cg-st-border: rgba(255, 255, 255, 0.15); --cg-st-text-primary: #ececec; --cg-st-text-secondary: #b4b4b4; --cg-st-text-muted: #737373; --cg-st-tag-bg: rgba(255, 255, 255, 0.06); --cg-st-shadow: 0 10px 25px -5px rgba(0,0,0,0.5), 0 8px 10px -6px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.1); --cg-st-focus-ring: #ececec; } .cg-st-card { pointer-events: auto; width: 320px; max-width: calc(100vw - 32px); box-sizing: border-box; background: var(--main-surface-primary, var(--cg-st-bg)); border: 1px solid var(--border-light, var(--cg-st-border)); border-radius: 10px; box-shadow: var(--cg-st-shadow); color: var(--text-primary, var(--cg-st-text-primary)); padding: 12px 14px; display: flex; flex-direction: column; gap: 6px; opacity: 0; transform: translateY(8px); transition: opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1), transform 0.2s cubic-bezier(0.16, 1, 0.3, 1); user-select: none; } .cg-st-card.is-visible { opacity: 1; transform: translateY(0); } .cg-st-card[hidden] { display: none !important; } @media (prefers-reduced-motion: reduce) { .cg-st-card { transition: none !important; } .cg-st-close-btn { transition: none !important; } } .cg-st-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .cg-st-title-group { display: flex; align-items: center; gap: 6px; min-width: 0; } .cg-st-title { font-size: 13px; font-weight: 600; color: var(--text-primary, var(--cg-st-text-primary)); line-height: 1.3; } .cg-st-burst-badge { font-size: 10px; font-weight: 500; padding: 1px 5px; border-radius: 4px; background: var(--cg-st-tag-bg); color: var(--text-secondary, var(--cg-st-text-secondary)); font-variant-numeric: tabular-nums; } .cg-st-close-btn { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; min-width: 36px; min-height: 36px; border-radius: 6px; border: none; background: transparent; color: var(--text-secondary, var(--cg-st-text-muted)); cursor: pointer; padding: 0; outline: none; transition: background-color 0.15s ease, color 0.15s ease; flex-shrink: 0; } .cg-st-close-btn:hover { background-color: var(--main-surface-secondary, var(--cg-st-bg-hover)); color: var(--text-primary, var(--cg-st-text-primary)); } .cg-st-close-btn:focus-visible { outline: 2px solid var(--cg-st-focus-ring); outline-offset: 1px; } @media (pointer: coarse) { .cg-st-close-btn { width: 44px; height: 44px; min-width: 44px; min-height: 44px; } } .cg-st-body { font-size: 12px; color: var(--text-secondary, var(--cg-st-text-secondary)); line-height: 1.4; word-break: break-word; } .cg-st-meta-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; } .cg-st-pill { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; font-variant-numeric: tabular-nums; padding: 2px 6px; border-radius: 4px; background: var(--cg-st-tag-bg); color: var(--text-secondary, var(--cg-st-text-secondary)); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cg-st-pill-k { font-weight: 400; opacity: 0.85; } .cg-st-pill-v { font-weight: 600; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; } `; function el(tag, cls, text, parent, attrs) { var node = document.createElement(tag); if (cls) node.className = cls; if (text !== undefined && text !== null) node.textContent = text; if (attrs) { for (var k in attrs) { if (k === 'hidden') node.hidden = Boolean(attrs[k]); else if (k === 'disabled') node.disabled = Boolean(attrs[k]); else node.setAttribute(k, attrs[k]); } } if (parent) parent.appendChild(node); return node; } var toastHost = document.createElement('div'); toastHost.id = '__chatgpt_team_seat_toast_host__'; toastHost.setAttribute('data-seat-toast', ''); (document.body || document.documentElement).appendChild(toastHost); var shadow = toastHost.attachShadow({ mode: 'open' }); var styleNode = document.createElement('style'); styleNode.textContent = THEME_CSS; shadow.appendChild(styleNode); var card = el('div', 'cg-st-card', null, shadow, { role: 'status', 'aria-live': 'polite', hidden: true }); var header = el('div', 'cg-st-header', null, card); var titleGroup = el('div', 'cg-st-title-group', null, header); var titleEl = el('span', 'cg-st-title', '检测到席位策略变化', titleGroup); var burstBadge = el('span', 'cg-st-burst-badge', null, titleGroup, { hidden: true }); var closeBtn = el('button', 'cg-st-close-btn', null, header, { type: 'button', 'aria-label': '关闭提示', title: '关闭提示' }); closeBtn.innerHTML = ICONS.close; var bodyText = el('div', 'cg-st-body', '已记录当前工作空间的新席位策略。', card); var metaRow = el('div', 'cg-st-meta-row', null, card); // Theme Management var darkMedia = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; function getExplicitTheme(node) { if (!node) return null; var dt = node.getAttribute('data-theme'); if (dt === 'dark' || dt === 'light') return dt; if (node.classList.contains('dark')) return 'dark'; if (node.classList.contains('light')) return 'light'; return null; } function updateTheme() { if (destroyed) return; var theme = getExplicitTheme(document.documentElement) || getExplicitTheme(document.body) || (darkMedia && darkMedia.matches ? 'dark' : 'light'); toastHost.setAttribute('data-theme', theme); } updateTheme(); if (darkMedia) darkMedia.addEventListener('change', updateTheme); var themeObserver = new MutationObserver(updateTheme); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] }); if (document.body) { themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] }); } // Auto-hide timing & Pause logic function isCardHovered() { try { return card.matches(':hover'); } catch (_) { return isHovered; } } function isCardFocused() { try { return card.contains(shadow.activeElement); } catch (_) { return isFocused; } } function shouldPause() { return isCardHovered() || isCardFocused(); } function finishHide() { if (hideFallbackTimer) { clearTimeout(hideFallbackTimer); hideFallbackTimer = null; } if (!isVisible) { card.hidden = true; while (metaRow.firstChild) { metaRow.removeChild(metaRow.firstChild); } } } function hideToast() { if (destroyed || !isVisible) return; isVisible = false; burstCount = 0; clearTimeout(hideTimeoutId); hideTimeoutId = null; card.classList.remove('is-visible'); var isReducedMotion = Boolean(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); if (isReducedMotion) { finishHide(); } else { if (hideFallbackTimer) clearTimeout(hideFallbackTimer); hideFallbackTimer = setTimeout(finishHide, 220); } } function startAutoHide() { if (destroyed || !isVisible || shouldPause()) return; clearTimeout(hideTimeoutId); hideTimeoutId = setTimeout(function () { if (!destroyed && !shouldPause()) { hideToast(); } }, HIDE_DELAY); } function pauseAutoHide() { clearTimeout(hideTimeoutId); hideTimeoutId = null; } card.addEventListener('pointerenter', function () { isHovered = true; pauseAutoHide(); }); card.addEventListener('pointerleave', function () { isHovered = false; if (!shouldPause() && isVisible) { startAutoHide(); } }); card.addEventListener('focusin', function () { isFocused = true; pauseAutoHide(); }); card.addEventListener('focusout', function () { Promise.resolve().then(function () { if (destroyed) return; isFocused = isCardFocused(); if (!shouldPause() && isVisible) { startAutoHide(); } }); }); card.addEventListener('transitionend', function (e) { if (e.target === card && !isVisible) { finishHide(); } }); closeBtn.addEventListener('click', function (e) { e.stopPropagation(); isHovered = false; isFocused = false; try { closeBtn.blur(); } catch (_) {} hideToast(); }); function makeKey(ev) { if (!ev || typeof ev !== 'object') return ''; var pol = ev.policy || {}; return [ ev.accountId || '', ev.userId || '', ev.capturedAt || '', pol.vacancyOrdinal != null ? pol.vacancyOrdinal : '', pol.freeVacancyThreshold != null ? pol.freeVacancyThreshold : '', pol.billingStartsAt || '', pol.expiresAt || '' ].join('|'); } function show(event) { if (destroyed || !event || typeof event !== 'object') return; if (event.saved !== true || typeof event.accountId !== 'string' || !event.accountId) return; // Verify event matches current account if options.getAccountId / options.currentAccountId is available var getAccFn = typeof options.getAccountId === 'function' ? options.getAccountId : typeof options.currentAccountId === 'function' ? options.currentAccountId : null; if (getAccFn) { try { var currentAcc = getAccFn(); if (typeof currentAcc !== 'string' || !currentAcc || currentAcc !== event.accountId) { return; } } catch (_) { return; } } // Deduplication: defensively deduplicate identical 4-tuples within small bounded memory var key = makeKey(event); if (key && seenKeys.indexOf(key) >= 0) { return; } if (key) { seenKeys.push(key); if (seenKeys.length > 50) { seenKeys.shift(); } } if (hideFallbackTimer) { clearTimeout(hideFallbackTimer); hideFallbackTimer = null; } var policy = event.policy || {}; // Update body content and metadata pills bodyText.textContent = '已记录当前工作空间的新席位策略。'; while (metaRow.firstChild) { metaRow.removeChild(metaRow.firstChild); } if (policy.vacancyOrdinal !== undefined && policy.vacancyOrdinal !== null) { var pill1 = el('span', 'cg-st-pill', null, metaRow); el('span', 'cg-st-pill-k', 'vacancy_ordinal:', pill1); el('span', 'cg-st-pill-v', String(policy.vacancyOrdinal), pill1); } if (policy.freeVacancyThreshold !== undefined && policy.freeVacancyThreshold !== null) { var pill2 = el('span', 'cg-st-pill', null, metaRow); el('span', 'cg-st-pill-k', 'free_vacancy_threshold:', pill2); el('span', 'cg-st-pill-v', String(policy.freeVacancyThreshold), pill2); } // Burst handling: if already showing, increment badge if (isVisible) { burstCount++; burstBadge.textContent = '+' + burstCount; burstBadge.hidden = false; } else { burstCount = 0; burstBadge.textContent = ''; burstBadge.hidden = true; card.hidden = false; // Force reflow for enter transition void card.offsetWidth; card.classList.add('is-visible'); isVisible = true; } if (!shouldPause()) { startAutoHide(); } } function clear() { if (destroyed) return; isVisible = false; burstCount = 0; isHovered = false; isFocused = false; clearTimeout(hideTimeoutId); hideTimeoutId = null; if (hideFallbackTimer) { clearTimeout(hideFallbackTimer); hideFallbackTimer = null; } card.classList.remove('is-visible'); card.hidden = true; bodyText.textContent = ''; while (metaRow.firstChild) { metaRow.removeChild(metaRow.firstChild); } } function destroy() { if (destroyed) return; destroyed = true; clearTimeout(hideTimeoutId); hideTimeoutId = null; if (hideFallbackTimer) { clearTimeout(hideFallbackTimer); hideFallbackTimer = null; } if (themeObserver) themeObserver.disconnect(); if (darkMedia) darkMedia.removeEventListener('change', updateTheme); toastHost.remove(); } return { show: show, clear: clear, destroy: destroy }; } function startNoticeHiding() { const TARGET_HEADING = "工作区有成员达到使用上限"; const TARGET_DESC = "开启自动充值,系统会自动补充额度,避免今后再次中断。"; const TARGET_CTA = "开启自动充值"; const HIDE_ATTR = "data-chatgpt-team-hide"; const STYLE_ID = "__chatgpt_team_assistant_style__"; // Inject minimal scoped stylesheet once if (!document.getElementById(STYLE_ID)) { const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = `[${HIDE_ATTR}="true"] { display: none !important; }`; (document.head || document.documentElement).appendChild(style); } // Strip all whitespace for robust text comparison function norm(str) { return (str || "").replace(/\s+/g, "").trim(); } // Chat and user-content containers where notices never appear const EXCLUDE_CONTAINERS_SELECTOR = [ "[data-message-author-role]", "[data-testid^='conversation-turn']", "article", ".markdown", ".prose", "pre", "code", "textarea", "input", "[contenteditable]", "script", "style", "template", "noscript" ].join(","); function isUnderExcluded(el) { return el.closest(EXCLUDE_CONTAINERS_SELECTOR) !== null; } // Broad containers that must never be hidden as a unit function isBroadContainer(el) { if (!el || el === document.body || el === document.documentElement) return true; if (el.id === "root" || el.id === "__next") return true; const tag = el.tagName.toLowerCase(); if (tag === "body" || tag === "html" || tag === "main" || tag === "dialog" || tag === "nav" || tag === "form") { return true; } const role = el.getAttribute("role"); if (role === "dialog" || role === "main" || role === "application") { return true; } const cls = typeof el.className === "string" ? el.className : ""; if (cls.includes("@container/main") || cls.includes("scroll-root")) { return true; } return false; } const UNRELATED_CONTROLS_SELECTOR = [ "table", "nav", "form", "article", "[data-message-author-role]", "[data-testid^='conversation-turn']", "input", "select", "textarea", "[contenteditable]", "[role='switch']", "[role='checkbox']", "[role='combobox']" ].join(","); // Check if an ancestor contains only the notice texts (heading, desc, optional CTA) function isElementPureNotice(el) { if (!el || !el.isConnected || isBroadContainer(el)) return false; if (isUnderExcluded(el)) return false; // Reject containers holding interactive form elements, tables, navigation, or chat turns if (el.querySelector(UNRELATED_CONTROLS_SELECTOR)) { return false; } // Any action element inside must strictly match the expected CTA text const actionElements = el.querySelectorAll("button, a, [role='button']"); for (const action of actionElements) { if (norm(action.textContent) !== TARGET_CTA) { return false; } } const text = norm(el.textContent); if (!text || text.length > 500) return false; if (!text.includes(TARGET_HEADING) || !text.includes(TARGET_DESC)) return false; // Strip heading, description, and optional CTA let remaining = text.replace(TARGET_HEADING, "").replace(TARGET_DESC, ""); if (remaining.includes(TARGET_CTA)) { remaining = remaining.replace(TARGET_CTA, ""); } return remaining.length === 0; } // Find the outermost pure container for a matched heading function findOutermostNoticeContainer(headingEl) { let curr = headingEl.parentElement; let best = null; let depth = 0; while (curr && depth < 6) { if (isBroadContainer(curr)) break; if (isElementPureNotice(curr)) { best = curr; } else if (best) { // Once we reached a pure boundary and the next parent is no longer pure, // we have found the outermost notice container. break; } curr = curr.parentElement; depth++; } return best; } // Track active hidden elements to safely verify React re-use / unhiding const hiddenElements = new Set(); function markHidden(el) { if (!el) return; if (el.getAttribute(HIDE_ATTR) !== "true") { el.setAttribute(HIDE_ATTR, "true"); } hiddenElements.add(el); } function unmarkHidden(el) { if (!el) return; el.removeAttribute(HIDE_ATTR); hiddenElements.delete(el); } function verifyHiddenElements() { for (const el of hiddenElements) { if (!el.isConnected) { hiddenElements.delete(el); continue; } if (!isElementPureNotice(el)) { unmarkHidden(el); } } } function scan() { // Re-validate previously hidden elements in case React reused their nodes verifyHiddenElements(); // Search for potential heading elements const candidates = document.querySelectorAll("p, h1, h2, h3, h4, h5, h6, [role='heading'], div.font-semibold"); for (const el of candidates) { if (isUnderExcluded(el)) continue; const text = norm(el.textContent); if (text !== TARGET_HEADING) continue; const container = findOutermostNoticeContainer(el); if (container) { markHidden(container); } } } // Debounced observer for SPA navigation and DOM changes let debounceTimer = null; function scheduleScan() { if (debounceTimer) return; debounceTimer = setTimeout(() => { debounceTimer = null; scan(); }, 100); } // Run initial scan scan(); // Observe subtree additions/removals and characterData updates without self-triggering attribute loops const observer = new MutationObserver((mutations) => { for (const m of mutations) { const targetEl = m.target.nodeType === Node.ELEMENT_NODE ? m.target : m.target.parentElement; if (targetEl && targetEl.closest(EXCLUDE_CONTAINERS_SELECTOR)) { continue; } scheduleScan(); break; } }); observer.observe(document.body || document.documentElement, { childList: true, subtree: true, characterData: true }); } /* Headless, read-only usage controller. This fragment deliberately has no exports. */ function createTeamUsageController(onChange) { 'use strict'; var DAY_MS = 86400000; var WEEK_SECONDS = 7 * 24 * 60 * 60; var MAX_RANGE_DAYS = 366; var MAX_CYCLE_SECONDS = MAX_RANGE_DAYS * 24 * 60 * 60; var TOKEN_MAX_AGE_MS = 9 * 60 * 1000; var REQUEST_TIMEOUT_MS = 18000; var state = emptySnapshot(); var lastReady = null; var restoreAfterCancel = null; var inFlight = null; var token = null; var tokenExpiresAt = 0; var generation = 0; var destroyed = false; var lastAccount = readAccountId(); var syncingAccount = false; var accountWatch = null; /* User-supplied reference rates in USD per 1M tokens, except Astra's confirmed standard rates (2026-09-08). */ var REFERENCE_RATES = { 'gpt-5.6-sol': [5, 0.5, 30, 2.5], 'gpt-5.6-terra': [2, 0.2, 12, 2.5], 'gpt-5.6-luna': [0.2, 0.02, 1.2, 2.5], 'gpt-5.5': [5, 0.5, 30, 2.5], 'gpt-5.4': [2.5, 0.25, 15, 2], 'gpt-5.4-mini': [0.75, 0.075, 4.5, 2], // Official: https://developers.openai.com/api/docs/models/gpt-6-astra (2026-09-08). 'gpt-6-astra': [10, 1, 50, 2] }; var TOKEN_KEYS = [ 'text_total_tokens', 'total_text_tokens', 'uncached_text_input_tokens', 'cached_text_input_tokens', 'text_output_tokens', 'output_tokens', 'uncached_image_input_tokens', 'cached_image_input_tokens', 'image_input_tokens', 'image_output_tokens', 'output_image_tokens' ]; function emptySnapshot() { return { status: 'idle', error: null, accountId: null, accountMode: null, viewMode: 'personal', range: { startDate: '', endDate: '' }, quota: emptyQuota(), summary: emptySummary(), models: [], daily: [], clients: [], modelActivity: [], notices: [], updatedAt: null }; } function emptyQuota() { return { available: false, usedRatio: null, remaining: null, limit: null, resetAt: null, durationSeconds: null, cycleStartDate: null, cycleEndDate: null, estimatedFullCycleUsd: null, estimatedRemainingUsd: null }; } function emptySummary() { return { tokens: null, uncachedInputTokens: null, cachedInputTokens: null, outputTokens: null, turns: null, threads: null, credits: null, estimatedUsd: null, activeMembersPeak: null }; } function clone(value) { return JSON.parse(JSON.stringify(value)); } function emit() { if (destroyed || typeof onChange !== 'function') return; try { onChange(clone(state)); } catch (_) { /* A UI callback must not affect collection. */ } } function setState(next) { if (destroyed) return; state = next; emit(); } function addNotice(list, text) { if (list.indexOf(text) === -1) list.push(text); } function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } function finiteNumber(value) { if (typeof value === 'number') return isFinite(value) && value >= 0 ? value : null; if (typeof value !== 'string' || !value.trim()) return null; var parsed = Number(value); return isFinite(parsed) && parsed >= 0 ? parsed : null; } function finiteOrNull(value) { return typeof value === 'number' && isFinite(value) ? value : null; } function numericInfo(object, keys) { var present = false; if (!isObject(object)) return { present: false, value: null }; for (var i = 0; i < keys.length; i += 1) { if (!Object.prototype.hasOwnProperty.call(object, keys[i])) continue; present = true; var value = finiteNumber(object[keys[i]]); if (value !== null) return { present: true, value: value }; } return { present: present, value: null }; } function numberFrom(object, keys) { return numericInfo(object, keys).value; } function boundedNumberFrom(object, keys, maximum) { if (!isObject(object)) return null; for (var i = 0; i < keys.length; i += 1) { if (!Object.prototype.hasOwnProperty.call(object, keys[i])) continue; var value = finiteNumber(object[keys[i]]); if (value !== null && value <= maximum) return value; } return null; } function hasAny(object, keys) { if (!isObject(object)) return false; for (var i = 0; i < keys.length; i += 1) { if (Object.prototype.hasOwnProperty.call(object, keys[i])) return true; } return false; } function sumAll(values) { if (!values.length) return null; var total = 0; for (var i = 0; i < values.length; i += 1) { if (values[i] === null || typeof values[i] !== 'number' || !isFinite(values[i])) return null; total += values[i]; } return finiteOrNull(total); } function maxKnown(values) { var result = null; for (var i = 0; i < values.length; i += 1) { if (typeof values[i] === 'number' && isFinite(values[i]) && (result === null || values[i] > result)) result = values[i]; } return result; } function readAccountId() { if (typeof document === 'undefined' || typeof document.cookie !== 'string') return null; var match = /(?:^|;\s*)_account=([^;]*)/.exec(document.cookie); if (!match || !match[1]) return null; try { var value = decodeURIComponent(match[1]).trim(); if (value.length >= 2 && ((value[0] === '"' && value[value.length - 1] === '"') || (value[0] === "'" && value[value.length - 1] === "'"))) value = value.slice(1, -1).trim(); return value && value.length <= 160 && !/[\x00-\x1f\x7f@]/.test(value) ? value : null; } catch (_) { return null; } } function resetForAccount(current) { lastAccount = current; generation += 1; if (inFlight) inFlight.abort(); inFlight = null; token = null; tokenExpiresAt = 0; lastReady = null; restoreAfterCancel = null; state = emptySnapshot(); state.accountId = current; } function syncAccount() { if (destroyed || syncingAccount) return false; syncingAccount = true; try { var current = readAccountId(); if (current === lastAccount) return false; resetForAccount(current); emit(); return true; } finally { syncingAccount = false; } } function accountRefreshHandler() { if (destroyed) return; if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return; try { syncAccount(); } catch (_) { /* Lifecycle hooks are isolated. */ } } if (typeof window !== 'undefined' && window.addEventListener) { window.addEventListener('focus', accountRefreshHandler); if (typeof document !== 'undefined' && document.addEventListener) { document.addEventListener('visibilitychange', accountRefreshHandler); } } if (typeof setInterval === 'function') { accountWatch = setInterval(function () { if (destroyed || !(state.status === 'ready' || state.status === 'loading' || token)) return; try { syncAccount(); } catch (_) { /* Cookie-only watcher stays isolated. */ } }, 1000); } function staleError() { var error = new Error('工作区已切换'); error.stale = true; return error; } function requestError(status, message, noActiveWorkspace) { var error = new Error(message || '请求失败'); error.status = status; error.noActiveWorkspace = !!noActiveWorkspace; return error; } function abortError() { var error = requestError(0, '请求已取消'); error.aborted = true; return error; } function isCurrent(captured, mine, signal) { if (destroyed || (signal && signal.aborted)) return false; if (syncAccount()) return false; return lastAccount === captured && (mine === undefined || mine === generation) && !(signal && signal.aborted); } function assertCurrent(captured, mine, signal) { if (signal && signal.aborted) throw abortError(); if (!isCurrent(captured, mine, signal)) throw staleError(); } function utcDate(value) { if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null; var year = Number(value.slice(0, 4)); var month = Number(value.slice(5, 7)); var day = Number(value.slice(8, 10)); var date = new Date(Date.UTC(year, month - 1, day)); return isFinite(date.getTime()) && date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day ? date : null; } function dateString(date) { return date && isFinite(date.getTime()) ? date.toISOString().slice(0, 10) : null; } function todayUtc() { var now = new Date(); return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); } function lastDays(days) { var end = todayUtc(); return { startDate: dateString(new Date(end.getTime() - (days - 1) * DAY_MS)), endDate: dateString(end) }; } function defaultRange() { return lastDays(7); } function parseRange(startDate, endDate) { var start = utcDate(startDate); var end = utcDate(endDate); if (!start || !end || end < start || ((end - start) / DAY_MS) + 1 > MAX_RANGE_DAYS) return null; return { startDate: startDate, endDate: endDate }; } function explicitRange(options) { var hasStart = Object.prototype.hasOwnProperty.call(options, 'startDate'); var hasEnd = Object.prototype.hasOwnProperty.call(options, 'endDate'); if (!hasStart && !hasEnd) return { supplied: false, range: null }; if (hasStart && hasEnd && options.startDate === undefined && options.endDate === undefined) { return { supplied: false, range: null }; } if (!hasStart || !hasEnd) return { supplied: true, range: null }; return { supplied: true, range: parseRange(options.startDate, options.endDate) }; } function exclusiveEnd(range) { var end = utcDate(range.endDate); return end ? dateString(new Date(end.getTime() + DAY_MS)) : null; } function usageUrl(path, range, extra) { var query = new URLSearchParams(); var end = exclusiveEnd(range); if (!end) return path; query.set('start_date', range.startDate); query.set('end_date', end); query.set('group_by', 'day'); if (isObject(extra)) Object.keys(extra).forEach(function (key) { query.set(key, String(extra[key])); }); return path + '?' + query.toString(); } function dateFromUnknown(value) { if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2}))?$/.test(value)) return null; var date = value.slice(0, 10); if (!utcDate(date)) return null; if (value.length === 10) return date; return isFinite(new Date(value).getTime()) ? date : null; } function isoTime(value) { var normalized; if (typeof value === 'number') { if (!isFinite(value) || value < 0) return null; normalized = value; } else if (typeof value === 'string') { var text = value.trim(); if (!text) return null; if (/^\d+(?:\.\d+)?$/.test(text)) normalized = Number(text); else normalized = text; } else { return null; } if (typeof normalized === 'number') { if (!isFinite(normalized) || normalized < 0) return null; if (normalized < 100000000000) normalized *= 1000; } var date = new Date(normalized); return isFinite(date.getTime()) ? date.toISOString() : null; } function timeFrom(object, keys) { if (!isObject(object)) return null; for (var i = 0; i < keys.length; i += 1) { if (!Object.prototype.hasOwnProperty.call(object, keys[i])) continue; var time = isoTime(object[keys[i]]); if (time) return time; } return null; } function tokenExpiry(accessToken) { var expiry = Date.now() + TOKEN_MAX_AGE_MS; var parts = accessToken.split('.'); if (parts.length !== 3) return expiry; try { var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); if (payload && typeof payload.exp === 'number' && isFinite(payload.exp)) { expiry = Math.min(expiry, Math.max(Date.now(), payload.exp * 1000 - 10000)); } } catch (_) { /* The session access token need not be a JWT. */ } return expiry; } function statusMessage(status) { if (status === 401) return '会话已失效(HTTP 401)'; if (status === 403) return '没有查看此使用情况的权限(HTTP 403)'; if (status === 400) return '请求范围不可用(HTTP 400)'; if (status >= 500) return '服务暂时不可用(HTTP ' + status + ')'; return '使用情况请求失败(HTTP ' + status + ')'; } async function fetchJSON(url, signal, captured, mine, withAuthorization) { assertCurrent(captured, mine, signal); var controller = new AbortController(); var abort = function () { controller.abort(); }; if (signal.aborted) throw abortError(); signal.addEventListener('abort', abort, { once: true }); if (signal.aborted) { signal.removeEventListener('abort', abort); throw abortError(); } var timedOut = false; var timeout = setTimeout(function () { timedOut = true; controller.abort(); }, REQUEST_TIMEOUT_MS); try { var headers = { Accept: 'application/json' }; if (withAuthorization) headers.Authorization = 'Bearer ' + await getToken(signal, captured, mine, false); assertCurrent(captured, mine, signal); if (controller.signal.aborted) { if (timedOut) throw requestError(0, '请求超时,请重试'); throw abortError(); } var response = await fetch(url, { method: 'GET', headers: headers, credentials: 'include', cache: 'no-store', signal: controller.signal }); var text = await response.text(); var data = null; try { data = text ? JSON.parse(text) : null; } catch (_) { /* Error details are not exposed. */ } if (!response.ok) { var noActive = response.status === 400 && /no active workspace/i.test(text || ''); throw requestError(response.status, statusMessage(response.status), noActive); } if (!isObject(data) && !Array.isArray(data)) throw requestError(response.status, '服务返回了无效数据'); assertCurrent(captured, mine, signal); return data; } catch (error) { if (error && (error.stale || error.status || error.aborted)) throw error; if (signal.aborted) throw abortError(); if (timedOut) throw requestError(0, '请求超时,请重试'); if (controller.signal.aborted) throw abortError(); throw requestError(0, '网络请求失败'); } finally { clearTimeout(timeout); signal.removeEventListener('abort', abort); } } async function getToken(signal, captured, mine, force) { assertCurrent(captured, mine, signal); if (!force && token && Date.now() < tokenExpiresAt) return token; var result; try { result = await fetchJSON('/api/auth/session', signal, captured, mine, false); } catch (error) { if (error) error.sessionRequest = true; throw error; } assertCurrent(captured, mine, signal); var accessToken = isObject(result) && typeof result.accessToken === 'string' ? result.accessToken : isObject(result) && isObject(result.data) && typeof result.data.accessToken === 'string' ? result.data.accessToken : null; if (!accessToken) { throw requestError(0, '无法建立会话'); } token = accessToken; tokenExpiresAt = tokenExpiry(token); return token; } async function apiGet(url, signal, captured, mine) { try { return await fetchJSON(url, signal, captured, mine, true); } catch (error) { if (!error || error.status !== 401 || error.sessionRequest) throw error; token = null; tokenExpiresAt = 0; await getToken(signal, captured, mine, true); try { return await fetchJSON(url, signal, captured, mine, true); } catch (retryError) { if (retryError && retryError.status === 401) { token = null; tokenExpiresAt = 0; } throw retryError; } } } function quotaWindow(payload) { var roots = []; var keys = ['rate_limit', 'rateLimit', 'rate_limits', 'rateLimits', 'limits']; if (!isObject(payload)) return null; keys.forEach(function (key) { if (payload[key] !== undefined) roots.push(payload[key]); }); var primary = []; var secondary = []; var windowKeys = ['limit_window_seconds', 'window_seconds', 'duration_seconds', 'reset_at', 'resetAt', 'resets_at', 'end_at', 'window_end', 'limit', 'max', 'capacity', 'credit_limit', 'remaining', 'remaining_amount', 'remaining_credits', 'used_ratio', 'usedRatio', 'fraction_used', 'used_percent', 'usedPercent', 'used', 'usage', 'used_credits']; function looksLikeWindow(value) { return hasAny(value, windowKeys); } function add(value, isSecondary) { var target = isSecondary ? secondary : primary; if (Array.isArray(value)) { value.forEach(function (item) { if (isObject(item)) target.push({ raw: item, secondary: isSecondary }); }); } else if (isObject(value) && looksLikeWindow(value)) { target.push({ raw: value, secondary: isSecondary }); } } roots.forEach(function (root) { if (Array.isArray(root)) add(root, false); if (!isObject(root)) return; if (looksLikeWindow(root)) add(root, false); add(root.primary_window, false); add(root.primaryWindow, false); add(root.windows, false); add(root.secondary_window, true); add(root.secondaryWindow, true); }); var candidates = primary.concat(secondary); if (!candidates.length) return null; var best = null; var distance = Infinity; candidates.forEach(function (candidate) { var duration = numberFrom(candidate.raw, ['limit_window_seconds', 'window_seconds', 'duration_seconds']); if (duration !== null && duration > 0 && Math.abs(duration - WEEK_SECONDS) < distance) { best = candidate; distance = Math.abs(duration - WEEK_SECONDS); } }); if (best) return best; var primaryHasDuration = primary.some(function (candidate) { var duration = numberFrom(candidate.raw, ['limit_window_seconds', 'window_seconds', 'duration_seconds']); return duration !== null && duration > 0; }); return !primaryHasDuration && secondary.length ? secondary[0] : primary[0] || secondary[0]; } function normalizeQuota(payload, notices) { var picked = quotaWindow(payload); if (!picked) return emptyQuota(); var raw = picked.raw; var quota = emptyQuota(); quota.available = true; if (picked.secondary) addNotice(notices, '正在使用备用配额窗口。'); quota.durationSeconds = numberFrom(raw, ['limit_window_seconds', 'window_seconds', 'duration_seconds']); if (quota.durationSeconds !== null && quota.durationSeconds > MAX_CYCLE_SECONDS) quota.durationSeconds = null; quota.resetAt = timeFrom(raw, ['reset_at', 'resetAt', 'resets_at', 'end_at', 'window_end']); quota.limit = numberFrom(raw, ['limit', 'max', 'capacity', 'credit_limit']); quota.remaining = numberFrom(raw, ['remaining', 'remaining_amount', 'remaining_credits']); var ratioKeys = ['used_ratio', 'usedRatio', 'fraction_used']; var percentKeys = ['used_percent', 'usedPercent']; var ratio = boundedNumberFrom(raw, ratioKeys, 1); var ratioSpecified = hasAny(raw, ratioKeys); var percentSpecified = hasAny(raw, percentKeys); if (ratio === null && !ratioSpecified) { var percent = boundedNumberFrom(raw, percentKeys, 100); ratio = percent === null ? null : percent / 100; } if (ratio === null && !ratioSpecified && !percentSpecified && quota.limit !== null && quota.limit > 0) { var used = numberFrom(raw, ['used', 'usage', 'used_credits']); if (used !== null && used <= quota.limit) ratio = used / quota.limit; else if (quota.remaining !== null && quota.remaining <= quota.limit) ratio = 1 - quota.remaining / quota.limit; } quota.usedRatio = ratio !== null && ratio >= 0 && ratio <= 1 ? ratio : null; if (quota.limit !== null && quota.remaining !== null && quota.remaining > quota.limit) quota.remaining = null; if (quota.remaining === null && quota.limit !== null && quota.usedRatio !== null) { quota.remaining = finiteOrNull(quota.limit * (1 - quota.usedRatio)); } if (quota.resetAt && quota.durationSeconds !== null && quota.durationSeconds > 0 && quota.durationSeconds <= MAX_CYCLE_SECONDS) { var resetMs = new Date(quota.resetAt).getTime(); var start = dateString(new Date(resetMs - quota.durationSeconds * 1000)); var end = dateString(new Date(resetMs)); if (start && end) { quota.cycleStartDate = start; quota.cycleEndDate = end; } } if (!quota.cycleStartDate) addNotice(notices, '配额周期信息不完整,日期范围默认最近 7 天。'); return quota; } function extractRows(payload) { if (Array.isArray(payload)) return payload; if (!isObject(payload)) return []; var keys = ['data', 'items', 'results', 'daily', 'daily_usage', 'dailyWorkspaceUsageCounts', 'daily_workspace_usage_counts', 'workspace_usage_counts']; for (var i = 0; i < keys.length; i += 1) { var value = payload[keys[i]]; if (Array.isArray(value)) return value; if (!isObject(value)) continue; for (var j = 0; j < keys.length; j += 1) if (Array.isArray(value[keys[j]])) return value[keys[j]]; } return []; } function safeModelName(raw) { var value = raw && (raw.model || raw.model_id || raw.model_name || raw.name || raw.id); if (typeof value !== 'string') return 'unknown'; value = value.trim(); return value && value.length <= 100 && !/[\x00-\x1f\x7f]/.test(value) && !/@|:\/\//.test(value) ? value : 'unknown'; } function safeSpeed(raw) { var value = raw && raw.speed; return typeof value === 'string' && /^[a-z0-9._-]{1,32}$/i.test(value) ? value.toLowerCase() : 'standard'; } function pricingSpeed(name, speed) { return name.toLowerCase() === 'gpt-6-astra' && speed === 'priority' ? 'fast' : speed; } function hasTokenFields(raw) { return hasAny(raw, TOKEN_KEYS); } function rawMetrics(raw, isImageModel) { raw = isObject(raw) ? raw : {}; var textTotalInfo = numericInfo(raw, ['text_total_tokens', 'total_text_tokens']); var textUncachedInfo = numericInfo(raw, ['uncached_text_input_tokens']); var textCachedInfo = numericInfo(raw, ['cached_text_input_tokens']); var textOutputInfo = numericInfo(raw, ['text_output_tokens']); var genericOutputInfo = numericInfo(raw, ['output_tokens']); var imageUncachedInfo = numericInfo(raw, ['uncached_image_input_tokens']); var imageCachedInfo = numericInfo(raw, ['cached_image_input_tokens']); var imageInputInfo = numericInfo(raw, ['image_input_tokens']); var imageOutputInfo = numericInfo(raw, ['image_output_tokens', 'output_image_tokens']); var imageInfos = [imageUncachedInfo, imageCachedInfo, imageInputInfo, imageOutputInfo]; var imagePlaceholdersOnly = imageInfos.some(function (info) { return info.present; }) && imageInfos.every(function (info) { return !info.present || info.value === 0; }); var hasImage = !!isImageModel || (!imagePlaceholdersOnly && imageInfos.some(function (info) { return info.present; })); var textOutput = textOutputInfo.present ? textOutputInfo.value : (hasImage ? null : genericOutputInfo.value); var completeText = textUncachedInfo.value !== null && textCachedInfo.value !== null && textOutput !== null; var textTotal = textTotalInfo.present ? textTotalInfo.value : (completeText ? finiteOrNull(textUncachedInfo.value + textCachedInfo.value + textOutput) : null); var imageInputTotal = null; if (imageUncachedInfo.present) { imageInputTotal = imageUncachedInfo.value !== null && imageCachedInfo.value !== null ? finiteOrNull(imageUncachedInfo.value + imageCachedInfo.value) : null; } else if (imageInputInfo.present && !imageCachedInfo.present) { imageInputTotal = imageInputInfo.value; } var invalid = false; [textTotalInfo, textUncachedInfo, textCachedInfo, textOutputInfo, genericOutputInfo, imageUncachedInfo, imageCachedInfo, imageInputInfo, imageOutputInfo].forEach(function (info) { if (info.present && info.value === null) invalid = true; }); if (!hasImage && textTotalInfo.value !== null && completeText && textTotalInfo.value !== textUncachedInfo.value + textCachedInfo.value + textOutput) invalid = true; var ambiguousImage = hasImage && (imageInputTotal === null || imageOutputInfo.value === null || genericOutputInfo.present || (textOutput !== null && textOutput !== 0) || (imageUncachedInfo.present && imageCachedInfo.value === null) || (!imageUncachedInfo.present && imageCachedInfo.present)); var total = null; if (!hasImage) total = textTotal; else if (textTotal !== null && imageInputTotal !== null && imageOutputInfo.value !== null) { total = finiteOrNull(textTotal + imageInputTotal + imageOutputInfo.value); } else if (textTotal === null && completeText && imageInputTotal !== null && imageOutputInfo.value !== null) { total = finiteOrNull(textUncachedInfo.value + textCachedInfo.value + textOutput + imageInputTotal + imageOutputInfo.value); } else if (textUncachedInfo.value === null && textCachedInfo.value === null && textOutput === null && imageInputTotal !== null && imageOutputInfo.value !== null) { total = finiteOrNull(imageInputTotal + imageOutputInfo.value); } return { present: hasTokenFields(raw), invalid: invalid, hasImage: hasImage, incompleteImage: ambiguousImage, tokens: total, uncachedInputTokens: hasImage ? (textUncachedInfo.value !== null && imageUncachedInfo.value !== null ? finiteOrNull(textUncachedInfo.value + imageUncachedInfo.value) : null) : textUncachedInfo.value, cachedInputTokens: hasImage ? (textCachedInfo.value !== null && imageCachedInfo.value !== null ? finiteOrNull(textCachedInfo.value + imageCachedInfo.value) : null) : textCachedInfo.value, outputTokens: hasImage ? (textOutput !== null && imageOutputInfo.value !== null ? finiteOrNull(textOutput + imageOutputInfo.value) : null) : textOutput, textUncached: textUncachedInfo.value, textCached: textCachedInfo.value, textOutput: textOutput, imageUncached: imageUncachedInfo.value, imageCached: imageCachedInfo.value, imageOutput: imageOutputInfo.value }; } function priceFor(name, speed) { var key = name.toLowerCase(); speed = key === 'gpt-6-astra' && speed === 'priority' ? 'fast' : speed; if (key === 'codex-auto-review') key = 'gpt-5.6-luna'; if (key === 'gpt-image-2' || key === 'image2') return { image: true, family: key, fallback: false, multiplier: 1 }; if (Object.prototype.hasOwnProperty.call(REFERENCE_RATES, key)) { return { image: false, family: key, fallback: false, multiplier: speed === 'fast' ? REFERENCE_RATES[key][3] : 1 }; } return { image: false, family: 'gpt-5.5', fallback: true, multiplier: speed === 'fast' ? REFERENCE_RATES['gpt-5.5'][3] : 1 }; } function moneyFor(metrics, pricing) { if (metrics.invalid) return null; if (pricing.image) { if (metrics.incompleteImage || metrics.imageUncached === null || metrics.imageCached === null || metrics.imageOutput === null || (metrics.textUncached === null) !== (metrics.textCached === null)) return null; return finiteOrNull(((metrics.textUncached === null ? 0 : metrics.textUncached * 5 + metrics.textCached * 1.25) + metrics.imageUncached * 8 + metrics.imageCached * 2 + metrics.imageOutput * 30) / 1000000); } if (metrics.hasImage || metrics.textUncached === null || metrics.textCached === null || metrics.textOutput === null) return null; var rate = REFERENCE_RATES[pricing.family]; return finiteOrNull((metrics.textUncached * rate[0] + metrics.textCached * rate[1] + metrics.textOutput * rate[2]) * pricing.multiplier / 1000000); } function calculationFor(metrics, pricing, estimated) { if (estimated === null) return metrics.hasImage ? '图像或文本令牌明细不完整,未估算费用' : '文本令牌明细不完整,未估算费用'; if (pricing.image) { var text = metrics.textUncached === null ? '' : '文本:输入=' + metrics.textUncached + '×$5/M,缓存=' + metrics.textCached + '×$1.25/M;'; return text + '图像:输入=' + metrics.imageUncached + '×$8/M,缓存=' + metrics.imageCached + '×$2/M,输出=' + metrics.imageOutput + '×$30/M;按参考费率估算,非实际账单'; } var rate = REFERENCE_RATES[pricing.family]; if (pricing.family === 'gpt-6-astra') { return '文本:输入=' + metrics.textUncached + '×$10/M,缓存=' + metrics.textCached + '×$1/M,输出=' + metrics.textOutput + '×$50/M;GPT-6 Astra 官网标准短上下文费率,快速×2(如适用);超过 272000 输入的长上下文及 $12.50/M 缓存写入未分列,实际费用可能不同。'; } return '文本:输入=' + metrics.textUncached + '×$' + rate[0] + '/M,缓存=' + metrics.textCached + '×$' + rate[1] + '/M,输出=' + metrics.textOutput + '×$' + rate[2] + '/M;' + pricing.family + (pricing.multiplier !== 1 ? ',快速×' + pricing.multiplier : '') + (pricing.fallback ? '(未知模型回退)' : '') + ';按参考费率估算,非实际账单'; } function tokenModel(raw, notices) { var name = safeModelName(raw); var speed = pricingSpeed(name, safeSpeed(raw)); var pricing = priceFor(name, speed); var metrics = rawMetrics(raw, pricing.image); if (pricing.family === 'gpt-6-astra') { addNotice(notices, 'GPT-6 Astra 按官网标准短上下文费率估算;日汇总不能区分长上下文及缓存写入,实际费用可能不同。'); } if (name.toLowerCase() === 'codex-auto-review') addNotice(notices, 'codex-auto-review 按用户指定映射为 gpt-5.6-luna。'); if (pricing.fallback) addNotice(notices, '未知文本模型按 gpt-5.5 参考费率显示,非实际账单。'); var estimated = moneyFor(metrics, pricing); if (estimated !== null) addNotice(notices, '费用按参考费率估算,非实际账单。'); return { name: name, speed: speed, tokens: metrics.tokens, uncachedInputTokens: metrics.uncachedInputTokens, cachedInputTokens: metrics.cachedInputTokens, outputTokens: metrics.outputTokens, estimatedUsd: estimated, estimatedAllocation: false, fallbackPricing: pricing.fallback, incompleteImage: metrics.incompleteImage, calculation: calculationFor(metrics, pricing, estimated) }; } function activityRow(raw) { var turns = numberFrom(raw, ['turns', 'turn_count', 'conversation_turns']); var threads = numberFrom(raw, ['threads', 'thread_count']); var credits = numberFrom(raw, ['credits', 'credit_count']); var users = numberFrom(raw, ['users', 'active_users', 'active_members']); if (turns === null && threads === null && credits === null && users === null) return null; var name = safeModelName(raw); return { name: name, speed: pricingSpeed(name, safeSpeed(raw)), turns: turns, threads: threads, credits: credits, users: users }; } function activityIsAllZero(activity) { return activity && activity.turns === 0 && activity.threads === 0 && activity.credits === 0 && activity.users === 0; } function hasUncoveredActivity(activities, tokenModels) { return activities.some(function (activity) { if (activityIsAllZero(activity)) return false; return !tokenModels.some(function (model) { return model.name === activity.name && model.speed === activity.speed; }); }); } function rawModelValues(row) { var total = isObject(row.totals) ? row.totals : isObject(row.total) ? row.total : null; var sources = [row, total]; var keys = ['models', 'model_usage', 'usage_by_model', 'breakdown']; for (var s = 0; s < sources.length; s += 1) { if (!isObject(sources[s])) continue; for (var k = 0; k < keys.length; k += 1) { var value = sources[sources.length === 1 ? 0 : s][keys[k]]; if (Array.isArray(value) && value.length) return value.filter(isObject); if (isObject(value)) { var list = []; Object.keys(value).forEach(function (name) { if (!isObject(value[name])) return; var copy = Object.assign({}, value[name]); if (!copy.name && !copy.model && !copy.model_id) copy.name = name; list.push(copy); }); if (list.length) return list; } } } return (row.model || row.model_id || row.model_name) ? [row] : []; } function splitModelRows(row, notices) { var tokens = []; var activities = []; rawModelValues(row).forEach(function (raw) { if (safeModelName(raw).toLowerCase() === 'gpt-6-astra') { addNotice(notices, 'GPT-6 Astra 按官网标准短上下文费率估算;日汇总不能区分长上下文及缓存写入,实际费用可能不同。'); } if (hasTokenFields(raw)) tokens.push(tokenModel(raw, notices)); var activity = activityRow(raw); if (activity) activities.push(activity); }); return { tokens: tokens, activities: activities, coverageIncomplete: hasUncoveredActivity(activities, tokens) }; } function totalSource(row) { return isObject(row.totals) ? row.totals : isObject(row.total) ? row.total : row; } function arrayFromDay(row, key) { var total = totalSource(row); if (Array.isArray(row[key]) && row[key].length) return row[key].filter(isObject); return isObject(total) && Array.isArray(total[key]) ? total[key].filter(isObject) : []; } function aggregateField(rows, field) { return sumAll(rows.map(function (row) { return row[field]; })); } function costForModels(models, unseen) { if (unseen || !models.length) return null; return sumAll(models.map(function (model) { return model.estimatedUsd; })); } function differsWhenKnown(left, right) { return left !== null && (right === null || left !== right); } function buildRecord(row, notices) { var date = dateFromUnknown(row.date || row.day || row.usage_date || row.timestamp); if (!date) return null; var totals = totalSource(row); var direct = rawMetrics(totals); var split = splitModelRows(row, notices); var aggregateTokens = aggregateField(split.tokens, 'tokens'); var aggregateUncached = aggregateField(split.tokens, 'uncachedInputTokens'); var aggregateCached = aggregateField(split.tokens, 'cachedInputTokens'); var aggregateOutput = aggregateField(split.tokens, 'outputTokens'); var unseen = false; if (direct.present) { unseen = !split.tokens.length || direct.invalid || direct.tokens === null || aggregateTokens === null || aggregateTokens !== direct.tokens || differsWhenKnown(direct.uncachedInputTokens, aggregateUncached) || differsWhenKnown(direct.cachedInputTokens, aggregateCached) || differsWhenKnown(direct.outputTokens, aggregateOutput); } var coverageIncomplete = split.coverageIncomplete || unseen; if (coverageIncomplete) addNotice(notices, '模型令牌覆盖不完整;费用和模型汇总保持不可用。'); if (unseen) split.tokens.forEach(function (model) { model.estimatedUsd = null; model.calculation = '日汇总与模型令牌不一致,未汇总费用'; }); var day = { date: date, tokens: direct.present ? direct.tokens : aggregateTokens, uncachedInputTokens: direct.uncachedInputTokens !== null ? direct.uncachedInputTokens : aggregateUncached, cachedInputTokens: direct.cachedInputTokens !== null ? direct.cachedInputTokens : aggregateCached, outputTokens: direct.outputTokens !== null ? direct.outputTokens : aggregateOutput, turns: numberFrom(totals, ['turns', 'turn_count', 'conversation_turns']), threads: numberFrom(totals, ['threads', 'thread_count']), credits: numberFrom(totals, ['credits', 'credit_count']), estimatedUsd: costForModels(split.tokens, unseen || coverageIncomplete), models: split.tokens }; if (day.turns === null) day.turns = aggregateField(split.activities, 'turns'); if (day.threads === null) day.threads = aggregateField(split.activities, 'threads'); if (day.credits === null) day.credits = aggregateField(split.activities, 'credits'); return { day: day, tokenModels: split.tokens, activities: split.activities, clients: arrayFromDay(row, 'clients'), directPresent: direct.present, hasImage: direct.hasImage, users: numberFrom(totals, ['users', 'active_users', 'active_members']), unseen: unseen, directInvalid: direct.invalid, coverageIncomplete: coverageIncomplete }; } function normalizeRecords(payload, range, notices) { var selected = Object.create(null); extractRows(payload).forEach(function (row) { if (!isObject(row)) return; var record = buildRecord(row, notices); if (!record || record.day.date < range.startDate || record.day.date > range.endDate) return; if (selected[record.day.date]) { addNotice(notices, '已忽略重复日期汇总,避免重复计数。'); return; } selected[record.day.date] = record; }); return Object.keys(selected).sort().map(function (date) { return selected[date]; }); } function mergeActivityRows(primary, secondary) { var map = Object.create(null); function put(row) { var key = row.name + '\u0000' + row.speed; if (!map[key]) map[key] = { name: row.name, speed: row.speed, turns: null, threads: null, credits: null, users: null }; var target = map[key]; ['turns', 'threads', 'credits', 'users'].forEach(function (field) { if (target[field] === null && row[field] !== null) target[field] = row[field]; }); } primary.forEach(put); secondary.forEach(put); return Object.keys(map).sort().map(function (key) { return map[key]; }); } function largestRemainder(total, weights) { if (total === null || Math.round(total) !== total) return null; var weightTotal = weights.reduce(function (sum, value) { return sum + value; }, 0); if (!(weightTotal > 0)) return null; var parts = weights.map(function (weight, index) { var exact = total * weight / weightTotal; return { index: index, value: Math.floor(exact), remainder: exact - Math.floor(exact) }; }); var remaining = total - parts.reduce(function (sum, part) { return sum + part.value; }, 0); parts.sort(function (a, b) { return b.remainder - a.remainder || a.index - b.index; }); for (var i = 0; i < remaining; i += 1) parts[i].value += 1; parts.sort(function (a, b) { return a.index - b.index; }); return parts.map(function (part) { return part.value; }); } function allocatedModel(activity, uncached, cached, output, notices) { var name = activity.name; var speed = activity.speed || 'standard'; var pricing = priceFor(name, speed); var metrics = { invalid: false, hasImage: false, incompleteImage: false, textUncached: uncached, textCached: cached, textOutput: output, imageUncached: null, imageCached: null, imageOutput: null }; var estimate = moneyFor(metrics, pricing); if (pricing.family === 'gpt-6-astra') { addNotice(notices, 'GPT-6 Astra 按官网标准短上下文费率估算;日汇总不能区分长上下文及缓存写入,实际费用可能不同。'); } if (pricing.fallback) addNotice(notices, '未知文本模型按 gpt-5.5 参考费率显示,非实际账单。'); if (name.toLowerCase() === 'codex-auto-review') addNotice(notices, 'codex-auto-review 按用户指定映射为 gpt-5.6-luna。'); return { name: name, speed: speed, tokens: uncached + cached + output, uncachedInputTokens: uncached, cachedInputTokens: cached, outputTokens: output, estimatedUsd: estimate, estimatedAllocation: true, fallbackPricing: pricing.fallback, incompleteImage: false, calculation: pricing.family === 'gpt-6-astra' ? '按积分与参考混合费率分配总令牌;' + calculationFor(metrics, pricing, estimate) : '按积分与参考混合费率分配总令牌;按参考费率估算,非实际账单' }; } function allocatePersonalModels(day, activities, notices, hasImage, directInvalid) { if (directInvalid || hasImage || day.models.length || day.tokens === null || Math.round(day.tokens) !== day.tokens || day.uncachedInputTokens === null || day.cachedInputTokens === null || day.outputTokens === null) return false; var total = day.uncachedInputTokens + day.cachedInputTokens + day.outputTokens; if (!(total > 0) || !isFinite(total) || total !== day.tokens) return false; var candidates = activities.filter(function (activity) { return activity.credits !== null && activity.credits > 0 && !priceFor(activity.name, activity.speed).image; }); if (!candidates.length || candidates.length !== activities.length) return false; var weights = candidates.map(function (activity) { var pricing = priceFor(activity.name, activity.speed); var rates = REFERENCE_RATES[pricing.family]; var blended = (day.uncachedInputTokens * rates[0] + day.cachedInputTokens * rates[1] + day.outputTokens * rates[2]) / total * pricing.multiplier; return blended > 0 ? activity.credits / blended : 0; }); var uncached = largestRemainder(day.uncachedInputTokens, weights); var cached = largestRemainder(day.cachedInputTokens, weights); var output = largestRemainder(day.outputTokens, weights); if (!uncached || !cached || !output) return false; day.models = candidates.map(function (activity, index) { return allocatedModel(activity, uncached[index], cached[index], output[index], notices); }); day.estimatedUsd = costForModels(day.models, false); addNotice(notices, '缺少按模型令牌明细;按积分和参考费率分配,非实际账单。'); return true; } function mergePersonalFallback(breakdown, counts, notices) { var byDate = Object.create(null); breakdown.forEach(function (record) { byDate[record.day.date] = { breakdown: record, counts: null }; }); counts.forEach(function (record) { if (!byDate[record.day.date]) byDate[record.day.date] = { breakdown: null, counts: record }; else byDate[record.day.date].counts = record; }); return Object.keys(byDate).sort().map(function (date) { var pair = byDate[date]; var count = pair.counts; var detail = pair.breakdown; var base = count || detail; var exactModels = detail && detail.tokenModels.length ? detail.tokenModels : (count ? count.tokenModels : []); var activities = mergeActivityRows(count ? count.activities : [], detail ? detail.activities : []); var countDay = count && count.day; var detailDay = detail && detail.day; function pick(field) { return countDay && countDay[field] !== null ? countDay[field] : detailDay ? detailDay[field] : null; } var dailyHasTotals = !!((count && count.directPresent) || (detail && detail.directPresent)); var directInvalid = !!((count && count.directInvalid) || (detail && detail.directInvalid)); var tokens = pick('tokens'); var exactTotal = aggregateField(exactModels, 'tokens'); var unseen = dailyHasTotals && (!exactModels.length || tokens === null || exactTotal === null || exactTotal !== tokens || differsWhenKnown(pick('uncachedInputTokens'), aggregateField(exactModels, 'uncachedInputTokens')) || differsWhenKnown(pick('cachedInputTokens'), aggregateField(exactModels, 'cachedInputTokens')) || differsWhenKnown(pick('outputTokens'), aggregateField(exactModels, 'outputTokens'))); var allocationEligible = !exactModels.length && dailyHasTotals && tokens !== null && !directInvalid; var day = { date: date, tokens: tokens, uncachedInputTokens: pick('uncachedInputTokens'), cachedInputTokens: pick('cachedInputTokens'), outputTokens: pick('outputTokens'), turns: pick('turns'), threads: pick('threads'), credits: pick('credits'), estimatedUsd: costForModels(exactModels, unseen), models: exactModels.slice() }; if (unseen) day.models.forEach(function (model) { model.estimatedUsd = null; model.calculation = '日汇总与模型令牌不一致,未汇总费用'; }); if (day.turns === null) day.turns = aggregateField(activities, 'turns'); if (day.threads === null) day.threads = aggregateField(activities, 'threads'); if (day.credits === null) day.credits = aggregateField(activities, 'credits'); var allocated = false; if (allocationEligible) { allocated = allocatePersonalModels(day, activities, notices, !!((count && count.hasImage) || (detail && detail.hasImage)), directInvalid); if (!allocated) { addNotice(notices, '缺少可安全分配的模型明细;费用保持不可用。'); } } var coverageIncomplete = unseen || directInvalid || !!((count && count.coverageIncomplete) || (detail && detail.coverageIncomplete)); if (allocated) coverageIncomplete = false; if (coverageIncomplete) day.estimatedUsd = null; return { day: day, tokenModels: day.models, activities: activities, clients: count && count.clients.length ? count.clients : detail ? detail.clients : [], directPresent: dailyHasTotals, hasImage: !!((count && count.hasImage) || (detail && detail.hasImage)), users: count && count.users !== null ? count.users : detail ? detail.users : null, unseen: unseen, directInvalid: directInvalid, coverageIncomplete: coverageIncomplete, base: base }; }); } function aggregateModels(records) { var groups = Object.create(null); var coverageIncomplete = records.some(function (record) { return record.coverageIncomplete; }); records.forEach(function (record) { record.day.models.forEach(function (model) { var key = model.name + '\u0000' + model.speed; if (!groups[key]) groups[key] = []; groups[key].push(model); }); }); return Object.keys(groups).sort().map(function (key) { var rows = groups[key]; var first = rows[0]; return { name: first.name, speed: first.speed, tokens: coverageIncomplete ? null : aggregateField(rows, 'tokens'), uncachedInputTokens: coverageIncomplete ? null : aggregateField(rows, 'uncachedInputTokens'), cachedInputTokens: coverageIncomplete ? null : aggregateField(rows, 'cachedInputTokens'), outputTokens: coverageIncomplete ? null : aggregateField(rows, 'outputTokens'), estimatedUsd: coverageIncomplete ? null : sumAll(rows.map(function (row) { return row.estimatedUsd; })), estimatedAllocation: rows.some(function (row) { return row.estimatedAllocation; }), fallbackPricing: rows.some(function (row) { return row.fallbackPricing; }), incompleteImage: rows.some(function (row) { return row.incompleteImage; }), calculation: first.calculation }; }); } function aggregateActivity(records) { var groups = Object.create(null); records.forEach(function (record) { record.activities.forEach(function (activity) { if (!groups[activity.name]) groups[activity.name] = []; groups[activity.name].push(activity); }); }); return Object.keys(groups).sort().map(function (name) { var rows = groups[name]; return { name: rows[0].name, turns: aggregateField(rows, 'turns'), threads: aggregateField(rows, 'threads'), credits: aggregateField(rows, 'credits'), activeMembersPeak: maxKnown(rows.map(function (row) { return row.users; })) }; }); } function safeClientName(raw) { var value = raw && (raw.client_id || raw.name || raw.id); if (typeof value !== 'string') return 'unknown'; value = value.trim(); if (!value || value.length > 80 || /[\x00-\x1f\x7f]/.test(value) || !/^[a-z][a-z0-9 ._-]*$/i.test(value)) return 'unknown'; return value; } function aggregateClients(records) { var groups = Object.create(null); records.forEach(function (record) { record.clients.forEach(function (raw) { var name = safeClientName(raw); if (!groups[name]) groups[name] = []; groups[name].push(raw); }); }); return Object.keys(groups).sort().map(function (name) { var rows = groups[name]; return { name: name, tokens: sumAll(rows.map(function (row) { return rawMetrics(row).tokens; })), turns: sumAll(rows.map(function (row) { return numberFrom(row, ['turns', 'turn_count']); })), threads: sumAll(rows.map(function (row) { return numberFrom(row, ['threads', 'thread_count']); })), credits: sumAll(rows.map(function (row) { return numberFrom(row, ['credits', 'credit_count']); })) }; }); } function summarize(records) { var days = records.map(function (record) { return record.day; }); return { tokens: aggregateField(days, 'tokens'), uncachedInputTokens: aggregateField(days, 'uncachedInputTokens'), cachedInputTokens: aggregateField(days, 'cachedInputTokens'), outputTokens: aggregateField(days, 'outputTokens'), turns: aggregateField(days, 'turns'), threads: aggregateField(days, 'threads'), credits: aggregateField(days, 'credits'), estimatedUsd: sumAll(days.map(function (day) { return day.estimatedUsd; })), activeMembersPeak: maxKnown(records.map(function (record) { return record.users; }).concat(records.reduce(function (all, record) { return all.concat(record.activities.map(function (activity) { return activity.users; })); }, []))) }; } function validSnapshot(snapshot) { return snapshot && snapshot.status === 'ready' && snapshot.range && typeof snapshot.range.startDate === 'string' && typeof snapshot.range.endDate === 'string'; } function cycleRange(quota, notices) { if (!quota.cycleStartDate || !quota.resetAt || quota.durationSeconds === null || quota.durationSeconds <= 0) return null; var reset = new Date(quota.resetAt).getTime(); if (!isFinite(reset)) return null; var end = new Date(Math.min(Date.now(), reset)); var range = parseRange(quota.cycleStartDate, dateString(new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate())))); if (!range) { addNotice(notices, '确认的配额周期不可安全查询,已改用最近 7 天。'); return null; } return range; } function fillCycleEstimate(snapshot) { snapshot.quota.estimatedFullCycleUsd = null; snapshot.quota.estimatedRemainingUsd = null; addNotice(snapshot.notices, '日汇总无法精确对齐配额周期,未折算周期费用。'); } function selectedRange(options, quota, notices) { var supplied = explicitRange(options); if (supplied.supplied) return supplied.range; if (!options.preset || options.preset === '7d') return defaultRange(); if (options.preset === '30d') return lastDays(30); if (options.preset === 'cycle' || options.preset === 'current-cycle') { var cycle = cycleRange(quota, notices); if (cycle) return cycle; addNotice(notices, '未确认配额周期,已显示最近 7 天。'); return defaultRange(); } return null; } function loadingRange(options) { var supplied = explicitRange(options); if (supplied.supplied) return supplied.range || { startDate: '', endDate: '' }; return options.preset === '30d' ? lastDays(30) : defaultRange(); } async function load(options) { if (destroyed) return null; options = isObject(options) ? options : {}; syncAccount(); var captured = lastAccount; var view = options.viewMode === 'team' ? 'team' : options.viewMode === 'personal' ? 'personal' : state.viewMode; var supplied = explicitRange(options); var validPreset = !options.preset || options.preset === 'cycle' || options.preset === 'current-cycle' || options.preset === '7d' || options.preset === '30d'; if ((supplied.supplied && !supplied.range) || !validPreset) { generation += 1; if (inFlight) inFlight.abort(); inFlight = null; restoreAfterCancel = null; setState(Object.assign(emptySnapshot(), { status: 'error', error: '日期必须成对为有效 YYYY-MM-DD,且最多 366 天。', accountId: captured, viewMode: view })); return clone(state); } if (view === 'team' && !captured) { generation += 1; if (inFlight) inFlight.abort(); inFlight = null; restoreAfterCancel = null; setState(Object.assign(emptySnapshot(), { status: 'error', error: '未确认当前工作区,无法读取团队使用情况。', accountId: null, viewMode: 'team' })); return clone(state); } generation += 1; var mine = generation; if (inFlight) inFlight.abort(); var controller = new AbortController(); inFlight = controller; restoreAfterCancel = lastReady && lastReady.accountId === captured ? clone(lastReady) : null; setState(Object.assign(emptySnapshot(), { status: 'loading', accountId: captured, viewMode: view, range: loadingRange(options), notices: ['统计可能有延迟。'] })); try { var notices = ['统计可能有延迟。']; var quota = emptyQuota(); try { var quotaPayload = await apiGet('/backend-api/wham/usage', controller.signal, captured, mine); assertCurrent(captured, mine, controller.signal); quota = normalizeQuota(quotaPayload, notices); } catch (quotaError) { if (quotaError && (quotaError.stale || quotaError.aborted || quotaError.sessionRequest || quotaError.status === 401 || controller.signal.aborted || mine !== generation)) throw quotaError; addNotice(notices, '配额信息不可用;仍显示可读取的用量。'); } var range = selectedRange(options, quota, notices); if (!range) throw requestError(0, '日期范围不可用。'); var endpoint = view === 'team' ? usageUrl('/backend-api/wham/analytics/daily-workspace-usage-counts', range) : usageUrl('/backend-api/wham/usage/daily-workspace-user-token-usage-breakdown', range); var payload = null; var fallback = false; try { payload = await apiGet(endpoint, controller.signal, captured, mine); } catch (error) { if (view !== 'personal' || !error.noActiveWorkspace) throw error; fallback = true; } assertCurrent(captured, mine, controller.signal); var records; var accountMode; if (fallback) { var personalPayload = await apiGet(usageUrl('/backend-api/wham/usage/daily-token-usage-breakdown', range), controller.signal, captured, mine); var countPayload = null; try { countPayload = await apiGet(usageUrl('/backend-api/wham/analytics/daily-workspace-usage-counts', range, { workspace_user: 'true' }), controller.signal, captured, mine); } catch (countError) { if (countError && (countError.stale || countError.aborted)) throw countError; addNotice(notices, '个人活动汇总不可用;缺失的回合、线程或积分保持不可用。'); } assertCurrent(captured, mine, controller.signal); records = mergePersonalFallback(normalizeRecords(personalPayload, range, notices), countPayload ? normalizeRecords(countPayload, range, notices) : [], notices); accountMode = 'personal'; addNotice(notices, '未检测到活动工作区;仅显示个人使用情况。'); } else { records = normalizeRecords(payload, range, notices); accountMode = 'team'; } var snapshot = { status: 'ready', error: null, accountId: captured, accountMode: accountMode, viewMode: view, range: range, quota: quota, summary: summarize(records), models: aggregateModels(records), daily: records.map(function (record) { return record.day; }), clients: aggregateClients(records), modelActivity: aggregateActivity(records), notices: notices, updatedAt: new Date().toISOString() }; if (view === 'team' && !records.length) addNotice(snapshot.notices, '团队端点未返回可显示的汇总数据;未用个人数据替代。'); var confirmedCycle = cycleRange(quota, snapshot.notices); var isCycle = confirmedCycle && confirmedCycle.startDate === range.startDate && confirmedCycle.endDate === range.endDate; if (view === 'personal' && isCycle) fillCycleEstimate(snapshot); else if (view === 'personal' && quota.cycleStartDate) { addNotice(snapshot.notices, '自定义日期未用于配额周期费用折算。'); } assertCurrent(captured, mine, controller.signal); lastReady = clone(snapshot); restoreAfterCancel = null; setState(snapshot); return getSnapshot(); } catch (error) { if (error && error.stale) return getSnapshot(); if (mine !== generation || controller.signal.aborted || (error && error.aborted)) return getSnapshot(); var personalUnavailable = view === 'team' && (error && error.noActiveWorkspace || lastReady && lastReady.accountMode === 'personal'); setState(Object.assign(emptySnapshot(), { status: 'error', error: error && error.message ? error.message : '无法读取使用情况。', accountId: captured, accountMode: personalUnavailable ? 'personal' : null, viewMode: view, range: loadingRange(options), notices: personalUnavailable ? ['个人帐户没有可用的团队汇总;未用零值替代。', '统计可能有延迟。'] : ['统计可能有延迟。'] })); return getSnapshot(); } finally { if (inFlight === controller) inFlight = null; } } function setViewMode(mode) { if (mode !== 'personal' && mode !== 'team') return getSnapshot(); syncAccount(); var prior = lastReady && lastReady.accountId === lastAccount ? lastReady : null; return prior ? load({ viewMode: mode, startDate: prior.range.startDate, endDate: prior.range.endDate }) : load({ viewMode: mode }); } function cancel() { if (destroyed) return; syncAccount(); generation += 1; if (inFlight) inFlight.abort(); inFlight = null; if (state.status !== 'loading') return; if (restoreAfterCancel && restoreAfterCancel.accountId === lastAccount) { setState(clone(restoreAfterCancel)); } else { var idle = emptySnapshot(); idle.accountId = lastAccount; idle.viewMode = state.viewMode; setState(idle); } restoreAfterCancel = null; } function getSnapshot() { syncAccount(); return clone(state); } function exportData(snapshot) { return { status: snapshot.status, viewMode: snapshot.viewMode, range: snapshot.range, quota: snapshot.quota, summary: snapshot.summary, models: snapshot.models, daily: snapshot.daily, clients: snapshot.clients, modelActivity: snapshot.modelActivity, notices: snapshot.notices, updatedAt: snapshot.updatedAt }; } function csvCell(value) { var text = value === null || value === undefined ? '' : String(value); text = text.replace(/[\r\n]+/g, ' '); if (/^[ \t]*[=+\-@]/.test(text)) text = "'" + text; return '"' + text.replace(/"/g, '""') + '"'; } function buildExport(format) { if (destroyed) return null; syncAccount(); if (!validSnapshot(state) || state.accountId !== lastAccount || (format !== 'csv' && format !== 'json')) return null; var data = exportData(state); var filename = 'team-usage-' + state.range.endDate; if (format === 'json') return { filename: filename + '.json', mime: 'application/json', text: JSON.stringify(data, null, 2) }; var header = ['section', 'viewMode', 'startDate', 'endDate', 'date', 'name', 'speed', 'tokens', 'uncachedInputTokens', 'cachedInputTokens', 'outputTokens', 'turns', 'threads', 'credits', 'estimatedUsd', 'activeMembersPeak', 'estimatedAllocation', 'fallbackPricing', 'incompleteImage']; var lines = [header]; function row(section, date, name, values) { var modelRow = section === 'daily-model' || section === 'model-total'; lines.push([section, state.viewMode, state.range.startDate, state.range.endDate, date || '', name || '', modelRow ? values.speed : '', values.tokens, values.uncachedInputTokens, values.cachedInputTokens, values.outputTokens, values.turns, values.threads, values.credits, values.estimatedUsd, values.activeMembersPeak, modelRow ? values.estimatedAllocation : '', modelRow ? values.fallbackPricing : '', modelRow ? values.incompleteImage : '']); } state.daily.forEach(function (day) { row('daily', day.date, '', day); day.models.forEach(function (model) { row('daily-model', day.date, model.name, model); }); }); state.models.forEach(function (model) { row('model-total', '', model.name, model); }); state.clients.forEach(function (client) { row('client', '', client.name, client); }); state.modelActivity.forEach(function (activity) { row('model-activity', '', activity.name, activity); }); return { filename: filename + '.csv', mime: 'text/csv;charset=utf-8', text: lines.map(function (line) { return line.map(csvCell).join(','); }).join('\r\n') }; } function destroy() { if (destroyed) return; generation += 1; if (inFlight) inFlight.abort(); inFlight = null; token = null; tokenExpiresAt = 0; lastReady = null; restoreAfterCancel = null; state = emptySnapshot(); if (accountWatch !== null && typeof clearInterval === 'function') clearInterval(accountWatch); accountWatch = null; destroyed = true; onChange = null; if (typeof window !== 'undefined' && window.removeEventListener) { window.removeEventListener('focus', accountRefreshHandler); if (typeof document !== 'undefined' && document.removeEventListener) { document.removeEventListener('visibilitychange', accountRefreshHandler); } } } return { getSnapshot: getSnapshot, load: load, setViewMode: setViewMode, cancel: cancel, destroy: destroy, buildExport: buildExport }; } /** * Codex Quota Compass - Original UI Restoration * Faithful verbatim restoration of original CodexQuotaCompass visual shell, CSS, * layout, icons, sections, tables, graphs, and copy style. * * Integrated via controller API: mountTeamUsagePanel(options) -> { update(snapshot), destroy() } */ function mountTeamUsagePanel(options = {}) { const CONFIG = { DAY_MS: 24 * 60 * 60 * 1000, MAX_RANGE_DAYS: 366, TARGET_WINDOW_SECONDS: 7 * 24 * 60 * 60, TOKEN_CACHE_FALLBACK_MS: 9 * 60 * 1000, USE_UTC_DAY: true, EPS: 1e-9, }; const SVG_BANKNOTE = ''; const GPT_IMAGE_2_PRICING_URL = 'https://platform.openai.com/docs/pricing'; let destroyed = false; let isModalOpen = false; let modalGeneration = 0; let lastFocusedTrigger = null; let currentActiveAccount = null; let initialAccountObserved = false; let dateInputDirty = false; let localStartDate = ''; let localEndDate = ''; const activeTimers = new Set(); const activeAnimFrames = new Set(); const activeObjectUrls = new Set(); function safeSetTimeout(fn, ms) { if (destroyed) return null; const id = setTimeout(() => { activeTimers.delete(id); if (!destroyed) fn(); }, ms); activeTimers.add(id); return id; } function safeRequestAnimationFrame(fn) { if (destroyed) return null; const id = requestAnimationFrame(() => { activeAnimFrames.delete(id); if (!destroyed) fn(); }); activeAnimFrames.add(id); return id; } function clearAllAsync() { activeTimers.forEach((id) => clearTimeout(id)); activeTimers.clear(); activeAnimFrames.forEach((id) => cancelAnimationFrame(id)); activeAnimFrames.clear(); } // Clean stale modal hosts const oldHost = document.getElementById('codex-compass-ultimate-host'); if (oldHost && oldHost.parentNode) { try { oldHost.parentNode.removeChild(oldHost); } catch (_) {} } const host = document.createElement('div'); host.id = 'codex-compass-ultimate-host'; host.style.display = 'none'; // Default closed host in body document.body.appendChild(host); const shadow = host.attachShadow({ mode: 'open' }); function explicitTheme(node) { if (!node) return null; const dt = (typeof node.getAttribute === 'function' ? node.getAttribute('data-theme') : null) || ''; const dtLower = dt.trim().toLowerCase(); if (dtLower === 'dark') return true; if (dtLower === 'light') return false; const cl = node.classList; if (cl) { if (cl.contains('dark')) return true; if (cl.contains('light')) return false; } return null; } const darkMedia = (typeof window !== 'undefined' && typeof window.matchMedia === 'function') ? window.matchMedia('(prefers-color-scheme: dark)') : null; function resolveIsDark() { const htmlTheme = explicitTheme(document.documentElement); if (htmlTheme !== null) return htmlTheme; const bodyTheme = explicitTheme(document.body); if (bodyTheme !== null) return bodyTheme; return Boolean(darkMedia && darkMedia.matches); } function syncTheme() { if (destroyed) return; const isDark = resolveIsDark(); if (isDark) host.classList.add('dark'); else host.classList.remove('dark'); } function handleMediaChange() { syncTheme(); } if (darkMedia) { if (typeof darkMedia.addEventListener === 'function') { darkMedia.addEventListener('change', handleMediaChange); } else if (typeof darkMedia.addListener === 'function') { darkMedia.addListener(handleMediaChange); } } const themeObserver = new MutationObserver(() => syncTheme()); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] }); if (document.body) { themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] }); } syncTheme(); const showPanelHost = () => { syncTheme(); host.style.display = 'block'; }; const hidePanel = () => { host.style.display = 'none'; }; const createHostAndShadow = () => shadow; const panelHost = () => host; const toggleButton = () => null; // Verbatim 1440-line original CSS const css = ` :host { --bg-backdrop: rgba(15, 23, 42, 0.48); --bg-modal: #ffffff; --bg-header: #ffffff; --bg-subtle: #f8fafc; --bg-card: #ffffff; --bg-card-hover: #f8fafc; --bg-input: #ffffff; --bg-table-header: #f8fafc; --bg-table-row-hover: #f8fafc; --bg-table-zebra: #fafbfc; --bg-table-footer: #f8fafc; --bg-details: #f8fafc; --bg-details-card: #ffffff; --bg-tag: #f1f5f9; --bg-pre: #f8fafc; --border: #e2e8f0; --border-subtle: #f1f5f9; --border-strong: #cbd5e1; --border-focus: #10a37f; --text-primary: #0f172a; --text-secondary: #475569; --text-muted: #94a3b8; --text-inverse: #ffffff; --brand-green: #10a37f; --brand-green-hover: #0d8c6d; --brand-green-active: #097056; --brand-green-subtle: #ecfdf5; --brand-green-border: #a7f3d0; --brand-green-text: #047857; --accent-blue: #2563eb; --accent-blue-subtle: #eff6ff; --accent-blue-border: #bfdbfe; --accent-blue-text: #1d4ed8; --accent-purple: #7c3aed; --accent-purple-subtle: #f5f3ff; --accent-purple-border: #ddd6fe; --accent-purple-text: #6d28d9; --accent-emerald: #059669; --accent-emerald-subtle: #ecfdf5; --accent-amber: #d97706; --accent-amber-subtle: #fffbeb; --accent-amber-border: #fde68a; --accent-amber-text: #b45309; --accent-red: #dc2626; --accent-red-subtle: #fef2f2; --accent-red-border: #fecaca; --accent-red-text: #991b1b; --track-bg: #f1f5f9; --scrollbar-thumb: #cbd5e1; --scrollbar-thumb-hover: #94a3b8; --shadow-modal: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(0, 0, 0, 0.06); --shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05); --radius-modal: 16px; --radius-card: 12px; --radius-btn: 8px; --radius-pill: 9999px; position: fixed; inset: 0; z-index: 2147483647; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; font-size: 13px; line-height: 1.5; color: var(--text-primary); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @media (prefers-color-scheme: dark) { :host { --bg-backdrop: rgba(0, 0, 0, 0.72); --bg-modal: #1e1e24; --bg-header: #22222a; --bg-subtle: #17171c; --bg-card: #23232b; --bg-card-hover: #2b2b35; --bg-input: #17171c; --bg-table-header: #1b1b22; --bg-table-row-hover: #272732; --bg-table-zebra: #202028; --bg-table-footer: #1b1b22; --bg-details: #18181f; --bg-details-card: #23232b; --bg-tag: #2a2a34; --bg-pre: #141418; --border: #33333f; --border-subtle: #282832; --border-strong: #4a4a58; --border-focus: #10a37f; --text-primary: #f1f5f9; --text-secondary: #cbd5e1; --text-muted: #8892a4; --text-inverse: #0f172a; --brand-green: #10a37f; --brand-green-hover: #1ab890; --brand-green-active: #0d8c6d; --brand-green-subtle: rgba(16, 163, 127, 0.16); --brand-green-border: rgba(16, 163, 127, 0.42); --brand-green-text: #34d399; --accent-blue: #3b82f6; --accent-blue-subtle: rgba(59, 130, 246, 0.16); --accent-blue-border: rgba(59, 130, 246, 0.42); --accent-blue-text: #93c5fd; --accent-purple: #a78bfa; --accent-purple-subtle: rgba(167, 139, 250, 0.16); --accent-purple-border: rgba(167, 139, 250, 0.42); --accent-purple-text: #c4b5fd; --accent-emerald: #34d399; --accent-emerald-subtle: rgba(52, 211, 153, 0.15); --accent-amber: #fbbf24; --accent-amber-subtle: rgba(251, 191, 36, 0.16); --accent-amber-border: rgba(251, 191, 36, 0.42); --accent-amber-text: #fde68a; --accent-red: #f87171; --accent-red-subtle: rgba(248, 113, 113, 0.16); --accent-red-border: rgba(248, 113, 113, 0.42); --accent-red-text: #fca5a5; --track-bg: #141418; --scrollbar-thumb: #3e3e4e; --scrollbar-thumb-hover: #58586c; --shadow-modal: 0 25px 50px -12px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.08); --shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.35), 0 1px 2px -1px rgba(0, 0, 0, 0.25); } } * { box-sizing: border-box; } .modal-backdrop { position: fixed; inset: 0; background: var(--bg-backdrop); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; padding: max(14px, env(safe-area-inset-top, 14px)) max(14px, env(safe-area-inset-right, 14px)) max(14px, env(safe-area-inset-bottom, 14px)) max(14px, env(safe-area-inset-left, 14px)); animation: backdropFadeIn 0.2s ease-out; overflow: hidden; } @keyframes backdropFadeIn { from { opacity: 0; } to { opacity: 1; } } .modal-card { display: flex; flex-direction: column; width: clamp(320px, 94vw, 1560px); max-width: 95vw; max-height: 88vh; max-height: min(88vh, 88dvh); background: var(--bg-modal); border: 1px solid var(--border); border-radius: var(--radius-modal); box-shadow: var(--shadow-modal); overflow: hidden; animation: modalSlideUp 0.22s cubic-bezier(0.16, 1, 0.3, 1); } @keyframes modalSlideUp { from { opacity: 0; transform: translateY(14px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } } .header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 22px; background: var(--bg-header); border-bottom: 1px solid var(--border); flex-shrink: 0; } .header-left { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; min-width: 0; } .header-title { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; min-width: 0; font-size: 16px; font-weight: 700; letter-spacing: -0.01em; color: var(--text-primary); } .header-icon { font-size: 18px; line-height: 1; } .title-text { font-size: 16px; font-weight: 700; color: var(--text-primary); } .header-delay-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: var(--radius-pill); font-size: 11px; font-weight: 600; background: var(--accent-amber-subtle); color: var(--accent-amber-text); border: 1px solid var(--accent-amber-border); line-height: 1.3; white-space: nowrap; } .delay-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-amber); flex-shrink: 0; } .version, .mode-badge, .model-pill, .speed-pill, .estimate-pill, .client-label { display: inline-flex; align-items: center; border-radius: var(--radius-pill); font-size: 11px; font-weight: 600; white-space: nowrap; line-height: 1.2; } .version { padding: 2px 7px; color: var(--text-secondary); background: var(--bg-tag); border: 1px solid var(--border); } .mode-badge { padding: 2px 8px; color: var(--accent-blue-text); background: var(--accent-blue-subtle); border: 1px solid var(--accent-blue-border); } .mode-badge.personal { color: var(--brand-green-text); background: var(--brand-green-subtle); border-color: var(--brand-green-border); } .view-switch { display: inline-flex; padding: 3px; background: var(--bg-tag); border: 1px solid var(--border); border-radius: 8px; gap: 3px; } .view-switch-btn { min-height: 28px; padding: 0 12px; color: var(--text-secondary); background: transparent; border: 0; border-radius: 6px; font-size: 11px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; transition: all 0.15s ease; } .view-switch-btn:hover:not(:disabled) { color: var(--text-primary); } .view-switch-btn.active { color: var(--brand-green-text); background: var(--bg-modal); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .view-switch-btn:disabled { color: var(--text-muted); cursor: not-allowed; } .close-btn { width: 36px; height: 36px; min-width: 36px; min-height: 36px; display: flex; align-items: center; justify-content: center; padding: 0; color: var(--text-secondary); background: transparent; border: 0; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; transition: background 0.15s ease, color 0.15s ease; } .close-btn:hover { color: var(--text-primary); background: var(--bg-card-hover); } .close-btn:active { transform: scale(0.96); } .range-bar { padding: 14px 22px; background: var(--bg-subtle); border-bottom: 1px solid var(--border); flex-shrink: 0; } .date-fields { display: grid; grid-template-columns: 1fr 1fr auto; gap: 12px; align-items: end; } .field-group { min-width: 0; } .field-group label { display: block; margin-bottom: 4px; color: var(--text-secondary); font-size: 11px; font-weight: 600; letter-spacing: 0.02em; } input[type='date'] { display: block; width: 100%; height: 38px; min-height: 38px; padding: 0 10px; color: var(--text-primary); background: var(--bg-input); border: 1px solid var(--border-strong); border-radius: var(--radius-btn); font-family: inherit; font-size: 12px; outline: none; color-scheme: light; transition: border-color 0.15s ease, box-shadow 0.15s ease; } @media (prefers-color-scheme: dark) { input[type='date'] { color-scheme: dark; } } input[type='date']:focus { border-color: var(--brand-green); box-shadow: 0 0 0 3px var(--brand-green-subtle); } button { font-family: inherit; } .primary-btn { height: 38px; min-height: 38px; padding: 0 20px; color: #ffffff; background: var(--brand-green); border: 1px solid var(--brand-green); border-radius: var(--radius-btn); font-size: 13px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 6px; transition: all 0.15s ease; white-space: nowrap; } .primary-btn:hover { background: var(--brand-green-hover); border-color: var(--brand-green-hover); box-shadow: 0 2px 8px rgba(16, 163, 127, 0.25); } .primary-btn:active { transform: scale(0.97); background: var(--brand-green-active); } .primary-btn:disabled, .mini-btn:disabled, .footer-btn:disabled { opacity: 0.55; cursor: not-allowed; transform: none; box-shadow: none; } .presets-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-top: 10px; } .presets { display: flex; align-items: center; gap: 6px; } .preset-label { color: var(--text-muted); font-size: 11px; font-weight: 500; } .mini-btn { height: 28px; min-height: 28px; padding: 0 10px; color: var(--text-secondary); background: var(--bg-card); border: 1px solid var(--border-strong); border-radius: 6px; font-size: 11px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; transition: all 0.15s ease; } .mini-btn:hover { color: var(--text-primary); background: var(--bg-card-hover); border-color: var(--text-muted); } .mini-btn:active { transform: scale(0.96); } .cycle-hint { color: var(--text-muted); font-size: 11px; } .panel-body { flex: 1; min-height: 0; padding: clamp(14px, 2vw, 22px); overflow-y: auto; background: var(--bg-modal); position: relative; } .panel-body::-webkit-scrollbar, .table-box::-webkit-scrollbar, pre::-webkit-scrollbar { width: 6px; height: 6px; } .panel-body::-webkit-scrollbar-track, .table-box::-webkit-scrollbar-track, pre::-webkit-scrollbar-track { background: transparent; } .panel-body::-webkit-scrollbar-thumb, .table-box::-webkit-scrollbar-thumb, pre::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: var(--radius-pill); } .panel-body::-webkit-scrollbar-thumb:hover, .table-box::-webkit-scrollbar-thumb:hover, pre::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); } /* Two-Column Continuous Stream Layout (Academic Paper Style) */ .two-column-layout { display: flex; flex-direction: column; gap: 16px; width: 100%; min-width: 0; } .column-left, .column-right { display: flex; flex-direction: column; gap: 16px; min-width: 0; width: 100%; } .kpi-grid-left { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; min-width: 0; } .kpi-grid-right { display: flex; flex-direction: column; gap: 12px; min-width: 0; } .kpi-grid-right > .summary-card { height: 100%; } .notice-wrap { width: 100%; min-width: 0; } .quota-card, .summary-card, .section { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-card); box-shadow: var(--shadow-card); } .quota-card { padding: 16px 18px; margin: 0; } .card-top, .section-title-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .eyebrow-wrap { display: flex; align-items: center; gap: 6px; } .quota-icon { font-size: 14px; line-height: 1; } .eyebrow { color: var(--text-secondary); font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; } .quota-name { color: var(--text-muted); font-size: 11px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .progress-track { height: 10px; margin: 11px 0 9px; overflow: hidden; background: var(--track-bg); border: 1px solid var(--border); border-radius: var(--radius-pill); } .progress-fill { height: 100%; border-radius: inherit; transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1); } .progress-fill.ok { background: linear-gradient(90deg, #10b981, #34d399); } .progress-fill.warn { background: linear-gradient(90deg, #f59e0b, #fbbf24); } .progress-fill.danger { background: linear-gradient(90deg, #ef4444, #f87171); } .quota-stats { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; color: var(--text-secondary); font-size: 12px; } .quota-stat-item strong { color: var(--text-primary); } .quota-stat-divider { color: var(--border-strong); } /* Redesigned Prominent Quota Estimate Cards */ .quota-estimate-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 12px 0 10px; } .quota-estimate-card { padding: 12px 14px; background: var(--bg-subtle); border: 1px solid var(--border); border-radius: var(--radius-btn); display: flex; flex-direction: column; justify-content: space-between; } .estimate-card-label { font-size: 11px; font-weight: 600; color: var(--text-secondary); display: flex; align-items: center; justify-content: space-between; gap: 4px; } .estimate-tag { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px; background: var(--brand-green-subtle); color: var(--brand-green-text); border: 1px solid var(--brand-green-border); } .estimate-card-value { font-size: 20px; font-weight: 800; color: var(--text-primary); margin: 4px 0 2px; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; } .quota-estimate-card.remaining .estimate-card-value { color: var(--accent-emerald); } .estimate-card-hint { font-size: 10px; color: var(--text-muted); line-height: 1.3; } .quota-estimate-unavailable { display: flex; align-items: flex-start; gap: 6px; padding: 10px 12px; background: var(--bg-subtle); border: 1px solid var(--border); border-radius: 6px; font-size: 11px; color: var(--text-muted); margin: 10px 0 8px; line-height: 1.45; } .quota-notice-box { display: flex; align-items: flex-start; gap: 6px; padding: 8px 12px; background: var(--accent-blue-subtle); border: 1px solid var(--accent-blue-border); border-radius: 6px; font-size: 11px; color: var(--accent-blue-text); line-height: 1.45; margin-top: 10px; } .quota-notice-icon { font-size: 13px; line-height: 1.2; flex-shrink: 0; } .summary-card { min-width: 0; min-height: 110px; padding: 14px 16px; margin: 0; transition: border-color 0.15s ease, box-shadow 0.15s ease; } .summary-card:hover { border-color: var(--border-strong); box-shadow: 0 4px 8px -1px rgba(0, 0, 0, 0.08); } .card-header-row { display: flex; align-items: center; justify-content: space-between; } .card-label { color: var(--text-secondary); font-size: 11px; font-weight: 600; } .card-badge { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; } .card-badge.tokens { background: var(--accent-purple-subtle); color: var(--accent-purple-text); } .card-badge.cost { background: var(--brand-green-subtle); color: var(--brand-green-text); } .card-badge.activity { background: var(--accent-blue-subtle); color: var(--accent-blue-text); } .card-value { margin: 6px 0 4px; overflow: hidden; color: var(--text-primary); font-size: clamp(20px, 2.2vw, 24px); font-weight: 750; letter-spacing: -0.02em; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; } .cost-value { color: var(--accent-emerald); } .card-unit { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-left: 2px; } .card-sub { color: var(--text-secondary); font-size: 11px; line-height: 1.5; } .sub-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 4px; vertical-align: middle; } .sub-dot.uncached { background: var(--text-muted); } .sub-dot.cached { background: var(--accent-blue); } .mini-bars { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; } .mini-bar-item { display: flex; align-items: center; gap: 6px; } .mini-bar-name { width: 64px; font-size: 10px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .mini-bar { flex: 1; height: 5px; overflow: hidden; background: var(--track-bg); border-radius: var(--radius-pill); } .mini-bar span { display: block; height: 100%; border-radius: inherit; background: var(--accent-purple); } .section { margin: 0; padding: 16px 18px; } .section-title-row { margin-bottom: 12px; } .section-title { display: flex; align-items: center; gap: 6px; color: var(--text-primary); font-size: 13px; font-weight: 700; } .section-icon { font-size: 14px; line-height: 1; } .section-note { color: var(--text-muted); font-size: 11px; text-align: right; } .section-footnote { color: var(--text-muted); font-size: 11px; margin-top: 8px; line-height: 1.4; } .table-box { overflow-x: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-card); -webkit-overflow-scrolling: touch; } table { width: 100%; min-width: 540px; border-collapse: collapse; color: var(--text-primary); font-size: 12px; text-align: left; } th { position: sticky; top: 0; z-index: 1; padding: 10px 12px; color: var(--text-secondary); background: var(--bg-table-header); border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 600; letter-spacing: 0.02em; white-space: nowrap; } th.num-th { text-align: right; } td { padding: 10px 12px; border-bottom: 1px solid var(--border-subtle); vertical-align: middle; white-space: nowrap; color: var(--text-primary); } tbody tr:last-child td { border-bottom: none; } tbody tr:hover { background: var(--bg-table-row-hover); } tfoot tr { background: var(--bg-table-footer); border-top: 2px solid var(--border); } tfoot td { padding: 10px 12px; font-weight: 600; color: var(--text-primary); } .num { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; font-size: 12px; text-align: right; } .money { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; color: var(--accent-emerald); font-weight: 600; text-align: right; } .model-cell { display: flex; align-items: center; gap: 6px; } .model-color-indicator { width: 3px; height: 16px; border-radius: 2px; flex-shrink: 0; } .model-pill { max-width: 150px; padding: 2px 7px; overflow: hidden; color: var(--accent-purple-text); background: var(--accent-purple-subtle); border: 1px solid var(--accent-purple-border); text-overflow: ellipsis; vertical-align: middle; } .speed-pill { padding: 1px 5px; color: var(--accent-amber-text); background: var(--accent-amber-subtle); border: 1px solid var(--accent-amber-border); font-size: 10px; } .estimate-pill { padding: 1px 5px; color: var(--accent-blue-text); background: var(--accent-blue-subtle); border: 1px solid var(--accent-blue-border); font-size: 10px; } .fallback { color: var(--accent-amber); } .client-label { padding: 2px 8px; color: var(--text-secondary); background: var(--bg-tag); border: 1px solid var(--border-strong); } .user-name { display: block; max-width: 180px; overflow: hidden; text-overflow: ellipsis; font-weight: 500; color: var(--text-primary); } .user-email { display: block; max-width: 180px; overflow: hidden; color: var(--text-muted); font-size: 11px; text-overflow: ellipsis; margin-top: 1px; } .date-cell { display: flex; align-items: center; gap: 6px; font-weight: 500; } .expand-icon { display: inline-block; font-size: 9px; color: var(--text-muted); transition: transform 0.15s ease; line-height: 1; } .daily-row { cursor: pointer; user-select: none; } .daily-row.is-open { background: var(--bg-table-row-hover); font-weight: 500; } .daily-row.is-open .expand-icon { color: var(--brand-green); } .details-row td { padding: 0; white-space: normal; background: var(--bg-details); } .day-details { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--border); } .day-model { min-width: 0; padding: 8px 10px; border: 1px solid var(--border); border-left: 3px solid var(--accent-purple); background: var(--bg-details-card); border-radius: 6px; font-size: 11px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); } .day-model-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 3px; } .day-model-header strong { font-size: 12px; color: var(--text-primary); } .speed-tag { font-size: 10px; color: var(--text-muted); background: var(--bg-tag); padding: 1px 4px; border-radius: 3px; } .day-model-body { display: flex; align-items: center; justify-content: space-between; color: var(--text-secondary); } .notice { margin: 0; padding: 12px 14px; border: 1px solid var(--accent-blue-border); border-radius: 8px; color: var(--accent-blue-text); background: var(--accent-blue-subtle); font-size: 12px; line-height: 1.5; } .error-banner { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 14px; padding: 12px 16px; color: var(--accent-red-text); background: var(--accent-red-subtle); border: 1px solid var(--accent-red-border); border-radius: 8px; font-size: 12px; line-height: 1.5; } .error-icon { font-size: 16px; line-height: 1.2; flex-shrink: 0; } .error-body { flex: 1; min-width: 0; } .error-title { font-weight: 700; margin-bottom: 2px; } .error-text { word-break: break-word; white-space: pre-wrap; } .empty { padding: 24px 16px; color: var(--text-muted); font-size: 12px; text-align: center; line-height: 1.5; } .empty-state { padding: 48px 24px; text-align: center; } .empty-icon { font-size: 38px; margin-bottom: 12px; opacity: 0.65; } .empty-title { font-size: 15px; font-weight: 700; color: var(--text-primary); margin-bottom: 6px; } .empty-desc { font-size: 12px; color: var(--text-muted); max-width: 320px; margin: 0 auto; line-height: 1.5; } .loading-wrap { display: flex; flex-direction: column; gap: 14px; padding: 4px 0 10px; } .skeleton-layout { width: 100%; } .skeleton { background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 37%, #f1f5f9 63%); background-size: 400% 100%; animation: skeletonShimmer 1.4s ease infinite; border-radius: var(--radius-card); border: 1px solid var(--border); } @media (prefers-color-scheme: dark) { .skeleton { background: linear-gradient(90deg, #23232b 25%, #2d2d38 37%, #23232b 63%); background-size: 400% 100%; border-color: var(--border); } } @keyframes skeletonShimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } } .card-sk { height: 110px; min-height: 110px; } .quota-sk { height: 125px; min-height: 125px; } .table-sk-sm { height: 175px; min-height: 175px; } .table-sk-lg { height: 250px; min-height: 250px; } .loading-text { display: flex; align-items: center; justify-content: center; gap: 8px; color: var(--text-secondary); font-size: 12px; margin-top: 4px; } .content-loading-overlay { position: absolute; top: 0; left: 0; right: 0; z-index: 10; display: flex; flex-direction: column; align-items: center; padding-top: 12px; pointer-events: none; background: transparent; } .content-loading-overlay[hidden] { display: none !important; } .loading-progress-line { position: absolute; top: 0; left: 0; right: 0; height: 2.5px; background: linear-gradient(90deg, transparent, var(--brand-green), transparent); background-size: 200% 100%; animation: progressSlide 1.2s infinite linear; } @keyframes progressSlide { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } .content-loading-status { display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px; border: 1px solid var(--brand-green-border); border-radius: var(--radius-pill); background: var(--bg-card); color: var(--brand-green-text); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); font-size: 12px; font-weight: 600; animation: statusFadeIn 0.2s ease-out; } @keyframes statusFadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } .spinner { width: 14px; height: 14px; border: 2px solid var(--brand-green-border); border-top-color: var(--brand-green); border-radius: 50%; animation: spin 0.7s linear infinite; } .btn-spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid rgba(255, 255, 255, 0.4); border-top-color: #ffffff; border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: middle; } @keyframes spin { to { transform: rotate(360deg); } } .footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border); } .export-group { display: flex; align-items: center; gap: 8px; } .footer-btn { height: 36px; min-height: 36px; padding: 0 14px; background: var(--bg-card); border: 1px solid var(--border-strong); border-radius: var(--radius-btn); color: var(--text-secondary); font-size: 12px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 4px; transition: all 0.15s ease; } .footer-btn:hover { color: var(--text-primary); background: var(--bg-card-hover); border-color: var(--text-muted); } .footer-btn:active { transform: scale(0.97); } .footer-btn.refresh { background: var(--brand-green); color: #ffffff; border-color: var(--brand-green); font-weight: 600; } .footer-btn.refresh:hover { background: var(--brand-green-hover); border-color: var(--brand-green-hover); } .raw-json { width: 100%; margin-top: 6px; color: var(--text-muted); font-size: 11px; } .raw-json summary { color: var(--text-secondary); cursor: pointer; font-weight: 500; user-select: none; } pre { max-height: 250px; margin: 8px 0 0; padding: 10px 12px; overflow: auto; color: var(--text-secondary); background: var(--bg-pre); border: 1px solid var(--border); border-radius: 8px; font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: pre; } button:focus-visible, input:focus-visible, summary:focus-visible { outline: 2px solid var(--brand-green); outline-offset: 2px; } @media (prefers-reduced-motion: reduce) { *, ::before, ::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } /* Breakpoint: Wide Screens (>= 1100px) - Academic Paper Continuous Two-Column Layout */ @media (min-width: 1100px) { .two-column-layout { display: grid; grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr); gap: 20px; align-items: start; } .column-left { border-right: 1px solid var(--border-subtle); padding-right: 20px; } .range-bar { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; } .date-fields { display: flex; align-items: flex-end; gap: 10px; flex: 1 1 auto; } .field-group { width: 165px; } .presets-row { margin-top: 0; gap: 12px; flex: 0 0 auto; } } /* Breakpoint 1: Medium Screens (<= 900px) */ @media (max-width: 900px) { .range-bar { padding: 12px 18px; } .panel-body { padding: 16px 18px; } } /* Breakpoint 2: Small Screens / Mobile (<= 640px) */ @media (max-width: 640px) { .modal-backdrop { padding: max(8px, env(safe-area-inset-top, 8px)) max(8px, env(safe-area-inset-right, 8px)) max(8px, env(safe-area-inset-bottom, 8px)) max(8px, env(safe-area-inset-left, 8px)); } .modal-card { width: 100%; max-height: 94vh; max-height: min(94vh, 94dvh); border-radius: 12px; } .header { padding: 12px 14px; gap: 8px; } .header-title { font-size: 15px; } .title-text { font-size: 15px; } .header-delay-pill { font-size: 10px; padding: 2px 7px; } .range-bar { padding: 12px 14px; } .date-fields { grid-template-columns: 1fr; gap: 8px; } .primary-btn { width: 100%; min-height: 42px; font-size: 14px; } .presets-row { flex-direction: column; align-items: stretch; gap: 8px; } .presets { flex-wrap: wrap; justify-content: flex-start; } .mini-btn { min-height: 36px; padding: 0 12px; font-size: 12px; } .view-switch { width: 100%; display: flex; } .view-switch-btn { flex: 1; min-height: 36px; font-size: 12px; } .kpi-grid-left, .quota-estimate-cards { grid-template-columns: 1fr; gap: 10px; } .summary-card { min-height: auto; padding: 14px; } .panel-body { padding: 14px; } .section { padding: 14px; } .footer { flex-direction: column; align-items: stretch; gap: 10px; } .export-group { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .export-group .footer-btn { width: 100%; min-height: 40px; } .footer-btn.refresh { width: 100%; min-height: 42px; font-size: 13px; } } /* Breakpoint 3: Extra Narrow Screens (<= 420px) */ @media (max-width: 420px) { .modal-backdrop { padding: 4px; } .modal-card { max-height: 98vh; max-height: min(98vh, 98dvh); border-radius: 10px; } .header { padding: 10px 12px; } .title-text { font-size: 14px; } .range-bar { padding: 10px 12px; } .panel-body { padding: 12px 10px 18px; } .section { padding: 12px 10px; } th, td { padding: 8px 9px; font-size: 11px; } .model-pill { max-width: 110px; font-size: 10px; } .card-value, .estimate-card-value { font-size: 19px; } .day-details { grid-template-columns: 1fr; padding: 8px 10px; } } `; const HTML_ESCAPE = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (ch) => HTML_ESCAPE[ch]); const asArray = (value) => (Array.isArray(value) ? value : []); const hasOwn = (obj, key) => Boolean(obj && Object.prototype.hasOwnProperty.call(obj, key)); const n = (value) => { if (typeof value === 'string') value = value.replace(/,/g, '').trim(); const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; }; const clamp = (value, min = 0, max = 1) => Math.min(max, Math.max(min, n(value))); const trimFixed = (value, digits = 2) => n(value).toFixed(digits).replace(/\.?0+$/, ''); const pad2 = (value) => String(value).padStart(2, '0'); const isUnavailable = (value) => { if (value === null || value === undefined || value === '') return true; if (typeof value === 'boolean') return true; if (typeof value === 'string') { const cleaned = value.replace(/,/g, '').trim(); if (!cleaned) return true; return !Number.isFinite(Number(cleaned)); } return !Number.isFinite(Number(value)); }; const fmtNum = (value) => { if (isUnavailable(value)) return '—'; const number = typeof value === 'string' ? Number(value.replace(/,/g, '').trim()) : Number(value); const absolute = Math.abs(number); const sign = number < 0 ? '-' : ''; if (absolute >= 1e12) return `${sign}${trimFixed(absolute / 1e12)}T`; if (absolute >= 1e9) return `${sign}${trimFixed(absolute / 1e9)}B`; if (absolute >= 1e6) return `${sign}${trimFixed(absolute / 1e6)}M`; if (absolute >= 1e3) return `${sign}${trimFixed(absolute / 1e3)}K`; return number.toLocaleString('en-US'); }; const fmtFullNum = (value) => { if (isUnavailable(value)) return '—'; const number = typeof value === 'string' ? Number(value.replace(/,/g, '').trim()) : Number(value); return number.toLocaleString('en-US'); }; const fmtUsd = (value) => { if (isUnavailable(value)) return '—'; const number = typeof value === 'string' ? Number(value.replace(/,/g, '').trim()) : Number(value); return `$${number.toFixed(2)}`; }; const fmtTurns = (value) => { if (isUnavailable(value)) return '—'; const number = typeof value === 'string' ? Number(value.replace(/,/g, '').trim()) : Number(value); return Math.abs(number - Math.round(number)) < 1e-9 ? String(Math.round(number)) : number.toFixed(1); }; const hasNumericField = (obj, key) => hasOwn(obj, key) && obj[key] !== null && obj[key] !== undefined && typeof obj[key] !== 'boolean' && Number.isFinite(Number(obj[key])); const formatPercentFromRatio = (ratio) => { const percent = clamp(ratio) * 100; return `${percent >= 10 ? percent.toFixed(1) : percent.toFixed(2)}%`; }; const formatDuration = (seconds) => { const days = n(seconds) / 86400; if (!days) return '当前配额周期'; return days >= 1 ? `${trimFixed(days, 1)} 天周期` : `${trimFixed(n(seconds) / 3600, 1)} 小时周期`; }; const toEpochMs = (value) => { if (typeof value === 'string') { const numeric = Number(value.trim()); if (Number.isFinite(numeric) && numeric > 0) return numeric > 1e12 ? numeric : numeric * 1000; const parsed = Date.parse(value); return Number.isFinite(parsed) ? parsed : NaN; } const numeric = n(value); return numeric ? (numeric > 1e12 ? numeric : numeric * 1000) : NaN; }; const formatDateTime = (ms) => { const date = new Date(ms); if (!Number.isFinite(date.getTime())) return '—'; return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); }; const dateKeyFromMs = (ms) => { const date = new Date(ms); if (!Number.isFinite(date.getTime())) return ''; if (CONFIG.USE_UTC_DAY) return date.toISOString().slice(0, 10); return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`; }; const dayStartMs = (dateKey) => { const [year, month, day] = String(dateKey || '').slice(0, 10).split('-').map(Number); if (!year || !month || !day) return NaN; return CONFIG.USE_UTC_DAY ? Date.UTC(year, month - 1, day) : new Date(year, month - 1, day).getTime(); }; const addDays = (dateKey, days) => { const ms = dayStartMs(dateKey); return Number.isFinite(ms) ? dateKeyFromMs(ms + days * CONFIG.DAY_MS) : ''; }; const validateRange = (startDate, endDate) => { if (!startDate || !endDate) throw new Error('请同时选择开始日期和结束日期。'); const startMs = dayStartMs(startDate); const endMs = dayStartMs(endDate); if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) throw new Error('日期格式无效。'); if (startMs > endMs) throw new Error('开始日期不能晚于结束日期。'); if (Math.floor((endMs - startMs) / CONFIG.DAY_MS) + 1 > CONFIG.MAX_RANGE_DAYS) { throw new Error(`单次查询范围不能超过 ${CONFIG.MAX_RANGE_DAYS} 天。`); } }; const usedRatioFromWindow = (window) => { if (!window || window.available === false) return 0; for (const key of ['used_ratio', 'usedRatio', 'fraction_used']) { if (hasOwn(window, key) && window[key] !== null && window[key] !== undefined && typeof window[key] !== 'boolean' && Number.isFinite(Number(window[key]))) { return clamp(Number(window[key])); } } for (const key of ['used_percent', 'usedPercent']) { if (hasOwn(window, key) && window[key] !== null && window[key] !== undefined && typeof window[key] !== 'boolean' && Number.isFinite(Number(window[key]))) { return clamp(Number(window[key]) / 100); } } const limit = window.limit ?? window.credit_limit ?? window.credits_limit ?? window.max; const used = window.used ?? window.used_credits ?? window.consumed ?? window.consumed_credits; const remaining = window.remaining ?? window.remaining_credits; if (limit && Number.isFinite(Number(limit)) && Number(limit) > 0) { if (used !== null && used !== undefined && typeof used !== 'boolean' && Number.isFinite(Number(used))) { return clamp(Number(used) / Number(limit)); } if (remaining !== null && remaining !== undefined && typeof remaining !== 'boolean' && Number.isFinite(Number(remaining))) { return clamp((Number(limit) - Number(remaining)) / Number(limit)); } } return 0; }; const usedPercentTextFromWindow = (window) => { if (!window || window.available === false) return '未知'; for (const key of ['used_percent', 'usedPercent']) { if (hasOwn(window, key) && window[key] !== null && window[key] !== undefined && typeof window[key] !== 'boolean' && Number.isFinite(Number(window[key]))) { return `${trimFixed(Number(window[key]))}%`; } } const hasRatio = ['used_ratio', 'usedRatio', 'fraction_used'].some((k) => hasOwn(window, k) && window[k] !== null && window[k] !== undefined && typeof window[k] !== 'boolean' && Number.isFinite(Number(window[k]))); if (hasRatio) { const ratio = usedRatioFromWindow(window); return formatPercentFromRatio(ratio); } return '未知'; }; const quotaValue = (window, keys) => { for (const key of keys) { if (window && Object.prototype.hasOwnProperty.call(window, key) && window[key] !== null && window[key] !== undefined) { if (typeof window[key] === 'boolean') continue; return window[key]; } } return null; }; const getWindowDurationSeconds = (window) => { if (!window) return 0; for (const key of ['limit_window_seconds', 'window_seconds', 'duration_seconds']) { if (hasOwn(window, key) && window[key] !== null && window[key] !== undefined && typeof window[key] !== 'boolean' && Number.isFinite(Number(window[key])) && Number(window[key]) > 0) { return Number(window[key]); } } return 0; }; const personalCycleCostEstimate = () => null; const isCodexAutoReviewModel = (name) => String(name || '').trim().toLowerCase() === 'codex-auto-review'; const isImageModel = (name) => { const m = String(name || '').trim().toLowerCase(); return m === 'gpt-image-2' || m === 'image2'; }; const getModelDisplayName = (item = {}) => String(item.model || item.model_id || item.model_name || item.name || item.id || 'unknown').trim(); const normalizeSpeed = (value, fallback = 'standard') => String(value || fallback).trim().toLowerCase(); const estimateModelUsd = (row = {}) => row?.estimatedUsd !== undefined ? row.estimatedUsd : null; const formatModelCostCalculation = (row = {}) => { if (row && typeof row.calculation === 'string' && row.calculation.trim()) { return row.calculation; } return row?.estimatedUsd !== null && row?.estimatedUsd !== undefined ? `预估: ${fmtUsd(row.estimatedUsd)}` : ''; }; const tokenTotal = (obj = {}) => { if (!obj || typeof obj !== 'object') return null; if (hasOwn(obj, 'tokens')) { if (obj.tokens === null || obj.tokens === undefined) return null; const parsed = Number(obj.tokens); return Number.isFinite(parsed) ? parsed : null; } if (hasOwn(obj, 'text_total_tokens')) { if (obj.text_total_tokens === null || obj.text_total_tokens === undefined) return null; const parsed = Number(obj.text_total_tokens); return Number.isFinite(parsed) ? parsed : null; } const uncached = obj.uncachedInputTokens ?? obj.uncached_text_input_tokens; const cached = obj.cachedInputTokens ?? obj.cached_text_input_tokens; const output = obj.outputTokens ?? obj.output_tokens ?? obj.text_output_tokens; const uNum = Number(uncached); const cNum = Number(cached); const oNum = Number(output); if (Number.isFinite(uNum) && Number.isFinite(cNum) && Number.isFinite(oNum)) { return uNum + cNum + oNum; } return null; }; let snapshot = (typeof options.getSnapshot === 'function' ? options.getSnapshot() : null) || {}; const state = { shadowRoot: shadow, shadowEventsBound: false, loading: false, error: '', accountMode: null, viewMode: 'personal', selectedStartDate: '', selectedEndDate: '', quotaWindow: null, cycleStartMs: NaN, cycleEndMs: NaN, expandedDates: new Set(), }; const syncStateFromSnapshot = () => { state.loading = (snapshot.status === 'loading'); state.error = snapshot.error || ''; state.accountMode = snapshot.accountMode || null; state.viewMode = snapshot.viewMode || 'personal'; const newStart = snapshot.range?.startDate || ''; const newEnd = snapshot.range?.endDate || ''; if (newStart !== state.selectedStartDate || newEnd !== state.selectedEndDate) { dateInputDirty = false; localStartDate = newStart; localEndDate = newEnd; state.selectedStartDate = newStart; state.selectedEndDate = newEnd; } // ALWAYS reset cycle start/end to NaN before mapping every snapshot state.cycleStartMs = NaN; state.cycleEndMs = NaN; if (snapshot.quota && typeof snapshot.quota === 'object') { const q = snapshot.quota; const isAvail = q.available !== false; state.quotaWindow = { _window_name: '配额窗口', available: q.available, duration_seconds: q.durationSeconds, limit_window_seconds: q.durationSeconds, usedRatio: isAvail && q.usedRatio !== undefined && q.usedRatio !== null ? q.usedRatio : null, used_ratio: isAvail && q.usedRatio !== undefined && q.usedRatio !== null ? q.usedRatio : null, remaining: isAvail && q.remaining !== undefined && q.remaining !== null && Number.isFinite(Number(q.remaining)) ? Number(q.remaining) : null, limit: isAvail && q.limit !== undefined && q.limit !== null && Number.isFinite(Number(q.limit)) ? Number(q.limit) : null, resetAt: q.resetAt || null, reset_at: q.resetAt || null, cycleStartDate: q.cycleStartDate || null, cycleEndDate: q.cycleEndDate || null, }; if (q.cycleStartDate) { state.cycleStartMs = dayStartMs(q.cycleStartDate); } else if (q.resetAt && q.durationSeconds) { const resetMs = toEpochMs(q.resetAt); if (Number.isFinite(resetMs)) { state.cycleStartMs = resetMs - (q.durationSeconds * 1000); } } if (q.cycleEndDate) { state.cycleEndMs = dayStartMs(q.cycleEndDate); } else if (q.resetAt) { state.cycleEndMs = toEpochMs(q.resetAt); } } else { state.quotaWindow = null; } }; syncStateFromSnapshot(); const sanitizeSnapshotForDebug = (snap) => { if (!snap || typeof snap !== 'object') return {}; const safe = {}; const allowed = [ 'status', 'error', 'accountMode', 'viewMode', 'range', 'quota', 'summary', 'models', 'daily', 'clients', 'modelActivity', 'notices', 'updatedAt' ]; for (const key of allowed) { if (hasOwn(snap, key)) { safe[key] = snap[key]; } } return safe; }; const hasVisibleData = () => { if (!snapshot) return false; if (Array.isArray(snapshot.daily) && snapshot.daily.length > 0) return true; if (Array.isArray(snapshot.models) && snapshot.models.length > 0) return true; if (snapshot.summary && (snapshot.summary.tokens !== null || snapshot.summary.turns !== null || snapshot.summary.estimatedUsd !== null)) { if (Number(snapshot.summary.tokens) >= 0 || Number(snapshot.summary.turns) >= 0 || Number(snapshot.summary.estimatedUsd) >= 0) return true; } return false; }; const getVisibleData = () => { const isTeamView = state.viewMode === 'team'; const modelRows = asArray(snapshot.models).map((m) => ({ name: m.name, speed: m.speed || 'standard', tokens: m.tokens !== undefined ? m.tokens : null, uncachedInputTokens: m.uncachedInputTokens !== undefined ? m.uncachedInputTokens : null, cachedInputTokens: m.cachedInputTokens !== undefined ? m.cachedInputTokens : null, outputTokens: m.outputTokens !== undefined ? m.outputTokens : null, estimatedUsd: m.estimatedUsd !== undefined ? m.estimatedUsd : null, hasEstimatedAllocation: Boolean(m.estimatedAllocation), hasRateFallback: Boolean(m.fallbackPricing), usesFallbackPricing: Boolean(m.fallbackPricing), hasImageFieldFallback: Boolean(m.incompleteImage), calculation: m.calculation || null, })); const modelSummary = { rows: modelRows, hasEstimatedAllocation: modelRows.some((r) => r.hasEstimatedAllocation), hasFallbackPricing: modelRows.some((r) => r.usesFallbackPricing), }; const dailyBreakdown = asArray(snapshot.daily).map((d) => { const dayModels = asArray(d.models).map((m) => ({ name: m.name, model: m.name, speed: m.speed || 'standard', tokens: m.tokens !== undefined ? m.tokens : null, uncachedInputTokens: m.uncachedInputTokens !== undefined ? m.uncachedInputTokens : null, cachedInputTokens: m.cachedInputTokens !== undefined ? m.cachedInputTokens : null, outputTokens: m.outputTokens !== undefined ? m.outputTokens : null, estimatedUsd: m.estimatedUsd !== undefined ? m.estimatedUsd : null, calculation: m.calculation || null, hasEstimatedAllocation: Boolean(m.estimatedAllocation), hasRateFallback: Boolean(m.fallbackPricing), usesFallbackPricing: Boolean(m.fallbackPricing), hasImageFieldFallback: Boolean(m.incompleteImage), })); const dateStr = d.date || ''; const sortTs = dayStartMs(dateStr) || 0; const hasTeamTokenModels = dayModels.length > 0 && dayModels.some((m) => (m.tokens !== null && m.tokens !== undefined && Number.isFinite(Number(m.tokens)))); return { _sortTs: sortTs, _dateKey: dateStr, _displayDate: dateStr, _tokenModels: dayModels, _hasTeamTokenModels: hasTeamTokenModels, _estimatedUsd: d.estimatedUsd !== undefined ? d.estimatedUsd : null, tokens: d.tokens !== undefined ? d.tokens : null, uncachedInputTokens: d.uncachedInputTokens !== undefined ? d.uncachedInputTokens : null, cachedInputTokens: d.cachedInputTokens !== undefined ? d.cachedInputTokens : null, outputTokens: d.outputTokens !== undefined ? d.outputTokens : null, turns: d.turns !== undefined ? d.turns : null, threads: d.threads !== undefined ? d.threads : null, credits: d.credits !== undefined ? d.credits : null, totals: { tokens: d.tokens !== undefined ? d.tokens : null, turns: d.turns !== undefined ? d.turns : null, threads: d.threads !== undefined ? d.threads : null, }, }; }); const summary = { tokens: snapshot.summary?.tokens !== undefined ? snapshot.summary.tokens : null, uncachedInputTokens: snapshot.summary?.uncachedInputTokens !== undefined ? snapshot.summary.uncachedInputTokens : null, cachedInputTokens: snapshot.summary?.cachedInputTokens !== undefined ? snapshot.summary.cachedInputTokens : null, outputTokens: snapshot.summary?.outputTokens !== undefined ? snapshot.summary.outputTokens : null, turns: snapshot.summary?.turns !== undefined ? snapshot.summary.turns : null, threads: snapshot.summary?.threads !== undefined ? snapshot.summary.threads : null, credits: snapshot.summary?.credits !== undefined ? snapshot.summary.credits : null, estimatedUsd: snapshot.summary?.estimatedUsd !== undefined ? snapshot.summary.estimatedUsd : null, activeMembersPeak: snapshot.summary?.activeMembersPeak !== undefined ? snapshot.summary.activeMembersPeak : null, activeMemberDays: null, }; const clientStats = asArray(snapshot.clients).map((c) => ({ name: c.name || '未知', tokens: c.tokens !== undefined ? c.tokens : null, turns: c.turns !== undefined ? c.turns : null, credits: c.credits !== undefined ? c.credits : null, })); const teamModelActivity = asArray(snapshot.modelActivity).map((ma) => ({ name: ma.name, turns: ma.turns !== undefined ? ma.turns : null, threads: ma.threads !== undefined ? ma.threads : null, credits: ma.credits !== undefined ? ma.credits : null, activeMembersPeak: ma.activeMembersPeak !== undefined ? ma.activeMembersPeak : null, })); return { generatedAt: snapshot.updatedAt || new Date().toISOString(), accountMode: state.accountMode, viewMode: state.viewMode, selectedRange: snapshot.range || { startDate: '', endDate: '' }, quota: snapshot.quota, summary, currentUserSummary: isTeamView ? null : summary, currentUserModelSummary: modelSummary, teamModelSummary: isTeamView ? modelSummary : null, teamModelActivity, clientStats, dailyBreakdown, }; }; // Exact verbatim render routines const renderQuotaCard = () => { const window = state.quotaWindow; const ratio = usedRatioFromWindow(window); const percent = usedPercentTextFromWindow(window); const className = ratio >= .85 ? 'danger' : ratio >= .6 ? 'warn' : 'ok'; const remaining = quotaValue(window, ['remaining', 'remaining_credits', 'available', 'available_credits']); const hasUsedPercentOrRatio = ['used_ratio', 'usedRatio', 'fraction_used', 'used_percent', 'usedPercent'] .some((key) => hasNumericField(window, key)); const remainingText = remaining !== null ? `剩余: ${escapeHtml(fmtNum(remaining))}` : hasUsedPercentOrRatio ? `剩余约 ${escapeHtml(formatPercentFromRatio(1 - ratio))}(配额比例)` : '剩余配额比例: 未知'; const reset = quotaValue(window, ['reset_at', 'resetAt', 'resets_at', 'end_at', 'window_end']); const cycleEstimate = personalCycleCostEstimate(); const estimateCardsHtml = cycleEstimate ? `
| 模型 | Token 总数 | 未缓存输入 | 已缓存输入 | 输出 | 预估费用 |
|---|---|---|---|---|---|
|
${escapeHtml(row.name)}
${row.speed !== 'standard' ? `${escapeHtml(row.speed === 'fast' ? 'fast' : row.speed)}` : ''}
${row.hasEstimatedAllocation ? `${row.hasRateFallback ? '兜底估算' : '折算分摊'}` : ''}
|
${fmtNum(row.tokens)} | ${fmtNum(row.uncachedInputTokens)} | ${fmtNum(row.cachedInputTokens)} | ${fmtNum(row.outputTokens)} | ${fmtUsd(row.estimatedUsd)}${row.usesFallbackPricing ? '*' : ''} |
| 模型 | 对话轮数 | 主题数 | 日活成员峰值 |
|---|---|---|---|
| ${escapeHtml(row.name)} | ${fmtTurns(row.turns)} | ${fmtTurns(row.threads)} | ${fmtNum(row.activeMembersPeak)} |
| 客户端 | Token 数 | 对话轮数 | 额度 (Credits) |
|---|---|---|---|
| ${escapeHtml(row.name)} | ${fmtNum(row.tokens)} | ${fmtTurns(row.turns)} | ${fmtNum(row.credits)} |
| 日期 | Token 数 | 轮数 | 主题数 | ${isTeamView ? '费用' : '预估费用'} |
|---|---|---|---|---|
|
${escapeHtml(row._displayDate)}
|
${fmtNum(tokenTotal(totals))} | ${fmtTurns(totals.turns)} | ${fmtTurns(totals.threads)} | ${isTeamView && !row._hasTeamTokenModels ? '不可用' : fmtUsd(row._estimatedUsd)} |
|
${models.length ? models.map((model) => `
${escapeHtml(getModelDisplayName(model))}
${escapeHtml(normalizeSpeed(model.speed) === 'fast' ? 'fast' : '标准')}
${fmtNum(tokenTotal(model))} Token
${fmtUsd(estimateModelUsd(model))}
该日期暂无模型 Token 明细数据。 '}
|
||||
| 所选范围内无每日明细数据。 | ||||
| 合计 | ${fmtNum(stats.tokens)} | ${fmtTurns(stats.turns)} | ${fmtTurns(stats.threads)} | ${isTeamView && !rows.some((row) => row._hasTeamTokenModels) ? '不可用' : fmtUsd(stats.estimatedUsd)} |