From d8b87bd5cb8a753a72d53c8cae05f24bba42606e Mon Sep 17 00:00:00 2001 From: lizhouyang Date: Wed, 8 Apr 2026 23:14:23 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(user-token):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E9=80=89=E6=8B=A9UI=E5=B9=B6=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E4=B8=AD=E6=96=87=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改进模型选择界面:添加精美图标和动画效果 - 使用 Sparkles/Check/X 图标增强视觉体验 - 添加渐变背景和悬停动画效果 - 完善 user_token 模块的中文翻译 - 更新所有默认显示文本为中文 --- src-tauri/src/commands/user_token.rs | 4 + src-tauri/src/modules/user_token_db.rs | 48 +++- src-tauri/src/proxy/middleware/auth.rs | 3 +- src/locales/zh.json | 17 +- src/pages/UserToken.tsx | 375 +++++++++++++++++++++---- 5 files changed, 378 insertions(+), 69 deletions(-) diff --git a/src-tauri/src/commands/user_token.rs b/src-tauri/src/commands/user_token.rs index 6b0f38b08..2f3fe0de8 100644 --- a/src-tauri/src/commands/user_token.rs +++ b/src-tauri/src/commands/user_token.rs @@ -10,6 +10,7 @@ pub struct CreateTokenRequest { pub curfew_start: Option, pub curfew_end: Option, pub custom_expires_at: Option, // 自定义过期时间戳 (秒) + pub allowed_models: Vec, // 允许访问的模型列表,空列表表示不限制 } #[derive(Debug, Serialize, Deserialize)] @@ -20,6 +21,7 @@ pub struct UpdateTokenRequest { pub max_ips: Option, pub curfew_start: Option>, pub curfew_end: Option>, + pub allowed_models: Option>, // 允许访问的模型列表,None=不更新 } // 命令实现 @@ -41,6 +43,7 @@ pub async fn create_user_token(request: CreateTokenRequest) -> Result Resul request.max_ips, request.curfew_start, request.curfew_end, + request.allowed_models, ) } diff --git a/src-tauri/src/modules/user_token_db.rs b/src-tauri/src/modules/user_token_db.rs index ac068b67f..6138c9c55 100644 --- a/src-tauri/src/modules/user_token_db.rs +++ b/src-tauri/src/modules/user_token_db.rs @@ -6,6 +6,7 @@ use rusqlite::{params, Connection, OptionalExtension}; use serde::{Deserialize, Serialize}; +use serde_json; use std::path::PathBuf; use uuid::Uuid; use chrono::{Utc, Local, Timelike, FixedOffset}; @@ -28,6 +29,7 @@ pub struct UserToken { pub last_used_at: Option, pub total_requests: i64, pub total_tokens_used: i64, + pub allowed_models: Vec, // 允许访问的模型列表,空列表表示不限制 } /// 令牌 IP 绑定结构体 @@ -105,6 +107,8 @@ pub fn init_db() -> Result<(), String> { let _ = conn.execute("ALTER TABLE user_tokens ADD COLUMN last_used_at INTEGER", []); let _ = conn.execute("ALTER TABLE user_tokens ADD COLUMN curfew_start TEXT", []); let _ = conn.execute("ALTER TABLE user_tokens ADD COLUMN curfew_end TEXT", []); + let _ = conn.execute("ALTER TABLE user_tokens ADD COLUMN allowed_models TEXT", []); + let _ = conn.execute("UPDATE user_tokens SET allowed_models = '[]' WHERE allowed_models IS NULL", []); // 创建 token_ip_bindings 表 conn.execute( @@ -161,7 +165,8 @@ pub fn create_token( max_ips: i32, curfew_start: Option, curfew_end: Option, - custom_expires_at: Option // 自定义过期时间戳 (秒) + custom_expires_at: Option, + allowed_models: Vec // 允许访问的模型列表 ) -> Result { let conn = connect_db()?; let id = Uuid::new_v4().to_string(); @@ -192,14 +197,15 @@ pub fn create_token( last_used_at: None, total_requests: 0, total_tokens_used: 0, + allowed_models, }; conn.execute( "INSERT INTO user_tokens ( id, token, username, description, enabled, expires_type, expires_at, max_ips, curfew_start, curfew_end, - created_at, updated_at, total_requests, total_tokens_used - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + created_at, updated_at, total_requests, total_tokens_used, allowed_models + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", params![ user_token.id, user_token.token, @@ -215,6 +221,7 @@ pub fn create_token( user_token.updated_at, user_token.total_requests, user_token.total_tokens_used, + serde_json::to_string(&user_token.allowed_models).unwrap_or_else(|_| "[]".to_string()), ], ).map_err(|e| format!("Failed to insert user token: {}", e))?; @@ -244,6 +251,9 @@ pub fn list_tokens() -> Result, String> { last_used_at: row.get("last_used_at").unwrap_or(None), total_requests: row.get("total_requests").unwrap_or(0), total_tokens_used: row.get("total_tokens_used").unwrap_or(0), + allowed_models: row.get::<_, Option>("allowed_models")? + .map(|s| serde_json::from_str(&s).unwrap_or_default()) + .unwrap_or_default(), }) }).map_err(|e| format!("Failed to query tokens: {}", e))?; @@ -278,6 +288,9 @@ pub fn get_token_by_id(id: &str) -> Result, String> { last_used_at: row.get("last_used_at")?, total_requests: row.get("total_requests")?, total_tokens_used: row.get("total_tokens_used")?, + allowed_models: row.get::<_, Option>("allowed_models")? + .map(|s| serde_json::from_str(&s).unwrap_or_default()) + .unwrap_or_default(), }) }).optional().map_err(|e| format!("Failed to query token: {}", e))?; @@ -307,6 +320,9 @@ pub fn get_token_by_value(token: &str) -> Result, String> { last_used_at: row.get("last_used_at")?, total_requests: row.get("total_requests")?, total_tokens_used: row.get("total_tokens_used")?, + allowed_models: row.get::<_, Option>("allowed_models")? + .map(|s| serde_json::from_str(&s).unwrap_or_default()) + .unwrap_or_default(), }) }).optional().map_err(|e| format!("Failed to query token: {}", e))?; @@ -321,7 +337,8 @@ pub fn update_token( enabled: Option, max_ips: Option, curfew_start: Option>, - curfew_end: Option> + curfew_end: Option>, + allowed_models: Option> ) -> Result<(), String> { let conn = connect_db()?; let now = Utc::now().timestamp(); @@ -366,6 +383,12 @@ pub fn update_token( param_idx += 1; } + if let Some(models) = allowed_models { + query.push_str(&format!(", allowed_models = ?{}", param_idx)); + params_vec.push(Box::new(serde_json::to_string(&models).unwrap_or_else(|_| "[]".to_string()))); + param_idx += 1; + } + query.push_str(&format!(" WHERE id = ?{}", param_idx)); params_vec.push(Box::new(id.to_string())); @@ -498,9 +521,9 @@ pub fn record_token_usage_and_ip( Ok(()) } -/// 检查 Token 是否有效 (包含过期时间检查和 IP 限制检查) +/// 检查 Token 是否有效 (包含过期时间检查、IP 限制检查和模型访问控制) /// 返回: (是否有效, 拒绝原因) -pub fn validate_token(token_str: &str, ip: &str) -> Result<(bool, Option), String> { +pub fn validate_token(token_str: &str, ip: &str, model: Option<&str>) -> Result<(bool, Option), String> { let token_opt = get_token_by_value(token_str)?; if let Some(token) = token_opt { @@ -560,6 +583,19 @@ pub fn validate_token(token_str: &str, ip: &str) -> Result<(bool, Option } } + // 4. 检查模型访问限制 + if let Some(model_name) = model { + if !token.allowed_models.is_empty() { + if !token.allowed_models.contains(&model_name.to_string()) { + return Ok((false, Some(format!( + "Model '{}' is not allowed for this token. Allowed models: {}", + model_name, + token.allowed_models.join(", ") + )))); + } + } + } + // 一切正常,Token 有效 Ok((true, None)) } else { diff --git a/src-tauri/src/proxy/middleware/auth.rs b/src-tauri/src/proxy/middleware/auth.rs index 1bdc9744f..d1514a97e 100644 --- a/src-tauri/src/proxy/middleware/auth.rs +++ b/src-tauri/src/proxy/middleware/auth.rs @@ -182,7 +182,8 @@ async fn auth_middleware_internal( .unwrap_or_else(|| "127.0.0.1".to_string()); // Default fallback // 验证 Token - match crate::modules::user_token_db::validate_token(token, &client_ip) { + // 注意:模型验证在后续请求处理器中进行,这里暂时传递 None + match crate::modules::user_token_db::validate_token(token, &client_ip, None) { Ok((true, _)) => { // Token 有效,查询信息以便传递 if let Ok(Some(user_token)) = crate::modules::user_token_db::get_token_by_value(token) { diff --git a/src/locales/zh.json b/src/locales/zh.json index c96bcd866..3638b0183 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1348,6 +1348,21 @@ "placeholder_desc": "选填备注", "placeholder_max_ips": "0 = 不限制", "hint_max_ips": "0 表示不限制", - "hint_curfew": "留空则禁用。基于服务器时间。" + "hint_curfew": "留空则禁用。基于服务器时间。", + "allowed_models": "允许使用的模型", + "select_all": "全选", + "clear_all": "清空", + "models_unlimited": "留空 = 所有模型可用", + "models_selected": "个模型已选", + "custom_expires_at": "自定义过期时间", + "hint_custom_expires": "选择此 Token 过期的确切日期和时间", + "expires_custom": "自定义", + "copy_failed": "复制失败", + "load_failed": "加载失败", + "custom_expires_required": "请选择自定义过期时间", + "expires_in": "有效期", + "curfew_time_range": "宵禁时间段", + "curfew_to": "至", + "curfew_hint": "留空则禁用宵禁。基于服务器时间。" } } \ No newline at end of file diff --git a/src/pages/UserToken.tsx b/src/pages/UserToken.tsx index fd0ce3784..617398069 100644 --- a/src/pages/UserToken.tsx +++ b/src/pages/UserToken.tsx @@ -1,10 +1,11 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Plus, Trash2, RefreshCw, Copy, Activity, User, Settings, Shield, Clock, Users } from 'lucide-react'; +import { Plus, Trash2, RefreshCw, Copy, Activity, User, Settings, Shield, Clock, Users, Check, Sparkles, X } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { request as invoke } from '../utils/request'; import { showToast } from '../components/common/ToastContainer'; import { copyToClipboard } from '../utils/clipboard'; +import { MODEL_CONFIG } from '../config/modelConfig'; interface UserToken { id: string; @@ -22,6 +23,7 @@ interface UserToken { last_used_at?: number; total_requests: number; total_tokens_used: number; + allowed_models: string[]; } interface UserTokenStats { @@ -50,6 +52,7 @@ const UserToken: React.FC = () => { const [editCurfewStart, setEditCurfewStart] = useState(''); const [editCurfewEnd, setEditCurfewEnd] = useState(''); const [updating, setUpdating] = useState(false); + const [editAllowedModels, setEditAllowedModels] = useState([]); // Create Form State const [newUsername, setNewUsername] = useState(''); @@ -59,6 +62,7 @@ const UserToken: React.FC = () => { const [newCurfewStart, setNewCurfewStart] = useState(''); const [newCurfewEnd, setNewCurfewEnd] = useState(''); const [newCustomExpires, setNewCustomExpires] = useState(''); // datetime-local value + const [newAllowedModels, setNewAllowedModels] = useState([]); const loadData = async () => { setLoading(true); @@ -71,7 +75,7 @@ const UserToken: React.FC = () => { setStats(statsData); } catch (e) { console.error('Failed to load user tokens', e); - showToast(t('common.load_failed') || 'Failed to load data', 'error'); + showToast(t('user_token.load_failed') || '加载数据失败', 'error'); } finally { setLoading(false); } @@ -83,13 +87,13 @@ const UserToken: React.FC = () => { const handleCreate = async () => { if (!newUsername) { - showToast(t('user_token.username_required') || 'Username is required', 'error'); + showToast(t('user_token.username_required') || '用户名不能为空', 'error'); return; } // 验证自定义时间 if (newExpiresType === 'custom' && !newCustomExpires) { - showToast(t('user_token.custom_expires_required') || 'Please select a custom expiration time', 'error'); + showToast(t('user_token.custom_expires_required') || '请选择自定义过期时间', 'error'); return; } @@ -108,7 +112,8 @@ const UserToken: React.FC = () => { max_ips: newMaxIps, curfew_start: newCurfewStart || null, curfew_end: newCurfewEnd || null, - custom_expires_at: customExpiresAt || null + custom_expires_at: customExpiresAt || null, + allowed_models: newAllowedModels } }); showToast(t('common.create_success') || 'Created successfully', 'success'); @@ -120,6 +125,7 @@ const UserToken: React.FC = () => { setNewCurfewStart(''); setNewCurfewEnd(''); setNewCustomExpires(''); + setNewAllowedModels([]); loadData(); } catch (e) { console.error('Failed to create token', e); @@ -144,9 +150,10 @@ const UserToken: React.FC = () => { setEditingToken(token); setEditUsername(token.username); setEditDesc(token.description || ''); - setEditMaxIps(token.max_ips ?? 0); // 使用 ?? 确保 null/undefined 变为 0 + setEditMaxIps(token.max_ips ?? 0); setEditCurfewStart(token.curfew_start ?? ''); setEditCurfewEnd(token.curfew_end ?? ''); + setEditAllowedModels(token.allowed_models || []); setShowEditModal(true); }; @@ -165,9 +172,9 @@ const UserToken: React.FC = () => { username: editUsername, description: editDesc || undefined, max_ips: editMaxIps, - // 使用双层包装: undefined = 不更新, null = 清空, string = 设置值 curfew_start: editCurfewStart === '' ? null : editCurfewStart, - curfew_end: editCurfewEnd === '' ? null : editCurfewEnd + curfew_end: editCurfewEnd === '' ? null : editCurfewEnd, + allowed_models: editAllowedModels } }); showToast(t('common.update_success') || 'Updated successfully', 'success'); @@ -195,9 +202,9 @@ const UserToken: React.FC = () => { const handleCopyToken = async (text: string) => { const success = await copyToClipboard(text); if (success) { - showToast(t('common.copied') || 'Copied to clipboard', 'success'); + showToast(t('common.copied') || '已复制到剪贴板', 'success'); } else { - showToast(t('common.copy_failed') || 'Failed to copy to clipboard', 'error'); + showToast(t('user_token.copy_failed') || '复制失败', 'error'); } }; @@ -254,7 +261,7 @@ const UserToken: React.FC = () => { className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium rounded-lg transition-all flex items-center gap-2 shadow-sm shadow-blue-500/20" > - {t('user_token.create', { defaultValue: 'Create Token' })} + {t('user_token.create', { defaultValue: '创建 Token' })} @@ -271,7 +278,7 @@ const UserToken: React.FC = () => {
{stats?.total_users || 0}
-
{t('user_token.total_users', { defaultValue: 'Total Users' })}
+
{t('user_token.total_users', { defaultValue: '用户总数' })}
{
{stats?.active_tokens || 0}
-
{t('user_token.active_tokens', { defaultValue: 'Active Tokens' })}
+
{t('user_token.active_tokens', { defaultValue: '活跃 Token' })}
{
{stats?.total_tokens || 0}
-
{t('user_token.total_created', { defaultValue: 'Total Tokens' })}
+
{t('user_token.total_created', { defaultValue: '累计创建' })}
{
{stats?.today_requests || 0}
-
{t('user_token.today_requests', { defaultValue: 'Today Requests' })}
+
{t('user_token.today_requests', { defaultValue: '今日请求数' })}
@@ -319,13 +326,13 @@ const UserToken: React.FC = () => { - + - - - - - + + + + + @@ -365,7 +372,7 @@ const UserToken: React.FC = () => { @@ -459,11 +484,11 @@ const UserToken: React.FC = () => { {showCreateModal && (
-

{t('user_token.create_title', { defaultValue: 'Create New Token' })}

+

{t('user_token.create_title', { defaultValue: '创建新 Token' })}

{
setNewDesc(e.target.value)} - placeholder={t('user_token.placeholder_desc', { defaultValue: 'Optional notes' })} + placeholder={t('user_token.placeholder_desc', { defaultValue: '选填备注' })} />
{ value={newMaxIps} onChange={e => setNewMaxIps(parseInt(e.target.value) || 0)} min="0" - placeholder={t('user_token.placeholder_max_ips', { defaultValue: '0 = Unlimited' })} + placeholder={t('user_token.placeholder_max_ips', { defaultValue: '0 = 不限制' })} />
@@ -527,7 +552,7 @@ const UserToken: React.FC = () => { {newExpiresType === 'custom' && (
{ min={new Date().toISOString().slice(0, 16)} />
)}
{ value={newCurfewStart} onChange={e => setNewCurfewStart(e.target.value)} /> - to + {t('user_token.curfew_to', { defaultValue: '至' })} { />
+
+ + {/* 模型限制选择 */} +
+ +
+
+ + +
+
+ {Object.entries( + Object.entries(MODEL_CONFIG).reduce((acc, [id, config]) => { + const group = config.group; + if (!acc[group]) acc[group] = []; + acc[group].push({ id, ...config }); + return acc; + }, {} as Record>) + ).map(([group, models]) => ( +
+
+ + {group} +
+
+ {models.map((model) => { + const isSelected = newAllowedModels.includes(model.id); + return ( + +
+ + {isSelected && } + +
+
{model.shortLabel}
+ {model.label && ( +
{model.label}
+ )} +
+
+ { + if (e.target.checked) { + setNewAllowedModels([...newAllowedModels, model.id]); + } else { + setNewAllowedModels(newAllowedModels.filter(id => id !== model.id)); + } + }} + /> +
+ ); + })} +
+
+ ))} +
+
+
@@ -576,7 +715,7 @@ const UserToken: React.FC = () => { disabled={creating} > {creating && } - {t('common.create', { defaultValue: 'Create' })} + {t('common.create', { defaultValue: '创建' })}
@@ -587,11 +726,11 @@ const UserToken: React.FC = () => { {showEditModal && editingToken && (
-

{t('user_token.edit_title', { defaultValue: 'Edit Token' })}

+

{t('user_token.edit_title', { defaultValue: '编辑 Token' })}

{
setEditDesc(e.target.value)} - placeholder={t('user_token.placeholder_desc', { defaultValue: 'Optional notes' })} + placeholder={t('user_token.placeholder_desc', { defaultValue: '选填备注' })} />
{
{ value={editCurfewStart} onChange={e => setEditCurfewStart(e.target.value)} /> - to + {t('user_token.curfew_to', { defaultValue: '至' })} { />
+
+ + {/* 模型限制选择 */} +
+ +
+
+ + +
+
+ {Object.entries( + Object.entries(MODEL_CONFIG).reduce((acc, [id, config]) => { + const group = config.group; + if (!acc[group]) acc[group] = []; + acc[group].push({ id, ...config }); + return acc; + }, {} as Record>) + ).map(([group, models]) => ( +
+
+ + {group} +
+
+ {models.map((model) => { + const isSelected = editAllowedModels.includes(model.id); + return ( + +
+ + {isSelected && } + +
+
{model.shortLabel}
+ {model.label && ( +
{model.label}
+ )} +
+
+ { + if (e.target.checked) { + setEditAllowedModels([...editAllowedModels, model.id]); + } else { + setEditAllowedModels(editAllowedModels.filter(id => id !== model.id)); + } + }} + /> +
+ ); + })} +
+
+ ))} +
+
+
From d24ab8bc0742f56c98fa72c6769fd81564b66cc2 Mon Sep 17 00:00:00 2001 From: lizhouyang Date: Wed, 8 Apr 2026 23:23:41 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(proxy):=20=E6=B5=81=E9=87=8F=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=88=97=E8=A1=A8=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 ProxyMonitor 表格中新增用户列,显示发起请求的终端用户名 - 同步更新12种语言的国际化翻译 - 支持未使用 User Token 时显示 '-' --- src/components/proxy/ProxyMonitor.tsx | 5 +++++ src/locales/ar.json | 1 + src/locales/en.json | 1 + src/locales/es.json | 1 + src/locales/ja.json | 1 + src/locales/ko.json | 1 + src/locales/my.json | 1 + src/locales/pt.json | 1 + src/locales/ru.json | 1 + src/locales/tr.json | 1 + src/locales/vi.json | 1 + src/locales/zh-TW.json | 1 + src/locales/zh.json | 1 + 13 files changed, 17 insertions(+) diff --git a/src/components/proxy/ProxyMonitor.tsx b/src/components/proxy/ProxyMonitor.tsx index 112e1d80d..2ae608f35 100644 --- a/src/components/proxy/ProxyMonitor.tsx +++ b/src/components/proxy/ProxyMonitor.tsx @@ -28,6 +28,7 @@ interface ProxyRequestLog { output_tokens?: number; account_email?: string; protocol?: string; // "openai" | "anthropic" | "gemini" + username?: string; // User Token username } interface ProxyStats { @@ -66,6 +67,7 @@ const LogTable: React.FC = ({
+ @@ -105,6 +107,9 @@ const LogTable: React.FC = ({ + - + @@ -353,7 +389,7 @@ const UserToken: React.FC = () => { - {tokens.map((token, index) => ( + {sortedTokens.map((token, index) => (
{t('user_token.username', { defaultValue: 'Username' })}{t('user_token.username', { defaultValue: '用户名' })} {t('user_token.token', { defaultValue: 'Token' })}{t('user_token.expires', { defaultValue: 'Expires' })}{t('user_token.usage', { defaultValue: 'Usage' })}{t('user_token.ip_limit', { defaultValue: 'IP Limit' })}{t('user_token.created', { defaultValue: 'Created' })}{t('common.actions', { defaultValue: 'Actions' })}{t('user_token.expires', { defaultValue: '过期时间' })}{t('user_token.usage', { defaultValue: '使用量' })}{t('user_token.ip_limit', { defaultValue: 'IP 限制' })}{t('user_token.created', { defaultValue: '创建时间' })}{t('common.actions', { defaultValue: '操作' })}
- {token.expires_at ? formatTime(token.expires_at) : t('user_token.never', { defaultValue: 'Never' })} + {token.expires_at ? formatTime(token.expires_at) : t('user_token.never', { defaultValue: '永不过期' })}
@@ -376,21 +383,21 @@ const UserToken: React.FC = () => { onClick={() => handleRenew(token.id, token.expires_type)} className="text-[10px] text-blue-500 hover:underline font-medium" > - {t('user_token.renew_button', { defaultValue: 'Renew' })} + {t('user_token.renew_button', { defaultValue: '续费' })} )}
-
{token.total_requests} reqs
+
{token.total_requests} 次请求
- {(token.total_tokens_used / 1000).toFixed(1)}k tokens + {(token.total_tokens_used / 1000).toFixed(1)}k Token
{token.max_ips === 0 - ? {t('user_token.unlimited', { defaultValue: 'Unlimited' })} - : {token.max_ips} IPs + ? {t('user_token.unlimited', { defaultValue: '不限' })} + : {token.max_ips} IP } {token.curfew_start && token.curfew_end && (
@@ -398,6 +405,24 @@ const UserToken: React.FC = () => { {token.curfew_start} - {token.curfew_end}
)} + {token.allowed_models && token.allowed_models.length > 0 && ( +
+ {token.allowed_models.slice(0, 3).map((modelId) => ( + + {MODEL_CONFIG[modelId]?.shortLabel || modelId.substring(0, 8)} + + ))} + {token.allowed_models.length > 3 && ( + + +{token.allowed_models.length - 3} + + )} +
+ )}
{formatTime(token.created_at)} @@ -417,9 +442,9 @@ const UserToken: React.FC = () => { {t('monitor.table.model')} {t('monitor.table.protocol')} {t('monitor.table.account')}{t('monitor.table.user')} {t('monitor.table.path')} {t('monitor.table.usage')} {t('monitor.table.duration')} {log.account_email ? log.account_email.replace(/(.{3}).*(@.*)/, '$1***$2') : '-'} + {log.username || '-'} + {log.url} {log.input_tokens != null &&
I: {formatCompactNumber(log.input_tokens)}
} diff --git a/src/locales/ar.json b/src/locales/ar.json index 94d5bb01f..cbd62f0de 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -966,6 +966,7 @@ "model": "النموذج", "protocol": "البروتوكول", "account": "الحساب", + "user": "المستخدم", "path": "المسار", "usage": "توكنز", "duration": "المدة", diff --git a/src/locales/en.json b/src/locales/en.json index 6ed1f9e3d..6afe8d011 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1140,6 +1140,7 @@ "model": "Model", "protocol": "Protocol", "account": "Account", + "user": "User", "path": "Path", "usage": "Tokens", "duration": "Duration", diff --git a/src/locales/es.json b/src/locales/es.json index 6769e8369..2095878ee 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -962,6 +962,7 @@ "model": "Modelo", "protocol": "Protocolo", "account": "Cuenta", + "user": "Usuario", "path": "Ruta", "usage": "Tokens", "duration": "Duración", diff --git a/src/locales/ja.json b/src/locales/ja.json index da6eaca2c..468b1156f 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1086,6 +1086,7 @@ "model": "モデル", "protocol": "プロトコル", "account": "アカウント", + "user": "ユーザー", "path": "パス", "usage": "トークン", "duration": "所要時間", diff --git a/src/locales/ko.json b/src/locales/ko.json index db69ff50e..e58585ff4 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -996,6 +996,7 @@ "model": "모델", "protocol": "프로토콜", "account": "계정", + "user": "사용자", "path": "경로", "usage": "토큰", "duration": "소요 시간", diff --git a/src/locales/my.json b/src/locales/my.json index 6b8e9eb00..5c63666ff 100644 --- a/src/locales/my.json +++ b/src/locales/my.json @@ -962,6 +962,7 @@ "model": "Model", "protocol": "Protokol", "account": "Akaun", + "user": "Pengguna", "path": "Laluan", "usage": "Token", "duration": "Tempoh", diff --git a/src/locales/pt.json b/src/locales/pt.json index 346619517..4953325f6 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -962,6 +962,7 @@ "model": "Modelo", "protocol": "Protocolo", "account": "Conta", + "user": "Usuário", "path": "Caminho", "usage": "Tokens", "duration": "Duração", diff --git a/src/locales/ru.json b/src/locales/ru.json index 1e649d479..a281e3215 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -963,6 +963,7 @@ "model": "Модель", "protocol": "Протокол", "account": "Аккаунт", + "user": "Пользователь", "path": "Путь", "usage": "Токены", "duration": "Длительность", diff --git a/src/locales/tr.json b/src/locales/tr.json index 20a9069f1..7dc6ee7c7 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -938,6 +938,7 @@ "model": "Model", "protocol": "Protokol", "account": "Hesap", + "user": "Kullanıcı", "path": "Yol", "usage": "Token'lar", "duration": "Süre", diff --git a/src/locales/vi.json b/src/locales/vi.json index 74563d25a..7affc1250 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -969,6 +969,7 @@ "model": "Model", "protocol": "Giao thức", "account": "Tài khoản", + "user": "Người dùng", "path": "Đường dẫn (Path)", "usage": "Tokens", "duration": "Thời gian", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index 3b07a8b7b..4e7f49374 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -996,6 +996,7 @@ "model": "模型", "protocol": "協定", "account": "帳號", + "user": "用戶", "path": "路徑", "usage": "Token 消耗", "duration": "耗時", diff --git a/src/locales/zh.json b/src/locales/zh.json index 3638b0183..6ae1d2206 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1133,6 +1133,7 @@ "model": "模型", "protocol": "协议", "account": "账号", + "user": "用户", "path": "路径", "usage": "Token 消耗", "duration": "耗时", From edb525fa7ce2255e65ad98dab85d771d41b65ee3 Mon Sep 17 00:00:00 2001 From: lizhouyang Date: Wed, 8 Apr 2026 23:28:10 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20user-token=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E4=BB=8A=E6=97=A5=E8=AF=B7=E6=B1=82=E6=95=B0?= =?UTF-8?q?=E5=A7=8B=E7=BB=88=E6=98=BE=E7=A4=BA0=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题原因: - get_user_token_summary 函数将 today_requests 硬编码为 0 修复方案: - 从 security_db 模块调用 get_ip_stats() 获取真实的今日请求数 - 该函数从 ip_access_logs 表统计 timestamp >= 今日零点 的记录数 --- src-tauri/src/commands/user_token.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/commands/user_token.rs b/src-tauri/src/commands/user_token.rs index 2f3fe0de8..c46085314 100644 --- a/src-tauri/src/commands/user_token.rs +++ b/src-tauri/src/commands/user_token.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use crate::modules::user_token_db::{self, UserToken, TokenIpBinding}; +use crate::modules::security_db; #[derive(Debug, Serialize, Deserialize)] pub struct CreateTokenRequest { @@ -100,13 +101,13 @@ pub async fn get_user_token_summary() -> Result { users.insert(t.username.clone()); } - // 这里简单返回一些数据,请求数最好从数据库聚合查询 - // 目前仅作为演示,请求数暂不精确统计今日的 + // 从安全数据库获取今日请求数 + let ip_stats = security_db::get_ip_stats()?; Ok(UserTokenStats { total_tokens: tokens.len(), active_tokens, total_users: users.len(), - today_requests: 0, // TODO: Implement daily stats query + today_requests: ip_stats.today_requests as i64, }) } From 5bb2d23e8bc33264c867fabdbeb777eee7984989 Mon Sep 17 00:00:00 2001 From: lizhouyang Date: Wed, 8 Apr 2026 23:34:18 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20user-token=20=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BB=8A=E6=97=A5=20Tokens=20=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端:UserTokenStats 添加 today_tokens 字段 - 后端:从 token_stats 模块获取 24 小时内的 tokens 统计 - 前端:统计卡片布局从 4 列改为 5 列,新增今日 Tokens 卡片 - 新增 Zap 图标显示,使用黄色主题 --- src-tauri/src/commands/user_token.rs | 6 ++++++ src/locales/en.json | 1 + src/locales/zh.json | 1 + src/pages/UserToken.tsx | 20 ++++++++++++++++++-- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/commands/user_token.rs b/src-tauri/src/commands/user_token.rs index c46085314..260de3841 100644 --- a/src-tauri/src/commands/user_token.rs +++ b/src-tauri/src/commands/user_token.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::modules::user_token_db::{self, UserToken, TokenIpBinding}; use crate::modules::security_db; +use crate::modules::token_stats; #[derive(Debug, Serialize, Deserialize)] pub struct CreateTokenRequest { @@ -87,6 +88,7 @@ pub struct UserTokenStats { pub active_tokens: usize, pub total_users: usize, pub today_requests: i64, + pub today_tokens: i64, } /// 获取简单的统计信息 @@ -104,10 +106,14 @@ pub async fn get_user_token_summary() -> Result { // 从安全数据库获取今日请求数 let ip_stats = security_db::get_ip_stats()?; + // 从 token_stats 获取今日 tokens (24小时) + let token_summary = token_stats::get_summary_stats(24)?; + Ok(UserTokenStats { total_tokens: tokens.len(), active_tokens, total_users: users.len(), today_requests: ip_stats.today_requests as i64, + today_tokens: token_summary.total_tokens as i64, }) } diff --git a/src/locales/en.json b/src/locales/en.json index 6afe8d011..f91e895b8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1365,6 +1365,7 @@ "ip_limit": "IP Limit", "created": "Created", "today_requests": "Today Requests", + "today_tokens": "Today Tokens", "never": "Never", "renew": "Renew", "renew_button": "Renew", diff --git a/src/locales/zh.json b/src/locales/zh.json index 6ae1d2206..0808bdbe8 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1330,6 +1330,7 @@ "ip_limit": "IP 限制", "created": "创建时间", "today_requests": "今日请求数", + "today_tokens": "今日 Tokens", "never": "永不过期", "renew": "续期", "renew_button": "续费", diff --git a/src/pages/UserToken.tsx b/src/pages/UserToken.tsx index 617398069..70d441922 100644 --- a/src/pages/UserToken.tsx +++ b/src/pages/UserToken.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Plus, Trash2, RefreshCw, Copy, Activity, User, Settings, Shield, Clock, Users, Check, Sparkles, X } from 'lucide-react'; +import { Plus, Trash2, RefreshCw, Copy, Activity, User, Settings, Shield, Clock, Users, Check, Sparkles, X, Zap } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { request as invoke } from '../utils/request'; import { showToast } from '../components/common/ToastContainer'; @@ -31,6 +31,7 @@ interface UserTokenStats { active_tokens: number; total_users: number; today_requests: number; + today_tokens: number; } // interface CreateTokenRequest omitted as it's not explicitly used for typing variables @@ -267,7 +268,7 @@ const UserToken: React.FC = () => { {/* Stats Cards Row */} -
+
{
{stats?.today_requests || 0}
{t('user_token.today_requests', { defaultValue: '今日请求数' })}
+ + +
+
+ +
+
+
+ {stats?.today_tokens ? (stats.today_tokens / 1000).toFixed(1) + 'k' : '0'} +
+
{t('user_token.today_tokens', { defaultValue: '今日 Tokens' })}
+
{/* Token List */} From 37e0994021d225621ef6c6d0deb56c2a537933e8 Mon Sep 17 00:00:00 2001 From: lizhouyang Date: Wed, 8 Apr 2026 23:49:27 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat(=E7=94=A8=E6=88=B7=E4=BB=A4=E7=89=8C):?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E6=8C=89=E4=BD=BF=E7=94=A8=E9=87=8F?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在用户令牌管理页面中,为使用量列添加点击排序功能。用户可以通过点击表头按使用量进行升序或降序排列,排序时显示箭头图标以指示当前排序方向。使用 useMemo 优化排序性能,避免不必要的重新计算。 --- src/pages/UserToken.tsx | 44 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/pages/UserToken.tsx b/src/pages/UserToken.tsx index 70d441922..8c3146fb1 100644 --- a/src/pages/UserToken.tsx +++ b/src/pages/UserToken.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Plus, Trash2, RefreshCw, Copy, Activity, User, Settings, Shield, Clock, Users, Check, Sparkles, X, Zap } from 'lucide-react'; +import { Plus, Trash2, RefreshCw, Copy, Activity, User, Settings, Shield, Clock, Users, Check, Sparkles, X, Zap, ArrowUp, ArrowDown } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { request as invoke } from '../utils/request'; import { showToast } from '../components/common/ToastContainer'; @@ -55,6 +55,10 @@ const UserToken: React.FC = () => { const [updating, setUpdating] = useState(false); const [editAllowedModels, setEditAllowedModels] = useState([]); + // 排序状态 + const [sortField, setSortField] = useState<'usage' | null>(null); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + // Create Form State const [newUsername, setNewUsername] = useState(''); const [newDesc, setNewDesc] = useState(''); @@ -82,6 +86,28 @@ const UserToken: React.FC = () => { } }; + // 按使用量排序切换 + const toggleUsageSort = () => { + if (sortField !== 'usage') { + setSortField('usage'); + setSortOrder('desc'); + } else { + setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc'); + } + }; + + // 根据排序状态处理 tokens + const sortedTokens = useMemo(() => { + if (sortField === 'usage') { + return [...tokens].sort((a, b) => { + return sortOrder === 'desc' + ? b.total_requests - a.total_requests + : a.total_requests - b.total_requests; + }); + } + return tokens; + }, [tokens, sortField, sortOrder]); + useEffect(() => { loadData(); }, []); @@ -345,7 +371,17 @@ const UserToken: React.FC = () => {
{t('user_token.username', { defaultValue: '用户名' })} {t('user_token.token', { defaultValue: 'Token' })} {t('user_token.expires', { defaultValue: '过期时间' })}{t('user_token.usage', { defaultValue: '使用量' })} +
+ {t('user_token.usage', { defaultValue: '使用量' })} + {sortField === 'usage' && ( + sortOrder === 'desc' ? : + )} +
+
{t('user_token.ip_limit', { defaultValue: 'IP 限制' })} {t('user_token.created', { defaultValue: '创建时间' })} {t('common.actions', { defaultValue: '操作' })}