feat(stats,rules): api_key 用量统计(total_tokens 字段 + 回填)与 body rules 新增 append/insert/regex_replace/name_style 操作

- 新增迁移 20260422120000_add_api_key_usage_stats.sql,为 api_keys 表添加 total_tokens 列
- 新增回填 20260422120000_backfill_api_key_usage_stats.sql,按历史 usage 重建 api_key 维度汇总
- 在 UsageWriteRepository trait 中添加 rebuild_api_key_usage_stats,补齐 SQL/内存实现及上层调用链
- 内存实现中新增 apply_usage_stats_delta,支持增量更新 api_key 统计快照
- dev.sh:改进临时日志目录管理,并在网关异常退出时输出错误提示
- frontend EndpointFormDialog:将 append 操作从 insert 分支拆分,提供独立 path/value 输入 UI
- rules.rs:扩展 body rules 支持,新增 append/insert/regex_replace/name_style 操作及 WildcardSlice 路径段
This commit is contained in:
fawney19
2026-04-22 20:16:57 +08:00
parent 62153d7d36
commit cf6228f525
15 changed files with 1857 additions and 113 deletions

View File

@@ -9,6 +9,7 @@ use super::types::{
StandaloneApiKeyExportListQuery, StoredAuthApiKeyExportRecord, StoredAuthApiKeySnapshot,
UpdateStandaloneApiKeyBasicRecord, UpdateUserApiKeyBasicRecord,
};
use crate::repository::usage::{ApiKeyUsageContribution, ApiKeyUsageDelta};
use crate::DataLayerError;
#[derive(Debug, Default)]
@@ -111,6 +112,70 @@ impl InMemoryAuthApiKeySnapshotRepository {
.copied()
.unwrap_or(0)
}
pub(crate) fn apply_usage_stats_delta(
&self,
api_key_id: &str,
delta: &ApiKeyUsageDelta,
_recomputed_last_used_at_unix_secs: Option<u64>,
) {
let mut index = self
.index
.write()
.expect("auth api key snapshot repository lock");
let Some(record) = index.export_by_api_key_id.get_mut(api_key_id) else {
return;
};
record.total_requests = apply_i64_delta_to_u64(record.total_requests, delta.total_requests);
record.total_tokens = apply_i64_delta_to_u64(record.total_tokens, delta.total_tokens);
record.total_cost_usd = apply_f64_delta(record.total_cost_usd, delta.total_cost_usd);
}
pub(crate) fn rebuild_usage_stats(
&self,
contributions: &BTreeMap<String, ApiKeyUsageContribution>,
) {
let mut index = self
.index
.write()
.expect("auth api key snapshot repository lock");
for record in index.export_by_api_key_id.values_mut() {
record.total_requests = 0;
record.total_tokens = 0;
record.total_cost_usd = 0.0;
}
for (api_key_id, contribution) in contributions {
let Some(record) = index.export_by_api_key_id.get_mut(api_key_id) else {
continue;
};
record.total_requests = clamp_i64_to_u64(contribution.total_requests);
record.total_tokens = clamp_i64_to_u64(contribution.total_tokens);
record.total_cost_usd = contribution.total_cost_usd.max(0.0);
}
}
}
fn clamp_i64_to_u64(value: i64) -> u64 {
u64::try_from(value).unwrap_or_default()
}
fn apply_i64_delta_to_u64(current: u64, delta: i64) -> u64 {
clamp_i64_to_u64(
i64::try_from(current)
.unwrap_or(i64::MAX)
.saturating_add(delta),
)
}
fn apply_f64_delta(current: f64, delta: f64) -> f64 {
let next = current + delta;
if next.is_finite() {
next.max(0.0)
} else {
current.max(0.0)
}
}
#[async_trait]