'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { Card } from '@/components/ui' import { Plus, Trash2, ChevronDown, ChevronUp, Search, RefreshCw } from 'lucide-react' interface MonitorStatus { enabled: boolean status: string workerHealth: string lastCheck: string | null stats: { totalProcessed: number; todayProcessed: number; errors: number; lastError: string | null; lastErrorTime: string | null } ongoingFaults: number details: { todayProcessed: { ticket_no?: string; error?: string; created_at: string }[] ongoingFaults: { id: number; server_sn: string; server_ip: string | null; order_number: string | null; fault_type: string | null; fault_time: string; fault_detail: string | null }[] recentErrors: { error?: string; details?: string; created_at: string }[] } } interface WebhookConfig { id: string name: string url: string enabled: boolean params: { interval_seconds: number push_delay_ms: number silent_start_hour: number silent_end_hour: number } } interface MonitorConfig { monitor: { enabled: boolean; interval_seconds: number; push_delay_ms: number; silent_start_hour: number; silent_end_hour: number } mail: { address: string; imap_server: string; imap_port: number; pop3_server: string; pop3_port: number; smtp_server: string; smtp_port: number; password: string } filter: { subject_keywords: string[]; sender_email: string; oem_repair_keywords: { base_keyword: string; type_keyword: string; exclude_keyword: string }; oem_diag_keywords: { base_keyword: string; type_keyword: string; exclude_keyword: string } } wechat: { webhooks: WebhookConfig[] } } interface ScanState { status: 'idle' | 'running' | 'completed' | 'error' | 'cancelling' startedAt?: string completedAt?: string timeRange?: { value: number | null; unit: string } stats?: { total: number matched: number imported: number skipped: number errors: number } details?: { msg_id: string subject: string date: string order_number: string | null status: 'imported' | 'skipped' | 'error' ticket_no?: string error?: string }[] detailsTruncated?: boolean error?: string } type SectionKey = 'mail' | 'filter' | 'wechat' | 'monitor' function generateId() { return 'wh_' + Math.random().toString(36).slice(2, 10) } export default function MonitorSettingsPage() { const [status, setStatus] = useState(null) const [config, setConfig] = useState(null) const [originalConfig, setOriginalConfig] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(null) const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) const [newKeyword, setNewKeyword] = useState('') const [expandedPanel, setExpandedPanel] = useState<'today' | 'total' | 'errors' | 'ongoing' | null>(null) const [sectionMessage, setSectionMessage] = useState>>({}) const [expandedWebhooks, setExpandedWebhooks] = useState>(new Set()) const [testingWebhook, setTestingWebhook] = useState(null) // 立即检查冷却状态 const [triggerCooldown, setTriggerCooldown] = useState(0) const cooldownTimerRef = useRef(null) // 邮箱工单检测状态 const [scanState, setScanState] = useState({ status: 'idle' }) const [scanTimeValue, setScanTimeValue] = useState(7) const [scanTimeUnit, setScanTimeUnit] = useState('day') const scanTimerRef = useRef(null) const [importing, setImporting] = useState(false) // 从 scanState.status 派生 scanning 状态 const scanning = scanState.status === 'running' || scanState.status === 'cancelling' // 扫描历史状态 const [scanHistory, setScanHistory] = useState<{ items: Array<{ id: number status: string started_at: string completed_at: string | null time_range_value: number | null time_range_unit: string | null total_count: number matched_count: number imported_count: number skipped_count: number error_count: number details_truncated: number error_message: string | null created_by: number | null created_at: string }> pagination: { page: number; limit: number; total: number; totalPages: number } } | null>(null) const [historyPage, setHistoryPage] = useState(1) const [loadingHistory, setLoadingHistory] = useState(false) // 刷新扫描历史 const refreshScanHistory = useCallback(async (page = 1) => { setLoadingHistory(true) try { const res = await fetch(`/api/monitor/scan-history?page=${page}&limit=10`) const data = await res.json() setScanHistory(data) setHistoryPage(page) } catch (err) { console.error('[Scan] 历史加载失败:', err) } finally { setLoadingHistory(false) } }, []) // 自动刷新定时器 const statusTimerRef = useRef(null) const errorTimerRef = useRef(null) // 页面可见性状态 const isVisibleRef = useRef(true) // 刷新运行状态 const refreshStatus = useCallback(() => { fetch('/api/monitor/status') .then(r => r.json()) .then(setStatus) .catch(err => console.error('[Monitor] 状态刷新失败:', err)) }, []) // 刷新错误列表(更频繁)- 使用相同的 API,但只更新错误部分 const refreshErrors = useCallback(() => { fetch('/api/monitor/status') .then(r => r.json()) .then(data => { // 安全地合并错误列表,避免路径依赖 setStatus(prev => { if (!prev || !data) return prev return { ...prev, stats: data.stats || prev.stats, details: { ...prev.details, recentErrors: data.details?.recentErrors ?? prev.details?.recentErrors ?? [], }, } }) }) .catch(err => console.error('[Monitor] 错误列表刷新失败:', err)) }, []) // 刷新扫描状态 const refreshScanState = useCallback(() => { fetch('/api/monitor/scan-status') .then(r => r.json()) .then(data => { setScanState(data) // 扫描结束时停止轮询(scanning 从 status 派生) if (data.status !== 'running' && data.status !== 'cancelling') { if (scanTimerRef.current) { clearInterval(scanTimerRef.current) scanTimerRef.current = null } } }) .catch(err => { console.error('[Scan] 状态刷新失败:', err) // 网络异常时不停止轮询,等待下次重试 }) }, []) // 页面可见性变化 useEffect(() => { const handler = () => { isVisibleRef.current = !document.hidden if (isVisibleRef.current) { // 页面恢复可见,立即刷新 refreshStatus() refreshErrors() if (scanning) refreshScanState() } } document.addEventListener('visibilitychange', handler) return () => document.removeEventListener('visibilitychange', handler) }, [scanning, refreshStatus, refreshErrors, refreshScanState]) // 自动刷新定时器 useEffect(() => { // 运行状态:60 秒刷新 statusTimerRef.current = setInterval(() => { if (isVisibleRef.current) refreshStatus() }, 60000) // 错误列表:30 秒刷新 errorTimerRef.current = setInterval(() => { if (isVisibleRef.current) refreshErrors() }, 30000) return () => { if (statusTimerRef.current) clearInterval(statusTimerRef.current) if (errorTimerRef.current) clearInterval(errorTimerRef.current) } }, [refreshStatus, refreshErrors]) // 初始加载 useEffect(() => { Promise.all([ fetch('/api/monitor/status').then(r => r.json()), fetch('/api/monitor/settings').then(r => r.json()), fetch('/api/monitor/scan-status').then(r => r.json()), fetch('/api/monitor/scan-history?page=1&limit=10').then(r => r.json()), ]).then(([s, c, scan, history]) => { setStatus(s) setConfig(c) setOriginalConfig(JSON.parse(JSON.stringify(c))) setScanState(scan) setScanHistory(history) setLoading(false) }).catch(() => setLoading(false)) }, []) // 立即检查处理 const handleTrigger = async () => { if (triggerCooldown > 0) return const res = await fetch('/api/monitor/trigger', { method: 'POST' }) const data = await res.json() if (data.success) { setMessage({ type: 'success', text: data.message }) setTimeout(refreshStatus, 2000) } else { setMessage({ type: 'error', text: data.error }) } // 启动 30 秒冷却 setTriggerCooldown(30) cooldownTimerRef.current = setInterval(() => { setTriggerCooldown(prev => { if (prev <= 1) { if (cooldownTimerRef.current) clearInterval(cooldownTimerRef.current) return 0 } return prev - 1 }) }, 1000) } // 清理冷却定时器 useEffect(() => { return () => { if (cooldownTimerRef.current) clearInterval(cooldownTimerRef.current) } }, []) // 启动扫描 const handleStartScan = async () => { if (scanning) return setScanState({ status: 'running' }) const res = await fetch('/api/monitor/scan-emails', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ timeRange: { value: scanTimeValue, unit: scanTimeUnit } }), }) const data = await res.json() if (!data.success) { setMessage({ type: 'error', text: data.error }) setScanState({ status: 'error', error: data.error }) return } // 启动轮询 scanTimerRef.current = setInterval(refreshScanState, 3000) } // 取消扫描 const handleCancelScan = async () => { const res = await fetch('/api/monitor/scan-status', { method: 'DELETE' }) const data = await res.json() if (data.success) { setMessage({ type: 'success', text: data.message }) setScanState(prev => ({ ...prev, status: 'cancelling' })) } else { setMessage({ type: 'error', text: data.error }) } } // 导入跳过的邮件为工单 const handleImportSkipped = async () => { if (!scanState.details || scanState.status !== 'completed') return const skippedEmails = scanState.details.filter(d => d.status === 'skipped' && d.order_number && d.error !== '工单已存在' && d.error !== '邮件已处理过' ) if (skippedEmails.length === 0) { setMessage({ type: 'error', text: '没有可导入的邮件(已跳过的邮件需要有工单号)' }) return } if (!confirm(`确认导入 ${skippedEmails.length} 封邮件为工单?`)) return setImporting(true) try { const res = await fetch('/api/monitor/scan-emails/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emails: skippedEmails }), }) const data = await res.json() if (data.success) { setMessage({ type: 'success', text: data.message }) // 刷新扫描状态 refreshScanState() } else { setMessage({ type: 'error', text: data.error }) } } catch (err) { setMessage({ type: 'error', text: '导入失败' }) } finally { setImporting(false) } } // 清理扫描定时器 useEffect(() => { return () => { if (scanTimerRef.current) clearInterval(scanTimerRef.current) } }, []) // 其他原有函数保持不变... const handleStart = async () => { await fetch('/api/monitor/start', { method: 'POST' }) refreshStatus() setMessage({ type: 'success', text: '监控已启用' }) } const handleStop = async () => { if (!confirm('确认停用邮件监控?')) return await fetch('/api/monitor/stop', { method: 'POST' }) refreshStatus() setMessage({ type: 'success', text: '监控已停用' }) } const handleTestMail = async () => { setSectionMessage(prev => ({ ...prev, mail: { type: 'success', text: '正在测试邮箱连接...' } })) const res = await fetch('/api/monitor/test-mail', { method: 'POST' }) const data = await res.json() setSectionMessage(prev => ({ ...prev, mail: { type: data.success ? 'success' : 'error', text: data.success ? data.message : data.error } })) } const handleTestWebhook = async (webhookId: string) => { if (!config) return const wh = config.wechat.webhooks.find(w => w.id === webhookId) if (!wh || !wh.url) return setTestingWebhook(webhookId) const res = await fetch('/api/monitor/test-wechat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ webhook_url: wh.url }), }) const data = await res.json() setTestingWebhook(null) setSectionMessage(prev => ({ ...prev, wechat: { type: data.success ? 'success' : 'error', text: data.success ? `「${wh.name}」测试成功` : `「${wh.name}」${data.error}` }, })) setTimeout(() => setSectionMessage(prev => ({ ...prev, wechat: undefined })), 5000) } const handleSectionSave = async (section: SectionKey) => { if (!config) return setSaving(section) setSectionMessage(prev => ({ ...prev, [section]: undefined })) const sectionData = { [section]: config[section] } const res = await fetch('/api/monitor/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sectionData) }) const data = await res.json() setSaving(null) if (data.success) { setOriginalConfig(prev => prev ? { ...prev, [section]: JSON.parse(JSON.stringify(config[section])) } : prev) setSectionMessage(prev => ({ ...prev, [section]: { type: 'success', text: '已保存' } })) } else { setSectionMessage(prev => ({ ...prev, [section]: { type: 'error', text: data.error } })) } setTimeout(() => setSectionMessage(prev => ({ ...prev, [section]: undefined })), 3000) } const addKeyword = () => { if (!config || !newKeyword.trim()) return setConfig({ ...config, filter: { ...config.filter, subject_keywords: [...config.filter.subject_keywords, newKeyword.trim()] } }) setNewKeyword('') } const removeKeyword = (index: number) => { if (!config) return setConfig({ ...config, filter: { ...config.filter, subject_keywords: config.filter.subject_keywords.filter((_, i) => i !== index) } }) } const addWebhook = () => { if (!config) return const newWh: WebhookConfig = { id: generateId(), name: `推送 ${config.wechat.webhooks.length + 1}`, url: '', enabled: true, params: { interval_seconds: 60, push_delay_ms: 2000, silent_start_hour: 0, silent_end_hour: 7 }, } const webhooks = [...config.wechat.webhooks, newWh] setConfig({ ...config, wechat: { ...config.wechat, webhooks } }) setExpandedWebhooks(prev => new Set(prev).add(newWh.id)) } const removeWebhook = (id: string) => { if (!config) return if (config.wechat.webhooks.length <= 1) return setConfig({ ...config, wechat: { ...config.wechat, webhooks: config.wechat.webhooks.filter(w => w.id !== id) } }) } const updateWebhook = (id: string, updates: Partial) => { if (!config) return const webhooks = config.wechat.webhooks.map(w => w.id === id ? { ...w, ...updates } : w) setConfig({ ...config, wechat: { ...config.wechat, webhooks } }) } const updateWebhookParams = (id: string, paramUpdates: Partial) => { if (!config) return const webhooks = config.wechat.webhooks.map(w => w.id === id ? { ...w, params: { ...w.params, ...paramUpdates } } : w) setConfig({ ...config, wechat: { ...config.wechat, webhooks } }) } const toggleWebhookExpand = (id: string) => { setExpandedWebhooks(prev => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } function hasSectionChanged(original: MonitorConfig | null, current: MonitorConfig | null, section: SectionKey): boolean { if (!original || !current) return false return JSON.stringify(original[section]) !== JSON.stringify(current[section]) } const inputClass = (path: string, changed = false) => `w-full px-3 py-2 border rounded-lg text-sm transition-colors ${changed ? 'border-amber-400 bg-amber-50 dark:bg-amber-900/20 dark:border-amber-600' : 'border-slate-300 dark:border-slate-600'}` const SectionSaveButton = ({ section }: { section: SectionKey }) => { const changed = hasSectionChanged(originalConfig, config, section) if (!changed) return null return (
) } if (loading) return
加载中...
if (!config) return
加载失败
return (

邮件监控设置

配置邮件监控、微信推送与提醒规则

{message && (
{message.text}
)} {/* 运行状态 */}

运行状态

{status?.workerHealth === 'healthy' ? '运行中' : status?.workerHealth === 'error' ? '运行异常' : status?.workerHealth === 'unresponsive' ? '无响应' : '已停止'} {status?.lastCheck && 上次检查: {status.lastCheck}}
{/* 统计指标 */}
{[ { key: 'today' as const, label: '今日处理', value: status?.stats.todayProcessed || 0, color: 'text-blue-600 dark:text-blue-400' }, { key: 'total' as const, label: '累计', value: status?.stats.totalProcessed || 0, color: 'text-slate-700 dark:text-slate-300' }, { key: 'errors' as const, label: '错误', value: status?.stats.errors || 0, color: 'text-red-600 dark:text-red-400' }, { key: 'ongoing' as const, label: '进行中故障', value: status?.ongoingFaults || 0, color: 'text-amber-600 dark:text-amber-400' }, ].map(item => (
{item.label}:
))}
{/* 展开面板 */} {expandedPanel && (
{expandedPanel === 'today' && status?.details?.todayProcessed && (
今日处理记录
{status.details.todayProcessed.length === 0 ? (

今日暂无处理记录

) : (
{status.details.todayProcessed.map((item, i) => (
{item.ticket_no ? `工单 ${item.ticket_no}` : item.error || '处理完成'} {item.created_at}
))}
)}
)} {expandedPanel === 'total' && (
累计统计
总处理工单

{status?.stats.totalProcessed || 0}

今日处理

{status?.stats.todayProcessed || 0}

总错误数

{status?.stats.errors || 0}

进行中故障

{status?.ongoingFaults || 0}

)} {expandedPanel === 'errors' && status?.details?.recentErrors && (
最近错误记录
{status.details.recentErrors.length === 0 ? (

暂无错误记录

) : (
{status.details.recentErrors.map((item, i) => (
错误 {item.created_at}
                          {(() => {
                            const errStr = item.error || item.details || JSON.stringify(item)
                            try { return JSON.stringify(JSON.parse(errStr), null, 2) }
                            catch { return errStr }
                          })()}
                        
))}
)}
)} {expandedPanel === 'ongoing' && status?.details?.ongoingFaults && (
进行中故障列表
{status.details.ongoingFaults.length === 0 ? (

暂无进行中的故障

) : (
{status.details.ongoingFaults.map((fault, i) => ( ))}
工单号 服务器IP SN 类型 故障时间 故障信息
{fault.order_number || '-'} {fault.server_ip || '未知'} {fault.server_sn} {fault.fault_type === 'oem_diag' ? '诊断' : fault.fault_type === 'oem_repair' ? '维修' : '未知'} {fault.fault_time} {fault.fault_detail || '-'}
)}
)}
)}
{status?.enabled ? : }
{/* 邮箱配置 */}

邮箱配置

setConfig({ ...config, mail: { ...config.mail, address: e.target.value } })} className={inputClass('mail.address')} />
setConfig({ ...config, mail: { ...config.mail, password: e.target.value } })} className={inputClass('mail.password')} placeholder="••••••••" />
setConfig({ ...config, mail: { ...config.mail, imap_server: e.target.value } })} className={inputClass('mail.imap_server')} />
setConfig({ ...config, mail: { ...config.mail, imap_port: parseInt(e.target.value) || 993 } })} className={inputClass('mail.imap_port')} />
setConfig({ ...config, mail: { ...config.mail, pop3_server: e.target.value } })} className={inputClass('mail.pop3_server')} />
setConfig({ ...config, mail: { ...config.mail, pop3_port: parseInt(e.target.value) || 995 } })} className={inputClass('mail.pop3_port')} />
setConfig({ ...config, mail: { ...config.mail, smtp_server: e.target.value } })} className={inputClass('mail.smtp_server')} />
setConfig({ ...config, mail: { ...config.mail, smtp_port: parseInt(e.target.value) || 465 } })} className={inputClass('mail.smtp_port')} />
{sectionMessage.mail && ( {sectionMessage.mail.text} )}
{/* 邮件过滤 */}

