const ICONS = { activity: '', "arrow-left": '', "arrow-down": '', "arrow-right": '', "arrow-up": '', "arrow-up-right": '', award: '', bell: '', bot: '', camera: '', "book-open": '', "chart-no-axes-combined": '', "check-circle-2": '', "chevron-down": '', "chevron-right": '', "circle-alert": '', "circle-dollar-sign": '', "clipboard-check": '', "cloud-dust": '', cpu: '', database: '', "door-open": '', download: '', "file-text": '', filter: '', flame: '', folder: '', gauge: '', history: '', "layout-dashboard": '', "maximize-2": '', menu: '', mountain: '', network: '', pickaxe: '', play: '', "plug-zap": '', plus: '', "radio-tower": '', "refresh-cw": '', route: '', "scan-search": '', search: '', settings: '', "settings-2": '', "shield-alert": '', "shield-check": '', "sliders-horizontal": '', sparkles: '', thermometer: '', "trending-up": '', clock: '', "triangle-alert": '', "trash-2": '', pencil: '', upload: '', "user-round": '', users: '', "volume-2": '', video: '', wind: '', workflow: '', x: '' }; const VIEW_META = { dashboard: ["综合驾驶舱", "汇聚通防业务数据,洞察矿井通风系统运行态势"], data: ["数据感知", "统一接入业务资料、监测数据、视频监控与通防设施工况"], "facility-status": ["通防设施工况", "按设备类别查看通风设施运行、报警和断开状态"], "safety-monitoring": ["KJ 安全监控系统", "通防监测数据接入、实时曲线与智能趋势预测"], "video-platform": ["工业视频平台", "通道管理、实时视频、分屏预览与智能识别事件"], "business-data": ["业务资料", "统一管理规程、措施、报表、台账与验收资料,实现资料智能抽取与全文检索"], knowledge: ["通防知识中心", "融合规范标准、事故案例与专家经验,实现知识检索与智能问答"], diagnosis: ["隐患识别中心", "面向五类重大安全风险,编排专业智能体完成识别、研判与风险汇聚"], evaluation: ["综合评价", "建立多维度量化指标体系,结合大模型完成评价、解释与优化建议"], "closed-loop": ["闭环处置", "将诊断与评价结果转为任务,实现整改、复核、销号和案例沉淀"], settings: ["系统配置", "管理数据源、模型服务、指标规则、用户权限与平台运行参数"] }; const HAZARD_SCENES = [ { key: "local", name: "局部通风", icon: "wind", description: "局部通风机、风筒与循环风风险" }, { key: "facility", name: "通风设施", icon: "door-open", description: "风门、风窗、密闭与漏风风险" }, { key: "system", name: "通风系统管理", icon: "network", description: "主通风机、阻力与系统可靠性风险" }, { key: "volume", name: "风量分配", icon: "gauge", description: "采掘工作面、用风点与风速风险" }, { key: "disaster", name: "灾害防治", icon: "shield-alert", description: "瓦斯、粉尘、火灾与有毒有害气体风险" } ]; const HAZARD_RISK_LIBRARY = { "risk-gas": { id: "risk-gas", defect_name: "N1202 回风巷瓦斯浓度超限", defect_code: "RISK-GAS-001", scene_key: "disaster", scene: "灾害防治", severity: "high", confidence: 92.6, status: "processing", conclusion: "瓦斯浓度超过报警阈值,存在重大安全隐患。", recommendation: "立即停止受影响区域作业,核查局部通风机和回风系统。", occurred_at: "10:38:24", evidence: ["radio-tower", "wind", "video"] }, "risk-local-fan": { id: "risk-local-fan", defect_name: "T-17 局部通风机停风", defect_code: "RISK-LOCAL-001", scene_key: "local", scene: "局部通风", severity: "high", confidence: 91.2, status: "pending", conclusion: "局部通风机连续失电,存在无计划停风风险。", recommendation: "核查供电回路、风筒连接和受影响区域人员撤离情况。", occurred_at: "10:32:08", evidence: ["plug-zap", "wind"] }, "risk-door": { id: "risk-door", defect_name: "2# 调节风门状态不一致", defect_code: "RISK-FAC-001", scene_key: "facility", scene: "通风设施", severity: "medium", confidence: 88.7, status: "pending", conclusion: "视频画面与风门传感器反馈状态不一致。", recommendation: "安排现场复核风门开闭状态、闭锁装置和密封情况。", occurred_at: "10:28:42", evidence: ["video", "settings-2"] }, "risk-volume": { id: "risk-volume", defect_name: "S1301 采煤工作面风量偏小", defect_code: "RISK-AIR-001", scene_key: "volume", scene: "风量分配", severity: "medium", confidence: 89.3, status: "processing", conclusion: "实际风量低于计划风量,未满足工作面配风要求。", recommendation: "调整风窗开度,排查漏风点并完成风量复测。", occurred_at: "09:56:10", evidence: ["gauge", "file-text"] }, "risk-resistance": { id: "risk-resistance", defect_name: "北翼通风阻力较上月上升 6.2%", defect_code: "RISK-SYS-001", scene_key: "system", scene: "通风系统管理", severity: "low", confidence: 86.5, status: "closed", conclusion: "北翼主要风路阻力呈持续上升趋势。", recommendation: "排查巷道变形、堆积物及局部堵塞,持续跟踪阻力变化。", occurred_at: "08:30:00", evidence: ["activity", "file-text"] }, "risk-duct-leak": { id: "risk-duct-leak", defect_name: "T-17 风筒接口漏风", defect_code: "RISK-LOCAL-002", scene_key: "local", scene: "局部通风", severity: "medium", confidence: 87.8, status: "pending", conclusion: "风筒接口漏风导致末端风量不足。", recommendation: "检查风筒吊挂和接口连接,修复后进行末端风量复测。", occurred_at: "11:02:16", evidence: ["wind", "video", "gauge"] }, "risk-circulation": { id: "risk-circulation", defect_name: "N1201 局部通风机循环风风险", defect_code: "RISK-LOCAL-003", scene_key: "local", scene: "局部通风", severity: "high", confidence: 90.4, status: "pending", conclusion: "吸风口与巷道风流瓦斯浓度差及风筒位置满足循环风风险特征。", recommendation: "立即调整风筒位置并复测吸风口、巷道和末端风流参数。", occurred_at: "11:06:40", evidence: ["radio-tower", "wind", "route"] }, "risk-window": { id: "risk-window", defect_name: "S1301 调节风窗开度偏差", defect_code: "RISK-FAC-002", scene_key: "facility", scene: "通风设施", severity: "medium", confidence: 87.1, status: "pending", conclusion: "风窗实际开度与配风计划存在偏差。", recommendation: "复核风窗执行机构并按照配风计划重新标定开度。", occurred_at: "11:10:08", evidence: ["settings-2", "gauge"] }, "risk-seal-leak": { id: "risk-seal-leak", defect_name: "北翼 2# 密闭漏风", defect_code: "RISK-FAC-003", scene_key: "facility", scene: "通风设施", severity: "medium", confidence: 89.6, status: "pending", conclusion: "密闭压差与相邻风量变化显示存在漏风特征。", recommendation: "检查密闭墙体、观测孔和周边裂隙,完成封堵后复测。", occurred_at: "11:14:22", evidence: ["activity", "radio-tower"] }, "risk-main-fan": { id: "risk-main-fan", defect_name: "主通风机电流波动异常", defect_code: "RISK-SYS-002", scene_key: "system", scene: "通风系统管理", severity: "medium", confidence: 88.2, status: "pending", conclusion: "主通风机电流波动幅度超过历史稳定区间。", recommendation: "检查风机机械状态、供电质量和风阻变化,必要时切换备用风机。", occurred_at: "11:18:35", evidence: ["activity", "plug-zap"] }, "risk-reliability": { id: "risk-reliability", defect_name: "主要风路缺少可靠备用通风路径", defect_code: "RISK-SYS-003", scene_key: "system", scene: "通风系统管理", severity: "low", confidence: 85.9, status: "pending", conclusion: "部分主要风路在故障切换场景下缺少可靠备用路径。", recommendation: "复核通风网络可靠性,完善备用风路和应急调度方案。", occurred_at: "11:22:04", evidence: ["network", "file-text"] }, "risk-allocation": { id: "risk-allocation", defect_name: "N1202 用风点配风偏差", defect_code: "RISK-AIR-002", scene_key: "volume", scene: "风量分配", severity: "medium", confidence: 88.9, status: "pending", conclusion: "实测风量与计划配风偏差超过允许范围。", recommendation: "核查邻近风路和风窗状态,按计划重新分配风量。", occurred_at: "11:25:11", evidence: ["gauge", "network"] }, "risk-speed": { id: "risk-speed", defect_name: "回风巷风速低于规程下限", defect_code: "RISK-AIR-003", scene_key: "volume", scene: "风量分配", severity: "medium", confidence: 90.1, status: "pending", conclusion: "回风巷实测风速低于规程规定的最低风速。", recommendation: "核查巷道断面、堆积物和漏风情况,调整后复测风速。", occurred_at: "11:28:47", evidence: ["gauge", "wind"] }, "risk-dust": { id: "risk-dust", defect_name: "S1302 粉尘浓度接近上限", defect_code: "RISK-DIS-001", scene_key: "disaster", scene: "灾害防治", severity: "medium", confidence: 89.4, status: "pending", conclusion: "采煤工作面粉尘浓度接近规定上限。", recommendation: "检查喷雾降尘、净化水幕和除尘风机联动运行状态。", occurred_at: "11:31:19", evidence: ["cloud-dust", "video"] }, "risk-fire": { id: "risk-fire", defect_name: "采空区 CO 浓度趋势上升", defect_code: "RISK-DIS-002", scene_key: "disaster", scene: "灾害防治", severity: "high", confidence: 91.7, status: "pending", conclusion: "采空区 CO 浓度连续上升,存在自然发火风险。", recommendation: "核查灌浆、注氮和堵漏措施,持续监测 CO 与温度变化。", occurred_at: "11:35:02", evidence: ["flame", "radio-tower", "activity"] } }; const HAZARD_AGENTS = [ { id: "local-fan-state", sceneKey: "local", name: "局部通风机运行状态识别", description: "识别无计划停风、供电中断、风机切换异常和运行状态偏离。", icon: "plug-zap", model: "风机工况时序模型", cycle: "5 分钟", sources: ["风机工况", "供电状态", "视频画面"], findingIds: ["risk-local-fan"] }, { id: "local-duct-leak", sceneKey: "local", name: "风筒漏风与末端风量识别", description: "识别风筒破损、接口漏风、吊挂异常以及末端风量不足。", icon: "wind", model: "风量关联分析模型", cycle: "10 分钟", sources: ["风筒状态", "末端风量", "视频智能识别"], findingIds: ["risk-duct-leak"] }, { id: "local-circulation", sceneKey: "local", name: "循环风风险识别", description: "结合吸风口瓦斯、巷道风流、风筒位置和风量关系研判循环风。", icon: "refresh-cw", model: "循环风专家规则集", cycle: "5 分钟", sources: ["瓦斯监测", "巷道风速", "风筒位置"], findingIds: ["risk-circulation"] }, { id: "facility-door", sceneKey: "facility", name: "风门状态一致性识别", description: "识别风门未闭合、闭锁失效、传感器与视频状态冲突。", icon: "door-open", model: "多模态状态融合模型", cycle: "实时", sources: ["风门传感器", "视频画面", "闭锁状态"], findingIds: ["risk-door"] }, { id: "facility-window", sceneKey: "facility", name: "调节风窗异常识别", description: "识别风窗开度偏差、执行机构异常和配风响应滞后。", icon: "settings-2", model: "开度-风量回归模型", cycle: "10 分钟", sources: ["风窗开度", "计划风量", "实测风量"], findingIds: ["risk-window"] }, { id: "facility-leakage", sceneKey: "facility", name: "密闭与漏风识别", description: "识别密闭压差异常、墙体裂隙和相邻风路异常漏风。", icon: "network", model: "压差与风量平衡模型", cycle: "15 分钟", sources: ["密闭压差", "风量平衡", "巡检记录"], findingIds: ["risk-seal-leak"] }, { id: "system-resistance", sceneKey: "system", name: "通风阻力异常识别", description: "识别巷道阻力突增、持续上升和阻力分布失衡。", icon: "activity", model: "阻力趋势预测模型", cycle: "每小时", sources: ["风压监测", "风量数据", "阻力测定报告"], findingIds: ["risk-resistance"] }, { id: "system-main-fan", sceneKey: "system", name: "主通风机运行异常识别", description: "识别风机电流、风压、振动和效率偏离稳定区间。", icon: "cpu", model: "主通风机健康度模型", cycle: "5 分钟", sources: ["风机工况", "电气参数", "振动监测"], findingIds: ["risk-main-fan"] }, { id: "system-reliability", sceneKey: "system", name: "通风系统可靠性识别", description: "评估主要风路、备用路径和故障切换条件下的系统可靠性。", icon: "route", model: "通风网络可靠性模型", cycle: "每日", sources: ["通风网络", "设施状态", "应急预案"], findingIds: ["risk-reliability"] }, { id: "volume-face", sceneKey: "volume", name: "采掘工作面风量不足识别", description: "对比计划配风和实测风量,识别采掘工作面风量不足。", icon: "gauge", model: "配风偏差识别模型", cycle: "10 分钟", sources: ["计划风量", "实测风量", "工作面状态"], findingIds: ["risk-volume"] }, { id: "volume-allocation", sceneKey: "volume", name: "用风点配风偏差识别", description: "识别用风点风量偏差、风窗调节滞后和风量分配失衡。", icon: "network", model: "风量平衡分析模型", cycle: "10 分钟", sources: ["用风点风量", "风窗状态", "通风网络"], findingIds: ["risk-allocation"] }, { id: "volume-speed", sceneKey: "volume", name: "巷道风速异常识别", description: "识别风速低于规程下限、风速突降和断面异常影响。", icon: "wind", model: "风速阈值与趋势模型", cycle: "5 分钟", sources: ["风速监测", "巷道断面", "历史曲线"], findingIds: ["risk-speed"] }, { id: "disaster-gas", sceneKey: "disaster", name: "瓦斯超限识别", description: "识别瓦斯浓度超限、快速上升和异常积聚风险。", icon: "radio-tower", model: "瓦斯超限预警模型", cycle: "实时", sources: ["瓦斯监测", "风量数据", "视频画面"], findingIds: ["risk-gas"] }, { id: "disaster-dust", sceneKey: "disaster", name: "粉尘与防尘异常识别", description: "识别粉尘浓度异常、喷雾失效和防尘设施未联动。", icon: "cloud-dust", model: "粉尘风险识别模型", cycle: "10 分钟", sources: ["粉尘监测", "喷雾状态", "视频识别"], findingIds: ["risk-dust"] }, { id: "disaster-fire", sceneKey: "disaster", name: "自然发火与 CO 风险识别", description: "识别采空区 CO、温度和氧气浓度变化形成的自然发火风险。", icon: "flame", model: "自然发火趋势模型", cycle: "10 分钟", sources: ["CO 监测", "温度数据", "灌浆注氮记录"], findingIds: ["risk-fire"] } ]; const NODE_DETAILS = { "main-fan": { title: "主通风机", values: [["运行频率", "49.8 Hz"], ["风量", "7,860 m³/min"], ["状态", "正常"]], note: "主通风机运行平稳,当前能耗较昨日下降 1.3%。", status: "normal" }, intake: { title: "副井进风井", values: [["进风量", "7,920 m³/min"], ["温度", "18.6 ℃"], ["状态", "正常"]], note: "进风参数稳定,未发现异常波动。", status: "normal" }, "air-gate": { title: "2# 调节风门", values: [["开度", "42%"], ["状态", "不一致"], ["视频置信度", "91%"]], note: "视频显示风门未完全闭合,传感器反馈处于开启状态,建议现场复核。", status: "warning" }, gas: { title: "N1202 瓦斯监测点", values: [["瓦斯浓度", "0.86%"], ["报警阈值", "0.80%"], ["趋势", "上升"]], note: "已关联局部通风机 T-17 与 2# 调节风门,正在执行耦合诊断。", status: "danger" }, "north-face": { title: "N1201 采煤工作面", values: [["风量", "1,356 m³/min"], ["瓦斯", "0.42%"], ["状态", "正常"]], note: "工作面通防参数稳定,满足当前生产计划。", status: "normal" }, "air-volume": { title: "S1301 测风站", values: [["实测风量", "1,286 m³/min"], ["计划风量", "1,404 m³/min"], ["偏差", "-8.4%"]], note: "风量低于计划值,已生成调节风窗与复测任务。", status: "warning" }, dust: { title: "S1302 粉尘监测点", values: [["粉尘浓度", "4.2 mg/m³"], ["上限", "4.0 mg/m³"], ["喷雾状态", "间歇"]], note: "粉尘浓度短时偏高,建议检查喷雾降尘装置运行状态。", status: "warning" }, return: { title: "回风井底", values: [["回风量", "8,120 m³/min"], ["负压", "2.42 kPa"], ["状态", "正常"]], note: "回风系统运行平稳,当前阻力处于合理区间。", status: "normal" } }; const BUSINESS_DOCUMENTS = { n1202: { type: "PDF", title: "N1202 工作面通风安全技术措施", category: "通风专业", code: "TF-CS-2026-086", owner: "王强", pages: "42 页", size: "2.8 MB", date: "2026-10-07", status: "抽取完成", confidence: 92.4, summary: "通过版面分析、表格识别和领域实体抽取,已完成工作面通风方式、需风量、瓦斯管理及局部通风参数等关键字段结构化。", fields: [ ["工作面名称", "N1202 综采工作面", "98.6%"], ["通风方式", "U 型通风", "96.8%"], ["设计需风量", "1,404 m³/min", "94.2%"], ["瓦斯浓度上限", "0.80%", "97.1%"], ["局部通风机型号", "FBDNo6.3/2×30", "91.5%"], ["风筒直径", "1,000 mm", "93.8%"], ["测风周期", "每 10 天一次", "89.7%"], ["措施责任人", "王强", "95.4%"] ] }, resistance: { type: "DOC", title: "2026 年 9 月通风阻力测定报告", category: "通风专业", code: "TF-BB-2026-091", owner: "赵磊", pages: "28 页", size: "1.6 MB", date: "2026-09-30", status: "抽取完成", confidence: 90.8, summary: "已抽取通风阻力测点、巷道参数、风机工况和阻力变化趋势,并与系统内历史测点完成对象挂接。", fields: [ ["测定日期", "2026-09-28", "99.2%"], ["测定路线", "北翼回风系统", "95.7%"], ["最大阻力值", "2,860 Pa", "93.1%"], ["主通风机风量", "7,860 m³/min", "96.4%"], ["通风阻力增幅", "6.2%", "91.8%"], ["主要异常区段", "北翼回风巷", "89.6%"], ["测定负责人", "赵磊", "97.2%"] ] }, airlog: { type: "XLS", title: "矿井测风记录日报表", category: "监测监控", code: "TF-BB-2026-092", owner: "刘伟", pages: "1,240 行", size: "856 KB", date: "2026-10-08", status: "抽取中 76%", confidence: 88.5, summary: "正在按测风站、用风地点和日期进行表格结构识别,已完成 318 个数据项中的 242 个字段抽取。", fields: [ ["日报日期", "2026-10-08", "99.1%"], ["测风站数量", "18 个", "96.3%"], ["总进风量", "7,920 m³/min", "94.8%"], ["总回风量", "8,120 m³/min", "94.1%"], ["异常测点", "S1301 测风站", "87.6%"], ["风量偏差", "-8.4%", "92.4%"] ] }, s1302: { type: "PDF", title: "S1302 工作面综合防尘设计", category: "粉尘防治", code: "TF-SJ-2026-034", owner: "陈晨", pages: "36 页", size: "3.1 MB", date: "2026-10-05", status: "等待解析", confidence: 0, summary: "资料已入库,等待执行版面解析、表格结构识别和防尘设施参数抽取。", fields: [ ["工作面名称", "S1302 综采工作面", "待抽取"], ["防尘方式", "煤层注水 + 喷雾降尘", "待抽取"], ["喷雾点数量", "待识别", "待抽取"], ["除尘风机型号", "待识别", "待抽取"], ["设计负责人", "陈晨", "已识别"] ] }, regulation: { type: "PDF", title: "矿井通风管理制度汇编", category: "制度管理", code: "TF-GC-2026-012", owner: "周洋", pages: "118 页", size: "5.4 MB", date: "2026-09-18", status: "抽取完成", confidence: 89.6, summary: "已完成章节结构识别、制度条款切分和责任岗位抽取,可关联通风系统管理诊断规则。", fields: [ ["制度版本", "2026 版", "98.2%"], ["制度章节数", "12 章", "96.7%"], ["测风管理制度", "第 4 章", "94.3%"], ["瓦斯检查制度", "第 6 章", "95.1%"], ["责任岗位数", "28 个", "90.4%"], ["修订日期", "2026-09-15", "97.8%"] ] }, acceptance: { type: "DOC", title: "南翼通风系统调整验收资料", category: "通风专业", code: "TF-YS-2026-021", owner: "孙敏", pages: "22 页", size: "1.2 MB", date: "2026-09-26", status: "抽取完成", confidence: 93.1, summary: "已抽取调整范围、验收指标、现场测风结果和验收结论,并与南翼通风设施台账完成关联。", fields: [ ["验收对象", "南翼通风系统调整工程", "98.4%"], ["验收日期", "2026-09-25", "99.0%"], ["风量验收结果", "合格", "96.8%"], ["设施验收数量", "36 项", "94.2%"], ["遗留问题", "2 项", "91.4%"], ["验收负责人", "孙敏", "97.1%"] ] } }; function injectIcons(root = document) { root.querySelectorAll("[data-icon]").forEach((element) => { if (element.querySelector("svg")) return; const icon = ICONS[element.dataset.icon] || ICONS["circle-alert"]; element.innerHTML = ``; }); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function showToast(message, type = "success") { const container = document.querySelector("#toastContainer"); if (!container) return; const toast = document.createElement("div"); toast.className = "toast"; const iconName = type === "warning" ? "triangle-alert" : "check-circle-2"; toast.innerHTML = `${escapeHtml(message)}`; container.appendChild(toast); injectIcons(toast); window.setTimeout(() => { toast.classList.add("removing"); window.setTimeout(() => toast.remove(), 220); }, 2600); } function navigateTo(viewName, options = {}) { const targetView = document.querySelector(`#view-${viewName}`); if (!targetView) return; const navigationView = viewName === "business-data" ? "data" : viewName; document.querySelectorAll(".view").forEach((view) => view.classList.toggle("active", view === targetView)); document.querySelectorAll(".nav-item").forEach((item) => item.classList.toggle("active", item.dataset.view === navigationView)); document.querySelector("#pageTitle").textContent = VIEW_META[viewName]?.[0] || ""; document.querySelector("#pageSubtitle").textContent = VIEW_META[viewName]?.[1] || ""; closeMobileSidebar(); if (options.updateHash !== false && window.location.hash !== `#${viewName}`) { window.history.replaceState(null, "", `#${viewName}`); } if (!options.preserveScroll) { window.scrollTo({ top: 0, behavior: "smooth" }); } } function openMobileSidebar() { document.querySelector("#sidebar")?.classList.add("open"); document.querySelector("#sidebarScrim")?.classList.add("show"); document.body.style.overflow = "hidden"; } function closeMobileSidebar() { document.querySelector("#sidebar")?.classList.remove("open"); document.querySelector("#sidebarScrim")?.classList.remove("show"); document.body.style.overflow = ""; } function getAnswer(question) { if (/风门|漏风/.test(question)) { return { text: "风门漏风排查应重点检查门框密封、闭锁装置、门板变形、墙体裂隙和传感器状态。建议结合视频与风量变化进行联合判断,确认漏风后及时封堵并复测风量。", sources: ["《矿井通风设施管理标准》第 5.3 节", "通风设施漏风处置案例 2024-026"] }; } if (/循环风|局部通风机/.test(question)) { return { text: "判定局部通风机循环风,应比较局部通风机吸风口与巷道风流中的瓦斯浓度,并结合风筒出口到吸风口的距离、巷道风量和风流方向综合判断。发现循环风后应立即调整风筒位置或巷道风量。", sources: ["《煤矿安全规程》第 163 条", "局部通风机循环风判定专家规则 R-021"] }; } if (/粉尘|防尘/.test(question)) { return { text: "粉尘治理应形成“监测、喷雾、净化、冲洗、个体防护”的闭环。采掘工作面需重点检查喷雾降尘、净化水幕、除尘风机和风流净化装置的联动状态,并对高浓度点位持续跟踪。", sources: ["《煤矿井下粉尘综合防治技术规范》", "综合防尘管理流程 QF-04"] }; } return { text: "瓦斯浓度超限后,应立即停止作业、切断电源、撤出人员,并核查局部通风机、风筒和回风系统状态。处置完成后,需要复测瓦斯浓度并形成闭环记录,严禁未查明原因直接恢复生产。", sources: ["《煤矿安全规程》第 173 条", "瓦斯超限处置案例 2024-018"] }; } function renderAnswer(container, question, compact = false) { if (!container) return; const answer = getAnswer(question); if (compact) { container.innerHTML = `
通防知识助手正在检索 2,164 条知识

${escapeHtml(answer.text)}

${answer.sources.map((source) => `${escapeHtml(source)}`).join("")}
`; injectIcons(container); return; } container.innerHTML = `
智能问答

回答与依据

知识溯源已开启

问题:${escapeHtml(question)}

${escapeHtml(answer.text)}

${answer.sources .map( (source, index) => `` ) .join("")}
`; injectIcons(container); } function openEvidenceDrawer(row) { const title = row.querySelector(".defect-name strong")?.textContent?.trim() || "隐患详情"; const meta = row.querySelector(".defect-name small")?.textContent?.trim() || "实时识别"; const confidence = row.querySelector(".confidence b")?.textContent?.trim() || "88.0%"; const scene = row.dataset.scene || row.querySelector(".scene-tag")?.textContent?.trim() || "通防业务"; const state = row.dataset.state || row.querySelector(".state-tag")?.textContent?.trim() || "待处理"; const conclusion = row.dataset.conclusion || "多源数据联合分析表明,该异常具有较强业务关联性。"; const recommendation = row.dataset.recommendation || "按照知识库处置流程完成参数复测与隐患排除。"; const drawer = document.createElement("div"); drawer.className = "evidence-drawer"; drawer.innerHTML = `
`; document.body.appendChild(drawer); injectIcons(drawer); requestAnimationFrame(() => drawer.classList.add("open")); drawer.querySelectorAll("[data-close-drawer]").forEach((button) => { button.addEventListener("click", () => closeDrawer(drawer)); }); drawer.querySelector("[data-create-task]")?.addEventListener("click", () => { closeDrawer(drawer); showToast("整改任务已生成并推送至责任人"); }); } function closeDrawer(drawer) { drawer.classList.remove("open"); window.setTimeout(() => drawer.remove(), 220); } function animateNumber(element, target, decimals = 0) { const start = Number(String(element.textContent).replaceAll(",", "")) || 0; const duration = 520; const startTime = performance.now(); const targetNumber = Number(target); function frame(now) { const progress = Math.min((now - startTime) / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); const value = start + (targetNumber - start) * eased; element.textContent = decimals ? value.toFixed(decimals) : Math.round(value).toLocaleString("zh-CN"); if (progress < 1) requestAnimationFrame(frame); } requestAnimationFrame(frame); } function closeDocumentPreview() { const modal = document.querySelector("#documentPreviewModal"); if (modal) modal.hidden = true; document.body.style.overflow = ""; } function closeDocumentReprocess() { const modal = document.querySelector("#documentReprocessModal"); if (modal) modal.hidden = true; document.body.style.overflow = ""; } async function openDocumentReprocess(documentId) { const modal = document.querySelector("#documentReprocessModal"); const form = document.querySelector("#documentReprocessForm"); const templateSelect = document.querySelector("#reprocessTemplateSelect"); const errorPanel = document.querySelector("#documentReprocessError"); const errorText = document.querySelector("#documentReprocessErrorText"); const documentData = window.VentAPI?.getBusinessDocument(documentId); if (!modal || !form || !documentData) return; if ( documentData.status.includes("解析中") || documentData.status.includes("抽取中") ) { showToast("资料正在处理中,请等待当前任务完成", "warning"); return; } form.reset(); form.elements.document_id.value = documentId; const templates = window.VentAPI?.state?.templates || []; if (templateSelect) { templateSelect.innerHTML = '' + templates .map( (template) => `` ) .join(""); } let latestRun = null; try { latestRun = await window.VentAPI?.getDocumentProcessing?.(documentId); } catch { latestRun = null; } const parseMode = latestRun?.metadata?.parse_mode || (String(latestRun?.parser_engine || "").includes("precision") ? "precision" : "lightweight"); const parseModeInput = form.querySelector(`input[name="parse_mode"][value="${parseMode}"]`); if (parseModeInput) parseModeInput.checked = true; if (templateSelect && latestRun?.template_id) { templateSelect.value = latestRun.template_id; } const errorMessage = latestRun?.error || documentData.parseError || ""; if (errorPanel && errorText) { errorPanel.hidden = !errorMessage; errorText.textContent = errorMessage; } modal.hidden = false; document.body.style.overflow = "hidden"; } function downloadBusinessDocument(documentId) { if (!window.VentAPI?.getDocumentFileUrl) { showToast("文件下载服务不可用", "warning"); return; } const link = document.createElement("a"); link.href = window.VentAPI.getDocumentFileUrl(documentId, true); document.body.appendChild(link); link.click(); link.remove(); } function formatDocumentPreviewText(value) { const source = String(value || ""); const parsed = new DOMParser().parseFromString(source, "text/html"); parsed.body.querySelectorAll("td, th").forEach((cell) => { cell.append(parsed.createTextNode("\t")); }); parsed.body .querySelectorAll("p, div, tr, li, h1, h2, h3, h4, h5, h6, pre, blockquote") .forEach((element) => { element.append(parsed.createTextNode("\n")); }); parsed.body.querySelectorAll("br").forEach((element) => { element.replaceWith(parsed.createTextNode("\n")); }); return (parsed.body.textContent || "") .replace(/[ \t]+\n/g, "\n") .replace(/\n[ \t]+/g, "\n") .replace(/\n{3,}/g, "\n\n") .trim(); } async function openDocumentPreview(documentId) { const modal = document.querySelector("#documentPreviewModal"); const body = document.querySelector("#documentPreviewBody"); const title = document.querySelector("#documentPreviewTitle"); const typeBadge = document.querySelector("#documentPreviewType"); const downloadButton = document.querySelector("#documentPreviewDownload"); if (!modal || !body || !window.VentAPI?.getDocumentPreview) return; modal.hidden = false; document.body.style.overflow = "hidden"; if (typeBadge) typeBadge.textContent = "正在加载"; body.innerHTML = `
正在加载原文

请稍候,系统正在读取资料内容。

`; injectIcons(body); try { const preview = await window.VentAPI.getDocumentPreview(documentId); if (title) title.textContent = preview.title || "原文预览"; const typeLabels = { pdf: "PDF 原文", text: "解析文本", download_only: "仅支持下载", unavailable: "原文不可用" }; if (typeBadge) typeBadge.textContent = typeLabels[preview.preview_type] || "原文预览"; if (downloadButton) { downloadButton.disabled = !preview.can_download; downloadButton.dataset.documentId = preview.can_download ? documentId : ""; } if (preview.preview_type === "pdf" && preview.source_available) { body.innerHTML = ``; return; } if (preview.text_available) { body.innerHTML = `
${escapeHtml(formatDocumentPreviewText(preview.text))}
`; return; } if (preview.preview_type === "download_only") { body.innerHTML = `
当前格式暂不支持在线预览

可以下载原始文件后使用本机 Office 或 Excel 打开。

`; injectIcons(body); return; } body.innerHTML = `
暂无可预览内容

原始文件不存在或尚未完成解析,暂时无法展示原文。

`; injectIcons(body); } catch (error) { body.innerHTML = `
原文加载失败

${escapeHtml(error?.message || error || "请稍后重试")}

`; injectIcons(body); } } function renderBusinessDocument(documentId) { const documentData = window.VentAPI?.getBusinessDocument(documentId) || BUSINESS_DOCUMENTS[documentId]; const panel = document.querySelector("#businessDetailPanel"); if (!documentData || !panel) return; const statusClass = documentData.status.includes("完成") ? "success" : documentData.status.includes("抽取中") ? "running" : "pending"; const reprocessLabel = documentData.status.includes("失败") ? "失败重试" : "重新解析"; panel.innerHTML = `
智能抽取

资料解析详情

${escapeHtml(documentData.status)}
${escapeHtml(documentData.type)}

${escapeHtml(documentData.title)}

${escapeHtml(documentData.code)} · ${escapeHtml(documentData.category)}

资料责任人${escapeHtml(documentData.owner)}
页数 / 行数${escapeHtml(documentData.pages)}
文件大小${escapeHtml(documentData.size)}
入库日期${escapeHtml(documentData.date)}
${documentData.parseError ? `
上次处理失败

${escapeHtml(documentData.parseError)}

` : ""}

关键信息

${documentData.fieldGroups?.length ? `
${documentData.fieldGroups .map( (group, index) => `
${escapeHtml(group.name)} ${group.fields.length} 项
${group.fields .map( (field) => `
${escapeHtml(field.label)} ${escapeHtml(field.value)}
` ) .join("")}
` ) .join("")}
` : `
${(documentData.fields || []) .map( ([label, value]) => `
${escapeHtml(label)} ${escapeHtml(value)}
` ) .join("")}
`}

${escapeHtml(documentData.summary)}

`; injectIcons(panel); panel.querySelector("[data-preview-document]")?.addEventListener("click", () => { openDocumentPreview(documentId); }); panel.querySelector("[data-download-document]")?.addEventListener("click", () => { downloadBusinessDocument(documentId); }); panel.querySelector("[data-reprocess-document]")?.addEventListener("click", () => { openDocumentReprocess(documentId); }); panel.querySelectorAll("[data-toast]").forEach((button) => { button.addEventListener("click", () => showToast(button.dataset.toast)); }); const editButton = panel.querySelector("[data-edit-information]"); const businessSection = panel.querySelector(".business-section"); let editingInformation = false; editButton?.addEventListener("click", async () => { const inputs = [...panel.querySelectorAll(".business-field-input")]; if (!editingInformation) { editingInformation = true; businessSection?.classList.add("editing"); inputs.forEach((input) => { input.disabled = false; }); editButton.innerHTML = '保存修改 '; injectIcons(editButton); return; } const changedInputs = inputs.filter((input) => input.value !== input.dataset.original); if (!changedInputs.length) { editingInformation = false; businessSection?.classList.remove("editing"); inputs.forEach((input) => { input.disabled = true; }); editButton.innerHTML = '编辑信息 '; injectIcons(editButton); showToast("没有需要保存的修改", "warning"); return; } editButton.disabled = true; try { const updatedFields = await Promise.all( changedInputs.map((input) => window.VentAPI?.updateBusinessDocumentField( documentId, input.dataset.fieldId, input.value ) ) ); const extractionErrors = updatedFields.filter( (field) => field?.edited && field?.extraction_correct === false ).length; const pendingJudgements = updatedFields.filter( (field) => field?.edit_judgement_status === "pending" ).length; const revertedFields = updatedFields.filter( (field) => field?.edit_judgement_source === "reverted" ).length; showToast( revertedFields ? `已恢复 ${revertedFields} 项原字段值,准确率已修正` : pendingJudgements ? `已保存 ${changedInputs.length} 项修改,正在后台判定` : extractionErrors ? `已保存 ${changedInputs.length} 项修改,其中 ${extractionErrors} 项判定为抽取错误` : `已保存 ${changedInputs.length} 项修改` ); await window.VentAPI?.refreshBusinessDocument?.(documentId); } catch (error) { editButton.disabled = false; showToast(`保存失败:${error?.message || error}`, "warning"); } }); } const hazardState = { initialized: false, activeScene: HAZARD_SCENES[0].key, selectedAgentId: null, runningAgents: new Set(), runs: new Map(), risks: new Map(), riskFilter: "all", riskScene: "all", runCount: 0 }; const HAZARD_INITIAL_RISK_IDS = [ "risk-gas", "risk-local-fan", "risk-door", "risk-volume", "risk-resistance" ]; function wait(ms) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function getHazardScene(sceneKey) { return HAZARD_SCENES.find((scene) => scene.key === sceneKey) || HAZARD_SCENES[0]; } function getHazardAgent(agentId) { return HAZARD_AGENTS.find((agent) => agent.id === agentId) || null; } function hazardSceneKeyFromName(sceneName) { return HAZARD_SCENES.find((scene) => scene.name === sceneName)?.key || "system"; } function hazardRiskKey(risk) { return String(risk.defect_name || risk.defect_code || risk.id || "").trim(); } function hazardAgentForRisk(risk) { const sceneKey = risk.scene_key || hazardSceneKeyFromName(risk.scene); return ( HAZARD_AGENTS.find((agent) => agent.id === risk.source_agent_id) || HAZARD_AGENTS.find((agent) => agent.sceneKey === sceneKey) || HAZARD_AGENTS[0] ); } function knownHazardRisk(risk) { if (risk.id && HAZARD_RISK_LIBRARY[risk.id]) { return HAZARD_RISK_LIBRARY[risk.id]; } const name = String(risk.defect_name || ""); const code = String(risk.defect_code || risk.code || ""); if (name.includes("瓦斯") || code.includes("GAS")) { return HAZARD_RISK_LIBRARY["risk-gas"]; } if (name.includes("粉尘") || name.includes("防尘") || code.includes("DUST")) { return HAZARD_RISK_LIBRARY["risk-dust"]; } if (name.includes("风门") || name.includes("设施状态") || code.includes("FACILITY")) { return HAZARD_RISK_LIBRARY["risk-door"]; } if (name.includes("停风") || name.includes("局部通风机") || code.includes("LOCAL")) { return HAZARD_RISK_LIBRARY["risk-local-fan"]; } if (name.includes("阻力") || code.includes("SYS")) { return HAZARD_RISK_LIBRARY["risk-resistance"]; } if (name.includes("风量") || code.includes("AIRFLOW") || code.includes("AIR")) { return name.includes("工作面") ? HAZARD_RISK_LIBRARY["risk-volume"] : HAZARD_RISK_LIBRARY["risk-allocation"]; } return null; } function normalizeHazardRisk(risk, index = 0) { const known = knownHazardRisk(risk); const normalizedRisk = known ? { ...known, ...risk, id: known.id, defect_name: known.defect_name, defect_code: known.defect_code, scene_key: known.scene_key, scene: known.scene, evidence: known.evidence } : risk; const sceneKey = normalizedRisk.scene_key || hazardSceneKeyFromName(normalizedRisk.scene); const scene = getHazardScene(sceneKey); const sourceAgent = hazardAgentForRisk({ ...normalizedRisk, scene_key: sceneKey }); const confidenceValue = Number(normalizedRisk.confidence || sourceAgent?.confidence || 88); const confidence = confidenceValue <= 1 ? confidenceValue * 100 : confidenceValue; const severity = ["high", "medium", "low"].includes(normalizedRisk.severity) ? normalizedRisk.severity : "low"; const evidence = Array.isArray(normalizedRisk.evidence) && normalizedRisk.evidence.length ? normalizedRisk.evidence : sceneKey === "disaster" ? ["radio-tower", "video", "file-text"] : sceneKey === "volume" ? ["gauge", "file-text"] : ["radio-tower", "activity", "file-text"]; return { id: String(normalizedRisk.id || `risk-${index}-${hazardRiskKey(normalizedRisk) || Date.now()}`), defect_name: String(normalizedRisk.defect_name || normalizedRisk.defect_code || "未命名隐患"), defect_code: String(normalizedRisk.defect_code || `RISK-${index + 1}`), scene_key: sceneKey, scene: normalizedRisk.scene || scene.name, severity, confidence: Math.max(0, Math.min(confidence, 100)), status: String(normalizedRisk.status || "pending"), conclusion: String(normalizedRisk.conclusion || "智能体已识别到异常特征,等待人工复核。"), recommendation: String(normalizedRisk.recommendation || "安排现场复核并完成处置闭环。"), occurred_at: String(normalizedRisk.occurred_at || "刚刚"), evidence, source_agent_id: normalizedRisk.source_agent_id || sourceAgent?.id || "", source_agent: normalizedRisk.source_agent || sourceAgent?.name || "隐患识别智能体", is_new: Boolean(normalizedRisk.is_new) }; } function agentFindingRisks(agent) { if (!agent) return []; return (agent.findingIds || []) .map((riskId) => HAZARD_RISK_LIBRARY[riskId]) .filter(Boolean) .map((risk, index) => normalizeHazardRisk( { ...risk, source_agent_id: agent.id, source_agent: agent.name }, index ) ); } function mergeHazardRisks(risks) { const incoming = Array.isArray(risks) ? risks : []; incoming.forEach((risk, index) => { const normalized = normalizeHazardRisk(risk, index); const key = hazardRiskKey(normalized); const existing = hazardState.risks.get(key); if (existing) { hazardState.risks.set(key, { ...existing, ...normalized, id: existing.id, is_new: Boolean(normalized.is_new || existing.is_new) }); return; } hazardState.risks.set(key, normalized); }); renderHazardCenterMeta(); renderHazardRiskList(); } function setHazardRisks(risks) { const incoming = Array.isArray(risks) && risks.length ? risks : HAZARD_INITIAL_RISK_IDS.map((riskId) => HAZARD_RISK_LIBRARY[riskId]); mergeHazardRisks(incoming); return [...hazardState.risks.values()]; } function hazardRunMeta(agentId) { return hazardState.runs.get(agentId) || null; } function hazardAgentStatus(agent) { if (hazardState.runningAgents.has(agent.id)) { return { className: "running", label: "运行中" }; } const run = hazardRunMeta(agent.id); if (!run) return { className: "", label: "待运行" }; if (run.findings.length) { return { className: "risk", label: "已识别" }; } return { className: "clear", label: "未发现" }; } function renderHazardScenarios() { const grid = document.querySelector("#hazardScenarioGrid"); if (!grid) return; const risks = [...hazardState.risks.values()]; grid.innerHTML = HAZARD_SCENES.map((scene) => { const agentCount = HAZARD_AGENTS.filter((agent) => agent.sceneKey === scene.key).length; const sceneRisks = risks.filter((risk) => risk.scene_key === scene.key); const highCount = sceneRisks.filter((risk) => risk.severity === "high").length; return ` `; }).join(""); injectIcons(grid); } function renderHazardAgents() { const grid = document.querySelector("#hazardAgentGrid"); if (!grid) return; const scene = getHazardScene(hazardState.activeScene); const agents = HAZARD_AGENTS.filter((agent) => agent.sceneKey === scene.key); const countBadge = document.querySelector("#hazardSceneAgentCount"); if (countBadge) countBadge.textContent = `${agents.length} 个智能体`; grid.innerHTML = agents .map((agent) => { const status = hazardAgentStatus(agent); const run = hazardRunMeta(agent.id); const findingCount = run?.findings?.length || 0; const sources = agent.sources .slice(0, 3) .map((source) => `${escapeHtml(source)}`) .join(""); return `
${escapeHtml(agent.name)} ${escapeHtml(agent.model)}
${status.label}

${escapeHtml(agent.description)}

运行周期${escapeHtml(agent.cycle)}
风险结果${findingCount ? `${findingCount} 项待核查` : "尚未识别"}
${sources}
${run ? `最近运行 ${run.runAtLabel}` : "等待首次运行"}
`; }) .join(""); injectIcons(grid); } function renderHazardCenterMeta() { const risks = [...hazardState.risks.values()]; const counts = { high: 0, medium: 0, low: 0 }; risks.forEach((risk) => { counts[risk.severity] = (counts[risk.severity] || 0) + 1; }); const values = { hazardAgentTotal: HAZARD_AGENTS.length, hazardSceneTotal: HAZARD_SCENES.length, hazardRunCount: hazardState.runCount, hazardRiskTotal: risks.length, hazardMetricTotal: risks.length, hazardMetricHigh: counts.high, hazardMetricMedium: counts.medium, hazardMetricLow: counts.low }; Object.entries(values).forEach(([id, value]) => { const element = document.querySelector(`#${id}`); if (element) element.textContent = String(value); }); const toolbar = document.querySelector("#view-diagnosis .toolbar-copy p"); if (toolbar) { toolbar.textContent = `${HAZARD_SCENES.length} 个业务场景 · ${HAZARD_AGENTS.length} 个专业智能体 · 风险清单 ${risks.length} 项`; } renderHazardScenarios(); } function hazardSeverityMeta(severity) { if (severity === "high") return { label: "重大隐患", className: "red", rank: 0 }; if (severity === "medium") return { label: "一般隐患", className: "yellow", rank: 1 }; return { label: "提示风险", className: "blue", rank: 2 }; } function hazardStatusMeta(status) { if (["completed", "closed", "verified", "已记录"].includes(status)) { return { label: "已记录", className: "closed" }; } if (["processing", "处理中"].includes(status)) { return { label: "处理中", className: "processing" }; } return { label: "待核查", className: "pending" }; } function renderHazardRiskList() { const list = document.querySelector("#hazardRiskList"); if (!list) return; const risks = [...hazardState.risks.values()] .filter((risk) => hazardState.riskFilter === "all" || risk.severity === hazardState.riskFilter) .filter((risk) => hazardState.riskScene === "all" || risk.scene_key === hazardState.riskScene) .sort((left, right) => { if (left.is_new !== right.is_new) return left.is_new ? -1 : 1; const severityDiff = hazardSeverityMeta(left.severity).rank - hazardSeverityMeta(right.severity).rank; if (severityDiff) return severityDiff; return String(right.occurred_at).localeCompare(String(left.occurred_at)); }); if (!risks.length) { list.innerHTML = `
当前筛选条件下暂无风险
`; injectIcons(list); return; } list.innerHTML = risks .map((risk) => { const scene = getHazardScene(risk.scene_key); const severity = hazardSeverityMeta(risk.severity); const status = hazardStatusMeta(risk.status); const confidence = Number(risk.confidence || 0); const evidence = (risk.evidence || []) .map((icon) => ``) .join(""); return ` `; }) .join(""); injectIcons(list); list.querySelectorAll(".hazard-risk-row").forEach((row) => { row.addEventListener("click", () => openEvidenceDrawer(row)); }); } function renderHazardResultEmpty() { const result = document.querySelector("#hazardAgentResult"); const status = document.querySelector("#hazardResultStatus"); if (status) { status.textContent = "尚未执行"; status.className = "subtle-badge"; } if (!result) return; result.innerHTML = `
尚未执行

当前没有可展示的识别结果。

`; injectIcons(result); } function renderHazardResultLoading(agent) { const result = document.querySelector("#hazardAgentResult"); const status = document.querySelector("#hazardResultStatus"); if (status) { status.textContent = "运行中"; status.className = "subtle-badge"; } if (!result) return; result.innerHTML = `
${escapeHtml(agent.name)} 正在汇聚监测、视频、规则与历史案例
`; injectIcons(result); } function renderHazardAgentOverview(agent) { const result = document.querySelector("#hazardAgentResult"); const status = document.querySelector("#hazardResultStatus"); if (!result || !agent) return; if (status) { status.textContent = "待运行"; status.className = "subtle-badge"; } result.innerHTML = `
${escapeHtml(agent.name)}

${escapeHtml(agent.description)}

模型 ${escapeHtml(agent.model)}周期 ${escapeHtml(agent.cycle)}

检查范围

${agent.sources.map((source) => `
${escapeHtml(source)}数据源已接入识别范围就绪
`).join("")}
`; injectIcons(result); } function renderHazardResult(agent, findings, source = "demo") { const result = document.querySelector("#hazardAgentResult"); const status = document.querySelector("#hazardResultStatus"); if (!result || !agent) return; const hasRisk = findings.length > 0; const highest = findings.some((item) => item.severity === "high") ? "high" : findings.some((item) => item.severity === "medium") ? "medium" : "low"; if (status) { status.textContent = hasRisk ? `${findings.length} 项风险` : "未发现隐患"; status.className = `subtle-badge${hasRisk ? "" : " success"}`; } const checks = [ { icon: "database", className: "", label: "数据完整性", detail: `${agent.sources.length} 类数据源完成汇聚`, value: "100%" }, { icon: "cpu", className: "", label: "模型推理", detail: `${agent.model} 已完成计算`, value: "完成" }, { icon: "file-text", className: "warning", label: "规则与知识命中", detail: hasRisk ? "命中隐患判定规则和关联知识" : "未命中异常规则", value: hasRisk ? "命中" : "正常" }, { icon: hasRisk ? "triangle-alert" : "shield-check", className: hasRisk ? "danger" : "", label: "隐患分级", detail: hasRisk ? `最高等级:${hazardSeverityMeta(highest).label}` : "未形成风险项", value: hasRisk ? "需核查" : "通过" } ]; result.innerHTML = `
${hasRisk ? `识别到 ${findings.length} 项隐患风险` : "未发现新增隐患"}

${escapeHtml(hasRisk ? findings.map((item) => item.conclusion).join(";") : "本次运行的检查项均处于正常范围。")}

智能体 ${escapeHtml(agent.name)} 来源 ${source === "api" ? "实时数据" : "演示数据"}

运行检查

${checks.map((check) => `
${escapeHtml(check.label)}${escapeHtml(check.detail)} ${escapeHtml(check.value)}
`).join("")}
${hasRisk ? `

识别结果

${findings.map((finding) => `
${escapeHtml(finding.defect_name)}

${escapeHtml(finding.recommendation)}

${hazardSeverityMeta(finding.severity).label}
`).join("")}
` : ""} `; injectIcons(result); } async function runHazardAgent(agentId, options = {}) { const agent = getHazardAgent(agentId); if (!agent || hazardState.runningAgents.has(agent.id)) return null; hazardState.selectedAgentId = agent.id; hazardState.activeScene = agent.sceneKey; hazardState.runningAgents.add(agent.id); renderHazardScenarios(); renderHazardAgents(); renderHazardResultLoading(agent); try { let runResult; if (window.VentAPI?.runHazardAgent) { runResult = await window.VentAPI.runHazardAgent(agent); } else { await wait(900); runResult = { findings: agentFindingRisks(agent), source: "demo" }; } const findings = (runResult?.findings?.length ? runResult.findings : agentFindingRisks(agent) ).map((finding, index) => normalizeHazardRisk( { ...finding, source_agent_id: agent.id, source_agent: agent.name, is_new: !hazardState.risks.has(hazardRiskKey(finding)) }, index ) ); hazardState.runs.set(agent.id, { findings, source: runResult?.source || "demo", runAt: new Date(), runAtLabel: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }) }); hazardState.runCount += 1; mergeHazardRisks(findings); renderHazardResult(agent, findings, runResult?.source || "demo"); renderHazardCenterMeta(); renderHazardAgents(); if (!options.silent) { window.setTimeout(() => { findings.forEach((finding) => { const riskKey = hazardRiskKey(finding); const risk = hazardState.risks.get(riskKey); if (risk) hazardState.risks.set(riskKey, { ...risk, is_new: false }); }); renderHazardRiskList(); }, 5200); showToast( findings.length ? `${agent.name}运行完成,识别到 ${findings.length} 项隐患` : `${agent.name}运行完成,未发现新增隐患` ); } return { agent, findings }; } catch (error) { const fallbackFindings = agentFindingRisks(agent); hazardState.runs.set(agent.id, { findings: fallbackFindings, source: "demo", runAt: new Date(), runAtLabel: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }) }); hazardState.runCount += 1; mergeHazardRisks(fallbackFindings); renderHazardResult(agent, fallbackFindings, "demo"); renderHazardCenterMeta(); renderHazardAgents(); if (!options.silent) { showToast(`${agent.name}已使用本地识别结果,${error?.message || "实时接口不可用"}`, "warning"); } return { agent, findings: fallbackFindings }; } finally { hazardState.runningAgents.delete(agent.id); renderHazardAgents(); } } async function runCurrentHazardScene(button) { const agents = HAZARD_AGENTS.filter( (agent) => agent.sceneKey === hazardState.activeScene ); if (!agents.length) return; const original = button.innerHTML; button.disabled = true; button.innerHTML = '批量运行中'; injectIcons(button); let findingCount = 0; for (const agent of agents) { const result = await runHazardAgent(agent.id, { silent: true }); findingCount += result?.findings?.length || 0; } button.disabled = false; button.innerHTML = original; injectIcons(button); showToast( findingCount ? `${getHazardScene(hazardState.activeScene).name}场景运行完成,识别到 ${findingCount} 项隐患` : `${getHazardScene(hazardState.activeScene).name}场景运行完成,未发现新增隐患` ); } function initHazardCenter() { const root = document.querySelector("#view-diagnosis"); if (!root || hazardState.initialized) return; hazardState.initialized = true; setHazardRisks(HAZARD_INITIAL_RISK_IDS.map((riskId) => HAZARD_RISK_LIBRARY[riskId])); renderHazardScenarios(); renderHazardAgents(); renderHazardResultEmpty(); document.querySelector("#hazardScenarioGrid")?.addEventListener("click", (event) => { const card = event.target.closest("[data-hazard-scene]"); if (!card) return; hazardState.activeScene = card.dataset.hazardScene; hazardState.selectedAgentId = null; renderHazardScenarios(); renderHazardAgents(); renderHazardResultEmpty(); }); document.querySelector("#hazardAgentGrid")?.addEventListener("click", (event) => { const runButton = event.target.closest("[data-run-agent]"); if (runButton) { event.stopPropagation(); runHazardAgent(runButton.dataset.runAgent); return; } const card = event.target.closest("[data-agent-id]"); const agent = getHazardAgent(card?.dataset.agentId); if (!agent) return; hazardState.selectedAgentId = agent.id; renderHazardAgents(); const run = hazardRunMeta(agent.id); if (run) { renderHazardResult(agent, run.findings, run.source); } else { renderHazardAgentOverview(agent); } }); document.querySelector("#hazardRiskFilters")?.addEventListener("click", (event) => { const button = event.target.closest("[data-risk-filter]"); if (!button) return; hazardState.riskFilter = button.dataset.riskFilter; document.querySelectorAll("#hazardRiskFilters button").forEach((item) => { item.classList.toggle("active", item === button); }); renderHazardRiskList(); }); document.querySelector("#hazardRiskSceneFilter")?.addEventListener("change", (event) => { hazardState.riskScene = event.currentTarget.value; renderHazardRiskList(); }); document.querySelector("[data-run-scene-agents]")?.addEventListener("click", (event) => { runCurrentHazardScene(event.currentTarget); }); document.querySelector("[data-export-hazards]")?.addEventListener("click", () => { showToast(`识别报告已生成,共包含 ${hazardState.risks.size} 项风险`); }); } window.getHazardAgent = getHazardAgent; window.getHazardAgentFindings = agentFindingRisks; window.setHazardRisks = setHazardRisks; window.addHazardRisks = mergeHazardRisks; document.addEventListener("DOMContentLoaded", () => { injectIcons(); initHazardCenter(); document.querySelectorAll(".nav-item").forEach((item) => { item.addEventListener("click", () => navigateTo(item.dataset.view)); }); document.querySelectorAll("[data-jump-view]").forEach((button) => { button.addEventListener("click", (event) => { event.stopPropagation(); navigateTo(button.dataset.jumpView); }); }); document.querySelectorAll("[data-open-business]").forEach((button) => { button.addEventListener("click", () => navigateTo("business-data")); }); document.querySelectorAll("[data-open-processing-records]").forEach((button) => { button.addEventListener("click", () => { window.VentAPI?.openProcessingRecords?.(); }); }); document.querySelectorAll("[data-back-view]").forEach((button) => { button.addEventListener("click", () => navigateTo(button.dataset.backView)); }); let activeBusinessCategory = "all"; window.addEventListener("vent:business-category-changed", (event) => { activeBusinessCategory = event.detail?.category || "all"; }); const businessSearch = document.querySelector("#businessDocumentSearch"); const businessDocumentList = document.querySelector("#businessDocumentList"); function getBusinessRows() { return [...document.querySelectorAll("#businessDocumentList .document-row")]; } function filterBusinessDocuments() { const keyword = businessSearch?.value.trim().toLowerCase() || ""; getBusinessRows().forEach((row) => { const categoryMatches = activeBusinessCategory === "all" || row.dataset.category === activeBusinessCategory; const textMatches = !keyword || row.textContent.toLowerCase().includes(keyword); row.hidden = !(categoryMatches && textMatches); }); } document.querySelectorAll("#businessCategoryList button").forEach((button) => { button.addEventListener("click", () => { activeBusinessCategory = button.dataset.docCategory; document.querySelectorAll("#businessCategoryList button").forEach((item) => item.classList.toggle("active", item === button)); if (window.VentAPI?.setBusinessCategory) { window.VentAPI.setBusinessCategory(activeBusinessCategory); return; } filterBusinessDocuments(); }); }); let businessSearchTimer = null; businessSearch?.addEventListener("input", () => { window.clearTimeout(businessSearchTimer); businessSearchTimer = window.setTimeout(() => { const keyword = businessSearch.value.trim(); if (window.VentAPI?.setBusinessSearch) { window.VentAPI.setBusinessSearch(keyword); return; } filterBusinessDocuments(); }, 350); }); businessDocumentList?.addEventListener("click", (event) => { const row = event.target.closest(".document-row"); if (!row || event.target.closest("[data-delete-document]")) return; getBusinessRows().forEach((item) => item.classList.toggle("active", item === row)); renderBusinessDocument(row.dataset.documentId); }); renderBusinessDocument("n1202"); const initialView = window.location.hash.slice(1); if (initialView && VIEW_META[initialView]) { navigateTo(initialView, { preserveScroll: true, updateHash: false }); } document.querySelector("#mobileMenu")?.addEventListener("click", openMobileSidebar); document.querySelector("#sidebarClose")?.addEventListener("click", closeMobileSidebar); document.querySelector("#sidebarScrim")?.addEventListener("click", closeMobileSidebar); const notificationButton = document.querySelector("#notificationButton"); const notificationPopover = document.querySelector("#notificationPopover"); notificationButton?.addEventListener("click", (event) => { event.stopPropagation(); notificationPopover?.classList.toggle("open"); }); notificationPopover?.addEventListener("click", (event) => event.stopPropagation()); document.addEventListener("click", () => notificationPopover?.classList.remove("open")); document.querySelector("#markRead")?.addEventListener("click", () => { document.querySelectorAll(".notification-item.unread").forEach((item) => item.classList.remove("unread")); document.querySelector("#notificationButton i")?.remove(); showToast("消息已全部标记为已读"); }); document.querySelectorAll("[data-range-control] button").forEach((button) => { button.addEventListener("click", () => { const parent = button.closest("[data-range-control]"); parent?.querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button)); const range = button.dataset.range; const metric = document.querySelector('[data-metric="alerts"]'); const cardLabel = metric?.closest(".kpi-card")?.querySelector("p"); const values = { "今日": { value: 18, label: "今日告警" }, "近7天": { value: 126, label: "近7天告警" }, "近30天": { value: 486, label: "近30天告警" } }; if (metric && values[range]) { animateNumber(metric, values[range].value); if (cardLabel) cardLabel.textContent = values[range].label; } }); }); document.querySelectorAll("[data-refresh]").forEach((button) => { button.addEventListener("click", () => { const svg = button.querySelector("svg"); svg?.animate([{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }], { duration: 650, easing: "ease-in-out" }); showToast(button.closest(".mine-map") ? "通风网络数据已刷新" : "数据已刷新"); }); }); document.querySelectorAll("[data-toast]").forEach((button) => { button.addEventListener("click", () => showToast(button.dataset.toast)); }); const mapDetail = document.querySelector("#mapDetail"); const mineMap = document.querySelector("#mineMap"); function renderNodeDetail(nodeKey) { const data = NODE_DETAILS[nodeKey]; if (!data || !mapDetail) return; mapDetail.innerHTML = `
${escapeHtml(data.title)}
${data.values .map( ([label, value]) => `
${escapeHtml(label)}${escapeHtml(value)}
` ) .join("")}

${escapeHtml(data.note)}

`; injectIcons(mapDetail); mapDetail.classList.add("show"); mapDetail.querySelector("#closeMapDetail")?.addEventListener("click", () => mapDetail.classList.remove("show")); mapDetail.querySelector("[data-jump-view]")?.addEventListener("click", () => navigateTo("diagnosis")); } mineMap?.querySelectorAll(".map-node").forEach((node) => { const activate = () => { mineMap.querySelectorAll(".map-node").forEach((item) => item.classList.toggle("active", item === node)); renderNodeDetail(node.dataset.node); }; node.addEventListener("click", activate); node.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); activate(); } }); }); document.querySelector("#closeMapDetail")?.addEventListener("click", () => mapDetail?.classList.remove("show")); document.querySelectorAll("[data-map-layer]").forEach((button) => { button.addEventListener("click", () => { button.closest(".map-toolbar")?.querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button)); showToast(`已切换至${button.textContent.trim()}图层`); }); }); const dashboardKnowledgeForm = document.querySelector("#dashboardKnowledgeForm"); const knowledgeConsoleForm = document.querySelector("#knowledgeConsoleForm"); function resizeQuestionInput(field) { if (!field) return; const maxHeight = Number(field.dataset.maxHeight || 96); field.style.height = "auto"; field.style.height = `${Math.min(field.scrollHeight, maxHeight)}px`; field.style.overflowY = field.scrollHeight > maxHeight ? "auto" : "hidden"; } function bindQuestionInput(form) { const field = form?.querySelector('textarea[name="question"]'); if (!field) return; resizeQuestionInput(field); field.addEventListener("input", () => resizeQuestionInput(field)); field.addEventListener("keydown", (event) => { if (event.key !== "Enter" || event.shiftKey) return; event.preventDefault(); form.requestSubmit(); }); } bindQuestionInput(dashboardKnowledgeForm); bindQuestionInput(knowledgeConsoleForm); function lockKnowledgeAnswerToGraph() { const panel = document.querySelector("#knowledgeAnswerPanel"); const graphPanel = document.querySelector(".knowledge-grid > article:last-child"); if (!panel || !graphPanel || panel.dataset.heightLocked === "true") return; const graphHeight = Math.round(graphPanel.getBoundingClientRect().height); if (graphHeight <= 0) return; panel.style.setProperty("--knowledge-answer-fixed-height", `${graphHeight}px`); panel.dataset.heightLocked = "true"; } requestAnimationFrame(() => requestAnimationFrame(lockKnowledgeAnswerToGraph)); dashboardKnowledgeForm?.addEventListener("submit", (event) => { event.preventDefault(); const question = new FormData(dashboardKnowledgeForm).get("question")?.toString().trim(); if (!question) return; renderAnswer(document.querySelector("#answerPreview"), question, true); }); document.querySelectorAll("[data-question]").forEach((button) => { button.addEventListener("click", () => { const input = dashboardKnowledgeForm?.querySelector('[name="question"]'); if (input) { input.value = button.dataset.question; input.dispatchEvent(new Event("input", { bubbles: true })); } renderAnswer(document.querySelector("#answerPreview"), button.dataset.question, true); }); }); knowledgeConsoleForm?.addEventListener("submit", (event) => { event.preventDefault(); const question = new FormData(knowledgeConsoleForm).get("question")?.toString().trim(); if (!question) { showToast("请输入需要查询的通防业务问题", "warning"); return; } const answerPanel = document.querySelector("#knowledgeAnswerPanel"); renderAnswer(answerPanel, question); showToast("知识库检索完成"); }); const globalSearch = document.querySelector("#globalSearch"); globalSearch?.addEventListener("keydown", (event) => { if (event.key !== "Enter") return; const value = globalSearch.value.trim(); if (!value) return; navigateTo("knowledge"); const input = knowledgeConsoleForm?.querySelector('[name="question"]'); if (input) { input.value = value; input.dispatchEvent(new Event("input", { bubbles: true })); } renderAnswer(document.querySelector("#knowledgeAnswerPanel"), value); }); document.addEventListener("keydown", (event) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); globalSearch?.focus(); } if (event.key === "Escape") { closeMobileSidebar(); notificationPopover?.classList.remove("open"); document.querySelector(".evidence-drawer")?.classList.remove("open"); closeDocumentPreview(); closeDocumentReprocess(); window.VentAPI?.closeProcessingRecords?.(); } }); document.querySelectorAll("[data-preview-close]").forEach((element) => { element.addEventListener("click", closeDocumentPreview); }); document.querySelector("#documentPreviewDownload")?.addEventListener("click", (event) => { const documentId = event.currentTarget.dataset.documentId; if (documentId) downloadBusinessDocument(documentId); }); document.querySelectorAll("[data-reprocess-close]").forEach((element) => { element.addEventListener("click", closeDocumentReprocess); }); document.querySelectorAll("[data-records-close]").forEach((element) => { element.addEventListener("click", () => window.VentAPI?.closeProcessingRecords?.()); }); document.querySelector("#documentReprocessForm")?.addEventListener("submit", async (event) => { event.preventDefault(); const form = event.currentTarget; const submit = form.querySelector('[type="submit"]'); const documentId = form.elements.document_id.value; if (!documentId || !window.VentAPI?.reprocessBusinessDocument) return; const original = submit.innerHTML; submit.disabled = true; submit.innerHTML = '正在创建任务'; injectIcons(submit); try { await window.VentAPI.reprocessBusinessDocument(documentId, { parseMode: form.elements.parse_mode.value, templateId: form.elements.template_id.value || null }); closeDocumentReprocess(); showToast("重新解析任务已创建"); } catch (error) { showToast(`重新解析失败:${error?.message || error}`, "warning"); } finally { submit.disabled = false; submit.innerHTML = original; injectIcons(submit); } }); document.querySelectorAll("#diagnosisTabs button").forEach((button) => { button.addEventListener("click", () => { const scene = button.dataset.scene; document.querySelectorAll("#diagnosisTabs button").forEach((item) => item.classList.toggle("active", item === button)); document.querySelectorAll("#diagnosisTable .diagnosis-row").forEach((row) => { row.hidden = scene !== "all" && row.dataset.scene !== scene; }); }); }); document.querySelectorAll("#diagnosisTable .diagnosis-row").forEach((row) => { row.addEventListener("click", () => openEvidenceDrawer(row)); }); document.querySelector("[data-run-diagnosis]")?.addEventListener("click", (event) => { const button = event.currentTarget; const original = button.innerHTML; button.disabled = true; button.innerHTML = '诊断进行中'; injectIcons(button); button.querySelector("svg")?.animate([{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }], { duration: 900, iterations: 2 }); window.setTimeout(() => { button.disabled = false; button.innerHTML = original; injectIcons(button); showToast("全量诊断完成,识别到 18 项缺陷与风险"); }, 1800); }); document.querySelector("[data-start-evaluation]")?.addEventListener("click", (event) => { const button = event.currentTarget; const original = button.innerHTML; button.disabled = true; button.innerHTML = '评价计算中'; injectIcons(button); window.setTimeout(() => { button.disabled = false; button.innerHTML = original; injectIcons(button); showToast("综合评价已更新,综合得分 92.8,等级 A"); }, 1600); }); document.querySelectorAll(".settings-nav button").forEach((button) => { button.addEventListener("click", () => { button.closest(".settings-nav")?.querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button)); showToast(`已切换至${button.textContent.trim()}`); }); }); window.VentAPI?.init(); });