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)}
${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)}
${sources}
`;
})
.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();
});