邮件过滤

{config.filter.subject_keywords.map((kw, i) => ( {kw} ))}
setNewKeyword(e.target.value)} onKeyDown={e => e.key === 'Enter' && addKeyword()} className="flex-1 px-3 py-2 border rounded-lg text-sm" placeholder="输入关键词后回车" />
setConfig({ ...config, filter: { ...config.filter, sender_email: e.target.value } })} className={inputClass('filter.sender_email')} />
{/* 邮箱工单检测(新模块) */}

邮箱工单检测

扫描邮箱中的工单邮件,自动导入未存在的工单

{/* 时间范围选择 */}
时间范围: setScanTimeValue(parseInt(e.target.value) || 1)} className="w-20 px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm" min={1} max={365} disabled={scanning} /> {scanning && ( )}
{/* 扫描进度 */} {scanState.status === 'running' && (
扫描进行中...
{scanState.stats && (
已扫描 {scanState.stats.total} 封,导入 {scanState.stats.imported} 封,跳过 {scanState.stats.skipped} 封
)}
)} {/* 扫描取消中 */} {scanState.status === 'cancelling' && (
正在取消扫描...
{scanState.stats && (
已扫描 {scanState.stats.total} 封,已处理 {scanState.stats.imported + scanState.stats.skipped} 封
)}
)} {/* 扫描结果 */} {(scanState.status === 'completed' || scanState.status === 'cancelling') && scanState.stats && (
{scanState.error && (
{scanState.error}
)} {scanState.detailsTruncated && (
详情列表已截断(最多显示 500 条),完整统计请查看服务器日志
)}
已扫描

{scanState.stats.total}

匹配工单号

{scanState.stats.matched}

已导入

{scanState.stats.imported}

已跳过

{scanState.stats.skipped}

错误

{scanState.stats.errors}

{/* 详细结果列表 */} {scanState.details && scanState.details.length > 0 && (
{scanState.details.map((item, i) => ( ))}
工单号 主题 状态 说明
{item.order_number || '-'} {item.subject || '-'} {item.status === 'imported' ? '已导入' : item.status === 'skipped' ? '已跳过' : '错误'} {item.error || item.ticket_no || '-'}
)} {/* 导入按钮 */} {scanState.status === 'completed' && scanState.details && scanState.details.some(d => d.status === 'skipped' && d.order_number && d.error !== '工单已存在' && d.error !== '邮件已处理过' ) && (
可导入 {scanState.details.filter(d => d.status === 'skipped' && d.order_number && d.error !== '工单已存在' && d.error !== '邮件已处理过' ).length} 封邮件
)}
)} {/* 扫描错误 */} {scanState.status === 'error' && (
扫描失败: {scanState.error || '未知错误'}
)}
{/* 微信推送 - 多 Webhook */}

