WebUI 前端 & 后端超级大重构

This commit is contained in:
DrSmoothl
2026-03-14 21:06:36 +08:00
parent 6ca5a2939e
commit 172615f18a
69 changed files with 3128 additions and 6581 deletions

View File

@@ -1,136 +0,0 @@
import { fetchWithAuth } from './fetch-with-auth'
export interface TimeFootprintData {
total_online_hours: number
first_message_time: string | null
first_message_user: string | null
first_message_content: string | null
busiest_day: string | null
busiest_day_count: number
hourly_distribution: number[]
midnight_chat_count: number
is_night_owl: boolean
}
export interface SocialNetworkData {
total_groups: number
top_groups: Array<{
group_id: string
group_name: string
message_count: number
is_webui?: boolean
}>
top_users: Array<{
user_id: string
user_nickname: string
message_count: number
is_webui?: boolean
}>
at_count: number
mentioned_count: number
longest_companion_user: string | null
longest_companion_days: number
}
export interface BrainPowerData {
total_tokens: number
total_cost: number
favorite_model: string | null
favorite_model_count: number
model_distribution: Array<{
model: string
count: number
tokens: number
cost: number
}>
top_reply_models: Array<{
model: string
count: number
}>
most_expensive_cost: number
most_expensive_time: string | null
top_token_consumers: Array<{
user_id: string
cost: number
tokens: number
}>
silence_rate: number
total_actions: number
no_reply_count: number
avg_interest_value: number
max_interest_value: number
max_interest_time: string | null
avg_reasoning_length: number
max_reasoning_length: number
max_reasoning_time: string | null
}
export interface ExpressionVibeData {
top_emoji: {
id: number
path: string
description: string
usage_count: number
hash: string
} | null
top_emojis: Array<{
id: number
path: string
description: string
usage_count: number
hash: string
}>
top_expressions: Array<{
style: string
count: number
}>
rejected_expression_count: number
checked_expression_count: number
total_expressions: number
action_types: Array<{
action: string
count: number
}>
image_processed_count: number
late_night_reply: {
time: string
content: string
} | null
favorite_reply: {
content: string
count: number
} | null
}
export interface AchievementData {
new_jargon_count: number
sample_jargons: Array<{
content: string
meaning: string
count: number
}>
total_messages: number
total_replies: number
}
export interface AnnualReportData {
year: number
bot_name: string
generated_at: string
time_footprint: TimeFootprintData
social_network: SocialNetworkData
brain_power: BrainPowerData
expression_vibe: ExpressionVibeData
achievements: AchievementData
}
export async function getAnnualReport(year: number = 2025): Promise<AnnualReportData> {
const response = await fetchWithAuth(`/api/webui/annual-report/full?year=${year}`)
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || '获取年度报告失败')
}
return response.json()
}

View File

@@ -0,0 +1,93 @@
export type ShortcutKey =
| 'mod'
| 'shift'
| 'alt'
| 'enter'
| 'esc'
| 'up'
| 'down'
| 'left'
| 'right'
| string
const MAC_PLATFORMS = /(Mac|iPhone|iPod|iPad)/i
export function isMacLikePlatform(): boolean {
if (typeof navigator === 'undefined') {
return false
}
return MAC_PLATFORMS.test(navigator.platform || navigator.userAgent)
}
export function getShortcutKeyLabel(key: ShortcutKey): string {
const isMacLike = isMacLikePlatform()
const normalizedKey = key.toLowerCase()
switch (normalizedKey) {
case 'mod':
return isMacLike ? '⌘' : 'Ctrl'
case 'shift':
return isMacLike ? '⇧' : 'Shift'
case 'alt':
return isMacLike ? '⌥' : 'Alt'
case 'enter':
return isMacLike ? '↵' : 'Enter'
case 'esc':
case 'escape':
return 'Esc'
case 'up':
return '↑'
case 'down':
return '↓'
case 'left':
return '←'
case 'right':
return '→'
default:
return key.length === 1 ? key.toUpperCase() : key
}
}
export function getPlatformModifierAriaLabel(): string {
return isMacLikePlatform() ? 'Command' : 'Control'
}
export function matchesShortcut(event: KeyboardEvent | React.KeyboardEvent, keys: ShortcutKey[]): boolean {
const normalizedKeys = keys.map((key) => key.toLowerCase())
const eventKey = event.key.toLowerCase()
const modifierChecks = {
mod: isMacLikePlatform() ? event.metaKey : event.ctrlKey,
shift: event.shiftKey,
alt: event.altKey,
}
for (const key of normalizedKeys) {
if (key in modifierChecks) {
if (!modifierChecks[key as keyof typeof modifierChecks]) {
return false
}
continue
}
if (eventKey !== key) {
return false
}
}
return true
}
export function isEditableTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) {
return false
}
return (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable ||
target.getAttribute('role') === 'textbox'
)
}

View File

@@ -23,9 +23,6 @@ export const STORAGE_KEYS = {
WS_MAX_RECONNECT_ATTEMPTS: 'maibot-ws-max-reconnect-attempts',
// 用户数据
// 注意ACCESS_TOKEN 已弃用,现在使用 HttpOnly Cookie 存储认证信息
// 保留此常量仅用于向后兼容和清理旧数据
ACCESS_TOKEN: 'access-token',
COMPLETED_TOURS: 'maibot-completed-tours',
CHAT_USER_ID: 'maibot_webui_user_id',
CHAT_USER_NAME: 'maibot_webui_user_name',
@@ -211,10 +208,8 @@ export function clearLocalCache(): { clearedKeys: string[]; preservedKeys: strin
const keysToRemove: string[] = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key) {
if (key.startsWith('maibot') || key.startsWith('accent-color') || key === 'access-token') {
keysToRemove.push(key)
}
if (key && (key.startsWith('maibot') || key.startsWith('accent-color'))) {
keysToRemove.push(key)
}
}