feat: 增强文档和监控功能

主要更新:
- 完善 README 文档,新增中文详细使用说明与监控中心介绍
- 优化 README.md 文档内容和格式,增加英文和中文文档切换链接
- 新增监控中心模块,支持请求日志、统计分析和模型管理
- 增强 AI 提供商配置页面,添加配置搜索功能
- 更新 .gitignore,移除无效注释和调整条目名称
- 删除 README_CN.md 文件,统一文档结构

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
kongkongyo
2026-01-19 00:58:48 +08:00
parent d077b5dd26
commit e4850656a5
37 changed files with 6435 additions and 333 deletions

264
src/utils/monitor.ts Normal file
View File

@@ -0,0 +1,264 @@
/**
* 监控中心公共工具函数
*/
import type { UsageData } from '@/pages/MonitorPage';
/**
* 日期范围接口
*/
export interface DateRange {
start: Date;
end: Date;
}
/**
* 禁用模型状态接口
*/
export interface DisableState {
source: string;
model: string;
displayName: string;
step: number;
}
/**
* 脱敏 API Key
* @param key API Key 字符串
* @returns 脱敏后的字符串
*/
export function maskSecret(key: string): string {
if (!key || key === '-' || key === 'unknown') return key || '-';
if (key.length <= 8) {
return `${key.slice(0, 4)}***`;
}
return `${key.slice(0, 4)}***${key.slice(-4)}`;
}
/**
* 解析渠道名称(返回 provider 名称)
* @param source 来源标识
* @param providerMap 渠道映射表
* @returns provider 名称或 null
*/
export function resolveProvider(
source: string,
providerMap: Record<string, string>
): string | null {
if (!source || source === '-' || source === 'unknown') return null;
// 首先尝试完全匹配
if (providerMap[source]) {
return providerMap[source];
}
// 然后尝试前缀匹配(双向)
const entries = Object.entries(providerMap);
for (const [key, provider] of entries) {
if (source.startsWith(key) || key.startsWith(source)) {
return provider;
}
}
return null;
}
/**
* 格式化渠道显示名称:渠道名 (脱敏后的api-key)
* @param source 来源标识
* @param providerMap 渠道映射表
* @returns 格式化后的显示名称
*/
export function formatProviderDisplay(
source: string,
providerMap: Record<string, string>
): string {
if (!source || source === '-' || source === 'unknown') {
return source || '-';
}
const provider = resolveProvider(source, providerMap);
const masked = maskSecret(source);
if (!provider) return masked;
return `${provider} (${masked})`;
}
/**
* 获取渠道显示信息(分离渠道名和秘钥)
* @param source 来源标识
* @param providerMap 渠道映射表
* @returns 包含渠道名和秘钥的对象
*/
export function getProviderDisplayParts(
source: string,
providerMap: Record<string, string>
): { provider: string | null; masked: string } {
if (!source || source === '-' || source === 'unknown') {
return { provider: null, masked: source || '-' };
}
const provider = resolveProvider(source, providerMap);
const masked = maskSecret(source);
return { provider, masked };
}
/**
* 格式化时间戳为日期时间字符串
* @param timestamp 时间戳(毫秒数或 ISO 字符串)
* @returns 格式化后的日期时间字符串
*/
export function formatTimestamp(timestamp: number | string): string {
if (!timestamp) return '-';
const date = typeof timestamp === 'string' ? new Date(timestamp) : new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
/**
* 获取成功率对应的样式类名
* @param rate 成功率0-100
* @param styles 样式模块对象
* @returns 样式类名
*/
export function getRateClassName(
rate: number,
styles: Record<string, string>
): string {
if (rate >= 90) return styles.rateHigh || '';
if (rate >= 70) return styles.rateMedium || '';
return styles.rateLow || '';
}
/**
* 检查模型是否在配置中可用(未被移除)
* @param source 来源标识
* @param modelAlias 模型别名
* @param providerModels 渠道模型映射表
* @returns 是否可用
*/
export function isModelEnabled(
source: string,
modelAlias: string,
providerModels: Record<string, Set<string>>
): boolean {
if (!source || !modelAlias) return true; // 无法判断时默认显示
// 首先尝试完全匹配
if (providerModels[source]) {
return providerModels[source].has(modelAlias);
}
// 然后尝试前缀匹配
const entries = Object.entries(providerModels);
for (const [key, modelSet] of entries) {
if (source.startsWith(key) || key.startsWith(source)) {
return modelSet.has(modelAlias);
}
}
return true; // 找不到渠道配置时默认显示
}
/**
* 检查模型是否已禁用(会话中禁用或配置中已移除)
* @param source 来源标识
* @param model 模型名称
* @param disabledModels 已禁用模型集合
* @param providerModels 渠道模型映射表
* @returns 是否已禁用
*/
export function isModelDisabled(
source: string,
model: string,
disabledModels: Set<string>,
providerModels: Record<string, Set<string>>
): boolean {
// 首先检查会话中是否已禁用
if (disabledModels.has(`${source}|||${model}`)) {
return true;
}
// 然后检查配置中是否已移除
return !isModelEnabled(source, model, providerModels);
}
/**
* 创建禁用状态对象
* @param source 来源标识
* @param model 模型名称
* @param providerMap 渠道映射表
* @returns 禁用状态对象
*/
export function createDisableState(
source: string,
model: string,
providerMap: Record<string, string>
): DisableState {
const providerName = resolveProvider(source, providerMap);
const displayName = providerName
? `${providerName} / ${model}`
: `${maskSecret(source)} / ${model}`;
return { source, model, displayName, step: 1 };
}
/**
* 时间范围类型
*/
export type TimeRangeValue = number | 'custom';
/**
* 根据时间范围过滤数据
* @param data 原始数据
* @param timeRange 时间范围(天数或 'custom'
* @param customRange 自定义日期范围
* @returns 过滤后的数据
*/
export function filterDataByTimeRange(
data: UsageData | null,
timeRange: TimeRangeValue,
customRange?: DateRange
): UsageData | null {
if (!data?.apis) return null;
const now = new Date();
let cutoffStart: Date;
let cutoffEnd: Date = new Date(now.getTime());
cutoffEnd.setHours(23, 59, 59, 999);
if (timeRange === 'custom' && customRange) {
cutoffStart = customRange.start;
cutoffEnd = customRange.end;
} else if (typeof timeRange === 'number') {
cutoffStart = new Date(now.getTime() - timeRange * 24 * 60 * 60 * 1000);
cutoffStart.setHours(0, 0, 0, 0);
} else {
cutoffStart = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
cutoffStart.setHours(0, 0, 0, 0);
}
const filtered: UsageData = { apis: {} };
Object.entries(data.apis).forEach(([apiKey, apiData]) => {
if (!apiData?.models) return;
const filteredModels: Record<string, { details: UsageData['apis'][string]['models'][string]['details'] }> = {};
Object.entries(apiData.models).forEach(([modelName, modelData]) => {
if (!modelData?.details || !Array.isArray(modelData.details)) return;
const filteredDetails = modelData.details.filter((detail) => {
const timestamp = new Date(detail.timestamp);
return timestamp >= cutoffStart && timestamp <= cutoffEnd;
});
if (filteredDetails.length > 0) {
filteredModels[modelName] = { details: filteredDetails };
}
});
if (Object.keys(filteredModels).length > 0) {
filtered.apis[apiKey] = { models: filteredModels };
}
});
return filtered;
}