WebUI 前端 & 后端超级大重构
This commit is contained in:
@@ -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()
|
||||
}
|
||||
93
dashboard/src/lib/keyboard.ts
Normal file
93
dashboard/src/lib/keyboard.ts
Normal 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'
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user