135 文件 19,211 处自动修复:空格→Tab 缩进、括号间距、 函数声明空格、前置自增、尾逗号等纯格式化改动。 零逻辑变更,php -l + token 级对比验证通过。 新增 phpcs.xml.dist / phpstan.neon.dist 项目配置。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
528 lines
14 KiB
PHP
528 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Usage Tracker - Token 用量追踪
|
|
*
|
|
* 记录每个 Provider 的 token 用量和成本估算
|
|
*
|
|
* @package WPMind\Modules\CostControl
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace WPMind\Modules\CostControl;
|
|
|
|
use WPMind\Usage\Pricing;
|
|
|
|
class UsageTracker {
|
|
|
|
private const OPTION_KEY = 'wpmind_usage_stats';
|
|
private const HISTORY_KEY = 'wpmind_usage_history';
|
|
private const MAX_HISTORY = 1000; // 最多保留 1000 条记录
|
|
|
|
/**
|
|
* 各服务商的定价 (per 1M tokens)
|
|
*
|
|
* @deprecated Use WPMind\Usage\Pricing::DATA instead.
|
|
*/
|
|
private const PRICING = [];
|
|
|
|
/**
|
|
* 记录一次 API 调用的 token 用量
|
|
*
|
|
* @param string $provider Provider ID
|
|
* @param string $model 模型名称
|
|
* @param int $inputTokens 输入 tokens
|
|
* @param int $outputTokens 输出 tokens
|
|
* @param int $latencyMs 延迟(毫秒)
|
|
*/
|
|
public static function record(
|
|
string $provider,
|
|
string $model,
|
|
int $inputTokens,
|
|
int $outputTokens,
|
|
int $latencyMs = 0
|
|
): void {
|
|
// 输入验证:确保 tokens 非负
|
|
$inputTokens = max( 0, $inputTokens );
|
|
$outputTokens = max( 0, $outputTokens );
|
|
$latencyMs = max( 0, $latencyMs );
|
|
|
|
// 计算成本
|
|
$cost = self::calculate_cost( $provider, $model, $inputTokens, $outputTokens );
|
|
|
|
// 更新汇总统计
|
|
self::update_stats( $provider, $model, $inputTokens, $outputTokens, $cost );
|
|
|
|
// 只有有 token 使用时才添加到历史记录
|
|
if ( $inputTokens > 0 || $outputTokens > 0 ) {
|
|
self::add_to_history( $provider, $model, $inputTokens, $outputTokens, $cost, $latencyMs );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 计算成本
|
|
*
|
|
* @return float 成本(按服务商货币计价)
|
|
*/
|
|
public static function calculate_cost(
|
|
string $provider,
|
|
string $model,
|
|
int $inputTokens,
|
|
int $outputTokens
|
|
): float {
|
|
$allPricing = self::get_pricing();
|
|
$pricing = $allPricing[ $provider ] ?? [];
|
|
$modelPricing = $pricing[ $model ] ?? $pricing['default'] ?? [
|
|
'input' => 0,
|
|
'output' => 0,
|
|
];
|
|
|
|
$inputCost = ( $inputTokens / 1_000_000 ) * ( $modelPricing['input'] ?? 0 );
|
|
$outputCost = ( $outputTokens / 1_000_000 ) * ( $modelPricing['output'] ?? 0 );
|
|
|
|
return round( $inputCost + $outputCost, 6 );
|
|
}
|
|
|
|
/**
|
|
* 获取服务商的货币类型
|
|
*
|
|
* @param string $provider Provider ID
|
|
* @return string USD 或 CNY
|
|
*/
|
|
public static function get_currency( string $provider ): string {
|
|
$allPricing = self::get_pricing();
|
|
return $allPricing[ $provider ]['currency'] ?? 'USD';
|
|
}
|
|
|
|
/**
|
|
* 更新汇总统计(带并发锁)
|
|
*/
|
|
private static function update_stats(
|
|
string $provider,
|
|
string $model,
|
|
int $inputTokens,
|
|
int $outputTokens,
|
|
float $cost
|
|
): void {
|
|
$lock_key = 'wpmind_usage_stats_lock';
|
|
$max_retries = 3;
|
|
$retry_delay = 50000; // 50ms
|
|
|
|
// 尝试获取锁
|
|
for ( $i = 0; $i < $max_retries; $i++ ) {
|
|
if ( false === get_transient( $lock_key ) ) {
|
|
set_transient( $lock_key, true, 5 );
|
|
break;
|
|
}
|
|
usleep( $retry_delay );
|
|
}
|
|
|
|
try {
|
|
$stats = get_option( self::OPTION_KEY, [] );
|
|
|
|
if ( ! is_array( $stats ) ) {
|
|
$stats = [];
|
|
}
|
|
|
|
$today = wp_date( 'Y-m-d' );
|
|
$month = wp_date( 'Y-m' );
|
|
$currency = self::get_currency( $provider );
|
|
|
|
// 初始化结构
|
|
if ( ! isset( $stats['providers'] ) || ! is_array( $stats['providers'] ) ) {
|
|
$stats['providers'] = [];
|
|
}
|
|
if ( ! isset( $stats['providers'][ $provider ] ) ) {
|
|
$stats['providers'][ $provider ] = [
|
|
'total_input_tokens' => 0,
|
|
'total_output_tokens' => 0,
|
|
'total_cost' => 0,
|
|
'request_count' => 0,
|
|
'models' => [],
|
|
];
|
|
}
|
|
|
|
if ( ! isset( $stats['daily'] ) || ! is_array( $stats['daily'] ) ) {
|
|
$stats['daily'] = [];
|
|
}
|
|
if ( ! isset( $stats['daily'][ $today ] ) ) {
|
|
$stats['daily'][ $today ] = [
|
|
'input_tokens' => 0,
|
|
'output_tokens' => 0,
|
|
'cost_usd' => 0,
|
|
'cost_cny' => 0,
|
|
'requests' => 0,
|
|
];
|
|
}
|
|
|
|
if ( ! isset( $stats['monthly'] ) || ! is_array( $stats['monthly'] ) ) {
|
|
$stats['monthly'] = [];
|
|
}
|
|
if ( ! isset( $stats['monthly'][ $month ] ) ) {
|
|
$stats['monthly'][ $month ] = [
|
|
'input_tokens' => 0,
|
|
'output_tokens' => 0,
|
|
'cost_usd' => 0,
|
|
'cost_cny' => 0,
|
|
'requests' => 0,
|
|
];
|
|
}
|
|
|
|
// 更新 Provider 统计
|
|
$stats['providers'][ $provider ]['total_input_tokens'] += $inputTokens;
|
|
$stats['providers'][ $provider ]['total_output_tokens'] += $outputTokens;
|
|
$stats['providers'][ $provider ]['total_cost'] += $cost;
|
|
++$stats['providers'][ $provider ]['request_count'];
|
|
|
|
// 更新模型统计
|
|
if ( ! isset( $stats['providers'][ $provider ]['models'][ $model ] ) ) {
|
|
$stats['providers'][ $provider ]['models'][ $model ] = [
|
|
'input_tokens' => 0,
|
|
'output_tokens' => 0,
|
|
'cost' => 0,
|
|
'requests' => 0,
|
|
];
|
|
}
|
|
$stats['providers'][ $provider ]['models'][ $model ]['input_tokens'] += $inputTokens;
|
|
$stats['providers'][ $provider ]['models'][ $model ]['output_tokens'] += $outputTokens;
|
|
$stats['providers'][ $provider ]['models'][ $model ]['cost'] += $cost;
|
|
++$stats['providers'][ $provider ]['models'][ $model ]['requests'];
|
|
|
|
// 更新日统计(分货币)
|
|
$stats['daily'][ $today ]['input_tokens'] += $inputTokens;
|
|
$stats['daily'][ $today ]['output_tokens'] += $outputTokens;
|
|
if ( $currency === 'CNY' ) {
|
|
$stats['daily'][ $today ]['cost_cny'] = ( $stats['daily'][ $today ]['cost_cny'] ?? 0 ) + $cost;
|
|
} else {
|
|
$stats['daily'][ $today ]['cost_usd'] = ( $stats['daily'][ $today ]['cost_usd'] ?? 0 ) + $cost;
|
|
}
|
|
++$stats['daily'][ $today ]['requests'];
|
|
|
|
// 更新月统计(分货币)
|
|
$stats['monthly'][ $month ]['input_tokens'] += $inputTokens;
|
|
$stats['monthly'][ $month ]['output_tokens'] += $outputTokens;
|
|
if ( $currency === 'CNY' ) {
|
|
$stats['monthly'][ $month ]['cost_cny'] = ( $stats['monthly'][ $month ]['cost_cny'] ?? 0 ) + $cost;
|
|
} else {
|
|
$stats['monthly'][ $month ]['cost_usd'] = ( $stats['monthly'][ $month ]['cost_usd'] ?? 0 ) + $cost;
|
|
}
|
|
++$stats['monthly'][ $month ]['requests'];
|
|
|
|
// 更新总计(分货币)
|
|
if ( ! isset( $stats['total'] ) || ! is_array( $stats['total'] ) ) {
|
|
$stats['total'] = [
|
|
'input_tokens' => 0,
|
|
'output_tokens' => 0,
|
|
'cost_usd' => 0,
|
|
'cost_cny' => 0,
|
|
'requests' => 0,
|
|
];
|
|
}
|
|
$stats['total']['input_tokens'] = ( $stats['total']['input_tokens'] ?? 0 ) + $inputTokens;
|
|
$stats['total']['output_tokens'] = ( $stats['total']['output_tokens'] ?? 0 ) + $outputTokens;
|
|
if ( $currency === 'CNY' ) {
|
|
$stats['total']['cost_cny'] = ( $stats['total']['cost_cny'] ?? 0 ) + $cost;
|
|
} else {
|
|
$stats['total']['cost_usd'] = ( $stats['total']['cost_usd'] ?? 0 ) + $cost;
|
|
}
|
|
$stats['total']['requests'] = ( $stats['total']['requests'] ?? 0 ) + 1;
|
|
|
|
$stats['last_updated'] = time();
|
|
|
|
// 清理旧的日统计(保留 30 天)
|
|
$cutoffDate = wp_date( 'Y-m-d', strtotime( '-30 days' ) );
|
|
foreach ( array_keys( $stats['daily'] ?? [] ) as $date ) {
|
|
if ( $date < $cutoffDate ) {
|
|
unset( $stats['daily'][ $date ] );
|
|
}
|
|
}
|
|
|
|
// 清理旧的月统计(保留 12 个月)
|
|
$cutoffMonth = wp_date( 'Y-m', strtotime( '-12 months' ) );
|
|
foreach ( array_keys( $stats['monthly'] ?? [] ) as $m ) {
|
|
if ( $m < $cutoffMonth ) {
|
|
unset( $stats['monthly'][ $m ] );
|
|
}
|
|
}
|
|
|
|
update_option( self::OPTION_KEY, $stats, false );
|
|
wp_cache_set( 'wpmind_usage_stats', $stats, '', 300 );
|
|
} finally {
|
|
delete_transient( $lock_key );
|
|
}
|
|
}
|
|
/**
|
|
* 添加到历史记录(带并发锁)
|
|
*/
|
|
private static function add_to_history(
|
|
string $provider,
|
|
string $model,
|
|
int $inputTokens,
|
|
int $outputTokens,
|
|
float $cost,
|
|
int $latencyMs
|
|
): void {
|
|
$lock_key = 'wpmind_usage_history_lock';
|
|
$max_retries = 3;
|
|
$retry_delay = 50000;
|
|
|
|
for ( $i = 0; $i < $max_retries; $i++ ) {
|
|
if ( false === get_transient( $lock_key ) ) {
|
|
set_transient( $lock_key, true, 5 );
|
|
break;
|
|
}
|
|
usleep( $retry_delay );
|
|
}
|
|
|
|
try {
|
|
$history = get_option( self::HISTORY_KEY, [] );
|
|
|
|
if ( ! is_array( $history ) ) {
|
|
$history = [];
|
|
}
|
|
|
|
$history[] = [
|
|
'provider' => $provider,
|
|
'model' => $model,
|
|
'input_tokens' => $inputTokens,
|
|
'output_tokens' => $outputTokens,
|
|
'cost' => $cost,
|
|
'latency_ms' => $latencyMs,
|
|
'timestamp' => time(),
|
|
];
|
|
|
|
if ( count( $history ) > self::MAX_HISTORY ) {
|
|
$history = array_slice( $history, -self::MAX_HISTORY );
|
|
}
|
|
|
|
update_option( self::HISTORY_KEY, $history, false );
|
|
wp_cache_set( 'wpmind_usage_history', $history, '', 300 );
|
|
} finally {
|
|
delete_transient( $lock_key );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取汇总统计
|
|
*/
|
|
public static function get_stats(): array {
|
|
$cache_key = 'wpmind_usage_stats';
|
|
$stats = wp_cache_get( $cache_key );
|
|
|
|
if ( false === $stats ) {
|
|
$stats = get_option( self::OPTION_KEY, [] );
|
|
wp_cache_set( $cache_key, $stats, '', 300 );
|
|
}
|
|
|
|
if ( ! is_array( $stats ) ) {
|
|
$stats = [];
|
|
}
|
|
|
|
return $stats ?: [
|
|
'providers' => [],
|
|
'daily' => [],
|
|
'monthly' => [],
|
|
'total' => [
|
|
'input_tokens' => 0,
|
|
'output_tokens' => 0,
|
|
'cost_usd' => 0,
|
|
'cost_cny' => 0,
|
|
'requests' => 0,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取今日统计
|
|
*/
|
|
public static function get_today_stats(): array {
|
|
$stats = self::get_stats();
|
|
$today = wp_date( 'Y-m-d' );
|
|
|
|
return $stats['daily'][ $today ] ?? [
|
|
'input_tokens' => 0,
|
|
'output_tokens' => 0,
|
|
'cost_usd' => 0,
|
|
'cost_cny' => 0,
|
|
'requests' => 0,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取本月统计
|
|
*/
|
|
public static function get_month_stats(): array {
|
|
$stats = self::get_stats();
|
|
$month = wp_date( 'Y-m' );
|
|
|
|
return $stats['monthly'][ $month ] ?? [
|
|
'input_tokens' => 0,
|
|
'output_tokens' => 0,
|
|
'cost_usd' => 0,
|
|
'cost_cny' => 0,
|
|
'requests' => 0,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取本周统计(周一到周日)
|
|
*/
|
|
public static function get_week_stats(): array {
|
|
$stats = self::get_stats();
|
|
$daily = $stats['daily'] ?? [];
|
|
|
|
// 使用 WordPress 时区计算本周的日期范围
|
|
$weekStart = wp_date( 'Y-m-d', strtotime( 'monday this week' ) );
|
|
$today = wp_date( 'Y-m-d' );
|
|
|
|
$result = [
|
|
'input_tokens' => 0,
|
|
'output_tokens' => 0,
|
|
'cost_usd' => 0,
|
|
'cost_cny' => 0,
|
|
'requests' => 0,
|
|
];
|
|
|
|
// 汇总本周每天的数据
|
|
foreach ( $daily as $date => $dayStats ) {
|
|
if ( $date >= $weekStart && $date <= $today ) {
|
|
$result['input_tokens'] += $dayStats['input_tokens'] ?? 0;
|
|
$result['output_tokens'] += $dayStats['output_tokens'] ?? 0;
|
|
$result['cost_usd'] += $dayStats['cost_usd'] ?? 0;
|
|
$result['cost_cny'] += $dayStats['cost_cny'] ?? 0;
|
|
$result['requests'] += $dayStats['requests'] ?? 0;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 获取历史记录
|
|
*/
|
|
public static function get_history( int $limit = 50 ): array {
|
|
$cache_key = 'wpmind_usage_history';
|
|
$history = wp_cache_get( $cache_key );
|
|
|
|
if ( false === $history ) {
|
|
$history = get_option( self::HISTORY_KEY, [] );
|
|
wp_cache_set( $cache_key, $history, '', 300 );
|
|
}
|
|
|
|
if ( ! is_array( $history ) ) {
|
|
$history = [];
|
|
}
|
|
|
|
return array_slice( array_reverse( $history ), 0, $limit );
|
|
}
|
|
|
|
/**
|
|
* 获取定价信息
|
|
*/
|
|
public static function get_pricing(): array {
|
|
if ( ! class_exists( Pricing::class ) ) {
|
|
require_once WPMIND_PATH . 'includes/Usage/Pricing.php';
|
|
}
|
|
return Pricing::DATA;
|
|
}
|
|
|
|
/**
|
|
* 清除所有统计数据
|
|
*/
|
|
public static function clear_all(): void {
|
|
delete_option( self::OPTION_KEY );
|
|
delete_option( self::HISTORY_KEY );
|
|
wp_cache_delete( 'wpmind_usage_stats' );
|
|
wp_cache_delete( 'wpmind_usage_history' );
|
|
}
|
|
|
|
/**
|
|
* 格式化 token 数量
|
|
*/
|
|
public static function format_tokens( int $tokens ): string {
|
|
if ( $tokens >= 1_000_000 ) {
|
|
return round( $tokens / 1_000_000, 2 ) . 'M';
|
|
}
|
|
if ( $tokens >= 1_000 ) {
|
|
return round( $tokens / 1_000, 1 ) . 'K';
|
|
}
|
|
return (string) $tokens;
|
|
}
|
|
|
|
/**
|
|
* 格式化成本
|
|
*/
|
|
public static function format_cost( float $cost, string $currency = 'USD' ): string {
|
|
$symbol = $currency === 'CNY' ? '¥' : '$';
|
|
if ( $cost < 0.01 ) {
|
|
return $symbol . number_format( $cost, 4 );
|
|
}
|
|
return $symbol . number_format( $cost, 2 );
|
|
}
|
|
|
|
/**
|
|
* 格式化分货币费用显示
|
|
*/
|
|
public static function format_cost_by_currency( float $costUsd, float $costCny ): string {
|
|
$parts = [];
|
|
if ( $costUsd > 0 ) {
|
|
$parts[] = self::format_cost( $costUsd, 'USD' );
|
|
}
|
|
if ( $costCny > 0 ) {
|
|
$parts[] = self::format_cost( $costCny, 'CNY' );
|
|
}
|
|
if ( empty( $parts ) ) {
|
|
return '$0.00';
|
|
}
|
|
return implode( ' / ', $parts );
|
|
}
|
|
|
|
/**
|
|
* 获取 Provider 的显示名称
|
|
*/
|
|
public static function get_provider_display_name( string $provider ): string {
|
|
$names = [
|
|
'openai' => 'OpenAI',
|
|
'anthropic' => 'Anthropic',
|
|
'google' => 'Google AI',
|
|
'deepseek' => 'DeepSeek',
|
|
'qwen' => '通义千问',
|
|
'zhipu' => '智谱 AI',
|
|
'moonshot' => 'Moonshot',
|
|
'doubao' => '豆包',
|
|
'siliconflow' => '硅基流动',
|
|
'baidu' => '百度文心',
|
|
'minimax' => 'MiniMax',
|
|
];
|
|
return $names[ $provider ] ?? $provider;
|
|
}
|
|
|
|
/**
|
|
* 获取 Provider 的 Remixicon 图标类
|
|
*/
|
|
public static function get_provider_icon( string $provider ): string {
|
|
$icons = [
|
|
'openai' => 'ri-openai-line',
|
|
'anthropic' => 'ri-claude-line',
|
|
'google' => 'ri-gemini-line',
|
|
'deepseek' => 'ri-deepseek-line',
|
|
'qwen' => 'ri-qwen-ai-line',
|
|
'zhipu' => 'ri-zhipu-ai-line',
|
|
'moonshot' => 'ri-moon-line',
|
|
'doubao' => 'ri-fire-line',
|
|
'siliconflow' => 'ri-cpu-line',
|
|
'baidu' => 'ri-baidu-line',
|
|
'minimax' => 'ri-sparkling-line',
|
|
];
|
|
return $icons[ $provider ] ?? 'ri-robot-line';
|
|
}
|
|
|
|
/**
|
|
* 获取 Provider 的图标颜色
|
|
*/
|
|
public static function get_provider_color( string $provider ): string {
|
|
// 统一使用单色,与 WordPress 后台风格一致
|
|
return '#50575e';
|
|
}
|
|
}
|