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)
|
.fetch_one(&*state.db)
|
||||||
.await?;
|
.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!({
|
Ok(Json(json!({
|
||||||
"data": items,
|
"data": items,
|
||||||
"users": items,
|
"users": items,
|
||||||
@@ -182,7 +187,9 @@ pub async fn get_user(
|
|||||||
.fetch_optional(&*state.db)
|
.fetch_optional(&*state.db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))?;
|
.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
|
/// POST /api/admin/users
|
||||||
@@ -228,7 +235,9 @@ pub async fn create_user(
|
|||||||
let key = format!("sk-user-{}", uuid::Uuid::new_v4());
|
let key = format!("sk-user-{}", uuid::Uuid::new_v4());
|
||||||
let key_name = format!("{} 的默认密钥", payload.username.trim());
|
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_name)
|
||||||
.bind(&key)
|
.bind(&key)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
@@ -264,11 +273,11 @@ pub async fn get_user_usage(
|
|||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(l.id) as total_requests,
|
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
|
FROM usage_logs l
|
||||||
JOIN downstream_api_keys k ON l.downstream_key_id = k.id
|
JOIN downstream_api_keys k ON l.downstream_key_id = k.id
|
||||||
WHERE k.user_id = ?
|
WHERE k.user_id = ?
|
||||||
"#
|
"#,
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_one(&*state.db)
|
.fetch_one(&*state.db)
|
||||||
@@ -277,11 +286,11 @@ pub async fn get_user_usage(
|
|||||||
// Last 7 days cost
|
// Last 7 days cost
|
||||||
let last_7_days_cost: f64 = sqlx::query_scalar(
|
let last_7_days_cost: f64 = sqlx::query_scalar(
|
||||||
r#"
|
r#"
|
||||||
SELECT SUM(cost)
|
SELECT COALESCE(SUM(l.cost), 0.0)
|
||||||
FROM usage_logs l
|
FROM usage_logs l
|
||||||
JOIN downstream_api_keys k ON l.downstream_key_id = k.id
|
JOIN downstream_api_keys k ON l.downstream_key_id = k.id
|
||||||
WHERE k.user_id = ? AND l.created_at >= datetime('now', '-7 days')
|
WHERE k.user_id = ? AND l.created_at >= datetime('now', '-7 days')
|
||||||
"#
|
"#,
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_one(&*state.db)
|
.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')
|
WHERE k.user_id = ? AND l.created_at >= datetime('now', '-30 days')
|
||||||
GROUP BY day
|
GROUP BY day
|
||||||
ORDER BY day ASC
|
ORDER BY day ASC
|
||||||
"#
|
"#,
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_all(&*state.db)
|
.fetch_all(&*state.db)
|
||||||
@@ -314,7 +323,7 @@ pub async fn get_user_usage(
|
|||||||
WHERE k.user_id = ?
|
WHERE k.user_id = ?
|
||||||
ORDER BY l.id DESC
|
ORDER BY l.id DESC
|
||||||
LIMIT 50
|
LIMIT 50
|
||||||
"#
|
"#,
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_all(&*state.db)
|
.fetch_all(&*state.db)
|
||||||
@@ -325,10 +334,10 @@ pub async fn get_user_usage(
|
|||||||
json!({
|
json!({
|
||||||
"id": row.get::<i64, _>("id"),
|
"id": row.get::<i64, _>("id"),
|
||||||
"model": row.get::<Option<String>, _>("model"),
|
"model": row.get::<Option<String>, _>("model"),
|
||||||
"prompt_tokens": row.get::<i64, _>("prompt_tokens"),
|
"prompt_tokens": row.get::<Option<i64>, _>("prompt_tokens").unwrap_or(0),
|
||||||
"completion_tokens": row.get::<i64, _>("completion_tokens"),
|
"completion_tokens": row.get::<Option<i64>, _>("completion_tokens").unwrap_or(0),
|
||||||
"total_tokens": row.get::<i64, _>("total_tokens"),
|
"total_tokens": row.get::<Option<i64>, _>("total_tokens").unwrap_or(0),
|
||||||
"cost": row.get::<f64, _>("cost"),
|
"cost": row.get::<Option<f64>, _>("cost").unwrap_or(0.0),
|
||||||
"created_at": row.get::<Option<String>, _>("created_at"),
|
"created_at": row.get::<Option<String>, _>("created_at"),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -345,7 +354,6 @@ pub async fn get_user_usage(
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// PUT /api/admin/users/{id}
|
/// PUT /api/admin/users/{id}
|
||||||
pub async fn update_user(
|
pub async fn update_user(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -468,3 +476,28 @@ async fn ensure_email_available(
|
|||||||
}
|
}
|
||||||
Ok(())
|
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,
|
AppState,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State, Query},
|
extract::{Path, Query, State},
|
||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
routing::{delete, get},
|
routing::{delete, get},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
@@ -168,15 +168,23 @@ async fn get_user_logs(
|
|||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let user_id = current_user_id(&headers, &state)?;
|
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 limit = params
|
||||||
let offset = params.get("offset").and_then(|v| v.parse::<i64>().ok()).unwrap_or(0).max(0);
|
.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>(
|
let items: Vec<Value> = sqlx::query_as::<_, crate::routes::stats::ProxyLogRow>(
|
||||||
"SELECT l.* FROM proxy_logs l
|
"SELECT l.* FROM proxy_logs l
|
||||||
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
|
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
|
||||||
WHERE k.user_id = ?
|
WHERE k.user_id = ?
|
||||||
ORDER BY l.created_at DESC
|
ORDER BY l.created_at DESC
|
||||||
LIMIT ? OFFSET ?"
|
LIMIT ? OFFSET ?",
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(limit)
|
.bind(limit)
|
||||||
@@ -190,7 +198,7 @@ async fn get_user_logs(
|
|||||||
let total: i64 = sqlx::query_scalar(
|
let total: i64 = sqlx::query_scalar(
|
||||||
"SELECT COUNT(*) FROM proxy_logs l
|
"SELECT COUNT(*) FROM proxy_logs l
|
||||||
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
|
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
|
||||||
WHERE k.user_id = ?"
|
WHERE k.user_id = ?",
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_one(&*state.db)
|
.fetch_one(&*state.db)
|
||||||
@@ -216,7 +224,7 @@ async fn get_user_summary(
|
|||||||
COALESCE(SUM(l.estimated_cost), 0.0)
|
COALESCE(SUM(l.estimated_cost), 0.0)
|
||||||
FROM proxy_logs l
|
FROM proxy_logs l
|
||||||
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
|
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
|
||||||
WHERE k.user_id = ?"
|
WHERE k.user_id = ?",
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_one(&*state.db)
|
.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;
|
label: string;
|
||||||
platform: 'openai' | 'claude';
|
platform: 'openai' | 'claude';
|
||||||
defaultUrl: string;
|
defaultUrl: string;
|
||||||
initialSegment: string;
|
initialSegment: 'session' | 'apikey';
|
||||||
|
description?: string;
|
||||||
recommendedSkipModelFetch: boolean;
|
recommendedSkipModelFetch: boolean;
|
||||||
recommendedModels: string[];
|
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 listSiteInitializationPresets(): SiteInitializationPreset[];
|
||||||
export declare function detectSiteInitializationPreset(url: string, knownPlatform?: string): {
|
export declare function detectSiteInitializationPreset(url: string, knownPlatform?: string): {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ export interface SiteInitializationPreset {
|
|||||||
label: string;
|
label: string;
|
||||||
platform: 'openai' | 'claude';
|
platform: 'openai' | 'claude';
|
||||||
defaultUrl: string;
|
defaultUrl: string;
|
||||||
initialSegment: string;
|
initialSegment: 'session' | 'apikey';
|
||||||
|
description?: string;
|
||||||
recommendedSkipModelFetch: boolean;
|
recommendedSkipModelFetch: boolean;
|
||||||
recommendedModels: string[];
|
recommendedModels: string[];
|
||||||
}
|
}
|
||||||
@@ -158,7 +159,8 @@ const rootReselect: Array<{ rootPrefix: string; id: string }> = [
|
|||||||
{ rootPrefix: 'https://coding.dashscope.aliyuncs.com', id: 'codingplan-openai' },
|
{ 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);
|
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',
|
'再次输入密码': 'Re-enter password',
|
||||||
'你的昵称': 'Your nickname',
|
'你的昵称': 'Your nickname',
|
||||||
'用户中心': 'User Portal',
|
'用户中心': 'User Portal',
|
||||||
'邮箱': 'Email',
|
|
||||||
'密码': 'Password',
|
|
||||||
'个人信息': 'Profile',
|
|
||||||
'修改密码': 'Change Password',
|
|
||||||
'API 密钥': 'API Keys',
|
'API 密钥': 'API Keys',
|
||||||
'新建密钥': 'New Key',
|
'新建密钥': 'New Key',
|
||||||
'当前密码': 'Current Password',
|
'当前密码': 'Current Password',
|
||||||
'新密码': 'New Password',
|
'新密码': 'New Password',
|
||||||
'确认新密码': 'Confirm New Password',
|
'确认新密码': 'Confirm New Password',
|
||||||
'修改密码': 'Change Password',
|
|
||||||
'修改中...': 'Changing...',
|
'修改中...': 'Changing...',
|
||||||
'密码已更新': 'Password updated',
|
'密码已更新': 'Password updated',
|
||||||
'密钥名称': 'Key Name',
|
'密钥名称': 'Key Name',
|
||||||
'暂无 API 密钥': 'No API Keys',
|
'暂无 API 密钥': 'No API Keys',
|
||||||
'用户名': 'Username',
|
|
||||||
'角色': 'Role',
|
|
||||||
'状态': 'Status',
|
|
||||||
'注册时间': 'Registered',
|
'注册时间': 'Registered',
|
||||||
'正常': 'Active',
|
'正常': 'Active',
|
||||||
'已禁用': 'Disabled',
|
'已禁用': 'Disabled',
|
||||||
'管理员': 'Admin',
|
|
||||||
'普通用户': 'User',
|
'普通用户': 'User',
|
||||||
'启用': 'Enabled',
|
|
||||||
'禁用': 'Disabled',
|
|
||||||
'创建': 'Create',
|
'创建': 'Create',
|
||||||
'创建中...': 'Creating...',
|
|
||||||
'保存': 'Save',
|
|
||||||
'编辑': 'Edit',
|
|
||||||
'删除': 'Delete',
|
|
||||||
'取消': 'Cancel',
|
|
||||||
'密钥已创建,请立即复制(不会再次显示):': 'Key created. Copy now (won\'t show again):',
|
'密钥已创建,请立即复制(不会再次显示):': 'Key created. Copy now (won\'t show again):',
|
||||||
'关闭': 'Close',
|
|
||||||
'退出': 'Sign Out',
|
'退出': 'Sign Out',
|
||||||
'用户管理': 'User Management',
|
'用户管理': 'User Management',
|
||||||
'共': 'Total',
|
|
||||||
'个用户': ' users',
|
'个用户': ' users',
|
||||||
'总用户': 'Total Users',
|
'总用户': 'Total Users',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user