fix: complete gemini frontend and usage follow-up
This commit is contained in:
+47
-14
@@ -152,7 +152,12 @@ pub async fn list_users(
|
||||
.fetch_one(&*state.db)
|
||||
.await?;
|
||||
|
||||
let items: Vec<UserResponse> = users.into_iter().map(|u| UserResponse::from(u)).collect();
|
||||
let mut items = Vec::with_capacity(users.len());
|
||||
for user in users {
|
||||
let mut item = UserResponse::from(user);
|
||||
attach_usage_summary(&state, &mut item).await?;
|
||||
items.push(item);
|
||||
}
|
||||
Ok(Json(json!({
|
||||
"data": items,
|
||||
"users": items,
|
||||
@@ -182,7 +187,9 @@ pub async fn get_user(
|
||||
.fetch_optional(&*state.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))?;
|
||||
Ok(Json(json!({ "data": UserResponse::from(user) })))
|
||||
let mut item = UserResponse::from(user);
|
||||
attach_usage_summary(&state, &mut item).await?;
|
||||
Ok(Json(json!({ "data": item })))
|
||||
}
|
||||
|
||||
/// POST /api/admin/users
|
||||
@@ -228,7 +235,9 @@ pub async fn create_user(
|
||||
let key = format!("sk-user-{}", uuid::Uuid::new_v4());
|
||||
let key_name = format!("{} 的默认密钥", payload.username.trim());
|
||||
|
||||
sqlx::query("INSERT INTO downstream_api_keys (name, key, user_id, enabled) VALUES (?, ?, ?, ?)")
|
||||
sqlx::query(
|
||||
"INSERT INTO downstream_api_keys (name, key, user_id, enabled) VALUES (?, ?, ?, ?)",
|
||||
)
|
||||
.bind(&key_name)
|
||||
.bind(&key)
|
||||
.bind(user_id)
|
||||
@@ -264,11 +273,11 @@ pub async fn get_user_usage(
|
||||
r#"
|
||||
SELECT
|
||||
COUNT(l.id) as total_requests,
|
||||
SUM(l.cost) as total_cost
|
||||
COALESCE(SUM(l.cost), 0.0) as total_cost
|
||||
FROM usage_logs l
|
||||
JOIN downstream_api_keys k ON l.downstream_key_id = k.id
|
||||
WHERE k.user_id = ?
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(&*state.db)
|
||||
@@ -277,11 +286,11 @@ pub async fn get_user_usage(
|
||||
// Last 7 days cost
|
||||
let last_7_days_cost: f64 = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT SUM(cost)
|
||||
SELECT COALESCE(SUM(l.cost), 0.0)
|
||||
FROM usage_logs l
|
||||
JOIN downstream_api_keys k ON l.downstream_key_id = k.id
|
||||
WHERE k.user_id = ? AND l.created_at >= datetime('now', '-7 days')
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(&*state.db)
|
||||
@@ -299,7 +308,7 @@ pub async fn get_user_usage(
|
||||
WHERE k.user_id = ? AND l.created_at >= datetime('now', '-30 days')
|
||||
GROUP BY day
|
||||
ORDER BY day ASC
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(&*state.db)
|
||||
@@ -314,7 +323,7 @@ pub async fn get_user_usage(
|
||||
WHERE k.user_id = ?
|
||||
ORDER BY l.id DESC
|
||||
LIMIT 50
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(&*state.db)
|
||||
@@ -325,10 +334,10 @@ pub async fn get_user_usage(
|
||||
json!({
|
||||
"id": row.get::<i64, _>("id"),
|
||||
"model": row.get::<Option<String>, _>("model"),
|
||||
"prompt_tokens": row.get::<i64, _>("prompt_tokens"),
|
||||
"completion_tokens": row.get::<i64, _>("completion_tokens"),
|
||||
"total_tokens": row.get::<i64, _>("total_tokens"),
|
||||
"cost": row.get::<f64, _>("cost"),
|
||||
"prompt_tokens": row.get::<Option<i64>, _>("prompt_tokens").unwrap_or(0),
|
||||
"completion_tokens": row.get::<Option<i64>, _>("completion_tokens").unwrap_or(0),
|
||||
"total_tokens": row.get::<Option<i64>, _>("total_tokens").unwrap_or(0),
|
||||
"cost": row.get::<Option<f64>, _>("cost").unwrap_or(0.0),
|
||||
"created_at": row.get::<Option<String>, _>("created_at"),
|
||||
})
|
||||
})
|
||||
@@ -345,7 +354,6 @@ pub async fn get_user_usage(
|
||||
})))
|
||||
}
|
||||
|
||||
|
||||
/// PUT /api/admin/users/{id}
|
||||
pub async fn update_user(
|
||||
State(state): State<AppState>,
|
||||
@@ -468,3 +476,28 @@ async fn ensure_email_available(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn attach_usage_summary(state: &AppState, item: &mut UserResponse) -> AppResult<()> {
|
||||
let (total_requests, total_cost, last_7_days_cost): (i64, f64, f64) = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
COUNT(l.id) AS total_requests,
|
||||
COALESCE(SUM(l.cost), 0.0) AS total_cost,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN l.created_at >= datetime('now', '-7 days') THEN l.cost
|
||||
ELSE 0.0
|
||||
END), 0.0) AS last_7_days_cost
|
||||
FROM usage_logs l
|
||||
JOIN downstream_api_keys k ON l.downstream_key_id = k.id
|
||||
WHERE k.user_id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(item.id)
|
||||
.fetch_one(&*state.db)
|
||||
.await?;
|
||||
|
||||
item.total_requests = Some(total_requests);
|
||||
item.total_cost = Some(total_cost);
|
||||
item.last_7_days_cost = Some(last_7_days_cost);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+14
-6
@@ -5,7 +5,7 @@ use crate::{
|
||||
AppState,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State, Query},
|
||||
extract::{Path, Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
routing::{delete, get},
|
||||
Json, Router,
|
||||
@@ -168,15 +168,23 @@ async fn get_user_logs(
|
||||
) -> AppResult<Json<Value>> {
|
||||
let user_id = current_user_id(&headers, &state)?;
|
||||
|
||||
let limit = params.get("limit").and_then(|v| v.parse::<i64>().ok()).unwrap_or(20).clamp(1, 100);
|
||||
let offset = params.get("offset").and_then(|v| v.parse::<i64>().ok()).unwrap_or(0).max(0);
|
||||
let limit = params
|
||||
.get("limit")
|
||||
.and_then(|v| v.parse::<i64>().ok())
|
||||
.unwrap_or(20)
|
||||
.clamp(1, 100);
|
||||
let offset = params
|
||||
.get("offset")
|
||||
.and_then(|v| v.parse::<i64>().ok())
|
||||
.unwrap_or(0)
|
||||
.max(0);
|
||||
|
||||
let items: Vec<Value> = sqlx::query_as::<_, crate::routes::stats::ProxyLogRow>(
|
||||
"SELECT l.* FROM proxy_logs l
|
||||
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
|
||||
WHERE k.user_id = ?
|
||||
ORDER BY l.created_at DESC
|
||||
LIMIT ? OFFSET ?"
|
||||
LIMIT ? OFFSET ?",
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(limit)
|
||||
@@ -190,7 +198,7 @@ async fn get_user_logs(
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM proxy_logs l
|
||||
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
|
||||
WHERE k.user_id = ?"
|
||||
WHERE k.user_id = ?",
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_one(&*state.db)
|
||||
@@ -216,7 +224,7 @@ async fn get_user_summary(
|
||||
COALESCE(SUM(l.estimated_cost), 0.0)
|
||||
FROM proxy_logs l
|
||||
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
|
||||
WHERE k.user_id = ?"
|
||||
WHERE k.user_id = ?",
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_one(&*state.db)
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export declare function parseBatchApiKeys(input: unknown): string[];
|
||||
export declare function buildBatchApiKeyConnectionName(
|
||||
baseName: string | null | undefined,
|
||||
index: number,
|
||||
total: number,
|
||||
): string;
|
||||
+3
-2
@@ -3,11 +3,12 @@ export interface SiteInitializationPreset {
|
||||
label: string;
|
||||
platform: 'openai' | 'claude';
|
||||
defaultUrl: string;
|
||||
initialSegment: string;
|
||||
initialSegment: 'session' | 'apikey';
|
||||
description?: string;
|
||||
recommendedSkipModelFetch: boolean;
|
||||
recommendedModels: string[];
|
||||
}
|
||||
export declare function getSiteInitializationPreset(id: string): SiteInitializationPreset | undefined;
|
||||
export declare function getSiteInitializationPreset(id?: string | null): SiteInitializationPreset | undefined;
|
||||
export declare function listSiteInitializationPresets(): SiteInitializationPreset[];
|
||||
export declare function detectSiteInitializationPreset(url: string, knownPlatform?: string): {
|
||||
id: string;
|
||||
|
||||
@@ -3,7 +3,8 @@ export interface SiteInitializationPreset {
|
||||
label: string;
|
||||
platform: 'openai' | 'claude';
|
||||
defaultUrl: string;
|
||||
initialSegment: string;
|
||||
initialSegment: 'session' | 'apikey';
|
||||
description?: string;
|
||||
recommendedSkipModelFetch: boolean;
|
||||
recommendedModels: string[];
|
||||
}
|
||||
@@ -158,7 +159,8 @@ const rootReselect: Array<{ rootPrefix: string; id: string }> = [
|
||||
{ rootPrefix: 'https://coding.dashscope.aliyuncs.com', id: 'codingplan-openai' },
|
||||
];
|
||||
|
||||
export function getSiteInitializationPreset(id: string): SiteInitializationPreset | undefined {
|
||||
export function getSiteInitializationPreset(id?: string | null): SiteInitializationPreset | undefined {
|
||||
if (!id) return undefined;
|
||||
return presets.find((p) => p.id === id);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
export type UpdateVersionCandidateLike = {
|
||||
normalizedVersion?: string | null;
|
||||
displayVersion?: string | null;
|
||||
tagName?: string | null;
|
||||
digest?: string | null;
|
||||
};
|
||||
|
||||
export type UpdateHelperRuntimeLike = {
|
||||
imageTag?: string | null;
|
||||
imageDigest?: string | null;
|
||||
};
|
||||
|
||||
export type UpdateReminderCandidate = {
|
||||
source: 'github-release' | 'docker-hub-tag';
|
||||
kind: 'new-version' | 'new-digest';
|
||||
candidateKey: string;
|
||||
displayVersion: string;
|
||||
tagName: string;
|
||||
digest: string | null;
|
||||
};
|
||||
|
||||
export declare function normalizeStableVersion(value?: string | null): string;
|
||||
export declare function compareStableVersions(
|
||||
left?: string | null,
|
||||
right?: string | null,
|
||||
): number | null;
|
||||
export declare function isSameImageTarget(
|
||||
current: UpdateHelperRuntimeLike | null | undefined,
|
||||
target: { tag?: string | null; digest?: string | null },
|
||||
): boolean;
|
||||
export declare function buildUpdateReminderCandidateKey(
|
||||
source: 'github-release' | 'docker-hub-tag',
|
||||
candidate: { tagName?: string | null; digest?: string | null },
|
||||
): string;
|
||||
export declare function resolveUpdateReminderCandidate(input: {
|
||||
currentVersion?: string | null;
|
||||
helper: UpdateHelperRuntimeLike | null | undefined;
|
||||
githubRelease: UpdateVersionCandidateLike | null | undefined;
|
||||
dockerHubTag: UpdateVersionCandidateLike | null | undefined;
|
||||
}): UpdateReminderCandidate | null;
|
||||
@@ -333,41 +333,23 @@ const zhToEn: Record<string, string> = {
|
||||
'再次输入密码': 'Re-enter password',
|
||||
'你的昵称': 'Your nickname',
|
||||
'用户中心': 'User Portal',
|
||||
'邮箱': 'Email',
|
||||
'密码': 'Password',
|
||||
'个人信息': 'Profile',
|
||||
'修改密码': 'Change Password',
|
||||
'API 密钥': 'API Keys',
|
||||
'新建密钥': 'New Key',
|
||||
'当前密码': 'Current Password',
|
||||
'新密码': 'New Password',
|
||||
'确认新密码': 'Confirm New Password',
|
||||
'修改密码': 'Change Password',
|
||||
'修改中...': 'Changing...',
|
||||
'密码已更新': 'Password updated',
|
||||
'密钥名称': 'Key Name',
|
||||
'暂无 API 密钥': 'No API Keys',
|
||||
'用户名': 'Username',
|
||||
'角色': 'Role',
|
||||
'状态': 'Status',
|
||||
'注册时间': 'Registered',
|
||||
'正常': 'Active',
|
||||
'已禁用': 'Disabled',
|
||||
'管理员': 'Admin',
|
||||
'普通用户': 'User',
|
||||
'启用': 'Enabled',
|
||||
'禁用': 'Disabled',
|
||||
'创建': 'Create',
|
||||
'创建中...': 'Creating...',
|
||||
'保存': 'Save',
|
||||
'编辑': 'Edit',
|
||||
'删除': 'Delete',
|
||||
'取消': 'Cancel',
|
||||
'密钥已创建,请立即复制(不会再次显示):': 'Key created. Copy now (won\'t show again):',
|
||||
'关闭': 'Close',
|
||||
'退出': 'Sign Out',
|
||||
'用户管理': 'User Management',
|
||||
'共': 'Total',
|
||||
'个用户': ' users',
|
||||
'总用户': 'Total Users',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user