mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 11:20:50 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a66dc225d | ||
|
|
eadfd7a957 |
@@ -63,6 +63,7 @@
|
|||||||
"custom_connection_placeholder": "Eg: https://example.com:8317",
|
"custom_connection_placeholder": "Eg: https://example.com:8317",
|
||||||
"custom_connection_hint": "By default the current URL is used. Override it here if needed.",
|
"custom_connection_hint": "By default the current URL is used. Override it here if needed.",
|
||||||
"use_current_address": "Use Current URL",
|
"use_current_address": "Use Current URL",
|
||||||
|
"remember_password_label": "Remember password",
|
||||||
"management_key_label": "Management Key:",
|
"management_key_label": "Management Key:",
|
||||||
"management_key_placeholder": "Enter the management key",
|
"management_key_placeholder": "Enter the management key",
|
||||||
"connect_button": "Connect",
|
"connect_button": "Connect",
|
||||||
@@ -745,7 +746,11 @@
|
|||||||
"link_webui_repo": "WebUI Repository",
|
"link_webui_repo": "WebUI Repository",
|
||||||
"link_webui_repo_desc": "Management Center frontend source code",
|
"link_webui_repo_desc": "Management Center frontend source code",
|
||||||
"link_docs": "Documentation",
|
"link_docs": "Documentation",
|
||||||
"link_docs_desc": "Usage tutorials and configuration guides"
|
"link_docs_desc": "Usage tutorials and configuration guides",
|
||||||
|
"clear_login_title": "Local Login Data",
|
||||||
|
"clear_login_desc": "Clear locally saved login data and sign out. Usage stats pricing settings will remain untouched.",
|
||||||
|
"clear_login_button": "Clear login data",
|
||||||
|
"clear_login_confirm": "Clear local login data and sign out now?"
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"debug_updated": "Debug settings updated",
|
"debug_updated": "Debug settings updated",
|
||||||
@@ -758,6 +763,7 @@
|
|||||||
"logging_to_file_updated": "Logging settings updated",
|
"logging_to_file_updated": "Logging settings updated",
|
||||||
"request_log_updated": "Request logging setting updated",
|
"request_log_updated": "Request logging setting updated",
|
||||||
"ws_auth_updated": "WebSocket authentication setting updated",
|
"ws_auth_updated": "WebSocket authentication setting updated",
|
||||||
|
"login_storage_cleared": "Local login data cleared",
|
||||||
"api_key_added": "API key added successfully",
|
"api_key_added": "API key added successfully",
|
||||||
"api_key_updated": "API key updated successfully",
|
"api_key_updated": "API key updated successfully",
|
||||||
"api_key_deleted": "API key deleted successfully",
|
"api_key_deleted": "API key deleted successfully",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
"custom_connection_placeholder": "例如: https://example.com:8317",
|
"custom_connection_placeholder": "例如: https://example.com:8317",
|
||||||
"custom_connection_hint": "默认使用当前访问地址,若需要可手动输入其他地址。",
|
"custom_connection_hint": "默认使用当前访问地址,若需要可手动输入其他地址。",
|
||||||
"use_current_address": "使用当前地址",
|
"use_current_address": "使用当前地址",
|
||||||
|
"remember_password_label": "记住密码",
|
||||||
"management_key_label": "管理密钥:",
|
"management_key_label": "管理密钥:",
|
||||||
"management_key_placeholder": "请输入管理密钥",
|
"management_key_placeholder": "请输入管理密钥",
|
||||||
"connect_button": "连接",
|
"connect_button": "连接",
|
||||||
@@ -745,7 +746,11 @@
|
|||||||
"link_webui_repo": "WebUI 仓库",
|
"link_webui_repo": "WebUI 仓库",
|
||||||
"link_webui_repo_desc": "管理中心前端界面源代码",
|
"link_webui_repo_desc": "管理中心前端界面源代码",
|
||||||
"link_docs": "使用教程",
|
"link_docs": "使用教程",
|
||||||
"link_docs_desc": "配置指南和使用说明"
|
"link_docs_desc": "配置指南和使用说明",
|
||||||
|
"clear_login_title": "本地登录信息",
|
||||||
|
"clear_login_desc": "清理本地保存的登录信息并退出登录,不会影响使用统计中的价格设置。",
|
||||||
|
"clear_login_button": "清理登录信息",
|
||||||
|
"clear_login_confirm": "确认清理本地登录信息并退出登录?"
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"debug_updated": "调试设置已更新",
|
"debug_updated": "调试设置已更新",
|
||||||
@@ -758,6 +763,7 @@
|
|||||||
"logging_to_file_updated": "日志记录设置已更新",
|
"logging_to_file_updated": "日志记录设置已更新",
|
||||||
"request_log_updated": "请求日志设置已更新",
|
"request_log_updated": "请求日志设置已更新",
|
||||||
"ws_auth_updated": "WebSocket 鉴权设置已更新",
|
"ws_auth_updated": "WebSocket 鉴权设置已更新",
|
||||||
|
"login_storage_cleared": "本地登录信息已清理",
|
||||||
"api_key_added": "API密钥添加成功",
|
"api_key_added": "API密钥添加成功",
|
||||||
"api_key_updated": "API密钥更新成功",
|
"api_key_updated": "API密钥更新成功",
|
||||||
"api_key_deleted": "API密钥删除成功",
|
"api_key_deleted": "API密钥删除成功",
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ export function LoginPage() {
|
|||||||
const restoreSession = useAuthStore((state) => state.restoreSession);
|
const restoreSession = useAuthStore((state) => state.restoreSession);
|
||||||
const storedBase = useAuthStore((state) => state.apiBase);
|
const storedBase = useAuthStore((state) => state.apiBase);
|
||||||
const storedKey = useAuthStore((state) => state.managementKey);
|
const storedKey = useAuthStore((state) => state.managementKey);
|
||||||
|
const storedRememberPassword = useAuthStore((state) => state.rememberPassword);
|
||||||
|
|
||||||
const [apiBase, setApiBase] = useState('');
|
const [apiBase, setApiBase] = useState('');
|
||||||
const [managementKey, setManagementKey] = useState('');
|
const [managementKey, setManagementKey] = useState('');
|
||||||
const [showCustomBase, setShowCustomBase] = useState(false);
|
const [showCustomBase, setShowCustomBase] = useState(false);
|
||||||
const [showKey, setShowKey] = useState(false);
|
const [showKey, setShowKey] = useState(false);
|
||||||
|
const [rememberPassword, setRememberPassword] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [autoLoading, setAutoLoading] = useState(true);
|
const [autoLoading, setAutoLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -38,6 +40,7 @@ export function LoginPage() {
|
|||||||
if (!autoLoggedIn) {
|
if (!autoLoggedIn) {
|
||||||
setApiBase(storedBase || detectedBase);
|
setApiBase(storedBase || detectedBase);
|
||||||
setManagementKey(storedKey || '');
|
setManagementKey(storedKey || '');
|
||||||
|
setRememberPassword(storedRememberPassword || Boolean(storedKey));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setAutoLoading(false);
|
setAutoLoading(false);
|
||||||
@@ -45,7 +48,7 @@ export function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}, [detectedBase, restoreSession, storedBase, storedKey]);
|
}, [detectedBase, restoreSession, storedBase, storedKey, storedRememberPassword]);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
const redirect = (location.state as any)?.from?.pathname || '/';
|
const redirect = (location.state as any)?.from?.pathname || '/';
|
||||||
@@ -62,7 +65,11 @@ export function LoginPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
await login({ apiBase: baseToUse, managementKey: managementKey.trim() });
|
await login({
|
||||||
|
apiBase: baseToUse,
|
||||||
|
managementKey: managementKey.trim(),
|
||||||
|
rememberPassword
|
||||||
|
});
|
||||||
showNotification(t('common.connected_status'), 'success');
|
showNotification(t('common.connected_status'), 'success');
|
||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -148,6 +155,16 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="toggle-advanced">
|
||||||
|
<input
|
||||||
|
id="remember-password-toggle"
|
||||||
|
type="checkbox"
|
||||||
|
checked={rememberPassword}
|
||||||
|
onChange={(e) => setRememberPassword(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="remember-password-toggle">{t('login.remember_password_label')}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button fullWidth onClick={handleSubmit} loading={loading}>
|
<Button fullWidth onClick={handleSubmit} loading={loading}>
|
||||||
{loading ? t('login.submitting') : t('login.submit_button')}
|
{loading ? t('login.submitting') : t('login.submit_button')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -132,15 +132,24 @@ const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroupDefinition[] = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gemini',
|
id: 'gemini-3-pro',
|
||||||
label: 'Gemini',
|
label: 'Gemini 3 Pro',
|
||||||
identifiers: [
|
identifiers: ['gemini-3-pro-high', 'gemini-3-pro-low']
|
||||||
'gemini-3-pro-high',
|
},
|
||||||
'gemini-3-pro-low',
|
{
|
||||||
'gemini-2.5-flash',
|
id: 'gemini-2-5-flash',
|
||||||
'gemini-2.5-flash-lite',
|
label: 'Gemini 2.5 Flash',
|
||||||
'rev19-uic3-1p'
|
identifiers: ['gemini-2.5-flash', 'gemini-2.5-flash-thinking']
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
id: 'gemini-2-5-flash-lite',
|
||||||
|
label: 'Gemini 2.5 Flash Lite',
|
||||||
|
identifiers: ['gemini-2.5-flash-lite']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gemini-2-5-cu',
|
||||||
|
label: 'Gemini 2.5 CU',
|
||||||
|
identifiers: ['rev19-uic3-1p']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gemini-3-flash',
|
id: 'gemini-3-flash',
|
||||||
@@ -162,6 +171,51 @@ const GEMINI_CLI_REQUEST_HEADERS = {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface GeminiCliQuotaGroupDefinition {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
modelIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeminiCliParsedBucket {
|
||||||
|
modelId: string;
|
||||||
|
tokenType: string | null;
|
||||||
|
remainingFraction: number | null;
|
||||||
|
remainingAmount: number | null;
|
||||||
|
resetTime: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GEMINI_CLI_QUOTA_GROUPS: GeminiCliQuotaGroupDefinition[] = [
|
||||||
|
{
|
||||||
|
id: 'gemini-2-5-flash-series',
|
||||||
|
label: 'Gemini 2.5 Flash Series',
|
||||||
|
modelIds: ['gemini-2.5-flash', 'gemini-2.5-flash-lite']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gemini-2-5-pro',
|
||||||
|
label: 'Gemini 2.5 Pro',
|
||||||
|
modelIds: ['gemini-2.5-pro']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gemini-3-pro-preview',
|
||||||
|
label: 'Gemini 3 Pro Preview',
|
||||||
|
modelIds: ['gemini-3-pro-preview']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gemini-3-flash-preview',
|
||||||
|
label: 'Gemini 3 Flash Preview',
|
||||||
|
modelIds: ['gemini-3-flash-preview']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const GEMINI_CLI_GROUP_LOOKUP = new Map(
|
||||||
|
GEMINI_CLI_QUOTA_GROUPS.flatMap((group) =>
|
||||||
|
group.modelIds.map((modelId) => [modelId, group] as const)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const GEMINI_CLI_IGNORED_MODEL_PREFIXES = ['gemini-2.0-flash'];
|
||||||
|
|
||||||
interface CodexUsageWindow {
|
interface CodexUsageWindow {
|
||||||
used_percent?: number | string;
|
used_percent?: number | string;
|
||||||
usedPercent?: number | string;
|
usedPercent?: number | string;
|
||||||
@@ -472,6 +526,80 @@ function parseGeminiCliQuotaPayload(payload: unknown): GeminiCliQuotaPayload | n
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isIgnoredGeminiCliModel(modelId: string): boolean {
|
||||||
|
return GEMINI_CLI_IGNORED_MODEL_PREFIXES.some(
|
||||||
|
(prefix) => modelId === prefix || modelId.startsWith(`${prefix}-`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickEarlierResetTime(current?: string, next?: string): string | undefined {
|
||||||
|
if (!current) return next;
|
||||||
|
if (!next) return current;
|
||||||
|
const currentTime = new Date(current).getTime();
|
||||||
|
const nextTime = new Date(next).getTime();
|
||||||
|
if (Number.isNaN(currentTime)) return next;
|
||||||
|
if (Number.isNaN(nextTime)) return current;
|
||||||
|
return currentTime <= nextTime ? current : next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function minNullableNumber(current: number | null, next: number | null): number | null {
|
||||||
|
if (current === null) return next;
|
||||||
|
if (next === null) return current;
|
||||||
|
return Math.min(current, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGeminiCliQuotaBuckets(
|
||||||
|
buckets: GeminiCliParsedBucket[]
|
||||||
|
): GeminiCliQuotaBucketState[] {
|
||||||
|
if (buckets.length === 0) return [];
|
||||||
|
|
||||||
|
const grouped = new Map<string, GeminiCliQuotaBucketState & { modelIds: string[] }>();
|
||||||
|
|
||||||
|
buckets.forEach((bucket) => {
|
||||||
|
if (isIgnoredGeminiCliModel(bucket.modelId)) return;
|
||||||
|
const group = GEMINI_CLI_GROUP_LOOKUP.get(bucket.modelId);
|
||||||
|
const groupId = group?.id ?? bucket.modelId;
|
||||||
|
const label = group?.label ?? bucket.modelId;
|
||||||
|
const tokenKey = bucket.tokenType ?? '';
|
||||||
|
const mapKey = `${groupId}::${tokenKey}`;
|
||||||
|
const existing = grouped.get(mapKey);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
grouped.set(mapKey, {
|
||||||
|
id: `${groupId}${tokenKey ? `-${tokenKey}` : ''}`,
|
||||||
|
label,
|
||||||
|
remainingFraction: bucket.remainingFraction,
|
||||||
|
remainingAmount: bucket.remainingAmount,
|
||||||
|
resetTime: bucket.resetTime,
|
||||||
|
tokenType: bucket.tokenType,
|
||||||
|
modelIds: [bucket.modelId]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.remainingFraction = minNullableNumber(
|
||||||
|
existing.remainingFraction,
|
||||||
|
bucket.remainingFraction
|
||||||
|
);
|
||||||
|
existing.remainingAmount = minNullableNumber(existing.remainingAmount, bucket.remainingAmount);
|
||||||
|
existing.resetTime = pickEarlierResetTime(existing.resetTime, bucket.resetTime);
|
||||||
|
existing.modelIds.push(bucket.modelId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(grouped.values()).map((bucket) => {
|
||||||
|
const uniqueModelIds = Array.from(new Set(bucket.modelIds));
|
||||||
|
return {
|
||||||
|
id: bucket.id,
|
||||||
|
label: bucket.label,
|
||||||
|
remainingFraction: bucket.remainingFraction,
|
||||||
|
remainingAmount: bucket.remainingAmount,
|
||||||
|
resetTime: bucket.resetTime,
|
||||||
|
tokenType: bucket.tokenType,
|
||||||
|
modelIds: uniqueModelIds
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getAntigravityQuotaInfo(entry?: AntigravityQuotaInfo): {
|
function getAntigravityQuotaInfo(entry?: AntigravityQuotaInfo): {
|
||||||
remainingFraction: number | null;
|
remainingFraction: number | null;
|
||||||
resetTime?: string;
|
resetTime?: string;
|
||||||
@@ -517,8 +645,16 @@ function findAntigravityModel(
|
|||||||
|
|
||||||
function buildAntigravityQuotaGroups(models: AntigravityModelsPayload): AntigravityQuotaGroup[] {
|
function buildAntigravityQuotaGroups(models: AntigravityModelsPayload): AntigravityQuotaGroup[] {
|
||||||
const groups: AntigravityQuotaGroup[] = [];
|
const groups: AntigravityQuotaGroup[] = [];
|
||||||
let geminiResetTime: string | undefined;
|
let geminiProResetTime: string | undefined;
|
||||||
const [claudeDef, geminiDef, flashDef, imageDef] = ANTIGRAVITY_QUOTA_GROUPS;
|
const [
|
||||||
|
claudeDef,
|
||||||
|
geminiProDef,
|
||||||
|
flashDef,
|
||||||
|
flashLiteDef,
|
||||||
|
cuDef,
|
||||||
|
geminiFlashDef,
|
||||||
|
imageDef
|
||||||
|
] = ANTIGRAVITY_QUOTA_GROUPS;
|
||||||
|
|
||||||
const buildGroup = (
|
const buildGroup = (
|
||||||
def: AntigravityQuotaGroupDefinition,
|
def: AntigravityQuotaGroupDefinition,
|
||||||
@@ -565,10 +701,10 @@ function buildAntigravityQuotaGroups(models: AntigravityModelsPayload): Antigrav
|
|||||||
groups.push(claudeGroup);
|
groups.push(claudeGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
const geminiGroup = buildGroup(geminiDef);
|
const geminiProGroup = buildGroup(geminiProDef);
|
||||||
if (geminiGroup) {
|
if (geminiProGroup) {
|
||||||
geminiResetTime = geminiGroup.resetTime;
|
geminiProResetTime = geminiProGroup.resetTime;
|
||||||
groups.push(geminiGroup);
|
groups.push(geminiProGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
const flashGroup = buildGroup(flashDef);
|
const flashGroup = buildGroup(flashDef);
|
||||||
@@ -576,7 +712,22 @@ function buildAntigravityQuotaGroups(models: AntigravityModelsPayload): Antigrav
|
|||||||
groups.push(flashGroup);
|
groups.push(flashGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageGroup = buildGroup(imageDef, geminiResetTime);
|
const flashLiteGroup = buildGroup(flashLiteDef);
|
||||||
|
if (flashLiteGroup) {
|
||||||
|
groups.push(flashLiteGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cuGroup = buildGroup(cuDef);
|
||||||
|
if (cuGroup) {
|
||||||
|
groups.push(cuGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
const geminiFlashGroup = buildGroup(geminiFlashDef);
|
||||||
|
if (geminiFlashGroup) {
|
||||||
|
groups.push(geminiFlashGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageGroup = buildGroup(imageDef, geminiProResetTime);
|
||||||
if (imageGroup) {
|
if (imageGroup) {
|
||||||
groups.push(imageGroup);
|
groups.push(imageGroup);
|
||||||
}
|
}
|
||||||
@@ -1066,8 +1217,8 @@ export function QuotaPage() {
|
|||||||
const buckets = Array.isArray(payload?.buckets) ? payload?.buckets : [];
|
const buckets = Array.isArray(payload?.buckets) ? payload?.buckets : [];
|
||||||
if (buckets.length === 0) return [];
|
if (buckets.length === 0) return [];
|
||||||
|
|
||||||
return buckets
|
const parsedBuckets = buckets
|
||||||
.map((bucket, index) => {
|
.map((bucket) => {
|
||||||
const modelId = normalizeStringValue(bucket.modelId ?? bucket.model_id);
|
const modelId = normalizeStringValue(bucket.modelId ?? bucket.model_id);
|
||||||
if (!modelId) return null;
|
if (!modelId) return null;
|
||||||
const tokenType = normalizeStringValue(bucket.tokenType ?? bucket.token_type);
|
const tokenType = normalizeStringValue(bucket.tokenType ?? bucket.token_type);
|
||||||
@@ -1086,15 +1237,16 @@ export function QuotaPage() {
|
|||||||
}
|
}
|
||||||
const remainingFraction = remainingFractionRaw ?? fallbackFraction;
|
const remainingFraction = remainingFractionRaw ?? fallbackFraction;
|
||||||
return {
|
return {
|
||||||
id: `${modelId}-${tokenType ?? index}`,
|
modelId,
|
||||||
label: modelId,
|
tokenType,
|
||||||
remainingFraction,
|
remainingFraction,
|
||||||
remainingAmount,
|
remainingAmount,
|
||||||
resetTime,
|
resetTime
|
||||||
tokenType
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((bucket): bucket is GeminiCliQuotaBucketState => bucket !== null);
|
.filter((bucket): bucket is GeminiCliParsedBucket => bucket !== null);
|
||||||
|
|
||||||
|
return buildGeminiCliQuotaBuckets(parsedBuckets);
|
||||||
},
|
},
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
@@ -1479,6 +1631,10 @@ export function QuotaPage() {
|
|||||||
: t('gemini_cli_quota.remaining_amount', {
|
: t('gemini_cli_quota.remaining_amount', {
|
||||||
count: bucket.remainingAmount
|
count: bucket.remainingAmount
|
||||||
});
|
});
|
||||||
|
const titleBase =
|
||||||
|
bucket.modelIds && bucket.modelIds.length > 0
|
||||||
|
? bucket.modelIds.join(', ')
|
||||||
|
: bucket.label;
|
||||||
const quotaBarClass =
|
const quotaBarClass =
|
||||||
percent === null
|
percent === null
|
||||||
? styles.quotaBarFillMedium
|
? styles.quotaBarFillMedium
|
||||||
@@ -1494,7 +1650,7 @@ export function QuotaPage() {
|
|||||||
<span
|
<span
|
||||||
className={styles.quotaModel}
|
className={styles.quotaModel}
|
||||||
title={
|
title={
|
||||||
bucket.tokenType ? `${bucket.label} (${bucket.tokenType})` : bucket.label
|
bucket.tokenType ? `${titleBase} (${bucket.tokenType})` : titleBase
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{bucket.label}
|
{bucket.label}
|
||||||
|
|||||||
@@ -34,6 +34,12 @@
|
|||||||
margin: 0 0 $spacing-md 0;
|
margin: 0 0 $spacing-md 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clearLoginActions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.infoGrid {
|
.infoGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: $spacing-sm;
|
gap: $spacing-sm;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { IconGithub, IconBookOpen, IconExternalLink, IconCode } from '@/componen
|
|||||||
import { useAuthStore, useConfigStore, useNotificationStore, useModelsStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useNotificationStore, useModelsStore } from '@/stores';
|
||||||
import { apiKeysApi } from '@/services/api/apiKeys';
|
import { apiKeysApi } from '@/services/api/apiKeys';
|
||||||
import { classifyModels } from '@/utils/models';
|
import { classifyModels } from '@/utils/models';
|
||||||
|
import { STORAGE_KEY_AUTH } from '@/utils/constants';
|
||||||
import styles from './SystemPage.module.scss';
|
import styles from './SystemPage.module.scss';
|
||||||
|
|
||||||
export function SystemPage() {
|
export function SystemPage() {
|
||||||
@@ -104,6 +105,15 @@ export function SystemPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearLoginStorage = () => {
|
||||||
|
if (!window.confirm(t('system_info.clear_login_confirm'))) return;
|
||||||
|
auth.logout();
|
||||||
|
if (typeof localStorage === 'undefined') return;
|
||||||
|
const keysToRemove = [STORAGE_KEY_AUTH, 'isLoggedIn', 'apiBase', 'apiUrl', 'managementKey'];
|
||||||
|
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||||
|
showNotification(t('notification.login_storage_cleared'), 'success');
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig().catch(() => {
|
fetchConfig().catch(() => {
|
||||||
// ignore
|
// ignore
|
||||||
@@ -248,6 +258,15 @@ export function SystemPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card title={t('system_info.clear_login_title')}>
|
||||||
|
<p className={styles.sectionDescription}>{t('system_info.clear_login_desc')}</p>
|
||||||
|
<div className={styles.clearLoginActions}>
|
||||||
|
<Button variant="danger" onClick={handleClearLoginStorage}>
|
||||||
|
{t('system_info.clear_login_button')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const useAuthStore = create<AuthStoreState>()(
|
|||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
apiBase: '',
|
apiBase: '',
|
||||||
managementKey: '',
|
managementKey: '',
|
||||||
|
rememberPassword: false,
|
||||||
serverVersion: null,
|
serverVersion: null,
|
||||||
serverBuildDate: null,
|
serverBuildDate: null,
|
||||||
connectionStatus: 'disconnected',
|
connectionStatus: 'disconnected',
|
||||||
@@ -52,16 +53,25 @@ export const useAuthStore = create<AuthStoreState>()(
|
|||||||
secureStorage.getItem<string>('apiUrl', { encrypt: true });
|
secureStorage.getItem<string>('apiUrl', { encrypt: true });
|
||||||
const legacyKey = secureStorage.getItem<string>('managementKey');
|
const legacyKey = secureStorage.getItem<string>('managementKey');
|
||||||
|
|
||||||
const { apiBase, managementKey } = get();
|
const { apiBase, managementKey, rememberPassword } = get();
|
||||||
const resolvedBase = normalizeApiBase(apiBase || legacyBase || detectApiBaseFromLocation());
|
const resolvedBase = normalizeApiBase(apiBase || legacyBase || detectApiBaseFromLocation());
|
||||||
const resolvedKey = managementKey || legacyKey || '';
|
const resolvedKey = managementKey || legacyKey || '';
|
||||||
|
const resolvedRememberPassword = rememberPassword || Boolean(managementKey) || Boolean(legacyKey);
|
||||||
|
|
||||||
set({ apiBase: resolvedBase, managementKey: resolvedKey });
|
set({
|
||||||
|
apiBase: resolvedBase,
|
||||||
|
managementKey: resolvedKey,
|
||||||
|
rememberPassword: resolvedRememberPassword
|
||||||
|
});
|
||||||
apiClient.setConfig({ apiBase: resolvedBase, managementKey: resolvedKey });
|
apiClient.setConfig({ apiBase: resolvedBase, managementKey: resolvedKey });
|
||||||
|
|
||||||
if (wasLoggedIn && resolvedBase && resolvedKey) {
|
if (wasLoggedIn && resolvedBase && resolvedKey) {
|
||||||
try {
|
try {
|
||||||
await get().login({ apiBase: resolvedBase, managementKey: resolvedKey });
|
await get().login({
|
||||||
|
apiBase: resolvedBase,
|
||||||
|
managementKey: resolvedKey,
|
||||||
|
rememberPassword: resolvedRememberPassword
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Auto login failed:', error);
|
console.warn('Auto login failed:', error);
|
||||||
@@ -79,6 +89,7 @@ export const useAuthStore = create<AuthStoreState>()(
|
|||||||
login: async (credentials) => {
|
login: async (credentials) => {
|
||||||
const apiBase = normalizeApiBase(credentials.apiBase);
|
const apiBase = normalizeApiBase(credentials.apiBase);
|
||||||
const managementKey = credentials.managementKey.trim();
|
const managementKey = credentials.managementKey.trim();
|
||||||
|
const rememberPassword = credentials.rememberPassword ?? get().rememberPassword ?? false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
set({ connectionStatus: 'connecting' });
|
set({ connectionStatus: 'connecting' });
|
||||||
@@ -97,10 +108,15 @@ export const useAuthStore = create<AuthStoreState>()(
|
|||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
apiBase,
|
apiBase,
|
||||||
managementKey,
|
managementKey,
|
||||||
|
rememberPassword,
|
||||||
connectionStatus: 'connected',
|
connectionStatus: 'connected',
|
||||||
connectionError: null
|
connectionError: null
|
||||||
});
|
});
|
||||||
localStorage.setItem('isLoggedIn', 'true');
|
if (rememberPassword) {
|
||||||
|
localStorage.setItem('isLoggedIn', 'true');
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('isLoggedIn');
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
set({
|
set({
|
||||||
connectionStatus: 'error',
|
connectionStatus: 'error',
|
||||||
@@ -185,7 +201,8 @@ export const useAuthStore = create<AuthStoreState>()(
|
|||||||
})),
|
})),
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
apiBase: state.apiBase,
|
apiBase: state.apiBase,
|
||||||
managementKey: state.managementKey,
|
...(state.rememberPassword ? { managementKey: state.managementKey } : {}),
|
||||||
|
rememberPassword: state.rememberPassword,
|
||||||
serverVersion: state.serverVersion,
|
serverVersion: state.serverVersion,
|
||||||
serverBuildDate: state.serverBuildDate
|
serverBuildDate: state.serverBuildDate
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
export interface LoginCredentials {
|
export interface LoginCredentials {
|
||||||
apiBase: string;
|
apiBase: string;
|
||||||
managementKey: string;
|
managementKey: string;
|
||||||
|
rememberPassword?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 认证状态
|
// 认证状态
|
||||||
@@ -14,6 +15,7 @@ export interface AuthState {
|
|||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
apiBase: string;
|
apiBase: string;
|
||||||
managementKey: string;
|
managementKey: string;
|
||||||
|
rememberPassword: boolean;
|
||||||
serverVersion: string | null;
|
serverVersion: string | null;
|
||||||
serverBuildDate: string | null;
|
serverBuildDate: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface GeminiCliQuotaBucketState {
|
|||||||
remainingAmount: number | null;
|
remainingAmount: number | null;
|
||||||
resetTime: string | undefined;
|
resetTime: string | undefined;
|
||||||
tokenType: string | null;
|
tokenType: string | null;
|
||||||
|
modelIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeminiCliQuotaState {
|
export interface GeminiCliQuotaState {
|
||||||
|
|||||||
Reference in New Issue
Block a user