cc / scripts /test-billing-events.js
hequ's picture
Upload 224 files
75031b4 verified
#!/usr/bin/env node
/**
* 计费事件测试脚本
*
* 用于测试计费事件的发布和消费功能
*
* 使用方法:
* node scripts/test-billing-events.js [command]
*
* 命令:
* publish - 发布测试事件
* consume - 消费事件(测试模式)
* info - 查看队列状态
* clear - 清空队列(危险操作)
*/
const path = require('path')
const Redis = require('ioredis')
// 加载配置
require('dotenv').config({ path: path.join(__dirname, '../.env') })
const config = {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD || '',
db: parseInt(process.env.REDIS_DB) || 0
}
const redis = new Redis(config)
const STREAM_KEY = 'billing:events'
// ========================================
// 命令实现
// ========================================
/**
* 发布测试事件
*/
async function publishTestEvent() {
console.log('📤 Publishing test billing event...')
const testEvent = {
eventId: `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
eventType: 'usage.recorded',
timestamp: new Date().toISOString(),
version: '1.0',
apiKey: {
id: 'test-key-123',
name: 'Test API Key',
userId: 'test-user-456'
},
usage: {
model: 'claude-sonnet-4-20250514',
inputTokens: 1500,
outputTokens: 800,
cacheCreateTokens: 200,
cacheReadTokens: 100,
ephemeral5mTokens: 150,
ephemeral1hTokens: 50,
totalTokens: 2600
},
cost: {
total: 0.0156,
currency: 'USD',
breakdown: {
input: 0.0045,
output: 0.012,
cacheCreate: 0.00075,
cacheRead: 0.00003,
ephemeral5m: 0.0005625,
ephemeral1h: 0.0001875
}
},
account: {
id: 'test-account-789',
type: 'claude-official'
},
context: {
isLongContext: false,
requestTimestamp: new Date().toISOString()
}
}
try {
const messageId = await redis.xadd(
STREAM_KEY,
'MAXLEN',
'~',
100000,
'*',
'data',
JSON.stringify(testEvent)
)
console.log('✅ Event published successfully!')
console.log(` Message ID: ${messageId}`)
console.log(` Event ID: ${testEvent.eventId}`)
console.log(` Cost: $${testEvent.cost.total}`)
} catch (error) {
console.error('❌ Failed to publish event:', error.message)
process.exit(1)
}
}
/**
* 消费事件(测试模式,不创建消费者组)
*/
async function consumeTestEvents() {
console.log('📬 Consuming test events...')
console.log(' Press Ctrl+C to stop\n')
let isRunning = true
process.on('SIGINT', () => {
console.log('\n⏹️ Stopping consumer...')
isRunning = false
})
let lastId = '0' // 从头开始
while (isRunning) {
try {
// 使用 XREAD 而不是 XREADGROUP(测试模式)
const messages = await redis.xread('BLOCK', 5000, 'COUNT', 10, 'STREAMS', STREAM_KEY, lastId)
if (!messages || messages.length === 0) {
continue
}
const [streamKey, entries] = messages[0]
console.log(`📬 Received ${entries.length} messages from ${streamKey}\n`)
for (const [messageId, fields] of entries) {
try {
const data = {}
for (let i = 0; i < fields.length; i += 2) {
data[fields[i]] = fields[i + 1]
}
const event = JSON.parse(data.data)
console.log(`📊 Event: ${event.eventId}`)
console.log(` API Key: ${event.apiKey.name} (${event.apiKey.id})`)
console.log(` Model: ${event.usage.model}`)
console.log(` Tokens: ${event.usage.totalTokens}`)
console.log(` Cost: $${event.cost.total.toFixed(6)}`)
console.log(` Timestamp: ${event.timestamp}`)
console.log('')
lastId = messageId // 更新位置
} catch (parseError) {
console.error(`❌ Failed to parse message ${messageId}:`, parseError.message)
}
}
} catch (error) {
if (isRunning) {
console.error('❌ Error consuming messages:', error.message)
await new Promise((resolve) => setTimeout(resolve, 5000))
}
}
}
console.log('👋 Consumer stopped')
}
/**
* 查看队列状态
*/
async function showQueueInfo() {
console.log('📊 Queue Information\n')
try {
// Stream 长度
const length = await redis.xlen(STREAM_KEY)
console.log(`Stream: ${STREAM_KEY}`)
console.log(`Length: ${length} messages\n`)
if (length === 0) {
console.log('ℹ️ Queue is empty')
return
}
// Stream 详细信息
const info = await redis.xinfo('STREAM', STREAM_KEY)
const infoObj = {}
for (let i = 0; i < info.length; i += 2) {
infoObj[info[i]] = info[i + 1]
}
console.log('Stream Details:')
console.log(` First Entry ID: ${infoObj['first-entry'] ? infoObj['first-entry'][0] : 'N/A'}`)
console.log(` Last Entry ID: ${infoObj['last-entry'] ? infoObj['last-entry'][0] : 'N/A'}`)
console.log(` Consumer Groups: ${infoObj.groups || 0}\n`)
// 消费者组信息
if (infoObj.groups > 0) {
console.log('Consumer Groups:')
const groups = await redis.xinfo('GROUPS', STREAM_KEY)
for (let i = 0; i < groups.length; i++) {
const group = groups[i]
const groupObj = {}
for (let j = 0; j < group.length; j += 2) {
groupObj[group[j]] = group[j + 1]
}
console.log(`\n Group: ${groupObj.name}`)
console.log(` Consumers: ${groupObj.consumers}`)
console.log(` Pending: ${groupObj.pending}`)
console.log(` Last Delivered ID: ${groupObj['last-delivered-id']}`)
// 消费者详情
if (groupObj.consumers > 0) {
const consumers = await redis.xinfo('CONSUMERS', STREAM_KEY, groupObj.name)
console.log(' Consumer Details:')
for (let k = 0; k < consumers.length; k++) {
const consumer = consumers[k]
const consumerObj = {}
for (let l = 0; l < consumer.length; l += 2) {
consumerObj[consumer[l]] = consumer[l + 1]
}
console.log(` - ${consumerObj.name}`)
console.log(` Pending: ${consumerObj.pending}`)
console.log(` Idle: ${Math.round(consumerObj.idle / 1000)}s`)
}
}
}
}
// 最新 5 条消息
console.log('\n📬 Latest 5 Messages:')
const latest = await redis.xrevrange(STREAM_KEY, '+', '-', 'COUNT', 5)
if (latest.length === 0) {
console.log(' No messages')
} else {
for (const [messageId, fields] of latest) {
const data = {}
for (let i = 0; i < fields.length; i += 2) {
data[fields[i]] = fields[i + 1]
}
try {
const event = JSON.parse(data.data)
console.log(`\n ${messageId}`)
console.log(` Event ID: ${event.eventId}`)
console.log(` Model: ${event.usage.model}`)
console.log(` Cost: $${event.cost.total.toFixed(6)}`)
console.log(` Time: ${event.timestamp}`)
} catch (e) {
console.log(`\n ${messageId} (Parse Error)`)
}
}
}
} catch (error) {
console.error('❌ Failed to get queue info:', error.message)
process.exit(1)
}
}
/**
* 清空队列(危险操作)
*/
async function clearQueue() {
console.log('⚠️ WARNING: This will delete all messages in the queue!')
console.log(` Stream: ${STREAM_KEY}`)
// 简单的确认机制
const readline = require('readline')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question('Type "yes" to confirm: ', async (answer) => {
if (answer.toLowerCase() === 'yes') {
try {
await redis.del(STREAM_KEY)
console.log('✅ Queue cleared successfully')
} catch (error) {
console.error('❌ Failed to clear queue:', error.message)
}
} else {
console.log('❌ Operation cancelled')
}
rl.close()
redis.quit()
})
}
// ========================================
// CLI 处理
// ========================================
async function main() {
const command = process.argv[2] || 'info'
console.log('🔧 Billing Events Test Tool\n')
try {
switch (command) {
case 'publish':
await publishTestEvent()
break
case 'consume':
await consumeTestEvents()
break
case 'info':
await showQueueInfo()
break
case 'clear':
await clearQueue()
return // clearQueue 会自己关闭连接
default:
console.error(`❌ Unknown command: ${command}`)
console.log('\nAvailable commands:')
console.log(' publish - Publish a test event')
console.log(' consume - Consume events (test mode)')
console.log(' info - Show queue status')
console.log(' clear - Clear the queue (dangerous)')
process.exit(1)
}
await redis.quit()
} catch (error) {
console.error('💥 Fatal error:', error)
await redis.quit()
process.exit(1)
}
}
main()