diff --git a/src/app/api/monitor/settings/route.ts b/src/app/api/monitor/settings/route.ts index 9c62264..d7b075f 100644 --- a/src/app/api/monitor/settings/route.ts +++ b/src/app/api/monitor/settings/route.ts @@ -19,11 +19,22 @@ function isPrivateIPv4(host: string): boolean { return false } +// 拒绝非规范数字型 IP 写法(整数/十六进制/八进制/短式),防止绕过内网黑名单: +// net.isIPv4 只认标准点分十进制,但 socket 层会把 2130706433 / 0x7f000001 / 127.1 / 0177.0.0.1 +// 等写法同样解析为回环/内网地址,必须在校验层拦截 +function isNonCanonicalNumericHost(host: string): boolean { + if (/(^|\.)0x/i.test(host)) return true // 十六进制:0x7f000001 / 0x7f.0.0.1 + const segs = host.split('.') + if (segs.every((s) => /^\d+$/.test(s)) && !net.isIPv4(host)) return true // 整数型/短式/八进制数字 IP(含前导零) + return false +} + // 主机名格式校验(纵深防御):只允许合法主机名/IP 字符(IPv6 字面量含 ':' 天然被拒), -// 拒绝 URL/路径/空格及内网 IPv4,收窄 SSRF 面 +// 拒绝 URL/路径/空格、非规范数字型 IP 及内网 IPv4,收窄 SSRF 面 const isValidHost = (v: unknown): boolean => { if (typeof v !== 'string' || v.length === 0 || v.length > 253) return false if (!/^[a-zA-Z0-9.-]+$/.test(v)) return false + if (isNonCanonicalNumericHost(v)) return false if (isPrivateIPv4(v)) return false return true } diff --git a/src/components/tickets/ProcessForm.tsx b/src/components/tickets/ProcessForm.tsx index caf7349..87fff15 100644 --- a/src/components/tickets/ProcessForm.tsx +++ b/src/components/tickets/ProcessForm.tsx @@ -27,7 +27,7 @@ interface ProcessFormProps { } const FAULT_CATEGORIES = [ - '硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障', '其他', + '硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障', ] const TICKET_TYPES = ['OEM诊断', 'OEM维修'] @@ -35,7 +35,7 @@ const TICKET_TYPES = ['OEM诊断', 'OEM维修'] const FAULT_SUBCATEGORIES = [ 'GPU故障', 'CPU故障', '内存故障', '硬盘故障', '电源故障', '风扇故障', 'OS崩溃', '驱动故障', '软件崩溃', '配置错误', '网络丢包', '网络中断', - '存储掉盘', 'RAID故障', '无故障', '其他', + '存储掉盘', 'RAID故障', '其他', ] const HANDLER_OPTIONS = ['腾讯', '图灵'] diff --git a/src/components/tickets/TicketForm.tsx b/src/components/tickets/TicketForm.tsx index 35733bb..5d2687e 100644 --- a/src/components/tickets/TicketForm.tsx +++ b/src/components/tickets/TicketForm.tsx @@ -11,7 +11,7 @@ interface TicketFormProps { } const defaultFaultCategories = [ - '硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障', '其他', + '硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障', ] const defaultTicketTypes = ['OEM诊断', 'OEM维修'] @@ -19,7 +19,7 @@ const defaultTicketTypes = ['OEM诊断', 'OEM维修'] const defaultFaultSubcategories = [ 'GPU故障', 'CPU故障', '内存故障', '硬盘故障', '电源故障', '风扇故障', 'OS崩溃', '驱动故障', '软件崩溃', '配置错误', '网络丢包', '网络中断', - '存储掉盘', 'RAID故障', '无故障', '其他', + '存储掉盘', 'RAID故障', '其他', ] export default function TicketForm({ initialData, ticketId }: TicketFormProps) { diff --git a/src/components/tickets/TicketList.tsx b/src/components/tickets/TicketList.tsx index 0341b07..77648f2 100644 --- a/src/components/tickets/TicketList.tsx +++ b/src/components/tickets/TicketList.tsx @@ -28,7 +28,7 @@ const statusMap: Record - t.fault_category !== '无故障' && t.fault_subcategory !== '其他' + t.fault_category !== '无故障' && t.fault_category !== '误判' && t.fault_subcategory !== '其他' ) const dailyStats: DailyOnlineStats[] = dates.map(date => { @@ -137,7 +137,7 @@ export async function collectMonthlyReportData( }) // 5. 第二章:运营数据总览(仅 gpu/storage,排除"无故障"和"其他"工单) - const gpuStorageTickets = tickets.filter(t => t.device_type !== 'other' && t.fault_category !== '无故障' && t.fault_subcategory !== '其他') + const gpuStorageTickets = tickets.filter(t => t.device_type !== 'other' && t.fault_category !== '无故障' && t.fault_category !== '误判' && t.fault_subcategory !== '其他') const chapter2Map = new Map() for (const t of gpuStorageTickets) { const assignDate = t.assign_time.slice(0, 10) @@ -256,7 +256,7 @@ export async function collectMonthlyReportData( /** 从采集数据构建 metadata JSON(供 API 路由和 generate 函数共用) */ export function buildMonthlyMetadata(data: MonthlyReportData): string { const gpuStorageTickets = data.tickets.filter(t => - t.device_type !== 'other' && t.fault_category !== '无故障' && t.fault_subcategory !== '其他' + t.device_type !== 'other' && t.fault_category !== '无故障' && t.fault_category !== '误判' && t.fault_subcategory !== '其他' ) const resolvedTickets = gpuStorageTickets.filter(t => !t.isOngoing) diff --git a/src/lib/weekly-report.ts b/src/lib/weekly-report.ts index 7cc83aa..a800450 100644 --- a/src/lib/weekly-report.ts +++ b/src/lib/weekly-report.ts @@ -92,20 +92,20 @@ export async function collectWeeklyReportData( } }) - // 4. 按规则分三类 - // GPU服务器故障:OEM维修 + fault_category != '无故障' + device_type = 'gpu' + // 4. 按规则分三类(是否有真实故障 + 设备类型) + const isRealFault = (t: typeof entries[number]): boolean => + t.faultCategory !== null && t.faultCategory !== '无故障' && t.faultCategory !== '误判' + + // GPU服务器故障 const gpuFaultTickets = entries.filter(t => - t.ticketType === 'OEM维修' && t.faultCategory !== '无故障' && t.deviceType === 'gpu' + isRealFault(t) && t.deviceType === 'gpu' ) // 存储服务器故障 const storageFaultTickets = entries.filter(t => - t.ticketType === 'OEM维修' && t.faultCategory !== '无故障' && t.deviceType === 'storage' - ) - // 其他工单:OEM诊断 + (OEM维修且fault_category='无故障') - const otherTickets = entries.filter(t => - t.ticketType === 'OEM诊断' || - (t.ticketType === 'OEM维修' && t.faultCategory === '无故障') + isRealFault(t) && t.deviceType === 'storage' ) + // 其他工单:无故障、误判、或 deviceType 未匹配 + const otherTickets = entries.filter(t => !isRealFault(t)) // 5. 每日在线统计(GPU + 存储分别统计) const dates = getDateRange(periodStart, periodEnd)