codex额度查看工具开发完成1.0版本
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { closeSync, existsSync, fstatSync, openSync, readSync, readdirSync, statSync, watch } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
export const SHORT_WINDOW_MINUTES = 300
|
||||
export const WEEK_WINDOW_MINUTES = 10080
|
||||
|
||||
const MAX_FILES_TO_SCAN = 160
|
||||
const TAIL_BYTES_PER_FILE = 2 * 1024 * 1024
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
|
||||
export function parseUsageLine(line, sourceFile = '') {
|
||||
if (!line.includes('"rate_limits"') || !line.includes('"token_count"')) return []
|
||||
|
||||
try {
|
||||
const root = JSON.parse(line)
|
||||
if (root?.type !== 'event_msg' || root?.payload?.type !== 'token_count') return []
|
||||
|
||||
const observedAt = Number.isNaN(Date.parse(root.timestamp))
|
||||
? Date.now()
|
||||
: Date.parse(root.timestamp)
|
||||
|
||||
return ['primary', 'secondary'].flatMap((key) => {
|
||||
const window = root.payload.rate_limits?.[key]
|
||||
const windowMinutes = Number(window?.window_minutes)
|
||||
const usedPercent = Number(window?.used_percent)
|
||||
const resetsAtSeconds = Number(window?.resets_at)
|
||||
|
||||
if (![SHORT_WINDOW_MINUTES, WEEK_WINDOW_MINUTES].includes(windowMinutes)) return []
|
||||
if (![usedPercent, resetsAtSeconds].every(Number.isFinite)) return []
|
||||
|
||||
return [{
|
||||
windowMinutes,
|
||||
usedPercent: clamp(usedPercent, 0, 100),
|
||||
remainingPercent: clamp(100 - usedPercent, 0, 100),
|
||||
resetsAt: resetsAtSeconds * 1000,
|
||||
observedAt,
|
||||
sourceFile,
|
||||
}]
|
||||
})
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function collectRolloutFiles(rootPath) {
|
||||
const results = []
|
||||
const pending = [rootPath]
|
||||
|
||||
while (pending.length > 0) {
|
||||
const directory = pending.pop()
|
||||
let entries
|
||||
|
||||
try {
|
||||
entries = readdirSync(directory, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(directory, entry.name)
|
||||
if (entry.isDirectory()) pending.push(fullPath)
|
||||
else if (entry.isFile() && /^rollout-.*\.jsonl$/i.test(entry.name)) results.push(fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
.map((filePath) => {
|
||||
try {
|
||||
return { filePath, modifiedAt: statSync(filePath).mtimeMs }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b.modifiedAt - a.modifiedAt)
|
||||
.slice(0, MAX_FILES_TO_SCAN)
|
||||
}
|
||||
|
||||
function readFileTail(filePath) {
|
||||
let descriptor
|
||||
|
||||
try {
|
||||
descriptor = openSync(filePath, 'r')
|
||||
const size = fstatSync(descriptor).size
|
||||
const start = Math.max(0, size - TAIL_BYTES_PER_FILE)
|
||||
const length = size - start
|
||||
const buffer = Buffer.alloc(length)
|
||||
readSync(descriptor, buffer, 0, length, start)
|
||||
|
||||
const lines = buffer.toString('utf8').split(/\r?\n/).filter(Boolean)
|
||||
if (start > 0) lines.shift()
|
||||
return lines
|
||||
} catch {
|
||||
return []
|
||||
} finally {
|
||||
if (descriptor !== undefined) closeSync(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
export function readLatestUsage(sessionsPath) {
|
||||
if (!existsSync(sessionsPath)) {
|
||||
return {
|
||||
shortWindow: null,
|
||||
weekWindow: null,
|
||||
status: '未找到 Codex 会话目录',
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
let shortWindow = null
|
||||
let weekWindow = null
|
||||
|
||||
for (const { filePath, modifiedAt } of collectRolloutFiles(sessionsPath)) {
|
||||
for (const line of readFileTail(filePath)) {
|
||||
for (const snapshot of parseUsageLine(line, filePath)) {
|
||||
if (snapshot.windowMinutes === SHORT_WINDOW_MINUTES &&
|
||||
(!shortWindow || snapshot.observedAt > shortWindow.observedAt)) {
|
||||
shortWindow = snapshot
|
||||
}
|
||||
|
||||
if (snapshot.windowMinutes === WEEK_WINDOW_MINUTES &&
|
||||
(!weekWindow || snapshot.observedAt > weekWindow.observedAt)) {
|
||||
weekWindow = snapshot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shortWindow && weekWindow &&
|
||||
modifiedAt < shortWindow.observedAt && modifiedAt < weekWindow.observedAt) break
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
if (shortWindow?.resetsAt <= now) shortWindow = null
|
||||
if (weekWindow?.resetsAt <= now) weekWindow = null
|
||||
|
||||
return {
|
||||
shortWindow,
|
||||
weekWindow,
|
||||
status: shortWindow || weekWindow ? '已从本地日志同步' : '本地日志中暂无额度快照',
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
export class UsageReader extends EventEmitter {
|
||||
constructor(sessionsPath) {
|
||||
super()
|
||||
this.sessionsPath = sessionsPath
|
||||
this.fileWatcher = null
|
||||
this.refreshTimer = null
|
||||
this.safetyTimer = null
|
||||
}
|
||||
|
||||
readLatest() {
|
||||
return readLatestUsage(this.sessionsPath)
|
||||
}
|
||||
|
||||
refresh() {
|
||||
const snapshot = this.readLatest()
|
||||
this.emit('snapshot', snapshot)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
requestRefresh() {
|
||||
clearTimeout(this.refreshTimer)
|
||||
this.refreshTimer = setTimeout(() => this.refresh(), 250)
|
||||
}
|
||||
|
||||
start() {
|
||||
if (existsSync(this.sessionsPath)) {
|
||||
try {
|
||||
this.fileWatcher = watch(this.sessionsPath, { recursive: true }, (_event, fileName) => {
|
||||
if (!fileName || /(^|[\\/])rollout-.*\.jsonl$/i.test(fileName)) this.requestRefresh()
|
||||
})
|
||||
this.fileWatcher.on('error', () => this.requestRefresh())
|
||||
} catch {
|
||||
this.fileWatcher = null
|
||||
}
|
||||
}
|
||||
|
||||
this.safetyTimer = setInterval(() => this.requestRefresh(), 15_000)
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearTimeout(this.refreshTimer)
|
||||
clearInterval(this.safetyTimer)
|
||||
this.fileWatcher?.close()
|
||||
this.fileWatcher = null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user