微信推送

每个推送组可独立启用/禁用,并配置独立的运行参数

{sectionMessage.wechat && (
{sectionMessage.wechat.text}
)}
{config.wechat.webhooks.map((wh, index) => (
#{index + 1} updateWebhook(wh.id, { name: e.target.value })} className="flex-1 px-2 py-1 text-sm border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-400 rounded text-slate-900 dark:text-slate-100 font-medium" placeholder="推送组名称" /> {config.wechat.webhooks.length > 1 && ( )}
{expandedWebhooks.has(wh.id) && (
updateWebhook(wh.id, { url: e.target.value })} className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." />
updateWebhookParams(wh.id, { interval_seconds: parseInt(e.target.value) || 60 })} className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm" min={10} max={3600} />
updateWebhookParams(wh.id, { silent_start_hour: parseInt(e.target.value) || 0 })} className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm" min={0} max={23} />
updateWebhookParams(wh.id, { silent_end_hour: parseInt(e.target.value) || 7 })} className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm" min={0} max={23} />
)}
))}
{/* 全局运行参数 */}

全局运行参数

未在推送组中单独配置的参数将使用以下全局默认值

setConfig({ ...config, monitor: { ...config.monitor, interval_seconds: parseInt(e.target.value) || 60 } })} className={inputClass('monitor.interval_seconds')} min={10} max={3600} />
setConfig({ ...config, monitor: { ...config.monitor, silent_start_hour: parseInt(e.target.value) || 0 } })} className={inputClass('monitor.silent_start_hour')} min={0} max={23} />
setConfig({ ...config, monitor: { ...config.monitor, silent_end_hour: parseInt(e.target.value) || 7 } })} className={inputClass('monitor.silent_end_hour')} min={0} max={23} />
{/* 扫描历史 */}

扫描历史

查看历史扫描记录

{scanHistory && scanHistory.items.length > 0 ? (
{scanHistory.items.map((item) => ( ))}
时间 范围 状态 扫描 导入 跳过 错误 操作人
{item.started_at} {item.time_range_unit === 'all' ? '全部' : `${item.time_range_value || ''}${item.time_range_unit === 'minute' ? '分钟' : item.time_range_unit === 'hour' ? '小时' : item.time_range_unit === 'day' ? '天' : item.time_range_unit === 'week' ? '周' : '月'}`} {item.status === 'completed' ? '完成' : item.status === 'error' ? '错误' : item.status === 'cancelling' ? '已取消' : item.status} {item.total_count} {item.imported_count} {item.skipped_count} {item.error_count} {item.created_by || '-'}
{/* 分页 */} {scanHistory.pagination.totalPages > 1 && (
共 {scanHistory.pagination.total} 条记录
{historyPage} / {scanHistory.pagination.totalPages}
)}
) : (

{loadingHistory ? '加载中...' : '暂无扫描历史'}

)}
) }