康宇 2 tygodni temu
commit
a0b02c7d2c

+ 88 - 0
README.md

@@ -0,0 +1,88 @@
+# 煤矿通风 AI 技能库(Vent Skills)
+
+面向煤矿 **「一通三防」/通风** 业务的 Agent 技能(ZCode Skills)集合。仓库内每个子目录是一个独立技能,内含 `SKILL.md`(技能描述、触发条件、执行流程、红线规则),部分技能附带 `scripts/`、`references/`、`sample/` 等资源文件。这些技能将通风日常工作中依赖经验的环节——配风计划审查、通风监测数据解读、需风量计算、报表台账汇总、通风系统阻力检查等——封装为可供 AI 助手稳定复用的工作流。
+
+## 技能清单
+
+| 目录 | 定位 | 典型触发 | 主要依赖 |
+|---|---|---|---|
+| `knowledge-base` | 煤矿安全知识库检索:《煤矿安全规程》/ AQ、MT 标准条款原文、重大事故隐患判定等 | "风速限值是多少""AQ 1056 参数""什么构成重大隐患" | `query_knowledge_base` |
+| `dialog-interpret` | 对话式通风数据解读:自然语言提问 → 选巷道与指标 → 比对阈值/计划风量 → 结构化结论与建议 | "XX巷道风速风量怎么样""XX回风巷瓦斯为什么报红""近7天XX工作面温度趋势" | 平台注册工具 12+(巷道/设备/数据/知识四类)、`references/regulation-thresholds.md` |
+| `click-interpret-device` | 前端点选单条**设备** → 合规校验、风险定级、固定格式解读报告 | 在平台界面点击/选中某设备数据行 | `query_device_data_by_id`、`query_knowledge_base` |
+| `click-interpret-tun` | 前端点选单条**巷道** → 风速风量合规校验、四级风险细分、固定格式报告 | 在平台界面点击/选中某巷道数据行 | `query_tun_data_by_id`、`query_knowledge_base`、`query_tunnels_by_model` |
+| `needq-calc` | 需风量/配风量交互式计算:采煤/备用/掘进/硐室/其他地点及多地点汇总 | "计算XX工作面需风量""帮我算硐室需风量""配风量计算" | `calc_*` 系列 16 个 MCP 工具、`get_needq_all_data`、子智能体 `needq_calc_expert` |
+| `vent-device-daily-report` | 设备设施在线率统计与设备管理日报(按类型分组,🔴/🟠/🟢 告警) | "给我设备管理日报""统计设备在线率""主通风机在线情况" | TF-MCP `execute_sql_query`(vent 库)、`scripts/device_online_report.py` |
+| `vent-operation-daily-report` | 通风设备与传感器**运行日报**导出:前一日 7 类真实设备 max/min/avg、风门开闭次数、异常明细 | "帮我导出通风设备与传感器运行日报""昨天主通风机运行怎么样" | 平台 MCP(:8071/mcp)、`scripts/vent_daily_report.py`、fastmcp |
+| `vent-monthly-summary-product` | **公司端**通风月报汇总:各矿《通风月报表》xlsx → 《公司通风月报汇总表》并自动合计、入库 | "汇总通风月报""把各矿月报汇总到公司表""生成公司X月通风月报汇总表" | 函数工具链、子智能体 `monthly-report-extractor`、沙箱资产(提取契约 + 模板 xlsx) |
+| `vent-plan-review` | 配风计划 PDF 审查**顶层总流程**:并行委派三个子智能体审查,汇总输出 + 六节 Word 报告下载 | "审查/研判/审核这份配风计划"(上传 PDF) | `extract_uploaded_pdf_text`、`save_word_report`、`task` 委派 |
+| `vent-plan-review-form` | 配风计划**基本形式审查**(子技能):版本月份、签字、编制时间、计算过程完整性、语病逻辑 | 由上级 task 委派(plan-review-form) | `get_current_time`、`write_todos` |
+| `vent-plan-review-data` | 配风计划**数据一致性审查**(子技能):用风地点完整性、瓦斯/CO₂ 双重交叉验证、工作面参数与作业规程、风速温度匹配 | 由上级 task 委派(plan-review-data) | `query_mining_plan`、`query_face_procedure`、`query_gas_report`、`query_wind_report`、`calc_gas_emission_from_wind` |
+| `vent-plan-review-calc` | 配风计划**计算核验**(子技能):逐用风地点核验需风量公式/参数/结果(偏差分级) | 由上级 task 委派(plan-review-calc) | `calc_*` 15 个工具、`write_todos` |
+| `vent-resistance-check` | 矿井通风系统阻力检查:回风系统最大阻力路线与总阻力解算、核验、隐患分析报告 | "计算某回风井/回风系统的关键阻力路线及总阻力""通风阻力检查" | TF-MCP 解算引擎、vent 库 SQL、云通防知识库、`scripts/mcp_call.js`、`references/capability-map.md` |
+| `wind-hazard-diagnosis` | 风速风量监测与隐患判识:超限/风量不足/重大事故隐患(需风量 <75% 红线),固定格式 md 报告 | "查询当前风速风量""测风数据有没有问题/隐患" | TF-MCP(:8071)、云通防知识库(:8067)、`get_needq_all_data` |
+
+## 技能间协作关系
+
+```
+                        ┌─ vent-plan-review-form(形式审查)──┐
+用户上传配风计划 PDF ──► vent-plan-review(编排)──task 并行──┼─ vent-plan-review-data(数据一致性)┼─► 六节报告 + Word
+                        └─ vent-plan-review-calc(计算核验)──┘
+
+knowledge-base ◄── 被 dialog-interpret / click-interpret-* 引用作为"依据"来源
+
+needq-calc:由子智能体 needq_calc_expert 执行全部计算(主 Agent 不得自调工具)
+vent-monthly-summary-product:逐矿委派 monthly-report-extractor 提取关键内容后批量填表
+```
+
+- **配风计划审查三件套**(`vent-plan-review` + `vent-plan-review-form/data/calc`)构成一条完整链路:审查输入为扫描/图片 PDF,技能内建 OCR 误识风险须知(排版错乱、数字误识、偏差 >50% 需人工核对)。
+- `click-interpret-device` 与 `click-interpret-tun` 是"点选解读"姊妹技能,`dialog-interpret` 为其对话版超集;`needq-calc` 侧重"算多少风",`vent-plan-review-calc` 侧重"核验计划算得对不对"。
+- `vent-operation-daily-report`(运行日报)与 `vent-device-daily-report`(在线率/管理日报)刻意拆分,二者各自明示边界、不做合并。
+
+## 通用约定(铁律)
+
+各技能共享以下跨文件约束,新增技能请保持一致:
+
+1. **绝不编造数据**:所有判定必须基于工具返回、平台数据或规程条款原文;数据缺失一律明示("文档中未找到" / "外部数据查询失败"),缺失依赖时如实告知缺哪一环。
+2. **规程依据统一**:《煤矿安全规程》以 **2025 版**为准,配风/需风计算配套 **AQ 1056-2008**;引用条款须给出具体条款来源。
+3. **固定格式输出**:报告章节、表格列、判定用语与单位口径固定(风量 m³/min、风速 m/s、阻力/压差 Pa),禁止增删章节或改写固定用语;交付物含对话内 Markdown、落盘 md/json、可下载 xlsx、六节 Word 等多种形态。
+4. **全程简体中文**;面向用户的输出不得暴露内部工具名 / 子智能体名 / 设备 ID(用名称代替)。
+5. **优先并行委派**:编排类技能(plan-review、monthly-summary)要求同一轮并行 `task` 委派子智能体,禁止串行或跳过委派凭印象执行。
+
+## 平台依赖
+
+技能按取数方式分四层,运行前请确认对应依赖可用:
+
+| 依赖 | 用途 | 相关技能 |
+|---|---|---|
+| 通防管控平台 MCP(TF-MCP / tf_mcp,:8071) | 设备/巷道/传感实时与历史数据、报警、需风量、解算引擎 | dialog-interpret、click-*、needq-calc、plan-review-data、resistance-check、wind-hazard-diagnosis |
+| vent 库 SQL(`execute_sql_query`) | deviceinfo、sub_station、fan、ai_gate、tun/node 等表直查 | vent-device-daily-report、vent-resistance-check |
+| 云通防知识库(:8067,Obsidian Vault Knowledge Service) | 规程条款佐证、隐患分析 | wind-hazard-diagnosis、vent-resistance-check、knowledge-base |
+| 平台注册函数工具 + 沙箱资产 | PDF 提取、Word/Excel 处理、下载发布、入库登记 | vent-plan-review、vent-monthly-summary-product |
+
+跨工具容错策略不统一,各技能自洽:`vent-resistance-check` 提供 `references/capability-map.md`(C1~C7 能力契约与降级表,C4 解算引擎不可降级);`vent-operation-daily-report` 支持 Python 脚本直连平台实现宿主无关;`wind-hazard-diagnosis` 依赖写死、缺失只明示不静默降级。
+
+## 安装
+
+本仓库是技能的**源目录**,不直接运行。将所需技能目录(连同 `SKILL.md` 及该目录下全部附带文件)整体复制到 ZCode 技能目录即可,例如:
+
+```bash
+cp -r vent-plan-review vent-plan-review-form vent-plan-review-data vent-plan-review-calc \
+      ~/.zcode/skills/
+```
+
+> 含 `scripts/`、`references/`、`sample/`、`assets/` 的技能必须整目录复制并保持相对结构,脚本路径依赖目录内文件。
+
+## 技能内附文档
+
+部分技能自带独立使用文档,包含安装、字段表、输出样例与踩坑记录:
+
+- `vent-device-daily-report/README.md`(含 `scripts/device_online_report.py` 用法)
+- `vent-operation-daily-report/README.txt`(含 `scripts/vent_daily_report.py` 两种安装方式、`sample/` 输出样例、`requirements.txt`)
+- `vent-resistance-check/README.md`(含 `references/` 能力契约/隐患清单/报告模板、`scripts/mcp_call.js`)
+- `wind-hazard-diagnosis/README.md`
+- `dialog-interpret/references/regulation-thresholds.md`(2022 版阈值速查,正式判定以 2025 版规程为准)
+
+## 说明
+
+- 文中平台地址为部署示例,实际使用以现场环境为准;技能均以真实平台数据为唯一数据源。
+- 仓库根目录 `README.md` 即本文档;各技能目录内可另带独立 `README`,两处描述如有出入以对应技能 `SKILL.md` 为准。

+ 75 - 0
click-interpret-device/SKILL.md

@@ -0,0 +1,75 @@
+---
+name: click-interpret-device
+description: |
+  【强制触发判定】用户在前端界面点击、选中设备监测表格内单条设备数据行,需要针对该设备独立校验数据合规、分级安全风险、输出固定格式结构化设备解读报告时,必须启用本技能。
+  适用范围:仅处理单条设备数据专项分析;多设备批量计算、纯规程咨询、全局报警处置不适用本技能。
+tools:
+  - query_device_data_by_id
+  - query_knowledge_base
+  - list_ventanaly_monitor_data_days
+  - write_todos
+---
+
+# click-interpret-device 技能详细配置
+
+## 角色定位(role)
+设备监测数据专项分析专家,专注设备实时数据合规校验与风险定级
+
+## 触发场景(trigger_scene)
+- 执行设备数据解读任务
+- 用户点击或选中某设备数据行
+
+
+## 排除场景(exclude_scene)
+- 多设备批量对比、全系统数据解算
+- 仅查询《煤矿安全规程》条文,无设备实时数据
+- 全局报警处置、设备系统整体优化
+
+## 工具详细说明(tools_detail)
+- **query_device_data_by_id**:根据设备唯一ID查询设备实时数据和报警数据
+- **query_knowledge_base**:检索《煤矿安全规程》、AQ/MT行业标准、限值规范原文
+## 绝对禁止
+    禁止暴露设备的ID,统一用设备的名称代替
+
+## 标准化执行工作流程(work_flow)
+
+### Step0:初始化任务列表
+- **动作**:调用 `write_todos`,创建以下任务并标记第一个为 `in_progress`:
+  1. 拉取设备全量数据(`query_device_data_by_id`)
+  2. 合规校验 + 风险等级判定
+  3. 生成标准化结构化报告
+
+### Step1:拉取设备全量数据
+- **动作**:调用 `write_todos` 将"拉取设备全量数据"标记为 `in_progress`;然后调用 `query_device_data_by_id`,传入设备ID,获取设备实时监测数据
+- **完成后**:调用 `write_todos` 将"拉取设备全量数据"标记为 `completed`,将"合规校验 + 风险等级判定"标记为 `in_progress`
+- **返回字段释义**:
+  - `device_id`:设备唯一 ID
+  - `device_name`:设备名称
+  - `device_type`:设备类型
+  - `data`:实时数据对象
+  - `alarms`:报警信息数组
+
+### Step2:合规校验 + 风险等级判定
+- **动作**:调用 `write_todos` 将"合规校验 + 风险等级判定"标记为 `in_progress`(如果尚未标记);然后执行以下校验
+**校验维度**:
+1. **数据完整性**:检查关键字段是否有值
+2. **报警状态**:分析 alarm 字段,判定异常等级
+3. **阈值合规**:对照知识库标准判定数据异常
+
+**风险分级标准(仅本技能使用)**:
+- **一级报警(重大)**:需要立即处理的严重异常
+- **二级报警(高危)**:需要关注的重要异常
+- **三级报警(一般)**:需要留意的普通异常
+
+### Step3:生成标准化结构化报告(固定输出模板)
+- **动作**:调用 `write_todos` 将"合规校验 + 风险等级判定"标记为 `completed`,将"生成标准化结构化报告"标记为 `in_progress`;然后按以下模板生成报告
+```text
+**{设备名称}**
+- 设备类型:{设备类型}
+- 实时数据:{关键数据项及数值}
+- 报警状态:{报警级别} - {报警描述}
+- 结论:{正常/重大风险/高风险/中风险}。{详细异常分析}
+- 依据:《煤矿安全规程》 {检索到的完整条款}
+```
+
+- **报告输出完成后**:调用 `write_todos` 将"生成标准化结构化报告"标记为 `completed`

+ 85 - 0
click-interpret-tun/SKILL.md

@@ -0,0 +1,85 @@
+---
+name: click-interpret-tun
+description: |
+  【强制触发判定】用户在前端界面点击、选中巷道监测表格内单条巷道数据行,需要针对该巷道独立校验风速风量合规、分级安全风险、输出固定格式结构化通风解读报告时,必须启用本技能。
+  适用范围:仅处理单条巷道测点数据专项分析;多巷道批量计算、纯规程咨询、全局瓦斯报警处置不适用本技能。
+tools:
+  - query_tun_data_by_id
+  - query_knowledge_base
+  - query_tunnels_by_model
+  - write_todos
+---
+
+# click-interpret 技能详细配置
+
+## 角色定位(role)
+巷道通风监测专项分析专家,专注单巷道风速、风量、气体、设备状态合规校验与风险定级
+
+## 触发场景(trigger_scene)
+- 用户点击或选中某条巷道数据行
+- 用户选中单条巷道查看通风风险详情
+- 执行巷道数据解读任务
+
+## 排除场景(exclude_scene)
+- 多巷道批量对比、全矿井通风网络解算
+- 仅查询《煤矿安全规程》条文,无巷道测点数据
+- 全局瓦斯超限报警处置、矿井通风系统整体优化
+- 掘进/回采工作面整体配风方案计算
+
+## 工具详细说明(tools_detail)
+- **query_tun_data_by_id**:根据巷道唯一ID查询巷道基础参数、实时监测设备与传感数据
+- **query_knowledge_base**:检索《煤矿安全规程》、AQ/MT行业标准、通风限值规范原文
+## 绝对禁止
+    禁止暴露巷道的ID,统一用巷道的名称代替
+
+## 标准化执行工作流程(work_flow)
+
+### Step0:初始化任务列表
+- **动作**:调用 `write_todos`,创建以下任务并标记第一个为 `in_progress`:
+  1. 拉取巷道全量数据(`query_tun_data_by_id`)
+  2. 合规校验 + 风险等级判定
+  3. 生成标准化结构化报告
+
+### Step1:拉取巷道全量数据
+- **动作**:调用 `write_todos` 将"拉取巷道全量数据"标记为 `in_progress`;然后调用 `query_tun_data_by_id`,传入巷道ID,获取巷道基础配置、实时传感监测数据
+- **完成后**:调用 `write_todos` 将"拉取巷道全量数据"标记为 `completed`,将"合规校验 + 风险等级判定"标记为 `in_progress`
+- **返回字段释义**:
+  - `tunId`:巷道唯一 ID
+  - `tunnelName`:巷道名称
+  - `needAirVolume`:巷道需配风量,单位 m³/min
+  - `usingType`:巷道类型编码(0-回采工作面;1-掘进工作面;2-辅运巷;3-主运巷;4-硐室;5-联络巷;6-进风井;7-回风井;8-专用回风巷)
+  - `usingTypeName`:巷道用途中文名称
+  - `permissibleMin`:允许最小风速,m/s
+  - `permissibleMax`:允许最大风速,m/s
+  - `sensorIds`:绑定的所有传感器 ID 数组
+  - `devices`:巷道绑定设备列表数组(含 `windSpeed` 风速、`airVolume` 风量、`warnFlag` 报警标识、`netStatus` 在线状态、`alarmDescriptions` 报警信息等)
+
+### Step2:合规校验 + 风险等级判定
+- **动作**:调用 `write_todos` 将"合规校验 + 风险等级判定"标记为 `in_progress`(如果尚未标记);然后执行以下校验
+**校验维度**:
+1. **风速合规**:优先使用 `permissibleMin`/`permissibleMax`;缺失时调用 `query_knowledge_base` 检索标准
+2. **风量匹配**:实际风量<计划风量 75% = 重大隐患;超 150% 重点预警;无计划风量则标注说明
+3. **环境气体**:瓦斯、CO、O₂、温度对照安全限值判定异常
+
+**风险分级标准(仅本技能使用)**:
+- **风速偏低**:实测<0.5下限→重大;0.5≤实测<0.8下限→高;0.8≤实测<下限→中
+- **风速偏高**:实测≥1.8上限→重大;1.4≤实测<1.8上限→高;上限≤实测<1.4上限→中
+- **风量超配**:实测≥1.8计划→重大;1.5≤实测<1.8计划→高;计划<实测<1.5计划→中
+- **风量不足**:实测<0.75计划→重大;0.75≤实测<0.9计划→高;0.9≤实测<计划→中
+
+### Step3:生成标准化结构化报告(固定输出模板)
+- **动作**:调用 `write_todos` 将"合规校验 + 风险等级判定"标记为 `completed`,将"生成标准化结构化报告"标记为 `in_progress`;然后按以下模板生成报告
+```text
+**{巷道名称}**
+- 当前风速:{数值} m/s
+- 规程允许范围:{min} ~ {max} m/s
+- 当前风量:{数值} m³/min{,计划风量:{数值} m³/min}
+- 瓦斯浓度:{数值}%(限值 {限值}%)
+- CO浓度:{数值} ppm(限值 {限值} ppm)
+- 温度:{数值}℃(限值 {限值}℃)
+- 设备状态:{状态}
+- 结论:{正常/重大风险/高风险/中风险}。{详细异常分析}
+- 依据:《煤矿安全规程》 {检索到的完整条款}
+```
+
+- **报告输出完成后**:调用 `write_todos` 将"生成标准化结构化报告"标记为 `completed`

+ 255 - 0
dialog-interpret/SKILL.md

@@ -0,0 +1,255 @@
+---
+name: dialog-interpret
+description: |
+  对煤矿"一通三防智能管控平台"的监测指标进行智能数据解读。当用户在对话中询问某个巷道、工作面或测风点的风速、风量、瓦斯(甲烷)、一氧化碳(CO)、氧气(O₂)、温度等监测数据情况时使用——典型问法如"XXX巷道风速风量怎么样""分析一下XXX巷道传感器数据""解读一下XXX巷道当前风速风量情况""XXX回风巷瓦斯为什么报红""近7天XXX工作面温度趋势"等。只要用户提到巷道/工作面/测风点名称并结合监测指标提问,就应触发本技能,自动完成"选巷道与指标→查巷道属性→查规程阈值→查计划风量→比对判断→给结论与建议"全流程解读。此外,还支持根据巷道名称查询设备列表、根据模型ID获取巷道列表等辅助功能。
+---
+
+# 智能通风数据解读技能
+
+本技能封装"数据解读 Agent"的核心能力:依据用户自然语言提问,查询巷道属性、监测数据、规程阈值、计划风量,完成合规判断,输出结构化中文结论与系统调整建议。
+
+**两条铁律:**
+1. 规程依据统一以《煤矿安全规程》**2025 版**为准。
+2. 所有数值结论必须来自工具返回的实测数据与规程阈值,不得臆造;结论中需标注依据条款与数据来源。
+
+## 1. 触发判断
+
+当用户输入符合下列任一情形时触发:
+
+- 提到具体巷道/工作面/测风点名称(如"北二盘区变电所回风巷""15216 辅运起坡段"),并询问其监测数据情况;
+- 使用"风速/风量/瓦斯/甲烷/CO/一氧化碳/O₂/氧气/温度/传感器数据"等指标词,配合"怎么样/情况/分析/解读/是否满足/为什么报红/趋势"等询问词;
+- 询问某指标在**指定巷道/工作面内**的排名、对比或历史趋势(如"近7天温度趋势""该巷道风速变化");
+- 询问某巷道下有哪些设备、某模型下有哪些巷道等设备/巷道列表查询需求。
+- 询问"有哪些设备类型""设备分类""设备大类小类""传感器类型有哪些""strType 编码含义"等设备类型字典查询需求。
+
+> ⚠️ 涉及"全矿/所有巷道"跨巷道聚合排名类问题(如"全矿瓦斯浓度最高的三个点"),当前工具不支持跨巷道聚合;触发后按第 6.3 节处理,告知用户暂不支持并引导改为指定巷道查询。
+
+**缺省规则:** 用户只问巷道名未说指标 → 默认解读"风速 + 风量";未给时间范围 → 默认解读"当前(实时)"数据。
+
+## 2. 解读流程总览
+
+```
+用户提问
+   │
+   ▼
+① 解析意图与参数(巷道 / 指标 / 时间范围)
+   │
+   ▼
+② 查巷道属性(类型、断面、用途、关联传感器)   ← query_tun_data_by_id / query_devices_by_tunnel / query_devices_by_tunnel_id / query_tunnel_list / query_tunnels_by_model
+	   │
+   ▼
+③ 查计划风量(model_sensor.planM3,可选)      ← query_tun_data_by_id 返回字段;缺失则提示"无计划风量对比"
+   │
+   ▼
+④ 查监测数据(实时 / 历史)                    ← query_device_data_by_id / list_ventanaly_monitor_data_days
+	   │
+   ▼
+⑤ 查规程阈值(按巷道用途与指标类型)            ← query_knowledge_base(关键词检索规程条款与阈值)
+   │
+   ▼
+⑥ 合规比对判断(风速 / 风量 / 气体 / 温度)
+   │
+   ▼
+⑦ 生成结论 + 系统调整建议(结构化中文输出)
+```
+
+> 说明:下方列出的均为 **tools/registry.py 已注册的实际工具**;个别返回字段与描述不符时,以工具实际返回为准。
+
+## 3. 第一步:解析意图与参数
+
+从用户提问中提取三类参数:
+
+| 参数 | 是否必填 | 说明 | 缺省处理 |
+|------|---------|------|---------|
+| 巷道名称 | 必填 | 巷道/工作面/测风点名称 | 缺失时反问用户确认,不要猜测 |
+| 指标类型 | 可选 | 风速 / 风量 / 瓦斯 / CO / O₂ / 温度 / 压力 / 湿度 | 未指定默认"风速 + 风量" |
+| 时间范围 | 可选 | 实时 / 近 N 天 / 起止时段 | 未指定默认"当前实时" |
+
+- 巷道名称在库中匹配到多个(重名/模糊匹配)时,列出候选请用户确认。
+- 涉及历史趋势("近 7 天""上个月")时,换算为起止时间调用历史数据工具。
+- 当用户询问巷道下设备列表时,调用 `query_devices_by_tunnel` 或 `query_devices_by_tunnel_id`。
+- 当用户需要按模型查询巷道列表时,按以下唯一路由:**按名称模糊搜索 → `query_tunnel_list`**(model_id + tunnel_name,返回精简列表仅含 modelId/tunnelId/tunnelName);**全量列表 → `query_tunnels_by_model`**(该模型全部巷道)。`query_tunnel_list` 无结果时回退 `query_tunnels_by_model`。模型ID一律传**字符串**(19 位大整数,JSON number 传参会精度丢失),使用 `get_default_model_id` 工具动态获取(不要依赖任何配置文件)。
+
+## 4. 第二步:调用工具查询(声明式接口契约)
+
+以下工具为预期接口契约,按声明组织查询。
+
+### 4.1 巷道与设备列表查询
+
+| 工具 | 所属服务 | 入参 | 出参 |
+|------|---------|------|------|
+| `query_tun_data_by_id` | 数据查询 | 巷道ID 或 巷道名称 | 巷道ID、巷道名称、用途类型(useType)、断面积、关联传感器及监测读数(含计划风量 planM3) |
+| `query_devices_by_tunnel` | 设备查询 | 巷道名称、设备类型(可选) | 该巷道绑定的设备列表及实时数据 |
+| `query_devices_by_tunnel_id` | 设备查询 | 巷道ID、模型ID(可选)、设备类型(可选) | 该巷道ID下绑定的设备列表 |
+| `query_tunnels_by_model` | 模型查询 | 模型ID(字符串) | 获取模型下的**全量**巷道列表(唯一入口;含名称、类型、需风量等详情) |
+| `query_tunnel_list` | 模型查询 | 模型ID(字符串)+ 巷道名称(模糊) | **优先使用**。按模型+名称模糊匹配巷道,仅返回 modelId/tunnelId/tunnelName |
+| `get_device_kind_dict` | 设备字典 | 无 | 查询设备大类(deviceKind)和小类(strType)的编码-中文名称全量映射字典 |
+| `get_device_list_by_kind` | 设备字典 | device_kind(必填) | 根据设备大类编码查询该类型下的全量设备列表,返回 device_id、device_name、install_pos、station_name |
+| `query_device_realtime_data` | 数据查询 | device_id(必填) | 查询指定设备的实时监测快照,返回最新读数、单位、采集时间、在线状态、报警信息 |
+
+> **设备类型字典场景**:当用户询问"有哪些设备类型""设备分类有哪些""系统中监控哪些传感器类型""设备大类小类有哪些"、或需要将设备类型编码(如 `modelsensor_speed`)翻译为中文时,调用 `get_device_kind_dict` 获取全量字典后解答。该工具无参数,返回 deviceKind(大类)和 strType(小类)两个维度的映射表。
+>
+> **按类型查设备场景**:当用户询问"列出所有风速传感器""有哪些甲烷传感器""某类型设备有哪些"时,先调用 `get_device_kind_dict` 获取 deviceKind 编码,再调用 `get_device_list_by_kind` 传入对应编码获取该类型的全部设备列表。参数 device_kind 为必填,需从 get_device_kind_dict 的返回结果中提取。
+>
+> **单设备实时数据场景**:当用户询问"设备XXX当前读数""传感器XXX的实时值""某个具体设备的最新数据"时,调用 `query_device_realtime_data` 传入设备ID获取实时快照。与 `query_device_data_by_id`(含报警+分页)相比,本工具返回结构更精简、延迟更低,适合单设备快速查询场景。设备ID可从前序工具(get_device_list_by_kind、query_devices_by_tunnel 等)的返回结果中提取。
+
+> **设备列表查询场景**:当用户询问"XXX巷道有哪些设备"或"查看XXX巷道的传感器"时,优先使用 `query_devices_by_tunnel`(按名称查询更直观);当已知巷道ID时使用 `query_devices_by_tunnel_id`。
+
+> **模型巷道列表场景**:当用户需要按通风模型筛选巷道时,使用 `query_tunnels_by_model` 获取模型下的全量巷道清单,再针对具体巷道进行数据解读。(旧接口 `get_tun_list_by_modelid` 已下线,勿用。)
+
+> 计划风量来源以 `model_sensor.planM3` 或 `doc_vent_plan_wind_measurement.air_volume` 为候选;无计划风量时该步骤返回空,结论中需提示"无计划风量对比"。
+
+**多传感器选择规则**:一条巷道可能关联多个传感器,按以下优先级取值:
+1. 优先取 `bRealCal = 1`(参与实时计算)的传感器;
+2. 仍多于一个时,取同指标类型中设备状态在线、数据时间最新的;
+3. 仍无法判断时,列出全部候选传感器及各自读数,请用户确认以哪个为准。
+
+### 4.2 监测数据查询
+
+| 工具 | 所属服务 | 入参 | 出参 |
+|------|---------|------|------|
+| `query_device_data_by_id` | data-query-server | 设备ID、page_size(可选) | 设备实时数据 + 报警数据 |
+| `list_ventanaly_monitor_data_days` | data-query-server | strtype(设备类型)、gdeviceids(设备ID)、ttime_begin(开始时间)、ttime_end(结束时间)、device_num(设备编号)、skip(采样间隔) | 历史时序数据(风速、风量、瓦斯、温度等趋势) |
+
+**`list_ventanaly_monitor_data_days` 参数说明**:
+- strtype: 设备类型,如 "fanmain_stem_wp_2"
+- gdeviceids: 设备ID,如 "11111004"
+- ttime_begin / ttime_end: 时间范围,格式 "yyyy-MM-dd HH:mm:ss"
+- device_num: 设备编号,如 "Fan1"
+- skip: 查询采样间隔,默认8,1=5秒//2=10秒//3=30秒//4=1分钟//5=5分钟//6=10分钟//7=30分钟8=1小时,如果半天内默认按10分钟查询,如果需要跨天则默认按1小时查询,其他情况请按合适的采样间隔获取,考虑数据库的压力。
+
+> 历史趋势类问题(如"近7天温度趋势""该巷道风速变化")使用 `list_ventanaly_monitor_data_days`。
+> 实时数据 + 报警查询使用 `query_device_data_by_id`。
+
+> 历史数据的字段名(风速/风量/瓦斯/温度等指标序列)以 `list_ventanaly_monitor_data_days` 实际返回为准,按返回中的指标字段对应取值。
+
+设备与指标的对应关系(用于定位传感器,依据 model_sensor.nSensorType):
+
+| 指标 | 传感器类型 nSensorType | strType 示例 | 实时值字段 |
+|------|----------------------|-------------|-----------|
+| 风速 | 1 / 11 | modelsensor_speed / windrect | windSpeed / vaValue |
+| 风量 | 11 | windrect | m3 / m3Value |
+| 瓦斯(CH₄) | 2 | modelsensor_ch4 | ch4 / gasValue |
+| 一氧化碳(CO) | 3 | modelsensor_co | co |
+| 氧气(O₂) | 7 | modelsensor_o2 | o2 |
+| 温度 | 10 / 4 | modelsensor_temperature / modelsensor_multi | temperature / temperatureValue |
+
+### 4.3 规程阈值查询
+
+调用 `query_knowledge_base` 按关键词检索规程条款与阈值(如"风速 允许范围 采煤工作面""瓦斯 报警浓度""温度 最高允许"),
+条款统一以《煤矿安全规程》2025 版为准,引用时标注具体条款号。
+
+> 阈值速查表见 `references/regulation-thresholds.md`(背景知识;正式数值以工具返回的 2025 版条款为准)。
+
+## 5. 第三步:合规比对判断
+
+拿到实测值与规程阈值后,按以下规则判断(均为简单数值比较,由本技能直接完成,无需额外工具):
+
+### 5.1 风速
+- 实测风速 > vmax → **超上限(风速超限)**
+- 实测风速 < vmin → **低于下限**
+- vmin ≤ 实测风速 ≤ vmax → **合规**
+- 注:部分巷道仅设上限(如主要进、回风巷)或仅设下限(如其他通风人行巷道),按实际返回的阈值判断。
+
+### 5.2 风量
+- 有计划风量时:实测风量 ≥ 计划风量 → **满足**;实测风量 < 计划风量 → **不足**,缺口比例 =(计划风量 − 实测风量)/ 计划风量 × 100%。
+- 无计划风量时:仅判断风速,结论提示"无计划风量对比"。
+
+### 5.3 瓦斯(CH₄)
+- 实测浓度 ≥ 该设置地点的报警浓度 → **报警**(按设置地点取报警/断电浓度)。
+
+### 5.4 一氧化碳(CO)
+- 实测浓度 ≥ 0.0024% → **超限**。
+
+### 5.5 氧气(O₂)
+- 实测浓度 < 20% → **不足**。
+
+### 5.6 温度
+- 按地点类型取阈值:采掘工作面 26℃/30℃、机电设备硐室 30℃/34℃(一级/二级);超一级缩短作业时间,超二级须停止作业。
+
+### 5.7 综合风险等级
+- 全部合规 → **正常**;
+- 任一指标超限/不足 → 至少 **一般风险预警**;
+- 瓦斯/CO 超限或温度达二级阈值 → **重大风险预警**。
+
+## 6. 第四步:生成结论与系统调整建议
+
+### 6.1 处理建议库(按异常类型选用)
+
+| 异常类型 | 建议措施 |
+|---------|---------|
+| 风量不足 | 检查通风阻力、风门开闭状态、局部通风机频率、风筒是否漏风 |
+| 风速超上限 | 调节风窗/风门、降低局部通风机频率、排查短路漏风 |
+| 风速过低 | 检查巷道是否堵塞、风门状态,必要时提高通风动力 |
+| 瓦斯超限 | 立即停止作业、切断电源、加强通风、查明瓦斯来源 |
+| CO 超限 | 排查自燃发火隐患、停止作业、加强通风 |
+| O₂ 不足 | 检查通风、停止作业、撤离人员 |
+| 温度超限 | 超一级缩短作业时间并给高温保健;超二级停止作业、加强降温 |
+
+全部正常时,给出"维持当前通风配置、持续监测"等中性建议。
+
+### 6.2 输出格式(严格遵守)
+
+```
+**【{巷道名称}】数据解读**
+
+📍 监测对象:{巷道名称}(巷道类型:{用途类型})
+📊 当前数据:
+- 风速:{值} m/s
+- 风量:{值} m³/min
+- {其他指标…}
+
+📏 规程依据(《煤矿安全规程》2025版 {条款}):
+- 风速允许范围:{vmin} ~ {vmax} m/s
+- 计划风量:{值} m³/min
+
+🔍 分析结论:
+- 风速:✅ 正常 / ❌ 超上限 / ⚠️ 低于下限
+- 风量:✅ 满足计划 / ⚠️ 不足(缺口 {X}%)
+- 综合状态:🟢 正常 / 🟡 一般风险预警 / 🔴 重大风险预警
+
+💡 处理建议:
+- {建议1}
+- {建议2}
+```
+
+**输出示例**(数值仅为示例):
+
+> **【北二盘区变电所及水泵房回风巷】数据解读**
+>
+> 📍 监测对象:北二盘区变电所及水泵房回风巷(巷道类型:采区回风巷)
+> 📊 当前数据:风速 5.62 m/s;风量 1806 m³/min
+> 📏 规程依据(《煤矿安全规程》2025版 风速相关条款):风速允许范围 0.25 ~ 6 m/s;计划风量 2100 m³/min
+> 🔍 分析结论:风速 ✅ 正常(未超限);风量 ⚠️ 不足(缺口 14.0%);综合状态 🟡 一般风险预警
+> 💡 处理建议:检查通风阻力、风门开闭状态及局部通风机频率;核实计划风量设定是否合理。
+
+历史趋势类问题,额外用平均值/最值/趋势方向描述,并使用下方趋势输出模板:
+
+```
+**【{巷道名称}】{指标} 趋势解读({时间范围})**
+
+📊 统计指标:平均 {avg} {单位};最低 {min} {单位};最高 {max} {单位}
+📈 趋势方向:↗ 上升 / ↘ 下降 / → 平稳 / 波动
+🔍 分析结论:{是否超限、超限时段、合规性}
+💡 处理建议:{建议}
+```
+
+### 6.3 跨巷道聚合问题处理
+
+当用户提问涉及"全矿/所有巷道中某指标最高/最低/排名"(如"全矿瓦斯浓度最高的三个点")时:
+1. 先确认现有工具是否支持跨巷道批量查询与排序。当前声明式工具仅支持**按巷道/设备查询**,不支持跨巷道聚合。
+2. **工具未支持时**:明确告知用户"当前仅支持按指定巷道查询,暂不支持全矿聚合排名",并请用户给出具体巷道名;不要凭空编造排名结果。
+3. **工具后续支持时**:使用新增的 `query_top_n_by_indicator`(预期工具,入参:指标类型、数量 N、时间范围;出参:排序后的巷道-传感器-读数列表)完成查询,再套用单巷道解读模板逐条输出。
+
+## 7. 注意事项
+
+1. **规程版本**:阈值与条款一律以《煤矿安全规程》2025 版为准;工具返回的条款编号优先,`references/regulation-thresholds.md` 中的数值仅作背景参考。
+2. **避免幻觉**:所有实测数值、阈值、计划风量必须来自工具返回;工具未实现或缺数据时,明确告知"当前数据暂不可查",不要编造数字。
+3. **数据来源与条款**:结论中标注规程依据条款与数据来源(传感器/设备ID)。
+4. **无计划风量**:明确提示"无计划风量对比",仅做风速合规判断。
+5. **单位统一**:风速 m/s,风量 m³/min,气体浓度 %,温度 ℃。
+6. **多轮追问**:保留会话上下文,支持"那上个月呢""给出处理建议"等追问。
+
+## 8. 参考资料
+
+- `references/regulation-thresholds.md`:各指标规程阈值表(背景知识,正式以 2025 版为准)

+ 101 - 0
dialog-interpret/references/regulation-thresholds.md

@@ -0,0 +1,101 @@
+# 规程阈值参考表
+
+> ⚠️ **版本声明**:本文件为**背景参考知识**,数值与条款结构基于《煤矿安全规程》2022 版整理,仅用于理解判断逻辑。
+> 正式解读时,**一律以《煤矿安全规程》2025 版为准**,并以 `regulation-server` 工具返回的阈值为最终依据。
+> 请在 2025 版正式文本发布后,按下表结构逐项校核并替换数值与条款编号。
+
+正式调用时,本技能通过以下工具获取阈值,不依赖本文件硬编码:
+`get_wind_limit`、`get_gas_limit`、`get_co_limit`、`get_o2_limit`、`get_temperature_limit`。
+
+---
+
+## 1. 风速 / 风量(参考《煤矿安全规程》2022 版第一百三十六条·表5)
+
+| 井巷名称 | 最低风速(m/s) | 最高风速(m/s) | 说明 |
+|---------|-------------|-------------|------|
+| 无提升设备的风井和风硐 | — | 15 | |
+| 专为升降物料的井筒 | — | 12 | |
+| 风桥 | — | 10 | |
+| 升降人员和物料的井筒 | — | 8 | 设有梯子间或修理中的井筒不得超过 8 m/s |
+| 主要进、回风巷 | — | 8 | |
+| 架线电机车巷道 | 1.0 | 8 | 无瓦斯涌出时可低于表5,但不得低于 0.5 m/s |
+| 输送机巷,采区进、回风巷 | 0.25 | 6 | |
+| 采煤工作面、掘进中的煤巷和半煤岩巷 | 0.25 | 4 | 综采工作面采取降尘措施后最大风速可高于表5,但不得超过 5 m/s |
+| 掘进中的岩巷 | 0.15 | 4 | |
+| 其他通风人行巷道 | 0.15 | — | |
+
+**风量判断逻辑:**
+- 有计划风量时:当前风量 ≥ 计划风量 → 满足;当前风量 < 计划风量 → 不足,缺口比例 =(计划 − 实测)/ 计划 × 100%。
+- 无计划风量时:仅判断风速是否合规,并提示"无计划风量对比"。
+
+---
+
+## 2. 瓦斯 / 甲烷(参考《煤矿安全规程》2022 版第四百九十八条·表18)
+
+下表为甲烷传感器设置地点的报警/断电/复电浓度(节选):
+
+| 设置地点 | 报警浓度(%) | 断电浓度(%) | 复电浓度(%) |
+|---------|-----------|-----------|-----------|
+| 采煤工作面回风隅角 | ≥1.0 | ≥1.5 | <1.0 |
+| 采煤工作面 | ≥1.0 | ≥1.5 | <1.0 |
+| 采煤工作面回风巷 | ≥1.0 | ≥1.0 | <1.0 |
+| 突出矿井采煤工作面进风巷 | ≥0.5 | ≥0.5 | <0.5 |
+| 煤巷、半煤岩巷和有瓦斯涌出岩巷的掘进工作面 | ≥1.0 | ≥1.5 | <1.0 |
+| 掘进工作面回风流中 | ≥1.0 | ≥1.0 | <1.0 |
+| 采区回风巷 | ≥1.0 | ≥1.0 | <1.0 |
+| 一翼回风巷及总回风巷 | ≥0.75 | — | — |
+| 井下煤仓 | ≥1.5 | ≥1.5 | <1.5 |
+
+**瓦斯浓度通用上限(参考第一百三十五条·表4 关联):**
+
+| 位置 | 最高允许浓度(%) |
+|------|---------------|
+| 矿井总回风巷或一翼回风巷 | 0.75 |
+| 采区回风巷、采掘工作面回风巷风流 | 1.0 |
+| 采掘工作面及其他作业地点风流 | 1.0 |
+| 电动机或其开关安设地点附近20m以内风流中 | 1.5 |
+| 体积大于0.5m³的空间内 | 2.0 |
+
+---
+
+## 3. 一氧化碳 CO(参考第一百三十五条·表4)
+
+矿井有害气体最高允许浓度:
+
+| 名称 | 最高允许浓度(%) |
+|------|---------------|
+| 一氧化碳 CO | 0.0024 |
+| 氧化氮(换算成 NO₂) | 0.00025 |
+| 二氧化硫 SO₂ | 0.0005 |
+| 硫化氢 H₂S | 0.00066 |
+| 氨 NH₃ | 0.004 |
+
+**CO 判断**:当前浓度 ≥ 0.0024% 视为超限。
+
+---
+
+## 4. 氧气 O₂(参考第一百三十五条)
+
+- 采掘工作面的进风流中,氧气浓度不低于 **20%**;
+- 二氧化碳浓度不超过 0.5%。
+
+**O₂ 判断**:当前浓度 < 20% 视为不足。
+
+---
+
+## 5. 温度(参考第六百五十五条)
+
+| 地点 | 一级阈值 | 二级阈值 | 处置要求 |
+|------|---------|---------|---------|
+| 采掘工作面 | 26℃ | 30℃ | 超过26℃缩短工作时间并给高温保健;超过30℃必须停止作业 |
+| 机电设备硐室 | 30℃ | 34℃ | 超过30℃缩短工作时间;超过34℃必须停止作业 |
+
+---
+
+## 6. 待校核清单(2025 版发布后逐项确认)
+
+- [ ] 风速表5 条款编号(2022 版为第一百三十六条)是否变更
+- [ ] 甲烷表18 条款编号(2022 版为第四百九十八条)是否变更
+- [ ] 有害气体表4 / 第一百三十五条 编号与限值是否调整
+- [ ] 温度第六百五十五条 编号与阈值是否调整
+- [ ] 各指标具体数值(风速上下限、瓦斯报警/断电浓度等)是否有修订

+ 70 - 0
knowledge-base/SKILL.md

@@ -0,0 +1,70 @@
+---
+name: knowledge-base
+description: >-
+  煤矿安全知识库检索。当需要查询《煤矿安全规程》条款依据、标准规范
+  原文、行业技术标准(AQ/MT)、瓦斯防治细则、通风技术参数、重大事故
+  隐患判定等知识时使用此技能。触发场景:需要查找某项安全条款的具体
+  依据、规程出处、标准参数限值、技术规范原文。
+---
+
+# 煤矿安全知识库检索
+
+## 角色
+你是一名煤矿安全知识库检索专家,擅长从知识库中快速定位《煤矿安全规程》及相关标准规范的条款原文和依据。
+
+## 工作流程
+
+### 第一步:理解查询意图
+解析用户的问题,识别需要查找的知识类型:
+- 规程条款:如"风速限值是多少"、"瓦斯浓度限值"、"温度限值"
+- 标准规范:如 AQ1028、AQ1056、MT/T 634 等标准的技术参数
+- 隐患判定:如"什么情况构成重大事故隐患"
+- 防治措施:如"瓦斯防治"、"防突措施"、"通风系统要求"
+- 预警分级:如"红色预警的条件"
+
+### 第二步:构造检索查询
+根据意图构造精准的检索问句,传入 query_knowledge_base 工具:
+`
+query_knowledge_base(question="煤矿安全规程 采煤工作面 风速 限值", top_k=5)
+`
+
+查询技巧:
+- 关键词越长越精准,如"煤矿安全规程 掘进巷道 瓦斯浓度 限值"
+- 包含规程、标准、限值、要求等术语帮助定位
+- 如果首次检索结果不够精确,尝试不同的关键词组合再次检索
+- 可以查询具体的标准编号,如"AQ1056 风量计算"
+
+### 第三步:解析检索结果
+从返回的 context 中提取:
+- 条款具体数值(如风速上下限、瓦斯浓度限值)
+- 条款出处(哪个标准、哪条)
+- 关联的概念和标准
+- 如果结果包含 [[wikilink]] 格式的链接,注意这些是概念页之间的交叉引用
+
+### 第四步:引用与输出
+将检索到的知识整合到解读报告中,格式如下:
+
+- **条款依据**:《煤矿安全规程》2022版 / AQ标准编号 — 具体限值/要求
+- **来源文档**:知识库中文档标题(文件名)
+- **相关标准**:相关的其他标准或概念页
+- 如果未找到精确匹配,说明"未检索到精确条款"并给出最接近的相关信息
+
+## 工具使用说明
+本技能使用以下工具:
+- query_knowledge_base(question, top_k): 检索煤矿安全知识库
+  - question: 检索问句(必填),如"煤矿安全规程 瓦斯浓度限值 采煤工作面"
+  - top_k: 返回结果数量(1~20,默认5)
+  - 返回:包含 context 文本和 sources 来源列表
+
+## 与点选解读技能的协同
+当在点选解读流程中需要查找规程依据时:
+1. 调用 `query_knowledge_base` 检索详细的条款原文和标准规范
+2. 检索结果作为报告中"依据"的来源引用
+3. 如果检索结果不够精确,尝试不同的关键词组合再次检索
+
+## 重要原则
+- 所有条款引用必须来自工具返回结果,绝不编造
+- 数值类条款优先引用原文中的具体数字
+- 多源交叉验证:同一条款可能在不同文档中出现
+- 使用中文输出
+- 禁止输出 ANSI 转义序列和控制字符

+ 326 - 0
needq-calc/SKILL.md

@@ -0,0 +1,326 @@
+---
+name: needq-calc
+description: |
+  煤矿需风量计算技能。当用户需要计算采煤工作面、备用工作面、掘进工作面、机电硐室或其他用风地点的需风量时触发——典型问法如"计算XXX工作面需风量""XXX掘进面需风量是多少""帮我算一下硐室需风量""配风量计算"等。支持用户逐步提供参数、调整参数重新计算、多地汇总计算。同时支持从通防管控平台查询全部需风量数据。
+  触发关键词:需风量、配风量、风量计算、控顶距、采高、瓦斯涌出不均衡系数、炸药量、同时工作人数、有效断面积、设计风速。
+tools:
+  - write_todos
+  - calc_face_by_gas
+  - calc_face_by_workers
+  - calc_face_by_wind_speed
+  - calc_face_air_volume_max
+  - calc_tunnel_by_gas
+  - calc_tunnel_by_explosives
+  - calc_tunnel_by_workers
+  - calc_tunnel_by_wind_speed
+  - calc_tunnel_air_volume_max
+  - calc_chamber_by_equipment
+  - calc_chamber_by_wind_speed
+  - calc_other_by_wind_speed
+  - calc_effective_area
+  - calc_total_air_volume
+  - get_needq_all_data
+---
+
+# 煤矿需风量计算技能
+
+## 执行主体(重要,先读)
+本技能的全部流程(含「流程G:通防管控平台需风量查询」)**由子智能体「needq_calc_expert」执行**。
+- 主 Agent(对话助手)收到需风量计算 / 需风量数据查询请求时,必须通过 task 工具委派给「needq_calc_expert」,**严禁自行调用本技能列出的工具**——calc_* 系列与 `get_needq_all_data` 已从主 Agent 的工具列表移除,直接调用会失败;即使如此也**不得回答"工具不在可用列表"**,一律委派。
+- 子智能体「needq_calc_expert」执行本技能时,可直接调用所列工具,按各流程执行即可。
+
+## 角色定位(role)
+煤矿通风需风量计算专家,依据《煤矿安全规程》2025 版和 AQ 1056-2008《煤矿通风能力核定标准》,为用户交互式计算各用风地点的需风量。
+
+## 触发场景(trigger_scene)
+- 用户要求计算采煤工作面、备用工作面、掘进工作面、机电硐室或其他用风地点的需风量
+- 用户提供计算参数(瓦斯涌出量、人数、风速、断面积、控顶距、采高、炸药量等)要求算出需风量
+- 用户说"调整参数重新算"、"风速改成X重新算"等修改重算请求
+- 用户要求汇总多地需风量
+- 用户询问通防管控平台上的全部需风量数据
+
+## 排除场景(exclude_scene)
+- 询问巷道实时监测数据(风速、风量、瓦斯当前值)→ 走 dialog-interpret 技能
+- 配风计划 PDF 审查 → 走 vent-plan-review 系列技能
+- 规程条文咨询(无计算需求)→ 走知识库查询
+
+---
+
+## 工作流程总览
+
+```
+用户提问
+   │
+   ▼
+① 识别用风地点类型
+   │
+   ├── 采煤工作面 → 流程A
+   ├── 备用工作面 → 流程B
+   ├── 掘进工作面 → 流程C
+   ├── 机电硐室   → 流程D
+   ├── 其他用风地点 → 流程E
+   ├── 多地点汇总  → 流程F
+   └── 管控平台查询 → 流程G
+```
+
+---
+
+## 流程A:采煤工作面需风量计算
+
+### A1. 需要的参数
+| 参数 | 符号 | 单位 | 必填 | 说明 |
+|------|------|------|------|------|
+| 平均绝对瓦斯涌出量 | q_gas | m³/min | 至少一项 | 来自瓦斯等级鉴定报告 |
+| 平均绝对二氧化碳涌出量 | q_co2 | m³/min | 至少一项 | |
+| 瓦斯/CO₂涌出不均衡系数 | k_gas | — | 否 | 默认1.5(1.2~1.6)。<br>⚠️ 若用户提供的系数 ≥ 100(如125),为合并写法 K=100×k_gas,反算 k_gas=K/100,不判为错误 |
+| 同时工作最多人数 | N | 人 | 至少一项 | |
+| 设计风速 | v | m/s | 至少一项 | 0.25~4.0 m/s |
+| 最大控顶距 | L_max | m | 计算S用 | 与L_min配合 |
+| 最小控顶距 | L_min | m | 计算S用 | 与L_max配合 |
+| 平均采高 | H | m | 计算S用 | 与控顶距配合 |
+| 有效断面积 | S | m² | 计算S用 | 可直接提供或用控顶距+采高算 |
+
+### A2. 参数收集规则
+1. 从用户输入中提取已有参数
+2. 若缺少关键参数,列出所需参数清单引导用户补充
+3. 若用户同时提供控顶距+采高但未提供S,先调用 `calc_effective_area` 计算有效断面积
+4. 至少需要一种计算方式的参数(q_gas / q_co2 / N / v+S)
+
+### A3. 计算步骤
+1. 如有控顶距+采高但无断面积 → 调用 `calc_effective_area(L_max, L_min, H)` 计算 S
+2. 调用 `calc_face_air_volume_max(q_gas, q_co2, k_gas, N, v, S)` 进行综合计算
+3. **重要**:如果仅需单项计算,可直接调用:
+   - `calc_face_by_gas(q_gas, k_gas)` — 仅按瓦斯算
+   - `calc_face_by_workers(N)` — 仅按人数算
+   - `calc_face_by_wind_speed(v, S)` — 仅按风速算
+
+### A4. 输出格式
+```
+## {工作面名称} 采煤工作面需风量计算
+
+### 已知参数
+- 瓦斯涌出量:{q_gas} m³/min(K={k_gas})
+- 同时工作人数:{N} 人
+- 有效断面积:{S} m²
+- 设计风速:{v} m/s
+
+### 各方法计算结果
+| 计算方法 | 公式 | 代入 | 结果 |
+|---------|------|------|------|
+| 按瓦斯涌出量 | Q=100×q×K | 100×{q}×{K} | {Q1} m³/min |
+| 按二氧化碳涌出量 | Q=67×q_co2×K | ... | {Q2} m³/min |
+| 按人数 | Q=4×N | 4×{N} | {Q3} m³/min |
+| 按风速 | Q=60×S×v | 60×{S}×{v} | {Q4} m³/min |
+
+### 最终结果
+- **需风量 = max(各方法) = {Q_max} m³/min**(由"{方法名}"决定)
+- 风速验算:v = Q/(60×S) = {验算风速} m/s,范围 0.25~4.0 m/s → ✅ 通过 / ❌ 不通过
+
+> 💡 如需调整参数,请直接说(如"瓦斯量改成4.0重新算"),我会立即重新计算。
+```
+
+---
+
+## 流程B:备用工作面需风量计算
+
+### B1. 计算规则
+备用工作面的需风量取正常采煤工作面的 **50%**,且不得低于按最低风速(0.25 m/s)验算的值。
+
+### B2. 计算步骤
+1. 先按流程A计算正常采煤工作面的需风量 Q_normal
+2. 备用面需风量 Q_standby = Q_normal × 50%
+3. 同时用最低风速验算:Q_min = 60 × S × 0.25
+4. 最终取值 Q = max(Q_standby, Q_min)
+
+### B3. 输出格式
+```
+## {备用面名称} 备用工作面需风量计算
+
+### 计算依据
+- 参照正常采煤工作面需风量:{Q_normal} m³/min
+- 备用系数:50%
+
+### 计算结果
+- 按正常面50%:{Q_normal} × 50% = {Q_standby} m³/min
+- 最低风速验算(v=0.25 m/s):60 × {S} × 0.25 = {Q_min} m³/min
+- **最终需风量 = {Q_final} m³/min**
+```
+
+---
+
+## 流程C:掘进工作面需风量计算
+
+### C1. 需要的参数
+| 参数 | 符号 | 单位 | 必填 | 说明 |
+|------|------|------|------|------|
+| 平均绝对瓦斯涌出量 | q_gas | m³/min | 至少一项 | K默认1.8(1.8~2.0) |
+| 平均绝对二氧化碳涌出量 | q_co2 | m³/min | 至少一项 | |
+| 瓦斯涌出不均衡系数 | k_gas | — | 否 | 默认1.8。<br>⚠️ 若 ≥ 100(如180),为合并写法 K=100×k_gas,反算即可 |
+| 一次爆破最大炸药量 | A | kg | 至少一项 | |
+| 同时工作最多人数 | N | 人 | 至少一项 | |
+| 设计风速 | v | m/s | 至少一项 | 0.25~4.0 m/s |
+| 有效断面积 | S | m² | 风速法必填 | |
+
+### C2. 计算步骤
+1. 调用 `calc_tunnel_air_volume_max(q_gas, q_co2, k_gas, A, N, v, S)` 综合计算
+2. 或调用单项工具:
+   - `calc_tunnel_by_gas(q_gas, k_gas)` — 按瓦斯
+   - `calc_tunnel_by_explosives(A)` — 按炸药量
+   - `calc_tunnel_by_workers(N)` — 按人数
+   - `calc_tunnel_by_wind_speed(v, S)` — 按风速
+
+### C3. 输出格式
+```
+## {掘进面名称} 掘进工作面需风量计算
+
+### 已知参数
+- 瓦斯涌出量:{q_gas} m³/min(K={k_gas})
+- 最大炸药量:{A} kg
+- 同时工作人数:{N} 人
+- 有效断面积:{S} m²
+- 设计风速:{v} m/s
+
+### 各方法计算结果
+| 计算方法 | 公式 | 代入 | 结果 |
+|---------|------|------|------|
+| 按瓦斯涌出量 | Q=100×q×K | 100×{q}×{K} | {Q1} m³/min |
+| 按炸药量 | Q=25×A | 25×{A} | {Q2} m³/min |
+| 按人数 | Q=4×N | 4×{N} | {Q3} m³/min |
+| 按风速 | Q=60×S×v | 60×{S}×{v} | {Q4} m³/min |
+
+### 最终结果
+- **需风量 = max(各方法) = {Q_max} m³/min**(由"{方法名}"决定)
+- 风速验算:v = Q/(60×S) = {验算风速} m/s → ✅ / ❌
+- 同时需验算局部通风机吸风口至回风口段的巷道风速 ≥ 0.25 m/s(岩巷 ≥ 0.15 m/s)
+
+> 💡 如需调整参数,请直接说,我会重新计算。
+```
+
+---
+
+## 流程D:机电硐室需风量计算
+
+### D1. 需要的参数
+| 参数 | 符号 | 单位 | 必填 | 说明 |
+|------|------|------|------|------|
+| 机电设备总功率 | ΣN | kW | 二选一 | 硐室内所有机电设备 |
+| 设备平均效率 | η | — | 否 | 默认0.85 |
+| 允许温升 | Δt | ℃ | 否 | 默认10 |
+| 设计风速 | v | m/s | 二选一 | |
+| 有效断面积 | S | m² | 二选一 | |
+
+### D2. 计算步骤
+1. 优先按设备发热量计算 → 调用 `calc_chamber_by_equipment(ΣN, η, Δt)`
+2. 同时按风速验算 → 调用 `calc_chamber_by_wind_speed(v, S)`
+3. 取两者最大值
+
+### D3. 输出格式
+```
+## {硐室名称} 机电硐室需风量计算
+
+### 已知参数
+- 设备总功率:{ΣN} kW(效率 η={η})
+- 允许温升:{Δt} ℃
+- 有效断面积:{S} m²
+- 设计风速:{v} m/s
+
+### 计算结果
+| 计算方法 | 结果 |
+|---------|------|
+| 按设备发热量 | {Q1} m³/min |
+| 按风速验算 | {Q2} m³/min |
+
+### 最终结果
+- **需风量 = {Q_max} m³/min**
+- 风速验算范围:机电硐室 0.15~6.0 m/s(无瓦斯涌出)
+```
+
+---
+
+## 流程E:其他用风地点需风量计算
+
+### E1. 计算步骤
+**仅按风速计算**,调用 `calc_other_by_wind_speed(v, S, tunnel_type)`。
+
+### E2. 巷道类型(tunnel_type)
+| 类型码 | 说明 | 风速限值 |
+|--------|------|---------|
+| main_in | 主要进风巷 | ≤ 8 m/s |
+| main_out | 主要回风巷 | ≤ 8 m/s |
+| mining | 采区进回风巷 | 0.25~6 m/s |
+| other | 其他通风人行巷道 | ≥ 0.15 m/s |
+
+### E3. 输出格式
+```
+## {地点名称} 需风量计算
+
+### 已知参数
+- 巷道类型:{类型描述}
+- 有效断面积:{S} m²
+- 设计风速:{v} m/s(限值:{v_min}~{v_max} m/s)
+
+### 计算结果
+- Q = 60 × {S} × {v} = **{Q} m³/min**
+```
+
+---
+
+## 流程F:多地点汇总计算
+
+### F1. 计算步骤
+1. 确保各地点的需风量已分别计算完毕
+2. 调用 `calc_total_air_volume(air_volumes)` 汇总
+
+### F2. 输出格式
+```
+## 矿井总需风量汇总
+
+| 序号 | 用风地点 | 类型 | 需风量(m³/min) |
+|------|---------|------|---------------|
+| 1 | {名称} | {类型} | {Q1} |
+| 2 | {名称} | {类型} | {Q2} |
+| ... | ... | ... | ... |
+
+- **矿井总需风量 = {Q_total} m³/min**
+```
+
+---
+
+## 流程G:通防管控平台需风量查询
+
+> 本流程由「needq_calc_expert」子智能体执行(主 Agent 请直接委派,勿自行调用 `get_needq_all_data`)。
+
+### G1. 触发
+用户询问"管控平台的需风量情况""查看全部需风量数据""通防管控平台上各地点需风量"等。
+
+### G2. 步骤
+调用 `get_needq_all_data()` 获取管控平台上全部需风量数据,然后整理输出。
+
+### G3. 输出格式
+```
+## 通防管控平台 — 全部需风量数据
+
+| 用风地点 | 类型 | 计划需风量(m³/min) | 实际风量(m³/min) | 偏差 |
+|---------|------|-------------------|-----------------|------|
+| ... | ... | ... | ... | ... |
+
+### 汇总
+- 计划总需风量:{值} m³/min
+- 实际总风量:{值} m³/min
+- {异常说明}
+```
+
+---
+
+## 重要规则(铁律)
+
+1. **必须调用工具** 🔴:所有计算必须通过调用 calc_tools 中的工具函数完成,**绝对禁止**凭 LLM 知识直接给出计算结果。此条为红线。
+2. **列式计算**:每个计算过程必须展示公式 → 代入数值 → 计算结果三步,不能只给结果。
+3. **参数缺失处理**:若用户未提供必要参数,明确列出所需参数清单并引导用户补充,**绝不编造参数**。
+4. **参数调整重算**:用户说"改参数重新算"时,更新对应参数后**重新调用工具**计算,不要凭记忆给出结果。
+5. **多轮对话承接**:记住当前会话中的地点类型和已有参数,用户补充参数时自动补全。
+6. **全程简体中文输出**。
+7. **禁止暴露内部工具名、函数名**——计算结果中不要出现"调用calc_xxx得到"等字样。
+8. **使用 write_todos** 实时更新任务进度。
+9. **数据来源标注**:从 MCP 获取的数据标注"来源:通防管控平台",参数缺失标注"用户未提供,无法计算"。
+10. **⚠️ 瓦斯涌出不均衡系数 k_gas 的两种写法**:部分矿井将 100×k_gas 合并为一个系数 K(如 K=125=100×1.25),此时 K ≥ 100 属于正常范围,**不判为系数错误**。核验时反算 k_gas = K/100 即可。常见值:125✅ 150✅ 160✅ 180✅(掘进面)

+ 153 - 0
vent-device-daily-report/README.md

@@ -0,0 +1,153 @@
+# vent-device-daily-report — 设备设施在线率统计与设备管理日报 Skill
+
+基于通防管控平台(tf_mcp)数据,自动统计设备设施当天在线情况(重点:主通风机、局部通风机、风门、风窗、各类传感器),生成设备管理日报(Markdown + JSON)。
+
+---
+
+## 一、这是什么
+
+| 能力 | 说明 |
+|---|---|
+| 设备在线统计 | 按设备类型分组统计:总数 / 在线 / 离线 / 未知 / 模拟数 / 在线率 |
+| 在线判定 | 分类型混合判定:主通风机看 fan 表开机状态、风门看 ai_gate 状态、其余看关联分站连接状态 |
+| 自动告警 | 🔴 主通风机 ≥1 套不在线即红色报警;🟠 其余类型在线率 <90% 橙色报警 |
+| 日报输出 | Markdown 日报(可读)+ JSON(可二次处理) |
+
+## 二、安装方法(在其他 agent 中使用)
+
+1. 解压 `vent-device-daily-report.zip`,得到 `vent-device-daily-report/` 文件夹
+2. 将整个文件夹放到目标机器的用户级技能目录:
+   - `~/.agents/skills/`(通用 agent 技能目录)
+3. 重启 agent,技能自动加载
+4. **前置条件**:目标 agent 必须已连接 `tf_mcp`(本 skill 通过 `mcp__tf_mcp__execute_sql_query` 从 vent 库取数)
+
+## 三、使用方法
+
+### 方式一:直接对 agent 说(推荐)
+
+用户说出以下任一需求即可自动触发本 skill:
+
+- "给我设备管理日报" / "生成今天的设备管理日报"
+- "统计设备在线率" / "设备设施在线情况"
+- "哪些设备在线/不在线" / "主通风机在线情况"
+- "传感器在线率" / "在线率低于90%报警"
+
+Agent 会按 SKILL.md 中的流程自动执行:取数(4 条 SQL)→ 判定 → 统计 → 告警 → 输出 Markdown + JSON。
+
+### 方式二:手动运行脚本(有 Python 3 的环境)
+
+```bash
+python scripts/device_online_report.py \
+  --devices devices.json \              # 必填:SQL-1 查询结果(见 SKILL.md)
+  [--fan fan.json] \                    # 可选:fan 表开机状态
+  [--aigate aigate.json] \              # 可选:ai_gate 风门状态
+  [--groups groups.json] \              # 可选:自定义设备分组
+  [--threshold 0.90] \                  # 可选:在线率告警阈值,默认 0.90
+  [--fan-tol 0] \                       # 可选:主通风机容忍离线台数,默认 0
+  [--outdir ./report] \                 # 可选:输出目录,默认当前目录
+  [--date 2026-08-10]                   # 可选:日报日期,默认当天
+```
+
+脚本无第三方依赖(纯 Python 标准库),输入 JSON 支持直接保存 tf_mcp 返回的 `{"result": [...]}` 完整结构。
+
+## 四、字段与参数含义详解
+
+### 4.1 设备字段(deviceinfo 表 + 关联分站字段)
+
+| 字段 | 含义 | 取值 |
+|---|---|---|
+| `ID` | 设备唯一 ID(bigint) | - |
+| `strName` | 设备名称 | - |
+| `deviceKind` | 设备大类编码 | fanmain / fanlocal / gate / window / modelsensor / safetymonitor / gasmonitor 等 36 类 |
+| `strType` | 设备小类编码(型号) | modelsensor_speed(风速传感器)、modelsensor_ch4(甲烷传感器)、gate_qd(气动风门)等 76 类 |
+| `nSubStationID` | 关联分站 ID | 对应 sub_station.ID;NULL=未关联分站 |
+| `StationName` | 分站名称 | 如"测试分站"、"元宝湾安全监控" |
+| `monitorFlag` | 监测标志 | 0=不监测;1=统一类型参数监测;2=独立参数监测 |
+| `testFlag` | 模拟数据标志 | 1=模拟数据;0=真实数据 |
+| `status` | 设备状态 | 1=正常;0=删除(删除的不参与统计) |
+| `linkStatus` | 分站连接状态(来自 sub_station) | 1=已连接;0=未连接 |
+| `isUse` | 分站启用状态(来自 sub_station) | 1=开启监控;0=删除;2=不开监控 |
+
+### 4.2 判定专用表字段
+
+| 表 | 字段 | 含义 |
+|---|---|---|
+| `fan`(主通风机) | `bOn` | 是否开机:1=开机(在线);0=停机(离线);NULL=无数据(跳过此规则) |
+| `ai_gate`(智能风门) | `name` / `status` | 风门名称 / 状态:1=正常;0=离线;2=故障;按名称与 deviceinfo.strName 匹配 |
+
+### 4.3 脚本参数
+
+| 参数 | 必填 | 含义 |
+|---|---|---|
+| `--devices` | ✅ | 设备明细 JSON(SQL-1 结果),包含每台设备的 ID/strName/deviceKind/strType/nSubStationID/StationName/monitorFlag/testFlag/linkStatus/isUse |
+| `--fan` | ❌ | fan 表查询结果 JSON(SQL-2),用于主通风机 bOn 开机判定 |
+| `--aigate` | ❌ | ai_gate 查询结果 JSON(SQL-3),用于风门状态判定 |
+| `--groups` | ❌ | 自定义分组 JSON,格式 `{"分组名": ["deviceKind", ...]}`,覆盖默认分组 |
+| `--threshold` | ❌ | 在线率告警阈值(小数),默认 0.90,低于即橙色报警 |
+| `--fan-tol` | ❌ | 主通风机允许离线台数,默认 0(1 台离线即红色报警) |
+| `--outdir` | ❌ | 输出目录,默认当前目录 |
+| `--date` | ❌ | 日报日期,默认当天 |
+
+### 4.4 输出文件
+
+**report.md**(Markdown 日报):
+| 章节 | 内容 |
+|---|---|
+| 一、总体概况 | 正常接入/在线/离线/未知/模拟设备数量与总体在线率 |
+| 二、分类型在线统计 | 每分组:总数/在线/离线/未知/在线率/告警标记(🔴🟠🟢) |
+| 三、告警信息 | 逐条告警级别与内容 |
+| 四、离线/异常设备明细 | 每台离线/未知设备:名称/类型/分站/状态/原因/是否模拟 |
+
+**report.json**(结构化数据):
+| 字段 | 含义 |
+|---|---|
+| `report_date` | 日报日期 |
+| `generated_at` | 生成时间 |
+| `summary` | 总体概况:total/online/offline/unknown/sim/online_rate |
+| `alarms` | 告警数组:level(RED/ORANGE/GREEN)/group/message |
+| `threshold` / `fanmain_offline_tolerance` | 本次使用的告警阈值 |
+| `groups` | 本次使用的设备分组定义 |
+| `detail` | 每个分组的统计与明细:total/online/offline/unknown/sim/online_rate/online_list/offline_list/unknown_list(列表内每项含 id/name/deviceKind/strType/stationName/monitorFlag/testFlag/state/reason/judge_source) |
+
+### 4.5 设备分组默认定义
+
+| 分组名 | 包含 deviceKind | 说明 |
+|---|---|---|
+| 主通风机 | fanmain | 矿井主扇,1 套不在线即红色报警 |
+| 局部通风机 | fanlocal | 掘进面局扇 |
+| 风门 | gate | 自动风门/气动风门等 |
+| 风窗 | window | 自动风窗 |
+| 传感器 | modelsensor, safetymonitor, gasmonitor, gas, gaspatrol, dusting, fiber, bundletube, windrect, atomizing, duaSpeCamera, imgFireDet | 各类传感器与监测装置 |
+| 其他 | 其余 deviceKind | 自动归入 |
+
+## 五、判定与告警规则速查
+
+**在线判定链**(按优先级,命中即止):
+1. 主通风机:`fan.bOn = 1` → 在线;`=0` → 离线
+2. 风门:`ai_gate.status = 1` → 在线;`0/2` → 离线
+3. 通用:关联分站 `linkStatus=1 且 isUse=1` → 在线;否则离线
+4. 兜底:未关联分站 → "未知"(不计入在线率分母,单独说明)
+
+**统计口径**:分母 = 正常接入设备(status=1);在线率 = 在线 ÷ (总数 − 未知)
+**告警**:主通风机离线+未知 ≥1 → 🔴 RED;其他分组在线率 < 阈值 → 🟠 ORANGE;全部达标 → 🟢 GREEN
+
+## 六、常见问题与踩坑
+
+| 问题 | 说明 |
+|---|---|
+| `window` 表报 SQL 语法错误 | `window` 是 MySQL 保留字,必须写成 `` `window` `` |
+| 名称列表被截断 | 服务端 group_concat_max_len=1024,大类型名称用明细查询(`SELECT strName FROM deviceinfo WHERE strType='...'`) |
+| 实时数据接口查不到 | `query_device_realtime_data`(Redis)多数设备无数据,在线判定不要依赖它 |
+| gate/window/fan 表数据量巨大 | 这些是模型数据(gate 2.5万条),设备在线统计以 deviceinfo 为准,专用表只作状态补充 |
+| 全部设备显示离线 | 检查 sub_station 的 linkStatus——分站未连接时设备全部离线,属真实状态 |
+| 总数对不上 | 聚合用 `COUNT(DISTINCT d.ID)` 防止 JOIN 膨胀 |
+
+## 七、文件清单
+
+```
+vent-device-daily-report/
+├── README.md                              # 本说明文件
+├── SKILL.md                               # 技能主文件(agent 执行流程、SQL 模板、规则)
+└── scripts/
+    └── device_online_report.py            # 日报生成脚本(纯标准库)
+```

+ 160 - 0
vent-device-daily-report/SKILL.md

@@ -0,0 +1,160 @@
+---
+name: vent-device-daily-report
+description: "煤矿设备设施在线率统计与设备管理日报生成(通防管控平台 tf_mcp)。当用户提出生成设备管理日报、统计设备在线率、查看当天哪些设备在线/不在线、设备设施在线情况、主通风机/局部通风机/风门/风窗/传感器在线状态、在线率低于90%报警、主通风机离线报警等需求时使用。覆盖:按设备类型分组统计在线/离线数量与在线率、主通风机1套不在线即红色报警、其余类型在线率不得低于90%的橙色报警、Markdown+JSON 双格式日报输出。"
+---
+
+# 设备设施在线率统计与设备管理日报
+
+基于通防管控平台数据,统计设备设施当天在线情况(重点:主通风机、局部通风机、风门、风窗、各类传感器),生成设备管理日报。
+
+## 适用场景
+用户说"给我设备管理日报"、"统计设备在线率"、"哪些设备在线/不在线"、"主通风机在线情况"、"传感器在线率"等。
+
+## 前置条件
+- **tf_mcp 必须已连接**(本 skill 通过 `mcp__tf_mcp__execute_sql_query` 取数,不走平台 API)
+- 数据源:vent 库 `deviceinfo`(设备主表)+ `sub_station`(分站,含 linkStatus 连接状态)+ `fan`(主通风机开机状态,可选)+ `ai_gate`(智能风门状态,可选)
+
+## 核心概念(判定规则,分类型混合判定,优先级从高到低)
+
+### 设备"在线"判定链(命中即止)
+| 优先级 | 适用类型 | 判定规则 | 结果 |
+|---|---|---|---|
+| 1 | 主通风机 (fanmain) | `fan` 表 `bOn=1`(开机) | 在线 / 离线(bOn=0) |
+| 2 | 风门 (gate) | `ai_gate` 表按设备名称匹配 `status=1`(1正常/0离线/2故障) | 在线 / 离线 |
+| 3 | 所有类型 | 关联分站 `sub_station.linkStatus=1 且 isUse=1`(开启监控且连接正常) | 在线 / 离线 |
+| - | 兜底 | 设备 `nSubStationID` 在 `sub_station` 中不存在 → 状态"未知"(不参与在线率分母,日报中单独说明) | 未知 |
+
+### 统计口径
+- **分母 = 正常接入设备**(`deviceinfo.status=1`,不含已删除标记的设备)
+- 在线率 = 在线数 / (总数 − 未知数);"未知"单独列出并说明
+- 模拟数据设备(`testFlag=1`)数量单独标注(这些是测试数据,注意区分)
+
+### 告警规则
+- 🔴 **红色报警**:主通风机有 ≥1 套不在线(离线+未知 > 0)→ "主通风机有 X 套不在线,须立即排查!"
+- 🟠 **橙色报警**:主通风机以外的分组在线率 < 90%(阈值可配置,默认 0.90)
+- 🟢 全部达标:无告警
+
+### 设备分组(默认,deviceKind 编码)
+| 分组 | deviceKind |
+|---|---|
+| 主通风机 | fanmain |
+| 局部通风机 | fanlocal |
+| 风门 | gate |
+| 风窗 | window |
+| 传感器 | modelsensor, safetymonitor, gasmonitor, gas, gaspatrol, dusting, fiber, bundletube, windrect, atomizing, duaSpeCamera, imgFireDet |
+| 其他 | 其余 deviceKind 自动归入 |
+
+## 执行流程
+
+### 第 1 步:取数(通过 mcp__tf_mcp__execute_sql_query 依次执行以下 SQL)
+
+**SQL-1 设备明细 + 分站连接状态**(核心,必查):
+```sql
+SELECT d.ID, d.strName, d.deviceKind, d.strType, d.nSubStationID, d.StationName,
+       d.monitorFlag, d.testFlag, s.linkStatus, s.isUse
+FROM deviceinfo d
+LEFT JOIN sub_station s ON d.nSubStationID = s.ID
+WHERE d.status = 1
+```
+> 801 台设备返回较大:若被截断,按 deviceKind 分批查询(`AND d.deviceKind IN (...)`),或用 SQL-4 聚合 + 第 2 步的离线名单组合。**注意 `window` 是 MySQL 保留字,涉及 `window` 表必须加反引号。**
+
+**SQL-2 主通风机开机状态**(可选,fan 表,当前环境 bOn 多为 NULL,无数据时跳过规则1):
+```sql
+SELECT nFanID, strName, bOn FROM fan WHERE status = 1
+```
+
+**SQL-3 智能风门状态**(可选,ai_gate 表):
+```sql
+SELECT name, status, update_time FROM ai_gate
+```
+
+**SQL-4 分组聚合统计**(替代 SQL-1 的轻量方案;也可用于校验):
+```sql
+SELECT d.deviceKind, COUNT(DISTINCT d.ID) AS total,
+       SUM(CASE WHEN d.testFlag = 1 THEN 1 ELSE 0 END) AS sim_cnt,
+       SUM(CASE WHEN COALESCE(s.linkStatus,0) = 1 AND s.isUse = 1 THEN 1 ELSE 0 END) AS online_cnt,
+       SUM(CASE WHEN s.ID IS NULL THEN 1 ELSE 0 END) AS no_station_cnt
+FROM deviceinfo d
+LEFT JOIN sub_station s ON d.nSubStationID = s.ID
+WHERE d.status = 1
+GROUP BY d.deviceKind
+```
+> 聚合计数务必用 `COUNT(DISTINCT d.ID)` 而不是 `COUNT(*)`,防止 JOIN 后行膨胀导致总数虚高(实测验证过两种写法的一致性,用 DISTINCT 更稳)。
+
+### 第 2 步:判定与统计
+对每台设备按"核心概念"中的判定链判定在线/离线/未知,然后按分组汇总:
+总数 / 在线 / 离线 / 未知 / 模拟数 / 在线率。
+
+### 第 3 步:告警判定
+按"告警规则"逐组判定,输出告警列表(RED/ORANGE/GREEN)。
+
+### 第 4 步:输出日报
+**方式 A(推荐,自动)**:若调用环境有 Python 3,使用本 skill 自带脚本,输入 SQL 结果 JSON:
+```bash
+python scripts/device_online_report.py \
+  --devices devices.json [--fan fan.json] [--aigate aigate.json] \
+  [--groups groups.json] [--threshold 0.90] [--fan-tol 0] \
+  --outdir ./report [--date 2026-08-10]
+```
+- `devices.json`:SQL-1 结果(支持直接保存 tf_mcp 返回的 `{result:[...]}` 完整结构)
+- 输出 `report.md` + `report.json`
+- 脚本无第三方依赖,纯标准库;分组/阈值可用 `--groups`/`--threshold`/`--fan-tol` 覆盖
+
+**方式 B(手动)**:无 Python 时,agent 依据第 2、3 步规则自行计算,按下方模板渲染。
+
+### 日报 Markdown 模板
+```markdown
+# 设备设施在线情况日报(YYYY-MM-DD)
+
+## 一、总体概况
+- 正常接入设备:**N** 台
+- 在线:**X** 台(xx.x%)
+- 离线:**Y** 台
+- 状态未知(未关联分站/无法判定):**Z** 台(不计入在线率分母)
+- 模拟数据设备:**S** 台(testFlag=1,仅供测试参考)
+
+## 二、分类型在线统计
+| 设备类型 | 总数 | 在线 | 离线 | 未知 | 在线率 | 告警 |
+|---|---|---|---|---|---|---|
+| 主通风机 | 4 | 2 | 2 | 0 | 50.0% | 🔴 |
+| 传感器 | 353 | ... | ... | ... | ... | 🟠 |
+
+## 三、告警信息
+- 🔴 [RED] 主通风机有 X 套不在线,须立即排查!
+- 🟠 [ORANGE] 传感器在线率 88.0% 低于 90%(在线 311/353,离线 30,未知 12)
+
+## 四、离线/异常设备明细
+| 设备名称 | 类型 | 分站 | 状态 | 原因 | 模拟 |
+|---|---|---|---|---|---|
+| 1号回风斜井 | fanmain_stem_wp_2 | 测试分站 | 离线 | 关联分站离线 | 是 |
+
+*统计口径:分母为正常接入设备(status=1);在线率 = 在线/(总数-未知);其余类型阈值 90%;主通风机离线 1 台即报警*
+```
+
+### 日报 JSON 结构
+```json
+{
+  "report_date": "2026-08-10",
+  "summary": {"total": 801, "online": 0, "offline": 0, "unknown": 0, "sim": 0, "online_rate": 0},
+  "alarms": [{"level": "RED", "group": "主通风机", "message": "..."}],
+  "threshold": 0.90,
+  "fanmain_offline_tolerance": 0,
+  "groups": {...},
+  "detail": {"主通风机": {"total":4,"online":0,"offline":4,"unknown":0,"online_rate":0,"online_list":[],"offline_list":[...],"unknown_list":[]}, ...}
+}
+```
+
+## 关键注意点(踩坑记录)
+1. **`window` 是 MySQL 保留字**:`SHOW FULL COLUMNS FROM window` 会报 1064 语法错误,必须写成 `` `window` ``;`SHOW TABLES` 里看名字正常,但 SQL 里引用会出错
+2. **group_concat_max_len=1024**:服务端 GROUP_CONCAT 结果超 1024 字节会被静默截断(如 modelsensor_speed 97 台、safetymonitor_yjsk 97 台);需要完整名称列表时用 `SELECT strName FROM ... WHERE strType='x'` 明细查询,或分页
+3. **在线状态不要用 Redis 实时数据**(`query_device_realtime_data`):实测多数设备 key 不存在,不可靠;`station_status_log` 表已停用(只有 2024 年测试记录)
+4. **gate/window/fan 业务表是模型数据**(gate 25174 条、window 15240 条、fan 180 条),与 deviceinfo 接入设备对不上,**不能**直接拿来做接入设备在线统计;设备以 `deviceinfo` 为准,`fan.bOn`/`ai_gate.status` 仅作设备级状态补充
+5. **分站脏数据**:大量设备挂在 `nSubStationID=2`(测试分站);设备 nSubStationID 在 sub_station 中找不到(如"太原研发中心"、"http多url分站"等)→ 状态"未知",日报中注明
+6. **测试/模拟数据区分**:当前环境约 800 台设备 testFlag=1(模拟),真实接入的只有元宝湾安全监控(safetymonitor_yjsk 97 台)、隔爆设施(explosionProof 68 台)等少数;生成日报时建议同时呈现"模拟设备数"以便读者判断
+7. **查询返回大 JSON 会被截断**:801 行明细约 250KB,必要时按 deviceKind 分批取(如 modelsensor 353 台单独一批),或改用 SQL-4 聚合 + 离线名单(离线设备通常少)
+
+## 与用户协作模式
+- 用户说"给我今天的设备管理日报" → 按流程生成,输出 Markdown 日报 + 保存 JSON 到工作区,并给出告警摘要(有红色告警时重点强调)
+- 用户说"哪些设备不在线" → 聚焦第四部分离线明细
+- 用户指定"只看主通风机/风门" → 只统计对应分组
+- 阈值/分组口径需要调整时 → 用 --threshold/--groups 覆盖,并在日报中注明口径

+ 331 - 0
vent-device-daily-report/scripts/device_online_report.py

@@ -0,0 +1,331 @@
+# -*- coding: utf-8 -*-
+"""
+设备设施在线率统计日报生成器(通防管控平台)
+
+功能:按设备类型(主通风机/局部通风机/风门/风窗/各类传感器/其他)统计在线情况,
+     生成当天设备在线日报(Markdown + JSON),并执行告警规则:
+       - 主通风机:有 1 套不在线即红色报警
+       - 其余类型:在线率 < 90% 橙色报警
+
+输入(JSON 文件,数据来源为 tf_mcp 的 execute_sql_query 查询结果):
+  --devices  设备明细(含分站连接状态),字段:ID/strName/deviceKind/strType/
+             nSubStationID/StationName/monitorFlag/testFlag/linkStatus/isUse
+  --fan      可选,主通风机开机状态(fan 表),字段:nFanID/strName/bOn
+  --aigate   可选,智能风门状态(ai_gate 表),字段:name/status(1正常0离线2故障)
+输出:
+  report.md / report.json (写入 --outdir,默认当前目录)
+
+用法示例:
+  python device_online_report.py --devices devices.json --outdir ./report
+  python device_online_report.py --devices devices.json --fan fan.json --aigate aigate.json --outdir ./report
+
+本脚本仅依赖 Python 标准库,无第三方依赖。
+"""
+
+import argparse
+import json
+import os
+from datetime import datetime
+
+# ============================================================
+# 一、类型分组配置(可覆盖:通过 --groups 传入自定义 JSON 文件)
+#     键为日报分组名,值为该组包含的 deviceKind 编码列表
+# ============================================================
+DEFAULT_GROUPS = {
+    "主通风机": ["fanmain"],
+    "局部通风机": ["fanlocal"],
+    "风门": ["gate"],
+    "风窗": ["window"],
+    "传感器": [
+        "modelsensor", "safetymonitor", "gasmonitor", "gas",
+        "gaspatrol", "dusting", "fiber", "bundletube", "windrect",
+        "atomizing", "duaSpeCamera", "imgFireDet",
+    ],
+    # 其余 deviceKind 自动归入"其他"
+}
+
+# 告警阈值(可覆盖)
+DEFAULT_THRESHOLD = 0.90       # 其余类型在线率下限
+DEFAULT_FANMAIN_OFFLINE_TOL = 0  # 主通风机允许离线台数(0 = 1台离线即报警)
+
+
+def load_json(path):
+    """读取 JSON 文件;支持 tf_mcp 返回的 {result: [...]} 包装结构。"""
+    if not path:
+        return []
+    with open(path, "r", encoding="utf-8") as f:
+        data = json.load(f)
+    if isinstance(data, dict):
+        # 兼容 {success/code/result:[...]} 或 {result:[...]}
+        for key in ("result", "data"):
+            if key in data and isinstance(data[key], list):
+                return data[key]
+        return []
+    return data
+
+
+def norm(s):
+    """规范化字符串用于匹配(去空白)。"""
+    return "" if s is None else str(s).strip()
+
+
+def get(d, key, default=None):
+    v = d.get(key, default)
+    return v
+
+
+# ============================================================
+# 二、在线判定(分类型混合判定,优先级链,命中即止)
+# ============================================================
+def judge_device(dev, fan_map, aigate_map, judge_mode="strict"):
+    """
+    判定单台设备在线状态,返回 (state, reason, detail)
+      state: "online" / "offline" / "unknown"
+    """
+    kind = norm(dev.get("deviceKind"))
+    name = norm(dev.get("strName"))
+    monitor_flag = get(dev, "monitorFlag")
+    test_flag = get(dev, "testFlag")
+    link_status = get(dev, "linkStatus")
+    is_use = get(dev, "isUse")
+    station_id = get(dev, "nSubStationID")
+
+    # 规则 1:主通风机 —— fan 表开机状态(bOn=1 在线 / 0 离线 / NULL 继续)
+    if kind == "fanmain" and fan_map:
+        b_on = fan_map.get(name)
+        if b_on is not None:
+            if int(b_on) == 1:
+                return "online", "fan表bOn=1(开机)", "fan.bOn"
+            return "offline", f"fan表bOn={b_on}(停机)", "fan.bOn"
+
+    # 规则 2:风门 —— ai_gate 表状态(按名称匹配,status=1 在线 / 0/2 离线 / 无匹配继续)
+    if kind == "gate" and aigate_map:
+        st = aigate_map.get(name)
+        if st is not None:
+            if int(st) == 1:
+                return "online", "ai_gate状态正常", "ai_gate.status"
+            return "offline", f"ai_gate状态={st}(离线/故障)", "ai_gate.status"
+
+    # 规则 3:通用主判定 —— 关联分站连接状态
+    #   分站在线 = linkStatus=1 且 isUse=1(开启监控)
+    if station_id is not None and str(station_id) != "":
+        if link_status is None and is_use is None:
+            # 分站 ID 在 sub_station 中不存在
+            return "unknown", "关联分站不存在于sub_station", "station"
+        if int(link_status or 0) == 1 and int(is_use or 0) == 1:
+            return "online", "关联分站在线(linkStatus=1)", "station"
+        return "offline", f"关联分站离线(linkStatus={link_status},isUse={is_use})", "station"
+
+    # 设备未关联分站
+    return "unknown", "未关联分站", "station"
+
+
+# ============================================================
+# 三、统计与告警
+# ============================================================
+def build_groups(custom_groups=None):
+    groups = dict(DEFAULT_GROUPS)
+    if custom_groups:
+        groups.update(custom_groups)
+    return groups
+
+
+def group_of(kind, groups):
+    for gname, kinds in groups.items():
+        if kind in kinds:
+            return gname
+    return "其他"
+
+
+def aggregate(devices, fan_map, aigate_map, groups, threshold, fan_tol):
+    """统计各分组:总数/在线/离线/未知/在线率/明细。"""
+    results = {}
+    for gname in list(groups.keys()) + ["其他"]:
+        results[gname] = {
+            "total": 0, "online": 0, "offline": 0, "unknown": 0,
+            "sim_cnt": 0, "online_rate": None,
+            "online_list": [], "offline_list": [], "unknown_list": [],
+        }
+
+    for dev in devices:
+        kind = norm(dev.get("deviceKind"))
+        gname = group_of(kind, groups)
+        r = results[gname]
+        state, reason, detail = judge_device(dev, fan_map, aigate_map)
+        item = {
+            "id": get(dev, "ID"),
+            "name": norm(dev.get("strName")),
+            "deviceKind": kind,
+            "strType": norm(dev.get("strType")),
+            "stationName": norm(dev.get("StationName")),
+            "monitorFlag": get(dev, "monitorFlag"),
+            "testFlag": get(dev, "testFlag"),
+            "state": state,
+            "reason": reason,
+            "judge_source": detail,
+        }
+        r["total"] += 1
+        if int(get(dev, "testFlag") or 0) == 1:
+            r["sim_cnt"] += 1
+        if state == "online":
+            r["online"] += 1
+            r["online_list"].append(item)
+        elif state == "offline":
+            r["offline"] += 1
+            r["offline_list"].append(item)
+        else:
+            r["unknown"] += 1
+            r["unknown_list"].append(item)
+
+    # 在线率:分子=在线数;分母=正常设备(total),unknown 不计入分母并单独说明
+    for r in results.values():
+        denom = r["total"] - r["unknown"]
+        if denom > 0:
+            r["online_rate"] = round(r["online"] / denom, 4)
+    return results
+
+
+def judge_alarms(results, threshold, fan_tol):
+    """告警规则:
+    - 主通风机:offline/unknown 超过容忍台数 -> 红色报警
+    - 其他分组:在线率 < threshold -> 橙色报警
+    """
+    alarms = []
+    fan = results.get("主通风机", {})
+    bad = (fan.get("offline", 0) or 0) + (fan.get("unknown", 0) or 0)
+    if bad > fan_tol:
+        alarms.append({
+            "level": "RED",
+            "group": "主通风机",
+            "message": f"主通风机有 {bad} 套不在线(在线 {fan.get('online',0)}/{fan.get('total',0)}),须立即排查!",
+        })
+    for gname, r in results.items():
+        if gname == "主通风机":
+            continue
+        rate = r.get("online_rate")
+        if rate is not None and rate < threshold:
+            alarms.append({
+                "level": "ORANGE",
+                "group": gname,
+                "message": f"{gname}在线率 {rate*100:.1f}% 低于 {threshold*100:.0f}%(在线 {r['online']}/{r['total']},离线 {r['offline']},未知 {r['unknown']})",
+            })
+    if not alarms:
+        alarms.append({"level": "GREEN", "group": "-", "message": "全部设备在线率达标"})
+    return alarms
+
+
+# ============================================================
+# 四、输出渲染
+# ============================================================
+def render_markdown(report_date, results, alarms, threshold, fan_tol, total_summary):
+    lines = []
+    lines.append(f"# 设备设施在线情况日报({report_date})")
+    lines.append("")
+    lines.append("## 一、总体概况")
+    lines.append("")
+    lines.append(f"- 正常接入设备:**{total_summary['total']}** 台")
+    lines.append(f"- 在线:**{total_summary['online']}** 台({total_summary['online_rate']*100:.1f}%)")
+    lines.append(f"- 离线:**{total_summary['offline']}** 台")
+    lines.append(f"- 状态未知(未关联分站/无法判定):**{total_summary['unknown']}** 台(不计入在线率分母)")
+    lines.append(f"- 模拟数据设备:**{total_summary['sim']}** 台(testFlag=1,仅供测试参考)")
+    lines.append("")
+    lines.append("## 二、分类型在线统计")
+    lines.append("")
+    lines.append("| 设备类型 | 总数 | 在线 | 离线 | 未知 | 在线率 | 告警 |")
+    lines.append("|---|---|---|---|---|---|---|")
+    order = list(results.keys())
+    for gname in order:
+        r = results[gname]
+        rate = f"{r['online_rate']*100:.1f}%" if r["online_rate"] is not None else "-"
+        lvl = next((a["level"] for a in alarms if a["group"] == gname), "")
+        flag = {"RED": "🔴", "ORANGE": "🟠", "GREEN": "🟢"}.get(lvl, "")
+        lines.append(f"| {gname} | {r['total']} | {r['online']} | {r['offline']} | {r['unknown']} | {rate} | {flag} |")
+    lines.append("")
+    lines.append("## 三、告警信息")
+    lines.append("")
+    for a in alarms:
+        icon = {"RED": "🔴", "ORANGE": "🟠", "GREEN": "🟢"}[a["level"]]
+        lines.append(f"- {icon} **[{a['level']}]** {a['message']}")
+    lines.append("")
+    lines.append("## 四、离线/异常设备明细")
+    lines.append("")
+    for gname in order:
+        r = results[gname]
+        if not r["offline_list"] and not r["unknown_list"]:
+            continue
+        lines.append(f"### {gname}(离线 {r['offline']} / 未知 {r['unknown']})")
+        lines.append("")
+        lines.append("| 设备名称 | 类型 | 分站 | 状态 | 原因 | 模拟 |")
+        lines.append("|---|---|---|---|---|---|")
+        for item in r["offline_list"] + r["unknown_list"]:
+            state_cn = "离线" if item["state"] == "offline" else "未知"
+            sim = "是" if item["testFlag"] == 1 else "否"
+            lines.append(f"| {item['name']} | {item['strType']} | {item['stationName'] or '-'} | {state_cn} | {item['reason']} | {sim} |")
+        lines.append("")
+    lines.append("---")
+    lines.append(f"*统计口径:分母为正常接入设备(status=1);在线率 = 在线 / (总数 - 未知);"
+                 f"其余类型在线率阈值 {threshold*100:.0f}%;主通风机离线 {fan_tol} 台即报警*")
+    return "\n".join(lines)
+
+
+def main():
+    parser = argparse.ArgumentParser(description="设备设施在线率统计日报生成器")
+    parser.add_argument("--devices", required=True, help="设备明细 JSON(tf_mcp execute_sql_query 结果)")
+    parser.add_argument("--fan", default=None, help="可选:fan 表开机状态 JSON")
+    parser.add_argument("--aigate", default=None, help="可选:ai_gate 风门状态 JSON")
+    parser.add_argument("--groups", default=None, help="可选:自定义分组 JSON {分组名:[deviceKind,...]}")
+    parser.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD, help="在线率阈值,默认0.90")
+    parser.add_argument("--fan-tol", type=int, default=DEFAULT_FANMAIN_OFFLINE_TOL, help="主通风机容忍离线台数,默认0")
+    parser.add_argument("--outdir", default=".", help="输出目录,默认当前目录")
+    parser.add_argument("--date", default=None, help="日报日期,默认当天")
+    args = parser.parse_args()
+
+    devices = load_json(args.devices)
+    fan_map = {}
+    for row in load_json(args.fan):
+        fan_map.setdefault(norm(row.get("strName")), row.get("bOn"))
+    aigate_map = {}
+    for row in load_json(args.aigate):
+        aigate_map.setdefault(norm(row.get("name")), row.get("status"))
+
+    groups = build_groups(load_json(args.groups) if args.groups else None)
+    results = aggregate(devices, fan_map, aigate_map, groups, args.threshold, args.fan_tol)
+    alarms = judge_alarms(results, args.threshold, args.fan_tol)
+
+    total = {"total": 0, "online": 0, "offline": 0, "unknown": 0, "sim": 0}
+    for r in results.values():
+        total["total"] += r["total"]
+        total["online"] += r["online"]
+        total["offline"] += r["offline"]
+        total["unknown"] += r["unknown"]
+        total["sim"] += r["sim_cnt"]
+    total["online_rate"] = round(total["online"] / (total["total"] - total["unknown"]), 4) \
+        if (total["total"] - total["unknown"]) > 0 else None
+
+    report_date = args.date or datetime.now().strftime("%Y-%m-%d")
+    os.makedirs(args.outdir, exist_ok=True)
+
+    md = render_markdown(report_date, results, alarms, args.threshold, args.fan_tol, total)
+    with open(os.path.join(args.outdir, "report.md"), "w", encoding="utf-8") as f:
+        f.write(md)
+
+    report = {
+        "report_date": report_date,
+        "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+        "summary": total,
+        "alarms": alarms,
+        "threshold": args.threshold,
+        "fanmain_offline_tolerance": args.fan_tol,
+        "groups": groups,
+        "detail": results,
+    }
+    with open(os.path.join(args.outdir, "report.json"), "w", encoding="utf-8") as f:
+        json.dump(report, f, ensure_ascii=False, indent=2)
+
+    print(f"✅ 已生成日报:{os.path.join(args.outdir, 'report.md')} / report.json")
+    for a in alarms:
+        print(f"  [{a['level']}] {a['message']}")
+
+
+if __name__ == "__main__":
+    main()

+ 211 - 0
vent-monthly-summary-product/SKILL.md

@@ -0,0 +1,211 @@
+---
+name: vent-monthly-summary-product
+description: |
+  煤矿通风月报公司端汇总技能。当用户要求把各矿《通风月报表》(xlsx) 汇总成《公司通风月报汇总表》时触发——典型问法:"汇总通风月报""把各矿月报汇总到公司表""生成公司XX月通风月报汇总表""通风月报汇总""月底汇总月报";用户提到"月报汇总""关键内容.json""excel2md""填公司表"等词也触发。
+  流程:collect_test_group_vent_reports 采集(缺失矿用 ask_user 询问)→ copy_folder_to_workspace 拷入沙箱 → excel_to_markdown_file 逐矿转 md → task 委派 monthly-report-extractor 并行按契约提取各矿关键内容 JSON → fill_summary_from_json 批量填充公司模板并自动计算合计/小计行与封面月份 → 自检交付下载链接 → save_group_vent_report_summary 登记入库。
+  输入:集团月报目录(MCP 采集)或一批矿端月报 xlsx(+可选公司模板,缺则用沙箱 assets/ 自带模板);输出:公司YYYY年MM月份通风月报汇总表.xlsx(可下载)+ 入库登记。数据缺失留空并汇报,绝不臆造。该任务消耗时间较长,通常可能达到30分钟以上,需提醒用户请勿关闭窗口。
+---
+
+# 通风月报公司端汇总
+
+把各矿《通风月报表》(表一~表七)汇总填充进《公司通风月报汇总表(模板)》(封面+表1~表13),输出填好的公司汇总表。全部数据在**沙箱**(`workspace/vent_monthly_summary/`)内流转,技能自带函数化工具链与提取契约,任何目录可用。
+
+## 工具链(已注册到主 agent / 子智能体,勿改)
+
+| 工具 | 可用方 | 作用 |
+|---|---|---|
+| `collect_test_group_vent_reports` | 主 agent | MCP 采集集团月报目录,返回 directoryId / directoryPath / missingFiles |
+| `copy_folder_to_workspace` | 主 agent | 把采集目录拷贝进沙箱(`workspace/vent_monthly_summary/<月份>/`) |
+| `excel_to_markdown_file` | 主 agent | 矿月报 xlsx → md 表格(XML 快速引擎,失败自动回退 openpyxl) |
+| `read_sandbox_file` / `write_sandbox_file` | **子智能体**(monthly-report-extractor) | 读 md 与契约 / 写关键内容 JSON,路径相对沙箱根 |
+| `fill_summary_from_json` | 主 agent | 关键内容 JSON → 公司汇总表;自动定位模板、自适应明细槽位、算合计/小计行、写封面月份 |
+| `publish_report_to_download` | 主 agent | 把沙箱汇总表复制到 static/reports/ 并返回下载链接 |
+| `save_group_vent_report_summary` | 主 agent | 汇总表登记入库(集团固定目录) |
+
+资产位置(**已迁入沙箱,agent 可直接读取**):
+
+| 资产 | 沙箱内路径(相对沙箱根) |
+|---|---|
+| 提取契约示例(含顶层 `_约定` 块,须先通读) | `assets/通风月报表关键内容提取示例.json` |
+| 公司模板 xlsx(fill 工具自动查找,**无需读取**) | `assets/公司xxxx年xx月份通风月报汇总表(模板).xlsx` |
+
+## 先查已生成结果(可直接复用返回)
+
+用户要求"汇总某月 / 生成公司XX月通风月报汇总表"时,**先判断是否必须重算**:
+
+- **用户没有明确要求"重新生成/重算/更新/换最新数据"** → 先检查该月是否已有生成过的汇总表:
+  沙箱月目录下形如 `workspace/vent_monthly_summary/<月份>/公司<YYYY>年<MM>月份通风月报汇总表.xlsx`
+  (批量模式产物名;单矿试填名为 `...汇总表_<矿名>填充.xlsx`,仅试填用,不作为复用结果)。
+- **已存在** → 不必重跑整套流程(采集/转 md/提取/填充常耗时 30 分钟以上),直接调用
+  `publish_report_to_download(rel_path="<月份>/公司<YYYY>年<MM>月份通风月报汇总表.xlsx")`
+  把它发布为下载链接返回给用户,并说明"这是此前已生成的 <月份> 汇总表;如数据有更新或需重算,请告知我再重新生成"。
+- **不存在**,或**用户明确要求重新生成**(含"漏了某矿/数据错了"等涉及本次结果不新鲜的情形)→ 走下方完整流程覆盖/新建。
+
+> 判断依赖:用户原话是否包含"重新/重算/再生成/更新/之前不对"等词,或明确提到本次有新矿/新数据要纳入;
+> 拿不准时可用 `ask_user` 一句确认("该月已有一份汇总表,直接返回还是重新汇总?"),不要擅自多跑一次长流程,也不要在用户要新结果时偷懒返回旧表。
+
+## 流程(严格按序)
+
+### 第0步 采集各矿通风月报(MCP,集团月报必走;本地文件场景可跳过)
+1. 调用 `collect_test_group_vent_reports(year, month)` 采集指定月份的集团通风月报。
+   - `year`/`month` 成对传入(如 `2026, 8`);不传时默认采集上个月。
+   - 返回 JSON 含:`directoryId`(本次采集目录ID,**务必记住**,第5步入库要用)、
+     `directoryPath`(各矿月报文件在服务器上的目录绝对路径)、`fileCount`、
+     `missingFiles`(缺失矿名单)。
+2. **缺失处理**:若 `missingFiles` 非空,**必须先用 `ask_user` 工具询问用户**:
+   - 提问内容:列出缺失矿名单 + 选项「等待上传后再开始 / 跳过缺失矿直接开始」。
+   - 用户选择"等待上传" → 说明需要用户先补齐文件,暂不继续(可稍后重试);
+   - 用户选择"直接开始" → 缺失矿在汇总表中留空,继续。
+3. 调用 `copy_folder_to_workspace(source_dir=directoryPath)` 把采集目录**拷贝到当前沙箱**
+   (`workspace/vent_monthly_summary/<月份>/`)。
+   - 返回 `target_path`(沙箱内真实路径)与文件清单;**后续所有步骤的工作目录 = target_path**。
+4. 确认沙箱内有矿月报(文件名含 `通风月报表` 的 `.xlsx`),并排除 `~$` 开头的 Excel 锁文件。
+
+### 第1步 识别输入与月份
+1. 扫描工作目录(即第0步的 `target_path`,或用户指定目录):
+   矿月报 = 文件名含 `通风月报表` 的 `.xlsx`(**排除 `~$` 开头的 Excel 锁文件**);
+   公司模板 = 文件名含 `汇总表(模板)` 或 `模板`。
+2. 从文件名推断月份/年份(如 `2024年11月`);模板或月份冲突时问用户。
+3. 无矿月报 → 停下说明;无模板 → 用沙箱 assets/ 自带模板,也可让用户提供。
+
+### 第2步 逐矿转 md(excel_to_markdown_file,可连续并行)
+对**每份矿月报**调用一次工具 `excel_to_markdown_file`(路径均为**相对沙箱根**的路径,
+即第0步拷贝产生的目录,如 `2026年08月/`):
+```json
+{"xlsx_rel_path": "2026年08月/上湾煤矿-2026-08通风月报表.xlsx",
+ "md_rel_path": "2026年08月/上湾煤矿-2026-08通风月报表.md"}
+```
+- 一个文件一次调用,多份矿月报可连续发起多次调用(互不依赖);
+- 转换完成即生成同名 `.md`(XML 引擎,单文件约 2-4s);已存在的 `.md` 可直接复用跳过;
+- 转换失败 → 汇报该文件与错误,不臆测原因。
+
+### 第3步 语义提取关键内容 JSON(每矿一个子 agent,全并行)
+调用 `task` 工具委派子智能体 `monthly-report-extractor`(已启用,tools=`read_sandbox_file`/`write_sandbox_file`),
+**每个矿一个任务,同时并行**(矿数多时每批 ≤3 个,避免并发模型连接过多触发网络层失败);主 agent 汇总各 agent 返回的 JSON 相对路径、⚠告警、存疑项后进入第4步。
+- 子智能体在任务描述(description)中拿到:该矿 md 文件沙箱相对路径、矿名/月份、输出 JSON 文件名;
+  **提取契约固定为沙箱 `assets/通风月报表关键内容提取示例.json`,子智能体可 `read_sandbox_file` 直接读取**(含 `_约定` 块),无需主 agent 转发。
+- 每个子智能体:`read_sandbox_file` 读 `<矿>.md` 全文 + 提取契约 → 按示例结构提取 →
+  `write_sandbox_file` 写 `<矿>-YYYY-MM通风月报表关键内容.json`(UTF-8、indent=2)。**提取约定见下,违者必错。**
+
+### 第4步 批量填充(fill_summary_from_json 一条调用出全表)
+调用工具 `fill_summary_from_json`(路径均为**相对沙箱根**路径):
+```json
+{"json_rel_paths": ["2026年08月/锦界煤矿-2026-08通风月报表关键内容.json",
+                    "2026年08月/上湾煤矿-2026-08通风月报表关键内容.json",
+                    "...所有矿.json..."],
+ "out_rel_path": "2026年08月/公司2026年08月份通风月报汇总表.xlsx"}
+```
+- 工具自动:按"矿井名称"定位模板各表 A 列行块叠加写入 → 计算各表合计/小计行 → 写封面月份;
+- `template_rel_path` 不传即可——模板自动取沙箱 `assets/` 自带模板;也可传入用户提供的模板相对路径;
+- 输出目录自动创建;返回 `{success, out_rel_path, mines, month}`。
+
+### 第5步 自检、交付与入库
+1. 抽查 2~3 个矿的关键数值与源 md 一致(如表1 进风量、表4 瓦斯相对量、表13 挡风墙在籍)。
+   主 agent 用 **`read_file('/workspace/vent_monthly_summary/<月份>/<矿>.md')`**(只读虚拟路径)抽查
+   (注:`read_sandbox_file`/`write_sandbox_file` 已分离给子智能体,主 agent 不可用)。
+2. 核对合计行:表1 合计进风 ≈ 各矿进风之和、表13 合计挡风墙在籍 ≈ 各矿之和(数值从各矿 md 读后心算核对即可)。
+3. 解读 ⚠ 告警(见"告警解读"):预期信息性告警照常交付;真问题(队别/键缺失)需修正 JSON 后重跑第4步。
+4. **交付下载链接**:调用 `publish_report_to_download(rel_path="<沙箱相对路径>/公司2026年08月份通风月报汇总表.xlsx")` 把汇总表复制到 `static/reports/`,
+   返回 `download_url`(如 `http://<host>/api/download/report/公司2026年08月份通风月报汇总表_20260902_....xlsx`)与服务器真实路径 `file_path`,
+   在会话中把 `download_url` 发给用户。
+5. **登记入库(必做)**:调用 `save_group_vent_report_summary(directory_id=<第0步返回的 directoryId>, source_file_path=<第5步第4点返回的 file_path>)`。
+   - `source_file_path` 用 `publish_report_to_download` 返回的**服务器真实路径** `file_path`(`static/reports/` 下),**不要**用沙箱相对路径或虚拟路径(`/reports/...`)。
+   - 调用成功 → 向用户说明"汇总表已登记入库(集团固定目录)";失败 → 如实转告错误,不影响已交付的下载链接。
+6. 向用户说明:下载链接 + 留空清单(哪矿哪表哪格为何空)+ 疑点。**不臆造数据。**
+
+## 路径语义(两通道,务必分清)
+
+| 通道 | 工具 | 路径写法 | 用途 |
+|---|---|---|---|
+| 主 agent 只读 | `read_file`(挂载 `/workspace/`) | 虚拟路径 `/workspace/vent_monthly_summary/<月份>/<文件>` | 抽查 md/JSON 等中间产物 |
+| 子智能体读写 | `read_sandbox_file` / `write_sandbox_file` | 相对沙箱根 `workspace/vent_monthly_summary/`,如 `2026年08月/矿.md`、`assets/通风月报表关键内容提取示例.json` | 读 md/契约、写关键内容 JSON |
+| 工具参数 | `excel_to_markdown_file` / `fill_summary_from_json` / `publish_report_to_download` | 同上,相对沙箱根 | 转 md、填充、发布 |
+
+**铁律**:
+- **禁止使用服务器真实绝对路径**(如 `/data/vent_agent/...`)读取任何文件——沙箱会拒绝,只会浪费时间;
+- 契约缺失时**禁止用 fill 工具报错迭代反推结构**——直接停下说明;
+- 沙箱 `assets/` 是只读资产(模板/契约),产出文件一律写到工作目录(如 `2026年08月/`),**不要覆盖 assets/ 内文件**;
+- 主 agent **不能** `write_sandbox_file`(已分离给子智能体),一切落盘写操作走子智能体或函数化工具。
+
+## 并行加速
+
+批处理时两处可全并行:
+
+- **第2步(纯计算)**:连续多次调用 `excel_to_markdown_file`(每矿一次),互不依赖,单文件 2-4s。
+- **第3步(语义提取)**:调用 `task` 工具委派子智能体 `monthly-report-extractor`,每个矿一个任务同时并行,互不依赖;**每批 ≤3 个 agent**(并发委派过多会同时建立多条模型流式连接,曾实测触发网络连接失败),全部完成后主 agent 汇总再发下一批,全部汇总后进入第4步。
+
+### 第3步委派说明(task → monthly-report-extractor)
+
+子智能体 `monthly-report-extractor` 已预置:tools = `read_sandbox_file` / `write_sandbox_file`,
+system_prompt 含提取铁律与自检说明。主 agent 每矿发一个 task,description 中写明:
+
+```
+目标文件(沙箱相对路径):<月份目录>/<矿>.md
+提取契约:assets/通风月报表关键内容提取示例.json(含 _约定,先通读)
+输出 JSON(沙箱相对路径):<月份目录>/<矿名>-YYYY-MM通风月报表关键内容.json
+矿名 / 报表月份:<矿名> / YYYY年MM月
+【要求】read_sandbox_file 读 md 与契约 → 按契约结构提取(键名逐字符一致、全量保留、缺数据填 null、数值保真)→ write_sandbox_file 落盘 JSON(UTF-8, indent=2)→ 汇报 ≤5 行。
+```
+
+注意:该子智能体的 `read_sandbox_file`/`write_sandbox_file` 已被自动从主 agent 移除(`_filter_main_tools`),
+因此**主 agent 无法直接读/写沙箱文件**——读文件抽查用 `read_file('/workspace/...')`,
+凡涉及 md/JSON 的落盘写入必须经 task 委派或由函数化工具完成(excel_to_markdown_file / fill_summary_from_json / publish_report_to_download)。
+
+## 提取约定(铁律)
+
+1. **键名逐字符一致**:与示例完全一致(含全角/半角括号、上下标、单位后缀),如 `便携式CO(0~0.1%)`、`光缆敷设长度(km)`、`局部通风机百米漏风率_百分数`。
+2. **键必须全量保留**:无数据填 `null`,**不能省略键**(省略会导致填充工具告警并跳过该表)。
+3. **数值保真**:原样写入(小数位/文本不动),不四舍五入。
+4. **投入运行时间**:源表若是 Excel 序列号(如 39783)换算为 `YYYY年MM月`(`date(1899,12,30)+timedelta(days=serial)`);已是文本/日期直接照写。
+5. **数组顺序以契约示例为锚**:综采面/掘进面/回风井等数组的槽位顺序遵循契约示例(示例即模板队别行序的固化);
+   若该矿 md 实际队别/井别与示例槽位对不上(矿队别不同或工作面增减),按 md 实际内容顺序排列并在汇报中标明存疑,**不要臆造模板结构**。
+6. **掘进面合并格队别**:源表合并格分组表示同一队别管辖的多工作面,提取时同组工作面保持相邻、同队别。
+7. **回风井明细顺序** = 模板表1 该块 B 列标签(`1#回风井`/`2#回风井`…)对应。**活鸡兔井例外**:其子回风井明细不在汇总表列出(JSON 仍按源表提取全部明细,fill 端收敛到"活井2#"一行),见"已知坑位-活鸡兔井"。
+8. **综采面"工作面"下含 CH4+CO2 两键**;回风隅角含 CH4/CO_ppm/O2;回风含 CO2。
+9. **隔爆设施组合计** = 主要隔爆设施 + 辅助隔爆设施(源表分开列时相加)。
+10. **设备类同机多行续行(注氮/注浆/束管/除尘等设备表通用)**:同一台设备因多个使用位置/采空区在源表占多行时,若行间设备主列(名称/型号/数量/功率/厂家等)**空白或重复**,视为**同一设备的续行** → 合并为**一个对象**,各行使用位置并入同一键(注氮为 `使用位置(注氮地点)`)用"、"连接;设备主列只写一份,**不要**拆成多个空值对象。月度/年度累计等数值只保留一份(源表合并格会逐行重复该值,勿重复求和)。反例(勿学):乌兰木伦移动制氮机 DM-1500 在源表按两个采空区分两行 → 应只提取 1 个对象,`使用位置(注氮地点)` = `"22201、22202采空区、12409、12410采空区"`,而非拆成 2 个对象(第二个全 null)。
+11. **源表空值**:`——`/`--`/空白 → `null`;`02(%)` 实为 `O2(%)`;防尘管路段孤立值 117705(疑为残留合计)忽略。
+12. **表六瓦斯抽采**:源表无抽放泵数据 → `"泵站": []`。
+13. **表四**:仪器完好数量取"完好"列;设备在用数量取"在用"行;监控系统=型号+生产厂家。
+14. **矿井名称/报表月份**:矿井名称=源文件名矿名;报表月份=`YYYY年MM月`。
+
+## 告警解读(fill_summary_from_json 返回的告警)
+
+**预期信息性(照常交付,无需改 JSON)**:
+- `表13 模板无'防爆密闭'列…未写入` —— 模板表13本身无该列;
+- `表1 第N行回风井名称…无独立列(矿名格与B列合并)` —— 单行汇总矿(乌兰木伦/哈拉沟/石圪台/寸草塔/寸草塔二/柳塔):模板矿名格与 B 列合并、无井名槽位,固定一行写合计(见"已知坑位-表1 单行汇总矿");正常路径不再插行,该告警仅异常路径出现。
+
+**需修正 JSON 后重跑第4步**:
+- `表2 队别不一致…可能错行` —— 综采面数组顺序与模板队别行序不符,调整顺序;
+- `表3 队别合并格内队别不一致` —— 掘进面同合并格工作面队别不同,调整顺序使同队别相邻;
+- `表N 填充失败——JSON缺少键 …` —— 键名拼错或删了键,对照契约示例补键(缺数据填 null)。
+
+**数据来源说明(不是告警,但要在汇报中说明)**:表5 合计只汇总有"小计"行的泵站(保德式);无小计行的矿块(布尔台/寸草塔二/寸草塔)抽采量不进入表5 合计行(模板无该槽位)。
+
+## 已知坑位
+
+- **汉字与拼音**:由于某些服务器不支持中文编码,因此用拼音命名了 excel 文件,例如原本寸草塔二矿.xlsx->cuncaota_2_mine.xlsx。
+- **沙箱路径**:`copy_folder_to_workspace` 固定拷到 `workspace/vent_monthly_summary/<源目录名>/`,
+  重复采集同名目录会覆盖合并(`dirs_exist_ok=True`);`target_path` 是服务器真实路径,仅用于第0步确认文件清单。
+- **download_url vs 入库路径**:`publish_report_to_download` 返回的 `download_url` 是**浏览器下载链接**;
+  `save_group_vent_report_summary` 要求的是**服务器真实路径**(`source_file_path` = 前者的 `file_path`),两者不能混用。
+- **沙箱读写分工**:主 agent 读沙箱文件用 `read_file('/workspace/vent_monthly_summary/...')`(只读,写被拒);
+  md/JSON 落盘只能经子智能体 `write_sandbox_file` 或函数化工具完成。**不要**尝试 `read_file` 读沙箱外路径或写文件。
+- **directoryId 传递**:第5步入库用的是**第0步采集返回的 directoryId**,不是拷贝工具或填充工具的返回值,
+  中间步骤要一直带着这个值。
+- **模板查找顺序**:fill 工具先找 `template_rel_path` 指定文件,不传则找沙箱 `assets/` 自带模板;
+  用户提供了新版模板 → 把它放到工作目录并传 `template_rel_path`(符合预期)。
+- **活鸡兔井**:无独立模板块,`find_block_mine` 自动定位大柳塔矿块内的"活井"子块(表8 有特殊逻辑),矿井名称照填"活鸡兔井"即可;大柳塔井只填"大井"行。**表1 严格按"大井五当沟/大井白家渠/活井2#"三行格式**:活鸡兔井的子回风井明细不在汇总表列出,`_need_rows` 对表1 活鸡兔返回 1(不为其插行),fill_t1 走单行分支——只把矿井合计(计划/进风/回风量合计/排风量合计/有效风量/等积孔等)与**首个回风井**的主扇/漏风率写入"活井2#"行;其余子回风井数据留在 JSON 中不落表。
+- **寸草塔 vs 寸草塔二**:模板行块先后有序,`mine_short` 去"煤矿/井/矿"后缀匹配,寸草塔不会误入寸草塔二块。
+- **表1 单行汇总矿**(寸草塔/寸草塔二/柳塔/乌兰木伦/哈拉沟/石圪台):模板矿名格与 B 列合并为**一行汇总行**(无独立回风井井名槽位),因此 JSON 回风井明细无论几项,汇总表都**只写一行合计**——resize 对该块恒按 1 行收口(不按回风井条数插行),fill_t1 走单行分支把矿井总回/排风(E/F)写为各回风井之和,其余风量类(C/D/G/H/K/S)写矿井合计。对比:榆家梁等单行但有独立 B 井名标签(A 不与 B 合并)的矿,仍按回风井数插行展开成多行。
+- **表5 泵站无 B 列标签**(布尔台/寸草塔二/寸草塔):fill 自动按 JSON 泵站顺序落位,无需手工处理。
+- **表5 双泵/多地点自适应**:表5 按"泵站"独立调整行数(不在全局 `resize_sheets` 内,fill_t5 内部处理)——某站泵数或抽放地点多于模板槽位时自动在小计行前插行,少于槽位时删多余空行;泵模型列(C~H)按泵重建合并格,抽气效率列(M)按抽放地点逐行写(不合并),保证每个泵的型号/功率/效率在表中可见、不被后泵覆盖。保德式带 B 列站名的块从下往上逐站处理,行号不漂移。
+- **保德矿**模板有块但目录无文件时留空(正常)。
+- **自适应槽位**:fill 会按各矿 JSON 明细行数自动调整模板预留行——工作面/设备比模板多则插行(后续矿块下移、矿名合并格扩展、新行复制样式),比模板少则删多余空行;各表合计/小计行动态定位,不受插删行影响。注意:插行后超出模板预印队别标签的行(如表2多出的综采面)无队别标签属正常。
+- **插行后矿级合并**:表1 回风井明细超过模板槽位而插行时,会按该矿实际占行把矿级合计列纵向合并,使合计值覆盖整矿——矿名(如榆家梁 `A10:A11`)、计划风量 `C10:C11`、进风 `D10:D11`、有效风量 `G10:G11`、有效风量率 `H10:H11`、等积孔 `K10:K11`、巷道长度 `S10:S11`。模板单行块原本无合并格时自动新建;多行块(如大柳塔大井 `C5:C6`、补连塔 `C8:C9`)已有合并且超过预印行数时自动扩展(如扩为 `C8:C10`)。B 列井名各自独立不合并;`fill_t1` 中 `_merge_span` 负责矿级列、`resize_sheets` 调 `_extend_block_name_merge` 负责矿名。
+- **多矿并行**:见"并行加速"小节——第2步连续多次 `excel_to_markdown_file` 转 md,
+  第3步 `task` 委派 `monthly-report-extractor` 每矿一个并行。
+
+## 依赖
+
+`openpyxl`(缺则 `pip install openpyxl`)。

+ 47 - 0
vent-operation-daily-report/README.txt

@@ -0,0 +1,47 @@
+vent-operation-daily-report v1.1
+通风设备与传感器运行日报系统(通防管控平台)
+
+【是什么】
+一条命令导出前一天 7 类真实通风设备(主通风机/局部通风机/风门/风窗/测风装置/
+多参数传感器/压差传感器)的运行日报:各参数最大/最小/平均三列、风门开闭次数、
+异常明细(同设备同类合并:次数/首末时间/最长持续/是否解决),统一 Markdown 模板。
+
+【v1.1 变更(相对 v1.0)】
+1. 异常明细合并:同设备同类型报警合并为一行(此前逐条列出,单日可膨胀至上百行)
+2. 数据可信度标注(†):局扇进出口风量(跨日恒定疑静态标定值)、多参数传感器
+   越界数值(温度/湿度数万、密度溢出等按物理合理域自动检测)
+3. 数据源健康检查:历史+实时接口均无返回时日报顶部输出平台故障警告,防止全空
+   日报被误读为设备全停
+4. 踩坑记录新增 4 条实测结论(windQuantity1 三重定义冲突、平台故障恒值冻结特征等)
+
+【安装(任选其一)】
+方式一:作为通用 Agent skill
+  将本目录整个文件夹复制/解压到用户级 skills 目录:
+    Windows: C:\Users\<用户名>\.agents\skills\vent-operation-daily-report\
+    Linux/Mac: ~/.agents/skills/vent-operation-daily-report/
+  之后对任何支持用户级 skill 的 Agent 说"帮我导出通风设备与传感器运行日报"即可触发。
+
+方式二:作为独立命令行工具
+  只需 scripts/vent_daily_report.py 单文件 + Python 3.8+ 与 fastmcp 依赖。
+
+【依赖】
+  pip install fastmcp
+(见 requirements.txt;脚本无其他第三方依赖)
+
+【使用】
+  python scripts/vent_daily_report.py [--date 2026-08-18] [--url http://39.97.59.228:8071/mcp] [--outdir .]
+  - --date  统计日,默认昨天(自然日 00:00:00~23:59:59)
+  - --url   通防管控平台 MCP 服务地址,按实际环境修改
+  - --outdir 日报输出目录,默认当前目录
+  输出:vent_operation_daily_report_YYYY-MM-DD.md,并在终端打印全文。
+
+【目录结构】
+  SKILL.md                                       skill 定义(Agent 加载入口,含完整取数/统计规则与 11 条踩坑记录)
+  scripts/vent_daily_report.py                   取数+统计+渲染脚本(fastmcp 直连平台)
+  sample/vent_operation_daily_report_2026-08-18.md   v1.1 输出效果样例
+  requirements.txt                               Python 依赖
+
+【已知平台限制】(详见 SKILL.md"核心规则"与"踩坑记录")
+  - 主通风机/压差传感器历史明细接口无记录体,自动回退实时快照
+  - 局扇历史仅存 windQuantity1(三重定义冲突已弃用),其余参数用实时快照
+  - 真实设备口径:deviceinfo.is_bus_sup=0/NULL 且 status=1

+ 136 - 0
vent-operation-daily-report/SKILL.md

@@ -0,0 +1,136 @@
+---
+name: vent-operation-daily-report
+description: "通风设备与传感器运行日报导出(通防管控平台 TF-MCP)。当用户说'帮我导出通风设备与传感器运行日报'、'生成通风运行日报'、'昨天主通风机/局部通风机/风门/风窗/测风装置/传感器运行情况'、'设备运行统计日报'等需求时使用。覆盖:拉取 7 类真实设备(主通风机/局部通风机/风门/风窗/测风装置/多参数传感器/压差传感器,isBusSup 过滤模拟数据)→ 取前一天历史数据计算各参数 max/min/avg、风门开闭次数 → 汇总异常(类型/时间/持续时长/是否解决)→ 按统一模板输出 Markdown 日报文件并在对话中完整展示。"
+---
+
+# 通风设备与传感器运行日报
+
+基于通防管控平台 MCP 服务,统计**前一天**(默认自然日 00:00:00 ~ 23:59:59)7 类通风设备与传感器的运行数据,生成统一格式的运行日报。
+
+## 适用场景
+- "帮我导出通风设备与传感器运行日报"
+- "生成昨天的通风运行日报 / 设备运行统计"
+- "昨天主通风机(或风门/风窗/测风装置/多参/压差)运行怎么样、有没有异常"
+
+## 统计对象(必须为真实设备)
+通过 `execute_sql_query` 查 `vent.deviceinfo`,口径:**`is_bus_sup = 0 OR is_bus_sup IS NULL`(排除业务模拟数据)且 `status = 1`(排除停用设备,如五盘区402辅运大巷风门)**。
+
+| 类别 | deviceKind / strType | 覆盖设备数(2026-08 实测) |
+|---|---|---|
+| 主通风机 | fanmain | 2 |
+| 局部通风机 | fanlocal | 2 |
+| 风门 | gate | 5 |
+| 风窗 | window | 6 |
+| 测风装置 | windrect | 15 |
+| 多参数传感器 | strType='modelsensor_multi' | 21 |
+| 压差传感器 | strType='modelsensor_dp' | 5 |
+
+设备清单 SQL(ID 必须 CAST AS CHAR 防大整数精度丢失):
+```sql
+SELECT devicekind, strtype, CAST(id AS CHAR) AS deviceId, strname, strinstallpos
+FROM deviceinfo
+WHERE ((devicekind IN ('fanmain','fanlocal','gate','window','windrect')
+        AND (is_bus_sup = 0 OR is_bus_sup IS NULL))
+       OR strtype IN ('modelsensor_multi','modelsensor_dp'))
+AND status = 1 ORDER BY devicekind, id
+```
+
+## 执行方式(二选一)
+
+### 方式 A(推荐):自带脚本,一条命令完成
+有 Python 3 + fastmcp 的环境直接运行(不依赖宿主是否注册 MCP,直连平台):
+```bash
+# Windows 本机示例(Python 路径按实际环境调整)
+D:\anaconda3\python.exe scripts/vent_daily_report.py --date 2026-08-17 --outdir .
+# 参数:--date 统计日(默认昨天)  --url MCP地址(默认 http://39.97.59.228:8071/mcp)  --outdir 输出目录(默认当前目录)
+```
+脚本自动完成:设备清单 → 历史分页取数 → 报警聚合 → 实时快照补齐 → 统计 → 渲染。
+输出文件 `vent_operation_daily_report_YYYY-MM-DD.md`,并**在 stdout 打印日报全文**。
+
+### 方式 B(兜底):Agent 手动调用已注册的 MCP 工具
+按"核心规则"逐台调用(工具名可能带 `mcp__TF-MCP__` 或 `mcp__tf_mcp__` 前缀,按功能名匹配):
+`execute_sql_query`(设备清单)→ `list_ventanaly_monitor_data_days`(历史)→ `query_device_data_by_id`(实时补齐)→ `get_alarm_log_history`(异常),统计与渲染规则同脚本。
+
+## 核心规则(实测结论,写死勿改)
+
+### 1. 历史数据接口 `list_ventanaly_monitor_data_days`
+- 必填:`strtype`(设备 strType 小类)、`gdeviceids`(设备 ID 字符串)、`device_num`(**固定传 "1"**,主通风机传 Fan1/Fan2 也无法取回,见已知缺陷)、`ttime_begin/ttime_end`(`yyyy-MM-dd HH:mm:ss`)、`skip`
+- **skip 是采样档位(1~8 整数,0 非法)**:1=1秒(86401条/天)、4=30秒(2881)、5=1分钟(1441)、6=10分钟(145)、7=30分钟(49)、8=1小时(25)
+- **默认 skip=6(10分钟粒度)做数值统计;风门开闭次数用 skip=5(1分钟粒度)**;`page_size` 上限 100,按 `total` 翻页
+- 返回结构:`result.datalist.records[]`,每条含 `ttime` + `readData`(参数键值对)+ `subDataList`(明细,与 readData 同字段)
+- 各类型历史 readData 可用字段:
+  | 类型 | 历史字段 |
+  |---|---|
+  | windrect | va(风速), m3(风量,m³/min) |
+  | fanlocal | windQuantity1(供风量) — 仅此一个 |
+  | gate | frontGateOpen/rearGateOpen(0/1 或 文本"打开/关闭" 混排,需归一化), frontRearDP, sourcePressure |
+  | window | fWindowM3, rWindowM3, frontPresentValue, rearPresentValue, frontArea, rearArea, pa1 |
+  | modelsensor_multi | pa, temperature, humidity, density |
+
+### 2. 平台已知缺陷(回退策略:实时快照补齐 + 日报注明)
+- **主通风机、压差传感器(modelsensor_dp)**:历史接口 total 有计数但 records 恒为空 → 用 `query_device_data_by_id` 实时快照呈现当前值
+- **局部通风机**:历史仅有供风量;频率/电流/电压/进出口风量用实时快照
+- **风窗**:历史无角度(OpenDegree)/压差(frontRearDP)/风速(wvel1) → 实时快照
+- **风窗查询超时**:skip=1 全量查询会 408 超时,必须 skip≥4
+- 实时快照关键字段:fanmain→`Fan1/Fan2StartStatus_str`(主备机)、`Fan1_WindSpeed`、`Fan1_m3`(风量)、`Fan1_FanPre`(负压)、`Fan1_MOT1_Current/DCVoltage`;fanlocal→`Fan1FreqHz`、`Fan1_electric_current`、`Fan1_output_voltage`、`inletAirVolume_merge`、`ductOutletAirVolume_merge`;window→`OpenDegree`、`frontRearDP`、`wvel1`;windrect→`forward_str`(风向)
+
+### 3. 报警/异常接口 `get_alarm_log_history`
+- **必须不传 `device_type`**(传大类编码如 gate/windrect 返回空),拉全量后按 `deviceID` 过滤到清单内设备
+- 传 `start_time`/`end_time`(统计日起止)
+- 网络断开类报警**计入**异常(用户确认口径)
+- 状态判定:`endtime` 非空→已解决;`endtime` 空且 `isok=0`→未解决(持续中);`endtime` 空且 `isok=-1`→已恢复(平台未闭环)
+- 持续时长:`endtime−starttime`;未闭环的**截断到统计日 23:59:59**,避免跨天虚增
+
+### 4. 统计与换算
+- 数值列计算 max/min/avg,**最大值、最小值、平均值分三列显示**;风速 2 位小数、风量整数、压差 2 位
+- **主通风机风量单位**:值 <500 判为 m³/s,×60 换算 m³/min(保留整数),换算后直接显示数值、表内不标注
+- **风门开闭次数**:frontGateOpen / rearGateOpen 归一化("打开"/1→开,"关闭"/0→关)后统计 0→1 上升沿次数,前门/后门分列;基于 1 分钟采样,短于采样间隔的动作可能漏计(日报注明)
+- 前一天无历史数据的设备:**保留在表中一行并标注**(不剔除)
+
+## 统一输出模板(顺序与结构固定)
+```
+# 通风设备与传感器运行日报(YYYY-MM-DD)
+> 数据来源 / 统计时段 / 真实设备口径 / 采样粒度 / 生成时间
+一、总体概况(设备总数、无数据数、异常设备数)
+二、主通风机(表:设备|运行机|风速|风量|负压|电流|电压|频率|异常摘要;实时快照)
+三、局部通风机(表:设备|运行机|进口风量*|出口风量*|风速*|频率*|电流*|电压*|异常摘要;不展示供风量——历史字段 windQuantity1 数据异常已弃用)
+四、风门(表:设备|开闭次数(前/后)|采样点数|异常摘要)
+五、风窗(表:设备|角度*|前窗过风量最大/最小/平均|后窗过风量最大/最小/平均|风速*|两侧压差*|异常摘要)
+六、测风装置(表:设备|风速最大/最小/平均|风量最大/最小/平均|风向*|异常摘要)
+七、多参数传感器(表:设备|大气压最大/最小/平均|温度…|湿度…|密度…|异常摘要)
+八、压差传感器(表:设备|压差当前*|历史记录数|异常摘要)
+九、异常明细汇总(表:设备|异常内容|开始时间|持续时长|是否解决;仅清单内设备)
+十、数据说明(固定 6 条口径说明,见脚本 render())
+```
+(最大/最小/平均**各占一列**;"*"=实时快照值,每节表后附"取数说明"与"无历史数据"备注;主通风机风量 m³/s 换算后直接显示数值,表内不做换算标注)
+
+## 输出要求
+1. 保存 Markdown 文件:`vent_operation_daily_report_YYYY-MM-DD.md`(YYYY-MM-DD 为统计日)到当前工作目录
+2. **同时在对话栏完整输出日报全文**(不能只给文件路径)
+3. 有未解决异常时,在对话末尾用醒目方式重点提示
+4. **数据源健康检查**:若全部设备的历史接口与实时接口均无有效返回(平台数据服务故障场景,实测发生过),必须在日报顶部输出"⚠️ 数据源异常警告",说明是平台故障而非设备全部离线,提示恢复后重新导出——防止全空日报被误读为设备全停
+
+## 踩坑记录(实测验证)
+1. ZCode 等 Agent 的 MCP 会话可能中途失效(Session not found)→ 走方式 A 脚本直连不受影响
+2. `deviceinfo` 无 `deviceNum` 列(探测脚本旧注释有误);历史接口 device_num 固定 "1" 即可
+3. 雪花 ID 在 SQL 中必须 `CAST(id AS CHAR)`,否则 JSON 序列化丢精度
+4. `window` 是 MySQL 保留字;`group_concat_max_len=1024` 会静默截断
+5. 报警记录 starttime 的多个时间点在一批设备上重复出现 → 分站级故障特征,异常按设备+类型聚合呈现
+6. 风窗 wvel1 测点部分设备数值量纲异常(数千量级),仅供趋势参考
+7. 主通风机实时快照中松定霍洛(lfjztfj)与明安木独(mamztfj)点表字段不同,字段映射需兼容(详见脚本 row_fanmain)
+8. **fanlocal 历史仅存 windQuantity1 一个参数**(device_num 取 1/Fan1/2/3 返回完全相同);该测点在
+   monitor_params 有**三重冲突定义**:两条"主机风量值(m³/s)"(重复配置)+ 一条"风筒入口风量(m³/min)"
+   (其计算公式 `出口风量/60/0.5027` 实际算出的是风速,如 774→25.64≈fengsu_merge)——数据乱跳乱单位的
+   根源,日报已弃用该字段,改用实时快照
+9. **局扇 inletAirVolume_merge/ductOutletAirVolume_merge 跨日恒定**(785/774)且不随频率(46→23Hz)
+   变化——疑为静态标定值而非真实测量,日报表中标†提示,不可当作可信标杆
+10. **平台数据服务故障特征**(8-18 14:00 起实测):多台设备历史尾部同时恒值(冻结在故障前最后值)、
+    历史/实时接口全空(触发健康检查警告)、故障时段报警批量"未闭环"、多参数传感器 max=min=avg 全等
+    ——注意与"单测点异常"区分,不要误判为设备问题
+11. **多参数传感器历史值大量越界乱码**(温度/湿度数万、大气压负值、密度 int64 溢出 9.2e16)——
+    脚本按物理合理域自动标†(温度-50~80℃、湿度0~100%、大气压30000~110000、密度0.3~3.0)
+
+## 与用户的协作口径(已确认)
+- 独立 skill,不与 vent-device-daily-report(在线率日报)合并
+- 断网类报警计入异常;无数据设备保留在表中并标注
+- 日报输出:md 文件 + 对话栏全文双呈现

+ 2 - 0
vent-operation-daily-report/requirements.txt

@@ -0,0 +1,2 @@
+requirements 
+fastmcp>=2.0.0

+ 146 - 0
vent-operation-daily-report/sample/vent_operation_daily_report_2026-08-18.md

@@ -0,0 +1,146 @@
+# 通风设备与传感器运行日报(2026-08-18)
+
+> 数据来源:通防管控平台 MCP(vent 库 + 历史监测接口)  
+> 统计时段:2026-08-18 00:00:00 ~ 23:59:59 | 真实设备口径:isBusSup=0/NULL 且 status=1  
+> 历史采样:10 分钟粒度(风门开闭 1 分钟粒度)| 生成时间:2026-08-19 10:26:40
+
+## 一、总体概况
+
+- 真实设备总数:**56** 台
+- 前一天无历史数据:**16** 台(表中已标注原因)
+- 存在异常记录:**7** 台(含网络断开类)
+
+## 二、主通风机(2 台)
+
+| 设备名称 | 运行机 | 风速(m/s) | 风量(m³/min) | 负压 | 电流(A) | 电压(V) | 频率/转速 | 异常摘要 |
+|---|---|---|---|---|---|---|---|---|
+| 松定霍洛回风井系统 | 主机运行 | 70.84 | 5630 | 59.82 | 18.92 | 83.21 | — | 一氧化碳含量高于限值×14次(10:45:10~15:13:20,最长13小时14分,未解决(持续中)) |
+| 明安木独回风井系统 | 主机运行 | — | 1974 | -3.92 | 12.97 | 966.23 | 73.36 | 无 |
+
+> 取数说明:实时快照(历史明细接口无记录)
+> 无历史数据:松定霍洛回风井系统:实时快照(历史明细接口无记录);明安木独回风井系统:实时快照(历史明细接口无记录)
+
+## 三、局部通风机(2 台)
+
+| 设备名称 | 运行机 | 进口风量(m³/min)* | 出口风量(m³/min)* | 风速(m/s)* | 频率(Hz)* | 电流(A)* | 电压(V)* | 异常摘要 |
+|---|---|---|---|---|---|---|---|---|
+| 81205胶运 | 主机运行 | 785† | 774† | 6.2 | 30.79 | 29.44 | 704.54 | 无 |
+| 81206辅运 | 主机运行 | 774† | 764† | 7.1 | 23.0 | 24.44 | 597.0 | 无 |
+
+> 取数说明:各参数为实时快照;标†的进出口风量跨日恒定(不随频率变化),疑为静态标定值待现场核实;历史仅存 windQuantity1 且该测点三重定义冲突(单位/公式错乱),已弃用
+
+## 四、风门(5 台)
+
+| 设备名称 | 开闭次数(前门/后门) | 采样点数 | 异常摘要 |
+|---|---|---|---|
+| 二盘区2号回风入口 | 0 / 0 | 1441 | 无 |
+| 81206辅运风门 | 0 / 0 | 1441 | 无 |
+| 81207辅运风门 | 0 / 0 | 1441 | 无 |
+| 五盘区主运27联巷风门 | 0 / 0 | 1441 | 无 |
+| 二盘区二号回风绕道过车风门 | 无数据 | 0 | 网络断开×14次(10:45:10~15:13:20,最长18小时35分,已解决) |
+
+> 取数说明:开闭次数基于1441个采样点(1分钟粒度)上升沿统计
+> 无历史数据:二盘区二号回风绕道过车风门:前一天无历史数据
+
+## 五、风窗(6 台)
+
+| 设备名称 | 角度(°)* | 前窗过风量(m³/min)最大 | 前窗过风量最小 | 前窗过风量平均 | 后窗过风量(m³/min)最大 | 后窗过风量最小 | 后窗过风量平均 | 风速(m/s)* | 两侧压差(Pa)* | 异常摘要 |
+|---|---|---|---|---|---|---|---|---|---|---|
+| 六盘区主运大巷东风窗 | 80.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 4274.78 | 14.32 | 无 |
+| 81206胶运1联巷风窗 | — | 255.4 | 255.3 | 255.3 | 1462.0 | 0.0 | 678.2 | 6087.37 | 340.75 | 无 |
+| 81203胶运一联巷分窗 | — | 181.8 | 180.1 | 181.0 | 1299.4 | 0.0 | 646.1 | 5992.52 | 269.1 | 无 |
+| 81206回风绕道 | — | 2953.1 | 2952.1 | 2952.6 | 0.0 | 0.0 | 0.0 | 6272.14 | 23.16 | 无 |
+| 联通辅运大巷64联巷风窗 | — | 433.2 | 433.1 | 433.2 | 0.0 | 0.0 | 0.0 | 5218.46 | 640.54 | 无 |
+| 二盘区辅运巷10联巷 | — | — | — | — | — | — | — | — | — | 网络断开×14次(10:45:10~15:13:20,最长13小时14分,未解决(持续中)) |
+
+> 取数说明:过风量最大/最小/平均为历史统计;角度/风速/两侧压差为实时快照
+> 无历史数据:二盘区辅运巷10联巷:无历史数据,角度等为实时快照
+
+## 六、测风装置(15 台)
+
+| 设备名称 | 风速(m/s)最大 | 风速最小 | 风速平均 | 风量(m³/min)最大 | 风量最小 | 风量平均 | 风向* | 异常摘要 |
+|---|---|---|---|---|---|---|---|---|
+| 二盘区一号回风联巷 | 0.07 | 0.07 | 0.07 | 8150 | 8090 | 8122 | 正风 | 无 |
+| 81204辅运回风绕道测风站 | 0.00 | 0.00 | 0.00 | 3200 | 3200 | 3200 | 正风 | 无 |
+| 五盘区井底辅运回风绕道 | 0.03 | 0.03 | 0.03 | 2950 | 2860 | 2906 | 正风 | 无 |
+| 8号煤联通辅运大巷39-40联巷 | 1.58 | 1.52 | 1.55 | 1310 | 1281 | 1296 | 正风 | 无 |
+| 8号煤1号辅运副巷400m处 | 2.68 | 2.63 | 2.66 | 2130 | 2091 | 2111 | 正风 | 无 |
+| 8号煤1号辅运大巷500m处 | 2.60 | 2.56 | 2.58 | 3410 | 3380 | 3395 | 正风 | 无 |
+| 8号煤1号进风大巷 | 1.66 | 1.60 | 1.63 | 1169 | 1131 | 1151 | 正风 | 无 |
+| 五盘区井底配电点 | 1.29 | 1.25 | 1.27 | 1189 | 1170 | 1180 | 正风 | 无 |
+| 二盘区辅运巷2-3联巷 | 5.33 | 5.27 | 5.30 | 6640 | 6591 | 6615 | 正风 | 无 |
+| 康井辅运平硐 | 7.63 | 7.55 | 7.59 | 6840 | 6830 | 6835 | 正风 | 无 |
+| 桥头辅运平硐 | 7.75 | 7.71 | 7.73 | 8334 | 8260 | 8299 | 正风 | 无 |
+| 8号煤联通胶运大巷39-40联巷 | 0.75 | 0.70 | 0.72 | 624 | 590 | 608 | 正风 | 无 |
+| 81301面二号回撤通道 | 2.24 | 2.18 | 2.21 | 1810 | 1781 | 1795 | 正风 | 无 |
+| 81203辅运4到5联巷 | — | — | — | — | — | — | — | 网络断开×14次(10:45:10~15:13:20,最长18小时35分,已解决) |
+| 81203辅运4到5联巷 | — | — | — | — | — | — | — | 网络断开×14次(10:45:10~15:13:20,最长18小时35分,已解决) |
+
+> 取数说明:风速/风量的最大/最小/平均为历史统计;风向为实时快照
+> 无历史数据:81203辅运4到5联巷:无历史数据;81203辅运4到5联巷:无历史数据
+
+## 七、多参数传感器(21 台)
+
+| 设备名称 | 大气压最大 | 大气压最小 | 大气压平均 | 温度(℃)最大 | 温度最小 | 温度平均 | 湿度(%)最大 | 湿度最小 | 湿度平均 | 密度最大 | 密度最小 | 密度平均 | 异常摘要 |
+|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
+| 22212辅运顺槽 | 86049 | 86049 | 86049 | 71989.0† | 71989.0† | 71989.0† | 71177.0† | 71177.0† | 71177.0† | 0.000† | 0.000† | 0.000† | 无 |
+| 22213主运顺槽5联巷 | 79902 | 79902 | 79902 | 21338.0† | 21338.0† | 21338.0† | 71410.0† | 71410.0† | 71410.0† | 0.010† | 0.010† | 0.010† | 无 |
+| 22211辅运顺槽 | -8466† | -8466† | -8466† | 9289.0† | 9289.0† | 9289.0† | 49616.0† | 49616.0† | 49616.0† | 0.000† | 0.000† | 0.000† | 无 |
+| 42208辅运顺槽 | 62815 | 62815 | 62815 | 25443.0† | 25443.0† | 25443.0† | 82709.0† | 82709.0† | 82709.0† | 0.010† | 0.010† | 0.010† | 无 |
+| 42上煤辅运上山 | 12772† | 12772† | 12772† | 69551.0† | 69551.0† | 69551.0† | 27238.0† | 27238.0† | 27238.0† | 0.000† | 0.000† | 0.000† | 无 |
+| 22212主运顺槽 | 97489 | 97489 | 97489 | 6547.0† | 6547.0† | 6547.0† | 31475.0† | 31475.0† | 31475.0† | 0.050† | 0.050† | 0.050† | 无 |
+| 12煤组辅运大巷100米 | 80025 | 80025 | 80025 | 2935.0† | 2935.0† | 2935.0† | 38270.0† | 38270.0† | 38270.0† | -27.880† | -27.880† | -27.880† | 无 |
+| 12煤组主运大巷100米 | — | — | — | — | — | — | — | — | — | — | — | — | 无 |
+| 12煤组主要进风上山150米 | 84408 | 84408 | 84408 | 38872.0† | 38872.0† | 38872.0† | 94930.0† | 94930.0† | 94930.0† | 0.010† | 0.010† | 0.010† | 无 |
+| 一水平南1回风大巷2800米 | 94510 | 94510 | 94510 | -779.0† | -779.0† | -779.0† | 64982.0† | 64982.0† | 64982.0† | 92233720368547760.000† | 92233720368547760.000† | 92233720368547808.000† | 无 |
+| 一水平南1回风大巷2700米 | 96757 | 96757 | 96757 | 36894.0† | 36894.0† | 36894.0† | 61495.0† | 61495.0† | 61495.0† | 0.010† | 0.010† | 0.010† | 无 |
+| 一水平中部回风大巷尾段 | 17915† | 17915† | 17915† | 14824.0† | 14824.0† | 14824.0† | 49969.0† | 49969.0† | 49969.0† | 0.000† | 0.000† | 0.000† | 无 |
+| 一水平中部辅运大巷650米 | 27436† | 27436† | 27436† | 57549.0† | 57549.0† | 57549.0† | 6117.0† | 6117.0† | 6117.0† | 0.000† | 0.000† | 0.000† | 无 |
+| 一水平中部主运大巷650米 | 88449 | 88449 | 88449 | 37028.0† | 37028.0† | 37028.0† | -9241.0† | -9241.0† | -9241.0† | 0.010† | 0.010† | 0.010† | 无 |
+| 辅运平硐 | — | — | — | — | — | — | — | — | — | — | — | — | 无 |
+| 42煤暗辅平硐 | — | — | — | — | — | — | — | — | — | — | — | — | 无 |
+| 12煤组回风大巷1300米 | 56446 | 56446 | 56446 | 31762.0† | 31762.0† | 31762.0† | 98085.0† | 98085.0† | 98085.0† | 0.010† | 0.010† | 0.010† | 无 |
+| 12煤组回风大巷1400米 | 47014 | 47014 | 47014 | 10311.0† | 10311.0† | 10311.0† | 39717.0† | 39717.0† | 39717.0† | 0.020† | 0.020† | 0.020† | 无 |
+| 12煤二盘区辅运350米 | — | — | — | — | — | — | — | — | — | — | — | — | 无 |
+| 42煤二盘区辅运大巷800米 | 46466 | 46466 | 46466 | 30129.0† | 30129.0† | 30129.0† | 39931.0† | 39931.0† | 39931.0† | 0.010† | 0.010† | 0.010† | 无 |
+| 42煤二盘区主运大巷800米 | — | — | — | — | — | — | — | — | — | — | — | — | 网络断开×9次(14:10:10~15:19:43,最长9小时49分,未解决(持续中)) |
+
+> 取数说明:各参数为历史统计;标†数值超出物理合理域(测点映射/量纲异常或平台故障期冻结值),仅供参考
+> 无历史数据:12煤组主运大巷100米:无历史数据(设备新接入或未上报);辅运平硐:无历史数据(设备新接入或未上报);42煤暗辅平硐:无历史数据(设备新接入或未上报);12煤二盘区辅运350米:无历史数据(设备新接入或未上报);42煤二盘区主运大巷800米:无历史数据(设备新接入或未上报)
+
+## 八、压差传感器(5 台)
+
+| 设备名称 | 压差(kPa)当前* | 历史记录数 | 异常摘要 |
+|---|---|---|---|
+| 81205辅运风门 | 34.3 | 0 | 无 |
+| 81202辅运风门 | 62.56 | 0 | 无 |
+| 81204辅运风门 | 150.6 | 0 | 无 |
+| 注氮泵站后门 | 90.64 | 0 | 无 |
+| 81203辅运风门 | — | 0 | 网络断开×13次(10:56:40~15:19:49,最长13小时03分,未解决(持续中)) |
+
+> 取数说明:实时快照(历史明细接口无记录)
+> 无历史数据:81205辅运风门:实时快照(历史明细接口无记录);81202辅运风门:实时快照(历史明细接口无记录);81204辅运风门:实时快照(历史明细接口无记录);注氮泵站后门:实时快照(历史明细接口无记录);81203辅运风门:实时快照(历史明细接口无记录)
+
+## 九、异常明细汇总
+
+| 设备 | 异常内容 | 次数 | 首次开始 | 最后开始 | 最长持续 | 是否解决(最新) |
+|---|---|---|---|---|---|---|
+| 二盘区二号回风绕道过车风门 | 网络断开 | 14 | 2026-08-18 10:45:10 | 2026-08-18 15:13:20 | 18小时35分 | 已解决 |
+| 81203辅运4到5联巷 | 网络断开 | 14 | 2026-08-18 10:45:10 | 2026-08-18 15:13:20 | 18小时35分 | 已解决 |
+| 81203辅运4到5联巷 | 网络断开 | 14 | 2026-08-18 10:45:10 | 2026-08-18 15:13:20 | 18小时35分 | 已解决 |
+| 二盘区辅运巷10联巷 | 网络断开 | 14 | 2026-08-18 10:45:10 | 2026-08-18 15:13:20 | 13小时14分 | 未解决(持续中) |
+| 松定霍洛回风井系统 | 一氧化碳含量高于限值 | 14 | 2026-08-18 10:45:10 | 2026-08-18 15:13:20 | 13小时14分 | 未解决(持续中) |
+| 81203辅运风门 | 网络断开 | 13 | 2026-08-18 10:56:40 | 2026-08-18 15:19:49 | 13小时03分 | 未解决(持续中) |
+| 42煤二盘区主运大巷800米 | 网络断开 | 9 | 2026-08-18 14:10:10 | 2026-08-18 15:19:43 | 9小时49分 | 未解决(持续中) |
+
+## 十、数据说明
+
+1. 最大值、最小值、平均值分三列显示;标注 * 的列为实时快照值(历史库无该参数)。
+2. 主通风机与压差传感器:历史明细接口当前无记录体(平台已知问题),数值列为实时快照。
+3. 主通风机风量:值 <500 时按 m³/s 换算为 m³/min(保留整数);电压为直流母线电压。
+4. 风门开闭次数:基于 1 分钟采样上升沿统计,短于采样间隔的开闭动作可能漏计。
+5. 异常口径:平台报警历史全量(含网络断开类),同设备同类型合并为一行;endtime 为空且 isok=0
+   视为未解决,isok=-1 视为已恢复(平台未闭环),时长统一截断至统计日结束。
+6. 局部通风机/风窗的电流电压、角度压差等参数历史库未存储,以实时快照呈现当前值;
+   标†数值为已知可疑项:局扇进出口风量疑为静态标定值(跨日恒定不随频率变化)、风窗风速
+   测点(wvel1)量纲异常、多参数传感器部分测点数值超出物理合理域(映射/量纲错误或平台故障期冻结),均仅供参考。

+ 582 - 0
vent-operation-daily-report/scripts/vent_daily_report.py

@@ -0,0 +1,582 @@
+# -*- coding: utf-8 -*-
+"""
+通风设备与传感器运行日报生成脚本(vent-operation-daily-report skill 配套)
+
+直连通防管控平台 MCP 服务,完成:
+1. 拉取 7 类真实设备(isBusSup=0/NULL 且 status=1)清单
+2. 拉取前一天历史数据(list_ventanaly_monitor_data_days,按类型采样 skip 档位)
+3. 拉取前一天报警历史(get_alarm_log_history,不传 device_type)
+4. 历史缺失类型(主通风机/压差传感器/局扇部分参数/风窗部分参数)用实时快照补齐并注明
+5. 统计 max/min/avg、风门开闭次数、异常明细(时间/持续/是否解决)
+6. 渲染统一格式 Markdown 日报:写文件 + 打印全文(供 Agent 在对话栏转述)
+
+用法:
+  python vent_daily_report.py [--date 2026-08-17] [--url http://39.97.59.228:8071/mcp] [--outdir .]
+"""
+import argparse
+import asyncio
+import io
+import json
+import sys
+from datetime import datetime, timedelta
+
+try:
+    from fastmcp import Client
+except ImportError:
+    print("缺少依赖:pip install fastmcp", file=sys.stderr)
+    sys.exit(1)
+
+if hasattr(sys.stdout, "reconfigure"):
+    sys.stdout.reconfigure(encoding="utf-8")
+
+MCP_URL_DEFAULT = "http://39.97.59.228:8071/mcp"
+
+# 真实设备清单 SQL:7 类,isBusSup=0/NULL,status=1(剔除停用/模拟)
+SQL_DEVICES = (
+    "SELECT devicekind, strtype, CAST(id AS CHAR) AS deviceId, strname, strinstallpos "
+    "FROM deviceinfo "
+    "WHERE ((devicekind IN ('fanmain','fanlocal','gate','window','windrect') "
+    "        AND (is_bus_sup = 0 OR is_bus_sup IS NULL)) "
+    "       OR strtype IN ('modelsensor_multi','modelsensor_dp')) "
+    "AND status = 1 "
+    "ORDER BY devicekind, id"
+)
+
+# 各类型历史采样档位(实测:skip=5→1min/1441条,skip=6→10min/145条)
+SKIP_BY_KIND = {
+    "fanmain": 6, "fanlocal": 6, "window": 6, "windrect": 6,
+    "modelsensor_multi": 6, "modelsensor_dp": 6,
+    "gate": 5,  # 风门开闭次数需更细采样(1min)
+}
+PAGE_SIZE = 100
+
+KIND_TITLE = {
+    "fanmain": "主通风机", "fanlocal": "局部通风机", "gate": "风门",
+    "window": "风窗", "windrect": "测风装置",
+    "modelsensor_multi": "多参数传感器", "modelsensor_dp": "压差传感器",
+}
+
+
+# ---------------- MCP 基础调用 ----------------
+
+async def call_json(client, tool, args, retries=2):
+    for i in range(retries + 1):
+        try:
+            r = await client.call_tool(tool, args)
+            txt = r.content[0].text
+            try:
+                return json.loads(txt)
+            except Exception:
+                return {"success": False, "message": txt[:200]}
+        except Exception as e:
+            if i == retries:
+                return {"success": False, "message": f"call failed: {e}"}
+            await asyncio.sleep(1.5 * (i + 1))
+
+
+async def fetch_history(client, strtype, device_id, begin, end, skip):
+    """分页拉取一台设备全天历史,返回 records 列表(可能为空)。"""
+    records, page = [], 1
+    while page <= 60:  # 防御上限
+        d = await call_json(client, "list_ventanaly_monitor_data_days", {
+            "strtype": strtype, "gdeviceids": str(device_id), "device_num": "1",
+            "ttime_begin": begin, "ttime_end": end,
+            "skip": skip, "page_no": page, "page_size": PAGE_SIZE})
+        dl = ((d or {}).get("result") or {}).get("datalist") or {}
+        recs = dl.get("records") or []
+        records.extend(recs)
+        total = dl.get("total") or 0
+        if not recs or len(records) >= total:
+            break
+        page += 1
+    return records
+
+
+async def fetch_realtime(client, device_id):
+    d = await call_json(client, "query_device_data_by_id",
+                        {"device_id": str(device_id), "page_size": 1})
+    try:
+        dev = ((d or {}).get("result") or {}).get("devices")[0]
+        return dev or {}
+    except Exception:
+        return {}
+
+
+async def fetch_alarms(client, begin, end):
+    d = await call_json(client, "get_alarm_log_history",
+                        {"start_time": begin, "end_time": end})
+    r = (d or {}).get("result")
+    return r if isinstance(r, list) else []
+
+
+# ---------------- 统计工具 ----------------
+
+def to_num(v):
+    if v is None:
+        return None
+    s = str(v).strip()
+    if s in ("", "-", "null", "None"):
+        return None
+    try:
+        return float(s)
+    except ValueError:
+        return None
+
+
+def mma(vals):
+    """max/min/avg,忽略 None。"""
+    xs = [v for v in vals if v is not None]
+    if not xs:
+        return None
+    return max(xs), min(xs), sum(xs) / len(xs)
+
+
+def fmt_mma3(m, dec=2):
+    """返回 [最大值, 最小值, 平均值] 三个显示串,无数据时全为 —。"""
+    if not m:
+        return ["—", "—", "—"]
+    mx, mn, av = m
+    return [f"{mx:.{dec}f}", f"{mn:.{dec}f}", f"{av:.{dec}f}"]
+
+
+def parse_dt(s):
+    if not s:
+        return None
+    for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"):
+        try:
+            return datetime.strptime(str(s)[:19], fmt)
+        except ValueError:
+            continue
+    return None
+
+
+def dur_str(sec):
+    if sec is None:
+        return "—"
+    sec = int(sec)
+    h, m = sec // 3600, sec % 3600 // 60
+    return f"{h}小时{m:02d}分" if h else f"{m}分钟"
+
+
+def gate_state(v):
+    """风门开闭态归一化:开=1 关=0 未知=None(历史 readData 存在 文本/数字 混排)。"""
+    s = str(v).strip()
+    if s in ("1", "打开", "开"):
+        return 1
+    if s in ("0", "关闭", "关"):
+        return 0
+    return None
+
+
+def fan_m3(v):
+    """主通风机风量单位归一:值过小视为 m3/s 转 m3/min(用户规则,保留整数)。"""
+    n = to_num(v)
+    if n is None:
+        return None, False
+    if n < 500:  # 经验阈值:回风井风量 m3/min 通常远大于 500
+        return round(n * 60), True
+    return round(n), False
+
+
+# ---------------- 各类型行构建 ----------------
+
+def row_fanmain(dev, hist, rt, alarms):
+    rd = (rt or {}).get("realTimeData") or {}
+    main = rd.get("Fan1StartStatus_str") or "—"
+    backup = rd.get("Fan2StartStatus_str") or "—"
+    run = "主机运行" if main == "运行" else (f"备机运行(Fan2={backup})" if backup == "运行" else "无运行机")
+    m3, _converted = fan_m3(rd.get("Fan1_m3") if main == "运行" else rd.get("Fan2_m3"))
+    ws = to_num(rd.get("Fan1_WindSpeed") if main == "运行" else rd.get("Fan2_WindSpeed"))
+    pre = to_num(rd.get("Fan1_FanPre") if main == "运行" else rd.get("Fan2_FanPre"))
+    cur = to_num(rd.get("Fan1_MOT1_Current") if main == "运行" else rd.get("Fan2_MOT1_Current"))
+    vol = to_num(rd.get("Fan1_MOT1_DCVoltage") if main == "运行" else rd.get("Fan2_MOT1_DCVoltage"))
+    freq = to_num(rd.get("Fan1_VFD1_OutputSpeed1") or rd.get("Fan1_VFD1_OutputSpeed"))
+    return {
+        "name": dev["strname"], "run": run,
+        "cells": [run, ws if ws is not None else "—", m3 if m3 is not None else "—",
+                  pre if pre is not None else "—", cur if cur is not None else "—",
+                  vol if vol is not None else "—",
+                  f"{freq}" if freq is not None else "—"],
+        "alarms": alarms, "source": "实时快照(历史明细接口无记录)",
+    }
+
+
+def row_fanlocal(dev, hist, rt, alarms):
+    rd = (rt or {}).get("realTimeData") or {}
+    main = rd.get("Fan1StartStatus_str") or "—"
+    run = "主机运行" if main == "运行" else ("备机运行" if rd.get("Fan2StartStatus_str") == "运行" else "无运行机")
+    ws = to_num(rd.get("windSpeed1"))
+    freq = to_num(rd.get("Fan1FreqHz"))
+    cur = to_num(rd.get("Fan1_electric_current"))
+    vol = to_num(rd.get("Fan1_output_voltage"))
+    inlet = to_num(rd.get("inletAirVolume_merge"))
+    outlet = to_num(rd.get("ductOutletAirVolume_merge"))
+    return {
+        "name": dev["strname"], "run": run,
+        "cells": [run,
+                  f"{inlet:.0f}†" if inlet is not None else "—",
+                  f"{outlet:.0f}†" if outlet is not None else "—",
+                  f"{ws}" if ws is not None else "—",
+                  f"{freq}" if freq is not None else "—",
+                  f"{cur}" if cur is not None else "—",
+                  f"{vol}" if vol is not None else "—"],
+        "alarms": alarms,
+        "source": "各参数为实时快照;标†的进出口风量跨日恒定(不随频率变化),疑为静态标定值待现场核实;"
+                  "历史仅存 windQuantity1 且该测点三重定义冲突(单位/公式错乱),已弃用",
+    }
+
+
+def row_gate(dev, hist, rt, alarms):
+    front = back = 0
+    prev_f = prev_b = None
+    for r in hist:
+        rd = r.get("readData") or {}
+        f, b = gate_state(rd.get("frontGateOpen")), gate_state(rd.get("rearGateOpen"))
+        if f is not None and prev_f == 0 and f == 1:
+            front += 1
+        if b is not None and prev_b == 0 and b == 1:
+            back += 1
+        if f is not None:
+            prev_f = f
+        if b is not None:
+            prev_b = b
+    open_cnt = f"{front} / {back}"
+    return {
+        "name": dev["strname"],
+        "cells": [open_cnt if hist else "无数据", len(hist)],
+        "alarms": alarms,
+        "source": f"开闭次数基于{len(hist)}个采样点(1分钟粒度)上升沿统计" if hist else "前一天无历史数据",
+    }
+
+
+def row_window(dev, hist, rt, alarms):
+    rd = (rt or {}).get("realTimeData") or {}
+    f3 = mma([to_num(r.get("readData", {}).get("fWindowM3")) for r in hist])
+    r3 = mma([to_num(r.get("readData", {}).get("rWindowM3")) for r in hist])
+    ang = to_num(rd.get("OpenDegree"))
+    dp = to_num(rd.get("frontRearDP"))
+    wv = to_num(rd.get("wvel1"))
+    return {
+        "name": dev["strname"],
+        "cells": ([f"{ang}" if ang is not None else "—"]
+                  + fmt_mma3(f3, 1) + fmt_mma3(r3, 1)
+                  + [f"{wv}" if wv is not None else "—",
+                     f"{dp}" if dp is not None else "—"]),
+        "alarms": alarms,
+        "source": "过风量最大/最小/平均为历史统计;角度/风速/两侧压差为实时快照" if hist else "无历史数据,角度等为实时快照",
+    }
+
+
+def row_windrect(dev, hist, rt, alarms):
+    rd = (rt or {}).get("realTimeData") or {}
+    va = mma([to_num(r.get("readData", {}).get("va")) for r in hist])
+    m3 = mma([to_num(r.get("readData", {}).get("m3")) for r in hist])
+    fwd = rd.get("forward_str") or "—"
+    return {
+        "name": dev["strname"],
+        "cells": fmt_mma3(va) + fmt_mma3(m3, 0) + [fwd],
+        "alarms": alarms,
+        "source": "风速/风量的最大/最小/平均为历史统计;风向为实时快照" if hist else "无历史数据",
+    }
+
+
+# 多参数传感器各参数物理合理域(越界标†:映射/量纲异常或平台冻结)
+MULTI_VALID = {"pa": (30000, 110000), "temperature": (-50, 80),
+               "humidity": (0, 100), "density": (0.3, 3.0)}
+
+
+def row_multi(dev, hist, rt, alarms):
+    fields = [("pa", 0), ("temperature", 1), ("humidity", 1), ("density", 3)]
+    cells, has_bad = [], False
+    for code, dec in fields:
+        m = mma([to_num(r.get("readData", {}).get(code)) for r in hist])
+        triple = fmt_mma3(m, dec)
+        lo, hi = MULTI_VALID[code]
+        if m and not (lo <= m[1] and m[0] <= hi):
+            triple = [v + "†" for v in triple]
+            has_bad = True
+        cells += triple
+    if not hist:
+        src = "无历史数据(设备新接入或未上报)"
+    elif has_bad:
+        src = "各参数为历史统计;标†数值超出物理合理域(测点映射/量纲异常或平台故障期冻结值),仅供参考"
+    else:
+        src = "各参数的最大/最小/平均为历史统计"
+    return {
+        "name": dev["strname"],
+        "cells": cells,
+        "alarms": alarms,
+        "source": src,
+    }
+
+
+def row_dp(dev, hist, rt, alarms):
+    rd = (rt or {}).get("realTimeData") or {}
+    kpa = to_num(rd.get("kpa"))
+    return {
+        "name": dev["strname"],
+        "cells": [f"{kpa}" if kpa is not None else "—", len(hist)],
+        "alarms": alarms,
+        "source": "实时快照(历史明细接口无记录)",
+    }
+
+
+TABLE_SPECS = {
+    "fanmain": {
+        "headers": ["运行机", "风速(m/s)", "风量(m³/min)", "负压", "电流(A)", "电压(V)", "频率/转速"],
+        "row": row_fanmain,
+    },
+    "fanlocal": {
+        "headers": ["运行机", "进口风量(m³/min)*", "出口风量(m³/min)*", "风速(m/s)*",
+                    "频率(Hz)*", "电流(A)*", "电压(V)*"],
+        "row": row_fanlocal,
+    },
+    "gate": {
+        "headers": ["开闭次数(前门/后门)", "采样点数"],
+        "row": row_gate,
+    },
+    "window": {
+        "headers": ["角度(°)*",
+                    "前窗过风量(m³/min)最大", "前窗过风量最小", "前窗过风量平均",
+                    "后窗过风量(m³/min)最大", "后窗过风量最小", "后窗过风量平均",
+                    "风速(m/s)*", "两侧压差(Pa)*"],
+        "row": row_window,
+    },
+    "windrect": {
+        "headers": ["风速(m/s)最大", "风速最小", "风速平均",
+                    "风量(m³/min)最大", "风量最小", "风量平均", "风向*"],
+        "row": row_windrect,
+    },
+    "modelsensor_multi": {
+        "headers": ["大气压最大", "大气压最小", "大气压平均",
+                    "温度(℃)最大", "温度最小", "温度平均",
+                    "湿度(%)最大", "湿度最小", "湿度平均",
+                    "密度最大", "密度最小", "密度平均"],
+        "row": row_multi,
+    },
+    "modelsensor_dp": {
+        "headers": ["压差(kPa)当前*", "历史记录数"],
+        "row": row_dp,
+    },
+}
+
+
+# ---------------- 报警处理 ----------------
+
+def summarize_alarms(alarm_rows, day_begin, day_end):
+    """按设备聚合报警,同设备同类型合并为一条(次数/首末时间/最长持续/最新状态)。
+
+    时长口径:endtime 为空时截断到统计日结束(次日 00:00),避免跨天虚增;
+    isok=0 且无 endtime → 未解决(持续中);isok=-1 → 平台已恢复但未闭环。
+    """
+    raw = {}
+    for a in alarm_rows:
+        did = str(a.get("deviceID") or a.get("deviceId") or "")
+        if not did:
+            continue
+        st = parse_dt(a.get("starttime"))
+        if st is None:
+            continue
+        et = parse_dt(a.get("endtime"))
+        if et is not None:
+            dur = (et - st).total_seconds()
+            state = "已解决"
+        elif a.get("isok") == 0:
+            dur = (min(day_end, datetime.now()) - st).total_seconds()
+            state = "未解决(持续中)"
+        else:
+            dur = (day_end - st).total_seconds()
+            state = "已恢复(未闭环)"
+        desc = a.get("warDescrip") or a.get("valuename") or "异常"
+        raw.setdefault((did, desc), []).append({"st": st, "dur": dur, "state": state})
+
+    out = {}
+    for (did, desc), lst in raw.items():
+        lst.sort(key=lambda x: x["st"])
+        first, last = lst[0], lst[-1]
+        out.setdefault(did, []).append({
+            "desc": desc,
+            "count": len(lst),
+            "first": first["st"].strftime("%H:%M:%S"),
+            "last": last["st"].strftime("%H:%M:%S"),
+            "dur": dur_str(first["dur"]),
+            "max_dur": dur_str(max(l["dur"] for l in lst)),
+            "state": last["state"],
+            "full_start": first["st"].strftime("%Y-%m-%d %H:%M:%S"),
+            "full_last": last["st"].strftime("%Y-%m-%d %H:%M:%S"),
+        })
+    return out
+
+
+def alarm_brief(items):
+    if not items:
+        return "无"
+    parts = []
+    for i in items[:3]:
+        if i.get("count", 1) > 1:
+            parts.append(f"{i['desc']}×{i['count']}次({i['first']}~{i['last']},最长{i['max_dur']},{i['state']})")
+        else:
+            parts.append(f"{i['desc']}({i['first']}起,{i['dur']},{i['state']})")
+    if len(items) > 3:
+        parts.append(f"等{len(items)}类")
+    return ";".join(parts)
+
+
+# ---------------- 主流程 ----------------
+
+async def run(args):
+    day = datetime.strptime(args.date, "%Y-%m-%d")
+    begin = day.strftime("%Y-%m-%d 00:00:00")
+    end = (day + timedelta(days=1)).strftime("%Y-%m-%d 00:00:00")
+
+    print(f"[1/5] 连接 MCP {args.url} ...")
+    async with Client(args.url) as client:
+        print(f"[2/5] 拉取真实设备清单(统计日 {args.date})...")
+        d = await call_json(client, "execute_sql_query", {"sql_query": SQL_DEVICES})
+        devices = d.get("result") or []
+        if not devices:
+            print("未查到真实设备,退出。原始返回:", json.dumps(d, ensure_ascii=False)[:500])
+            return
+        print(f"      共 {len(devices)} 台真实设备")
+
+        print("[3/5] 拉取前一天报警历史(全量,按设备聚合)...")
+        alarm_rows = await fetch_alarms(client, begin, end)
+        alarms_by_dev = summarize_alarms(alarm_rows, day, day + timedelta(days=1))
+        print(f"      报警记录 {len(alarm_rows)} 条,涉及设备 {len(alarms_by_dev)} 台")
+
+        sections = []
+        stats = {"total": len(devices), "no_data": 0, "abnormal": 0}
+        health = {"hist": False, "rt": False}
+        kind_order = ["fanmain", "fanlocal", "gate", "window", "windrect",
+                      "modelsensor_multi", "modelsensor_dp"]
+        for kind in kind_order:
+            devs = [x for x in devices
+                    if x.get("devicekind") == kind or (kind.startswith("modelsensor") and x.get("strtype") == kind)]
+            if not devs:
+                continue
+            spec = TABLE_SPECS[kind]
+            rows, notes = [], []
+            print(f"[4/5] {KIND_TITLE[kind]} {len(devs)} 台:拉历史+实时...")
+            for dev in devs:
+                did = dev["deviceId"]
+                hist = await fetch_history(client, dev["strtype"], did, begin, end,
+                                           SKIP_BY_KIND.get(dev["devicekind"], 6))
+                need_rt = kind in ("fanmain", "fanlocal", "window", "windrect", "modelsensor_dp")
+                rt = await fetch_realtime(client, did) if need_rt else {}
+                if hist:
+                    health["hist"] = True
+                rd = (rt or {}).get("realTimeData") or {}
+                if rd.get("netStatus") not in (None, "") or rd.get("readTime"):
+                    health["rt"] = True
+                if not hist:
+                    stats["no_data"] += 1
+                alm = alarms_by_dev.get(did, [])
+                if alm:
+                    stats["abnormal"] += 1
+                r = spec["row"](dev, hist, rt, alm)
+                r.setdefault("cells", [])
+                rows.append(r)
+                if not hist:
+                    notes.append(f"{dev['strname']}:{r.get('source') or '前一天无历史数据'}")
+            # 渲染该类型表格
+            lines = [f"## {KIND_TITLE[kind]}({len(devs)} 台)", ""]
+            header = ["设备名称"] + spec["headers"] + ["异常摘要"]
+            lines.append("| " + " | ".join(header) + " |")
+            lines.append("|" + "---|" * len(header))
+            for r in rows:
+                cells = [str(c) for c in r["cells"]]
+                lines.append("| " + " | ".join([r["name"]] + cells + [alarm_brief(r["alarms"])]) + " |")
+            lines.append("")
+            src = next((r["source"] for r in rows if r.get("source")), "")
+            if src:
+                lines.append(f"> 取数说明:{src}")
+            if notes:
+                lines.append("> 无历史数据:" + ";".join(notes))
+            lines.append("")
+            sections.append("\n".join(lines))
+
+        print("[5/5] 渲染日报 ...")
+        md = render(args.date, stats, sections, alarms_by_dev, devices, health)
+        out_file = f"{args.outdir}/vent_operation_daily_report_{args.date}.md"
+        with io.open(out_file, "w", encoding="utf-8") as f:
+            f.write(md)
+        print(f"\n日报已保存:{out_file}\n")
+        print("=" * 30 + " 日报全文 " + "=" * 30)
+        print(md)
+
+
+def render(date, stats, sections, alarms_by_dev, devices, health=None):
+    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+    cn_num = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"]
+    L = []
+    L.append(f"# 通风设备与传感器运行日报({date})")
+    L.append("")
+    L.append(f"> 数据来源:通防管控平台 MCP(vent 库 + 历史监测接口)  ")
+    L.append(f"> 统计时段:{date} 00:00:00 ~ 23:59:59 | 真实设备口径:isBusSup=0/NULL 且 status=1  ")
+    L.append(f"> 历史采样:10 分钟粒度(风门开闭 1 分钟粒度)| 生成时间:{now}")
+    L.append("")
+    # 数据源健康检查:历史与实时接口均无有效返回 → 平台数据服务故障,显式警告防误读
+    if health is not None and not health.get("hist") and not health.get("rt"):
+        L.append("## ⚠️ 数据源异常警告")
+        L.append("")
+        L.append("**历史数据接口与实时数据接口均无有效返回**(全部设备历史记录为空、实时快照为空)。")
+        L.append("这通常是平台数据服务(历史库/实时缓存)故障或维护所致,**并非所有设备同时离线**。")
+        L.append("本日报的数据内容不可用,请确认平台数据服务恢复后重新导出(重新运行本命令即可)。")
+        L.append("")
+    L.append("## 一、总体概况")
+    L.append("")
+    L.append(f"- 真实设备总数:**{stats['total']}** 台")
+    L.append(f"- 前一天无历史数据:**{stats['no_data']}** 台(表中已标注原因)")
+    L.append(f"- 存在异常记录:**{stats['abnormal']}** 台(含网络断开类)")
+    L.append("")
+    cn = 1
+    for s in sections:
+        cn += 1
+        L.append(s.replace("## ", f"## {cn_num[cn - 1]}、", 1))
+    # 异常明细汇总(仅统计清单内 7 类真实设备)
+    cn += 1
+    L.append(f"## {cn_num[cn - 1]}、异常明细汇总")
+    L.append("")
+    L.append("| 设备 | 异常内容 | 次数 | 首次开始 | 最后开始 | 最长持续 | 是否解决(最新) |")
+    L.append("|---|---|---|---|---|---|---|")
+    name_by_id = {d["deviceId"]: d["strname"] for d in devices}
+    n = 0
+    for did, items in alarms_by_dev.items():
+        if did not in name_by_id:
+            continue
+        for i in items:
+            if i.get("count", 1) > 1:
+                L.append(f"| {name_by_id[did]} | {i['desc']} | {i['count']} | {i['full_start']} | {i['full_last']} | {i['max_dur']} | {i['state']} |")
+            else:
+                L.append(f"| {name_by_id[did]} | {i['desc']} | 1 | {i['full_start']} | — | {i['dur']} | {i['state']} |")
+            n += 1
+    if n == 0:
+        L.append("| — | 无异常记录 | — | — | — | — | — |")
+    L.append("")
+    cn += 1
+    L.append(f"## {cn_num[cn - 1]}、数据说明")
+    L.append("")
+    L.append("1. 最大值、最小值、平均值分三列显示;标注 * 的列为实时快照值(历史库无该参数)。")
+    L.append("2. 主通风机与压差传感器:历史明细接口当前无记录体(平台已知问题),数值列为实时快照。")
+    L.append("3. 主通风机风量:值 <500 时按 m³/s 换算为 m³/min(保留整数);电压为直流母线电压。")
+    L.append("4. 风门开闭次数:基于 1 分钟采样上升沿统计,短于采样间隔的开闭动作可能漏计。")
+    L.append("5. 异常口径:平台报警历史全量(含网络断开类),同设备同类型合并为一行;endtime 为空且 isok=0")
+    L.append("   视为未解决,isok=-1 视为已恢复(平台未闭环),时长统一截断至统计日结束。")
+    L.append("6. 局部通风机/风窗的电流电压、角度压差等参数历史库未存储,以实时快照呈现当前值;")
+    L.append("   标†数值为已知可疑项:局扇进出口风量疑为静态标定值(跨日恒定不随频率变化)、风窗风速")
+    L.append("   测点(wvel1)量纲异常、多参数传感器部分测点数值超出物理合理域(映射/量纲错误或平台故障期冻结),均仅供参考。")
+    L.append("")
+    return "\n".join(L)
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument("--date", default=(datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d"))
+    ap.add_argument("--url", default=MCP_URL_DEFAULT)
+    ap.add_argument("--outdir", default=".")
+    args = ap.parse_args()
+    asyncio.run(run(args))
+
+
+if __name__ == "__main__":
+    main()

+ 184 - 0
vent-plan-review-calc/SKILL.md

@@ -0,0 +1,184 @@
+---
+name: vent-plan-review-calc
+description: |
+  配风计划需风量计算核验技能。在配风计划审查流程中由「plan-review-calc」审查子智能体执行本维度时启用——逐地点核验需风量计算过程:计算公式是否正确、参数代入是否准确、计算结果是否一致。
+  此技能由审查子智能体在上级(通风对话助手)task 委派时调用。
+tools:
+  - write_todos
+  - calc_face_by_gas
+  - calc_face_by_workers
+  - calc_face_by_wind_speed
+  - calc_face_air_volume_max
+  - calc_tunnel_by_gas
+  - calc_tunnel_by_explosives
+  - calc_tunnel_by_workers
+  - calc_tunnel_by_wind_speed
+  - calc_tunnel_by_vehicle
+  - calc_tunnel_air_volume_max
+  - calc_chamber_by_equipment
+  - calc_chamber_by_wind_speed
+  - calc_other_by_wind_speed
+  - calc_effective_area
+  - calc_total_air_volume
+---
+
+# 配风计划需风量计算核验技能
+
+## 角色定位(role)
+煤矿通风需风量计算核验专家,按照《煤矿安全规程》2025 版和 AQ 1056-2008 标准,逐地点核验配风计划中的需风量计算过程。
+
+> **输入约定**:上级通过 task 委派你执行本维度,并在委派内容中给出配风计划 PDF 全文
+> (或 /results/… 文件指针)。若为文件指针,先用 read_file 读取全文,再按本技能执行。
+
+## ⚠️ OCR 文档扫描须知
+本技能审查的 PDF 文本通过 OCR 从扫描件中提取,存在以下风险:
+1. **数字识别错误**:控顶距、采高、风速、人数等关键参数可能被 OCR 误识,导致计算核验结果偏差。若核验偏差 > 50%,必须标注"疑似 OCR 参数识别错误,建议人工核对原始文档中的参数值"
+2. **表格结构丢失**:参数表可能被 OCR 拆散,需仔细关联参数与对应的工作面名称
+3. **单位丢失**:m³/min、m/s 等单位符号容易在 OCR 中丢失或变形
+
+## 核验总流程
+
+```
+获取配风计划PDF文本
+      │
+      ▼
+提取所有用风地点及其参数
+      │
+      ▼
+按用风地点类型分类
+   ┌──┴──┬──────┐
+   ▼     ▼      ▼
+采煤面  掘进面  硐室/其他
+   │     │      │
+   ▼     ▼      ▼
+逐地点调用计算工具核验
+   │
+   ▼
+生成核验报告(表格形式)
+```
+
+## 各用风地点类型对应的计算公式
+
+### 采煤工作面
+| 计算方式 | 公式 | 说明 |
+|---------|------|------|
+| 按瓦斯涌出量 | Q = 100 × q_gas × K_gas | K_gas: 1.2~1.6,通常取 1.5。<br>⚠️ **系数合并写法**:部分矿井将 100×K_gas 合并为一个系数 K 直接写入配风计划,此时 K = 100×K_gas ≥ 100(如 125 = 100×1.25,150 = 100×1.5),**属于正常范围,不应判定为有误**。核验时需将配风计划中的 K 值反算回 K_gas = K/100 后再对比 |
+| 按二氧化碳涌出量 | Q = 67 × q_co2 × K_co2 | K_co2: 1.2~1.6 |
+| 按人数 | Q = 4 × N | 每人 ≥ 4 m³/min |
+| 按风速验算 | 0.25 × 60 × S ≤ Q ≤ 4.0 × 60 × S | 面风速 0.25~4.0 m/s |
+| 最终取值 | Q = max(上述各值) | 取最大值 |
+
+### 掘进工作面
+| 计算方式 | 公式 | 说明 |
+|---------|------|------|
+| 按瓦斯涌出量 | Q = 100 × q_gas × K_gas | K_gas: 1.8~2.0。<br>⚠️ **系数合并写法**:同上,若配风计划中的系数 K ≥ 100(如 180 = 100×1.8),属于合并写法的正常值,不判有误。反算 K_gas = K/100 |
+| 按二氧化碳涌出量 | Q = 67 × q_co2 × K_co2 | |
+| 按炸药量 | Q = 25 × A | A: 一次爆破最大炸药量 kg |
+| 按人数 | Q = 4 × N | 每人 ≥ 4 m³/min |
+| 按胶轮车 | Q = 4 × ΣP × K | ΣP: 防爆柴油机车总功率 kW, K: 0.5~1.0 |
+| 按风速验算 | 0.25 × 60 × S ≤ Q ≤ 4.0 × 60 × S | 岩巷最低 0.15 m/s |
+| 最终取值 | Q = max(上述各值) | 取最大值 |
+
+### 硐室
+| 计算方式 | 公式 | 说明 |
+|---------|------|------|
+| 按机电设备 | Q = 3600 × θ × ΣN / (ρ × C_p × Δt) | θ: 发热系数 |
+| 按风速验算 | Q = 60 × S × v | v_min: 0.15 m/s (无瓦斯) |
+
+### 其他巷道
+| 计算方式 | 公式 | 说明 |
+|---------|------|------|
+| 按风速 | Q = 60 × S × v | 类型不同风速限值不同 |
+
+## 核验步骤
+
+### Step 1: 提取用风地点信息
+从 PDF 文本中提取每个用风地点的:
+- 地点名称
+- 用风类型(采煤面/掘进面/硐室/其他)
+- 瓦斯涌出量(q_gas)
+- 二氧化碳涌出量(q_co2)
+- 同时工作人数(N)
+- 设计风速(v)
+- 有效断面积(S)或 最大控顶距/最小控顶距/平均采高
+- 配风计划中的需风量值
+
+### Step 2: 调用计算工具
+根据用风地点类型,调用对应的计算工具:
+- 采煤面:调用 `calc_face_air_volume_max` 或各分项计算工具
+- 掘进面:调用 `calc_tunnel_air_volume_max` 或各分项计算工具
+- 硐室:调用 `calc_chamber_by_equipment` 或 `calc_chamber_by_wind_speed`
+- 其他:调用 `calc_other_by_wind_speed`
+
+### Step 3: 对比判定
+将配风计划中的需风量值与工具计算值进行对比:
+- 偏差 ≤ 5%:**一致** ✅
+- 偏差 5%~10%:**符合**(四舍五入差异)✅
+- 偏差 > 10%:**不符** ❌
+
+同时检查:
+- 风速验算是否通过
+- 是否取各方法的最大值
+
+## 输出格式
+
+每个用风地点输出一张核验表:
+
+```
+## 计算核验结果
+
+### 用风地点: {地点名称}({类型})
+
+| 计算方式 | 计算公式 | 配风计划值 | 实际计算值 | 研判结果 |
+|---------|---------|-----------|-----------|---------|
+| 按瓦斯涌出量 | Q = 100 × {q} × {K} | —— | {值} m³/min | —— |
+| 按二氧化碳涌出量 | Q = 67 × {q} × {K} | —— | {值} m³/min | —— |
+| 按人数 | Q = 4 × {N} | —— | {值} m³/min | —— |
+| 按风速验算 | Q = 60 × {S} × {v} | —— | {值} m³/min | —— |
+| **最终取值** | Q = max(上述各值) | **{配风计划值} m³/min** | **{计算值} m³/min** | **{一致/符合/不符}** |
+
+**风速验算**:
+- 最小需风量(v=0.25 m/s):{值} m³/min
+- 最大需风量(v=4.0 m/s):{值} m³/min
+- 验算结果:{通过/不通过}
+
+**审查意见**:{详细说明}
+
+---
+```
+
+## 汇总表
+
+所有地点核验完成后,按以下格式输出汇总表。**关键要求:计算过程摘要中必须包含具体的计算公式和带入计算的值**。
+
+```
+### 计算核验汇总
+
+| 用风地点 | 类型 | 计算方法 | 计算公式(带入数值) | 配风计划值 | 实际计算值 | 偏差 | 研判结果 |
+|---------|------|---------|---------------------|-----------|-----------|------|---------|
+| {名称} | {采煤面/掘进面/硐室/其他} | {按瓦斯/按人数/按风速等} | Q=100×{q}×{K}=100×2.4×1.5={360.0} m³/min | {值} m³/min | {值} m³/min | {百分比} | {一致/符合/不符} |
+| {名称} | ... | ... | ... | ... | ... | ... | ... |
+| **全矿井合计** | | | | **{总值} m³/min** | **{总值} m³/min** | **{百分比}** | **{一致/符合/不符}** |
+```
+
+**汇总表填写说明**:
+- **计算方法**列:填写决定最终需风量的那个计算方法(即取最大值的来源)
+- **计算公式(带入数值)**列:必须完整写出公式→代入具体数值→计算结果。如"Q=100×q×K=100×2.4×1.5=360.0 m³/min"。此列是审查报告的核心,严禁只写结果不写过程
+- **配风计划值**:从 PDF 中提取的该地点需风量值
+- **实际计算值**:调用计算工具得到的结果
+- **偏差**:|配风计划值 - 计算值| / 计算值 × 100%,保留 1 位小数
+- 若某地点有多种计算方法,只列出最终取最大值的那一行,其余方法在详细核验表中展示
+
+## 重要规则
+
+1. **必须调用工具计算**:不能仅凭 LLM 知识计算,必须调用 calc_tools 中的工具函数获取准确计算结果
+2. **列式计算**:每个计算过程都要展示公式、代入数值、计算结果,不能只给结果
+3. **偏差计算**:偏差 = |配风计划值 - 计算值| / 计算值 × 100%
+4. **数据缺失处理**:若 PDF 中缺少某参数,标注"文档中无此数据,无法计算",不编造
+5. **全程使用简体中文**
+6. **禁止暴露内部工具名、函数名**
+7. **⚠️ 瓦斯涌出量系数 K_gas 的两种写法(重要!)**:
+   - **标准写法**:`Q = 100 × q × K_gas`,K_gas 取 1.2~1.6(采煤面)或 1.8~2.0(掘进面)
+   - **合并写法**:`Q = K × q`,将 100×K_gas 合并为一个系数 K,此时 K ≥ 100(如 100、125、150、160 等均属正常)
+   - **判定原则**:配风计划中若系数 ≥ 100,应判定为合并写法,**不判为系数填写错误**。核验时反算:K_gas = 配风计划中的K值 / 100,若反算值在 1.2~1.6(或1.8~2.0)范围内,则计算公式与结果均正确
+   - **常见值参考**:125 = 100×1.25 ✅ | 150 = 100×1.5 ✅ | 160 = 100×1.6 ✅ | 180 = 100×1.8 ✅(掘进面)

+ 167 - 0
vent-plan-review-data/SKILL.md

@@ -0,0 +1,167 @@
+---
+name: vent-plan-review-data
+description: |
+  配风计划数据一致性审查技能。在配风计划审查流程中由「plan-review-data」审查子智能体执行本维度时启用——检查用风地点是否完整列出、瓦斯/CO2数据与等级鉴定报告是否一致、工作面参数与作业规程是否一致、风速与温度数据是否匹配。
+  此技能由审查子智能体在上级(通风对话助手)task 委派时调用。
+tools:
+  - write_todos
+  - query_mining_plan
+  - query_face_procedure
+  - query_gas_report
+  - query_wind_report
+  - calc_gas_emission_from_wind
+---
+
+# 配风计划数据一致性审查技能
+
+## 角色定位(role)
+煤矿配风计划数据一致性审查专家,专注配风计划内部数据与外部数据源(采掘计划、瓦斯鉴定报告、作业规程、测风报表)的交叉校验。
+
+> **输入约定**:上级通过 task 委派你执行本维度,并在委派内容中给出配风计划 PDF 全文
+> (或 /results/… 文件指针)。若为文件指针,先用 read_file 读取全文,再按本技能执行。
+
+## ⚠️ OCR 文档扫描须知
+本技能审查的 PDF 文本通过 OCR(光学字符识别)从扫描件/图片中提取。OCR 技术无法保证 100% 准确,审查时需注意:
+1. **排版错乱**:扫描件表格行列对应关系可能错位
+2. **字符识别错误**:数字(5→6、3→8)、形近字(采→米、硐→同)、特殊符号(m³→m3、°→。)
+3. **数据提取存疑**:若提取的工作面名称、参数值与外部系统查询结果偏差过大(>50%),应标注"疑似 OCR 识别错误,而非实际数据偏差"
+4. **签名/印章干扰**:手写字迹和印章覆盖区域提取结果不可靠
+
+## 可用 MCP 工具
+
+| 工具 | 用途 | 参数 |
+|------|------|------|
+| `query_mining_plan` | 查询采掘计划 → 用风地点完整性 | mine_name, plan_month |
+| `query_face_procedure` | 查询作业规程 → 工作面参数一致性 | mine_name |
+| `query_gas_report` | 查询瓦斯鉴定报告 → 瓦斯/CO2一致性 | mine_name, year |
+| `query_wind_report` | 查询测风报表 → 风速温度匹配 | mine_name, report_month |
+
+> 使用时先从 PDF 中提取煤矿名称和计划月份,再调用对应工具。
+
+## 审查维度与规则
+
+### 1. 用风地点完整性审查
+**检查内容**:采煤工作面、掘进工作面是否在配风计划中完整列出。
+
+**审查规则**:
+- 调用 `query_mining_plan(mine_name, plan_month)` 获取当前在采掘的所有工作面清单
+- 逐项对比配风计划中列出的用风地点
+- 缺漏的工作面标注"XX工作面在采掘计划中存在但配风计划中未列出"
+- 多余的工作面标注"XX工作面在配风计划中列出但采掘计划中未找到"
+
+**判定标准**:
+- ✅ 完整:所有采掘工作面均已列出
+- ⚠️ 部分缺漏:缺少1-2个工作面
+- ❌ 严重缺漏:缺少3个及以上工作面
+
+### 2. 瓦斯与二氧化碳数据一致性校验
+**检查内容**:配风计划中各用风地点的平均绝对瓦斯涌出量、平均绝对二氧化碳涌出量是否与外部数据一致。
+
+**审查规则(双重交叉验证)**:
+
+**A. 与瓦斯等级鉴定报告对比**
+- 调用 `query_gas_report(mine_name, year)` 获取瓦斯鉴定报告数据
+- **不强制要求本年度报告**:近两年(当前年度及上一年度)的鉴定报告均可用于数据对比
+- 优先查询当前年度报告,若无数据则查询上一年度报告(year = 当前年份 - 1)
+- 对比配风计划中的数值,偏差超过 10% 标注"不一致"
+- **必须在审查结论中明确标注数据来源的报告年度**
+
+**B. 与测风报表交叉验证(新增)**
+- 调用 `query_wind_report(mine_name, report_month)` 获取当月测风报表
+- 在测风报表中找到该采煤/掘进工作面的 **回风顺槽(回风巷)** 测点
+- 使用工具 `calc_gas_emission_from_wind` 反算涌出量:
+  - **绝对瓦斯涌出量 = 回风顺槽实测风量 × 瓦斯浓度 / 100**
+  - **绝对二氧化碳涌出量 = 回风顺槽实测风量 × CO₂浓度 / 100**
+- 将反算值与配风计划中的涌出量数值对比
+- 例:回风顺槽实测风量 800 m³/min × 瓦斯浓度 0.3% → 绝对瓦斯涌出量 2.4 m³/min
+
+**对比流程**:
+1. 从配风计划中提取各用风地点的绝对瓦斯涌出量和绝对CO2涌出量
+2. 调用 query_gas_report 获取鉴定报告数据 → 对比(数据来源A)
+3. 调用 query_wind_report → 找到各工作面回风顺槽测点 → 调用 calc_gas_emission_from_wind 反算 → 对比(数据来源B)
+4. 综合判定:两方数据一致则可信度高;单方一致标注"仅与XX数据一致";两方均不一致则判定为"数据存疑"
+
+**判定标准**:
+- ✅ 一致:偏差 ≤ 10%
+- ⚠️ 偏差:偏差 10%~30%
+- ❌ 不一致:偏差 > 30%
+- ❓ 无数据:鉴定报告和测风报表均查询不到该地点数据
+
+### 3. 工作面参数与作业规程一致性校验
+**检查内容**:配风计划中用风地点的工作面长度、平均采高、有效断面积是否与作业规程一致。
+
+**审查规则**:
+- 调用 `query_face_procedure(mine_name)` 获取该用风地点的:最大控顶距、最小控顶距、平均采高
+- 有效断面积计算:S = (最大控顶距 + 最小控顶距) / 2 × 平均采高
+- 对比配风计划中的参数,偏差超过 5% 标注"不一致"
+
+**判定标准**:
+- ✅ 一致:偏差 ≤ 5%
+- ⚠️ 偏差:偏差 5%~15%
+- ❌ 不一致:偏差 > 15%
+
+### 4. 风速与温度数据匹配校验
+**检查内容**:根据当月温度监测数据,检验配风计划中温度系数选择是否合理。
+
+**审查规则**:
+- 调用 `query_wind_report(mine_name, report_month)` 获取当月温度监测数据
+- 温度与风速/需风量系数的关系:
+  - 温度 ≤ 20℃:系数 1.0
+  - 20℃ < 温度 ≤ 23℃:系数 1.0~1.1
+  - 23℃ < 温度 ≤ 26℃:系数 1.1~1.25
+  - 温度 > 26℃:系数 > 1.25
+- 检验配风计划中使用的温度系数是否与温度匹配
+
+**判定标准**:
+- ✅ 合理:系数选择在推荐范围内
+- ⚠️ 偏保守:系数偏大(多配风)
+- ⚠️ 偏激进:系数偏小(少配风)
+- ❌ 不合理:系数选择明显不符
+
+## 输出格式
+
+审查结果按以下格式输出:
+
+```
+## 数据一致性审查结果
+
+### 1. 用风地点完整性
+- 采掘计划中工作面数量:{数量}(数据来源:query_mining_plan)
+- 配风计划中列出数量:{数量}
+- 缺漏工作面:{列表}
+- 多余工作面:{列表}
+- 判定:{完整/部分缺漏/严重缺漏}
+
+### 2. 瓦斯与二氧化碳数据一致性
+- 检查用风地点数量:{数量}
+- 配风计划值 vs 鉴定报告一致:{数量}
+- 配风计划值 vs 测风报表反算一致:{数量}
+- 偏差数量:{数量}
+- 不一致数量:{数量}
+- 无数据数量:{数量}
+- 综合判定:{一致/有偏差/不一致/无数据}
+- 数据来源A:{yyyy年度瓦斯等级鉴定报告}(查询年份:{year})
+- 数据来源B:{yyyy-MM 测风报表回风顺槽反算}
+- 说明:{若当前年度无数据已回退查询上一年度,需注明;若某地点无回风顺槽测点,需注明}
+
+### 3. 工作面参数一致性
+- 检查用风地点数量:{数量}
+- 一致数量:{数量}
+- 偏差详情:{逐地点列出}
+- 判定:{一致/有偏差/不一致}
+- 数据来源:query_face_procedure
+
+### 4. 风速与温度匹配
+- 检查用风地点数量:{数量}
+- 系数合理:{数量}
+- 系数偏保守/偏激进:{数量}
+- 判定:{合理/偏保守/偏激进/不合理}
+- 数据来源:query_wind_report
+```
+
+## 重要规则
+- **必须优先调用 MCP 工具查询外部数据**:审查时第一步就是调用 query_mining_plan、query_gas_report、query_face_procedure、query_wind_report 获取真实数据,仅当工具明确返回错误信息(如连接超时、服务不可达)时才标注"外部数据查询失败"
+- 所有判定必须基于 MCP 工具返回的真实数据,绝不编造数据
+- 全程使用简体中文
+- 禁止暴露内部工具名、函数名
+- 外部数据源 MCP 接口已经就绪,请正常调用

+ 177 - 0
vent-plan-review-form/SKILL.md

@@ -0,0 +1,177 @@
+---
+name: vent-plan-review-form
+description: |
+  配风计划基本形式审查技能。在配风计划审查流程中由「plan-review-form」审查子智能体执行本维度时启用——检查版本是否为当月最新、签字是否齐全、编制时间是否合规、计算过程是否完整、是否存在语病逻辑矛盾。
+  此技能由审查子智能体在上级(通风对话助手)task 委派时调用。
+tools:
+  - write_todos
+  - get_current_time
+---
+
+# 配风计划基本形式审查技能
+
+## 角色定位(role)
+煤矿配风计划形式审查专家,专注文档格式、签字、版本、时间等基本合规性审查。
+
+> **输入约定**:上级通过 task 委派你执行本维度,并在委派内容中给出配风计划 PDF 全文
+> (或 /results/… 文件指针)。若为文件指针,先用 read_file 读取全文,再按本技能执行。
+
+## ⚠️ OCR 文档扫描须知
+本技能审查的 PDF 文本通过 OCR(光学字符识别)从扫描件/图片中提取。OCR 技术无法保证 100% 准确,审查时需注意以下常见问题:
+1. **排版错乱**:表格可能被识别为散落文字,行列对应关系可能错位,需根据上下文推断原始结构
+2. **字符识别错误**:
+   - 形近字误识(如"采"→"米"、"掘"→"掘"缺失偏旁、"硐"→"同")
+   - 数字识别偏差("5"→"6"、"3"→"8"、"0"→"O"、"1"→"l")
+   - 特殊符号丢失(m³→m3、°→。、≥→>)
+3. **签字区域**:手写签字常被 OCR 乱码化或无法识别,仅能参考打印体姓名
+4. **印章覆盖**:公章覆盖的文字区域通常无法正确识别
+5. **表格线丢失**:竖线和横线容易在扫描件中模糊,导致单元格合并或拆分错误
+
+> 审查时若发现明显不合理的数据(如风速 99 m/s、断面积 0.01 m²),应考虑 OCR 识别错误的可能,标注"疑似 OCR 识别错误,建议人工核对原始文档"。
+
+## 审查维度与规则
+
+### 1. 版本审查
+**检查内容**:配风计划封面/标题中标注的月份是否为当前最新版本。
+
+**审查规则**:
+- 首先调用 `get_current_time` 工具获取当前系统日期时间
+- 配风计划应标注编制月份,如"XX煤矿2026年7月配风计划"
+- 将计划月份与 `get_current_time` 返回的当前月份进行对比
+- 计划月份应与当前月份一致(或当月)
+- 若计划月份早于当前月份且无最新修订版,判定为**违规**
+- 若计划月份晚于当前月份(提前编制),标注"提前编制,需确认是否已审批"
+
+**判定标准**:
+- ✅ 合规:计划月份 = 当前月份
+- ⚠️ 待确认:计划月份早于当前月份但有修订说明
+- ❌ 违规:计划月份早于当前月份且无修订
+
+### 2. 签字审查(仅作提示)
+**检查内容**:配风计划中签字栏是否有编制人、通风科长、通风副总、总工程师的签字记录。
+
+**必需签字人**:
+1. **编制人** - 负责编制配风计划的技术人员
+2. **通风科长** - 通风科/通防科负责人
+3. **通风副总** - 通风副总工程师
+4. **总工程师** - 矿总工程师
+
+**审查规则**:
+- **仅检查签字栏是否有签字记录(姓名文字),不做手写体/打印体区分**
+- **不做笔迹鉴定、不做手写体真实性验证**
+- OCR 识别到的签字姓名即可视为"有签字"
+- 缺少任一签字,标注"缺少XX签字",仅作提示,不做违规判定
+- 签字人职务与姓名关系明显不合理时(如总工程师是编制人),标注提示
+
+**判定标准**:
+- ✅ 有签字记录:四类签字栏均有可识别的姓名
+- ⚠️ 提示:部分签字栏为空或无明确姓名
+- ℹ️ 说明:签字审查仅作提示,不纳入违规判定。最终以实际纸质签章文件为准
+
+### 3. 编制时间审查
+**检查内容**:编制日期是否在配风计划执行月份的上一个月。
+
+**审查规则**:
+- 使用 `get_current_time` 工具获取的当前日期作为对比基准
+- 配风计划应在执行前一个月编制完成
+- 例:7月配风计划应在6月编制
+- 编制时间晚于上月,标注"编制时间滞后,可能影响审核流程"
+- 编制时间早于上月过多(超过2个月),标注"编制时间过早,数据可能已失效"
+
+**判定标准**:
+- ✅ 合规:编制时间在执行月份的上一个月
+- ⚠️ 偏差:编制时间偏差在一个月内
+- ❌ 违规:编制时间在执行当月或之后
+
+### 4. 计算过程完整性审查
+**检查内容**:所有用风地点是否均具备完整的需风量计算与验算过程。
+
+**审查规则**:
+- 每个用风地点应有:明确的计算公式 → 参数代入 → 计算结果 → 风速验算
+- 缺少计算过程仅有结果的,标注"计算过程缺失"
+- 仅有部分地点有计算过程,标注"XX地点计算过程缺失"
+- 验算过程缺少的,标注"风速验算缺失"
+
+**判定标准**:
+- ✅ 合规:全部地点均有完整计算与验算过程
+- ⚠️ 部分缺失:部分地点缺少计算过程
+- ❌ 违规:全部地点均无计算过程
+
+### 5. 语病逻辑审查
+**检查内容**:前后文描述是否存在不一致、矛盾、笔误、单位错误等。
+
+**审查规则**(必须逐条列出所有发现的问题):
+- 同一用风地点前后描述不一致(名称不同、参数矛盾)
+- 总风量与各地点风量之和明显不匹配(偏差超过 5% 需列出)
+- 日期格式前后不一致(如混用 2026/7/1 和 2026-07-01)
+- 单位使用不一致或错误(如混用 m³/min 和 m³/s、风速单位写错)
+- 明显笔误(小数点位置错误、数量级异常、错别字)
+- 语义矛盾(如"风速 0.8 m/s 满足要求",但规程最低 0.25 m/s 则确实满足,需核实上下文)
+- 重复内容(同一段落出现两次)
+- 数据引用错误(引用的规程条款号不存在或与内容不符)
+
+**输出要求**:每个问题必须包含:
+- 原文引用(从 PDF 提取的原文片段)
+- 问题描述(哪里不对)
+- 严重程度(严重 / 一般 / 轻微)
+
+**判定标准**:
+- ✅ 无问题:未发现任何语病逻辑问题
+- ⚠️ 轻微:仅有少量不影响理解的笔误
+- ❌ 存在问题:存在影响数据准确性的逻辑矛盾
+
+## 输出格式
+
+审查结果按以下格式输出:
+
+```
+## 基本形式审查结果
+
+### 1. 版本审查
+- 配风计划标注月份:{月份}
+- 当前月份:{月份}
+- 判定:{合规/待确认/违规}
+- 说明:{详细说明}
+
+### 2. 签字审查
+- 编制人:{有/无}
+- 通风科长:{有/无}
+- 通风副总:{有/无}
+- 总工程师:{有/无}
+- 提示:{签字审查仅作提示,不纳入违规判定}
+- 说明:{详细说明}
+
+### 3. 编制时间审查
+- 编制日期:{日期}
+- 计划执行月份:{月份}
+- 判定:{合规/偏差/违规}
+- 说明:{详细说明}
+
+### 4. 计算过程完整性
+- 用风地点总数:{数量}
+- 有完整计算过程:{数量}
+- 缺失计算过程:{数量}
+- 缺失验算:{数量}
+- 判定:{合规/部分缺失/违规}
+- 说明:{详细说明}
+
+### 5. 语病逻辑问题
+- 发现问题总数:{数量}
+- 严重问题:{数量} 条
+- 一般问题:{数量} 条
+- 轻微问题:{数量} 条
+- 问题逐条列表:
+
+| 序号 | 严重程度 | 原文引用 | 问题描述 |
+|------|---------|---------|---------|
+| 1 | {严重/一般/轻微} | {PDF原文片段} | {具体问题说明} |
+| 2 | ... | ... | ... |
+
+- 判定:{无问题/轻微/存在问题}
+```
+
+## 重要规则
+- 所有判定必须基于 PDF 文档中能提取到的内容
+- 未提取到的信息标注"文档中未找到"而非编造
+- 全程使用简体中文
+- 禁止暴露内部工具名、函数名

+ 105 - 0
vent-plan-review/SKILL.md

@@ -0,0 +1,105 @@
+---
+name: vent-plan-review
+description: |
+  配风计划审查顶层总流程技能。当用户上传配风计划 PDF(或消息含「审查/研判/审核配风计划」)要求对配风计划进行审查时启用——组织三个审查子智能体(plan-review-form 基本形式审查、plan-review-data 数据一致性审查、plan-review-calc 计算核验)依次执行,输出六节配风计划审查报告并生成 Word/Markdown 下载。
+tools:
+  - task
+---
+
+# 配风计划审查(总流程)
+
+## 触发条件
+- 用户上传配风计划 PDF 并要求「审查 / 研判 / 审核 / 查一下这份配风计划」,或附件文件名含「配风计划」。
+- 必须调用本技能组织全流程,**不得跳过子智能体委派自行凭印象审查**。
+
+## 总体流程
+
+### 1. 提取全文
+- 从用户消息中取出「文件临时路径」,调用 `extract_uploaded_pdf_text(file_path)`。
+- 返回 JSON 时注意:
+  - 若 `persisted=true`:`content` 仅为前 3000 字符预览,完整全文在 `results_path`(`/results/…` 文件),后续委派时把该指针交给子智能体并提示其先 `read_file` 读取;
+  - 若 `persisted=false`:`content` 即全量文本,可直接放入委派内容。
+- 提取失败或确认不是配风计划文档时,按普通文件分析处理并如实告知用户。
+
+### 2. 确定审查背景
+- 从全文(尤其开头部分)识别:煤矿名称、计划月份。
+- 月份/编制时间的时效性判断以系统提示词注入的当前系统时间为准。
+
+### 3. 并行委派三个审查子智能体(同一轮发起,勿串行)
+三个维度彼此独立、必须**并行**执行。**在同一轮模型输出中一次性发起三个 task 委派**,
+`subagent_type` 分别为:
+
+1. `plan-review-form`(基本形式审查:版本、签字、编制时间、计算过程完整性、语病逻辑)
+2. `plan-review-data`(数据一致性审查:用风地点完整性、瓦斯/CO2 双重交叉验证、工作面参数与作业规程、风速温度匹配)
+3. `plan-review-calc`(需风量计算核验:逐地点公式/参数/结果对比)
+
+> 关键:三个 task 调用要在**同一条消息里同时发出**,让三个子智能体并行运行;
+> 不要「先委派第一个、等它返回后再委派第二个」。发起委派前**不要输出任何文字**
+> (包括「现在开始审查/先委派 XX」等过程旁白,系统会以卡片展示执行状态);
+> 在三个子智能体结果全部返回之前,正文不要输出报告内容。
+
+每个委派的 `description` 必须包含:
+- 本维度职责一句话(对应上述);
+- 配风计划 PDF 全文:未落盘时直接把全文文本放入 description;已落盘时写明 `results_path` 并指示「先 read_file 读取 /results/… 获取全文」;
+- 指示:「先 read_file('/skills/vent-plan-review-form/SKILL.md')」(data/calc 对应换名),严格按其中审查维度、判定规则与输出格式执行;
+- 期望输出:该维度的完整审查报告(含判定与依据),全部简体中文。
+
+### 4. 汇总输出「重点审查问题」回复(正文不再输出六节长文)
+三个子智能体全部完成后,最终回复只输出**重点汇总**(一、概况 / 五、违规项汇总 / 六、修改建议),
+**不要**在正文输出二~四节长文(形式审查详情、数据对比表、计算核验表),也**不要**逐字复述
+子智能体的返回,更**不要**「分段输出全文」。
+
+最终回复格式(标题严格按下文,系统据此把二~四节并入报告文件):
+
+```
+# {煤矿名}配风计划审查报告
+
+## 一、审查概况
+
+| 项目 | 内容 |
+|------|------|
+| 煤矿名称 | {…} |
+| 计划月份 | {…} |
+| 审查结论 | 通过 / 不通过 / 需整改(一句话理由) |
+
+(一到两句总体说明:发现的重点问题数量与性质,可省略细节)
+
+## 五、违规项汇总
+
+### 红线问题
+
+(逐条要点:问题描述 + 涉及的具体用风地点/数据项)
+
+### 一般问题
+
+(逐条要点:问题描述 + 涉及的具体用风地点/数据项)
+
+## 六、修改建议
+
+(每条对应上述一个违规项,给出具体可操作的修改方案要点)
+
+> 完整的形式审查 / 数据一致性审查 / 计算核验详情已写入下方下载的 Word 审查报告。
+```
+
+要求:
+- 红线问题 = 硬性违规;一般问题 = 需整改;每条违规至少对应一条可操作建议。
+- **正文禁止输出二~四节长文**:二~四节内容只作为下面 save_word_report 的「文件内容」,不要输出到聊天正文。
+- **最后一步(关键)**:调用 `save_word_report(content=完整六节md)` 导出公文 Word 报告,其中 content 必须是完整六节 Markdown:
+  - `# {煤矿名}配风计划审查报告`
+  - `## 一、审查概况`(你的汇总内容)
+  - `## 二、形式审查结果`(**逐字嵌入** plan-review-form 返回原文)
+  - `## 三、数据一致性审查结果`(**逐字嵌入** plan-review-data 返回原文)
+  - `## 四、计算核验结果`(**逐字嵌入** plan-review-calc 返回原文,含逐地点核验表与汇总表)
+  - `## 五、违规项汇总`(你的汇总内容)
+  - `## 六、修改建议`(你的汇总内容)
+  二~四节必须使用子智能体返回原文,禁止改写、禁止「见上文/详见附件/内容过长已省略」等托词——该 Word 就是用户下载的完整六节报告。
+- `save_word_report` 保存完成后,**在回复中向用户提供下载链接**(使用工具返回的 download_url),并概述重点审查问题。
+- **不要调用 save_report / save_review_docx**:详细内容只通过 save_word_report 导出 Word(save_report 会额外产出 md、save_review_docx 已下线)。
+- 若某维度子智能体执行失败/缺失,在「一、审查概况」与报告中如实注明,不编造。
+
+## 红线
+1. 所有判定必须基于 PDF 提取文本与子智能体/工具返回的真实数据,绝不编造;数据缺失标注「文档中未找到」。
+2. 引用规程必须真实并附具体条款来源。
+3. 外部数据(采掘计划/瓦斯鉴定/作业规程/测风报表)查询失败时如实说明,不得臆测数据。
+4. 禁止向用户暴露内部工具名、函数名、子智能体名、技能标识。
+5. 全程使用简体中文。

+ 56 - 0
vent-resistance-check/README.md

@@ -0,0 +1,56 @@
+# vent-resistance-check — 矿井通风系统阻力检查技能
+
+计算回风系统关键(最大)阻力路线及总阻力,逐段核验后按固定模板输出报告,并结合云通防知识库做问题与隐患分析。已在布尔台(松定霍洛/明安木独)、保德(枣林/刘家堰)两矿四套回风系统实测验证。
+
+- 版本:1.0(2026-08-18)
+- 数据与单位规范:风量 m³/min(库值×60,保留 1 位小数);阻力取整 Pa;风速 m/s
+
+## 目录结构
+
+```
+vent-resistance-check/
+├── SKILL.md                        # 主流程 + 技术红线(必读)
+├── README.md                       # 本文件:安装与使用
+├── references/
+│   ├── capability-map.md           # 能力映射层:C1~C7 能力契约 + 跨环境适配 + 降级策略
+│   ├── report-template.md          # 固定报告模板 + 成稿规则(终版报告必须干净)
+│   └── hazard-checklist.md         # 隐患判据:规程条款 + 工程经验 B1~B8 + 数据治理核查
+└── scripts/
+    └── mcp_call.js                 # MCP 直连应急取数脚本(Node.js ≥18,会话失效时使用)
+```
+
+## 安装
+
+**ZCode**:将整个 `vent-resistance-check/` 文件夹放入用户技能目录 `~/.agents/skills/`(Windows:`C:\Users\<用户名>\.agents\skills\`),新开会话即可触发;也可用 `/vent-resistance-check` 显式调用。
+
+**Claude Code / 其他支持 Agent Skills 规范的工具**:放入其对应的 skills 目录(如 `~/.claude/skills/`),规范相同(SKILL.md + references 渐进加载)。
+
+**Dify / Coze / 自研智能体等**:将 `SKILL.md` 全文作为系统提示词或工作流指令注入;三个 references 文件按需一并注入(建议至少包含 report-template.md 与 capability-map.md)。
+
+## 依赖与适配
+
+- **首选**:已接入 TF-MCP(通防管控平台 MCP,HTTP 传输)的环境,开箱即用。
+- **无 TF-MCP 的环境**:按 `references/capability-map.md` 的 C1~C7 能力清单映射等价工具(MCP/HTTP API/CLI/内部函数均可)。唯一不可降级的是 C4(最大阻力路线解算引擎)。
+- **应急通道**:MCP 客户端会话失效("Session not found")时,用 `scripts/mcp_call.js` 直连服务端取数:
+  ```
+  node scripts/mcp_call.js execute_sql_query "SELECT ..."
+  node scripts/mcp_call.js get_out_shafts "model_id=2012326636757958658"
+  node scripts/mcp_call.js get_max_resistance_path "model_id=2012326636757958658 node_id=3958"
+  ```
+  (脚本顶部 `BASE` 常量按实际部署修改端点)
+
+## 使用示例
+
+- “帮我计算松定霍洛回风系统的关键阻力路线及总阻力”
+- “分析布尔台煤矿两个主通风机关键阻力路线和总阻力”(未指明系统时自动遍历全部回风系统)
+- “帮我分析保德煤矿两回风系统关键阻力路线和总阻力”(按矿井名定位模型)
+
+输出:对话内按固定模板展示(总体结论/路线走向/Top10/分段构成/注意事项/隐患分析),并在当前工作目录 `vent-resistance-report/` 落盘带巷道明细附录的 Markdown 报告。
+
+## 关键约束(详见 SKILL.md 技术红线)
+
+1. 模型/巷道大整数 ID(>2^53)禁止经 JSON number 通道传输(字符串或 SQL 字面量);
+2. 回风井巷道 ID ≠ 节点 ID,解算终点取风硐 toId;
+3. `query_wind_by_tunid` 不按模型过滤,报告数值一律以 nModelID 过滤的 SQL 为准;
+4. Σ各段阻力与解算值偏差 ≤1% 才准出报告;段数取 path.length;
+5. 风量 m³/min、阻力取整、报告为干净终稿(无修订痕迹/过程叙述/引用块)。

+ 92 - 0
vent-resistance-check/SKILL.md

@@ -0,0 +1,92 @@
+---
+name: vent-resistance-check
+description: "煤矿通风系统关键阻力路线计算与阻力检查(通防管控平台 tf_mcp + 云通防知识库隐患分析)。当用户提出计算某回风井/回风系统的关键阻力路线及总阻力、最大阻力路线、通风阻力检查、全矿井各回风系统阻力对比等需求时使用。覆盖:解析回风系统名称(未指明则计算全部回风系统)、取平台默认模型、确定回风井出口节点、解算最大阻力路线、巷道名称映射与逐段阻力核验、固定模板输出(总体结论/路线走向/Top10高阻力巷道/分段阻力构成/注意事项)、结合云通防知识库做问题与隐患分析(合规项/隐患/建议动作)、报告落盘为 Markdown。跨工具通用:无 TF-MCP 环境按能力映射层适配。"
+---
+
+# 矿井通风系统阻力检查(关键阻力路线 + 隐患分析)
+
+基于通防管控平台解算引擎计算回风系统关键(最大)阻力路线及总阻力,核验后按固定模板输出,并结合云通防知识库分析问题与隐患。
+
+## 适用场景 / 触发词
+
+"帮我计算{某回风井/回风系统}的关键阻力路线及总阻力"、"最大阻力路线"、"通风阻力检查"、"全矿井回风系统阻力分析/对比"。
+
+- 用户指明回风系统(如"松定霍洛")→ 只算该系统;
+- 未指明 → 计算全矿井**所有**回风系统,逐个完整展示,最后统一汇总共性问题。
+
+## 前置条件与能力要求
+
+默认在已接入 TF-MCP(通防管控平台 MCP)的环境运行。其他智能体工具(Claude Code / Cursor / Dify / Coze / 自研 Agent 等)先按 `references/capability-map.md` 把能力清单映射为本环境的等价工具,再执行本流程。
+
+- **必需能力**:取默认模型、取回风井列表、查单条巷道详情、最大阻力路线解算、批量查巷道属性(SQL)、档案/知识库检索。
+- **增强能力(可缺失,超时或缺失不阻塞主流程)**:网络故障诊断、三区阻力分布、控风决策数据、设备实时数据。
+
+## 关键技术红线(必须遵守)
+
+1. **模型 ID 是大整数(> 2^53)**:经 JSON number 传参会精度失真。**所有模型 ID 参数一律传字符串**——本地工具层已统一(`query_tunnel_list` / `get_model_wind` / `get_sensor_wind` / `query_tunnels_by_model` 入参均为字符串,内部自动转换为服务端整数 schema;旧接口 `get_tun_list_by_modelid` 已下线)。SQL 中模型 ID 作为数字字面量直接书写无精度问题。
+2. **回风井 ID ≠ 节点 ID**:get_out_shafts 返回的是**巷道 ID**。最大阻力路线接口需要的是**节点 ID = 风硐巷道的 toId**(connectType="连接到地面"一侧,即主风机侧出口)。
+3. **巷道也有大 ID(> 2^53 的后补录巷道)**:整数参数的按巷道 ID 查询只对小 ID 可靠;批量取属性一律走 SQL(IN 列表 + ORDER BY FIELD 保持路线顺序)。
+4. **阻力口径**:fHTotal = 摩擦阻力 + 局部/调节阻力。总阻力、Top10、分段构成统一用 **fHTotal**;同时记录 fHFric,用于识别调节设施阻力(fHTotal − fHFric 差值大且 nWindowID > 0 → 调节风窗,属主动控风措施,不计入降阻对象,但计入能耗分析)。
+5. **必须核验**:路线各段 fHTotal 求和与解算接口 fMaxH 相对偏差 ≤ 1%(各段 0.1 Pa 舍入累积的微小偏差可接受)。核验不通过必须查明原因,不得出报告。
+6. **SQL 保留字**:`window` 表名必须加反引号。
+7. **query_wind_by_tunid 不按模型过滤**:该接口可能返回其他模型同ID巷道的记录(实测曾把模板模型 800014 的风量/风速/长度当成当前模型数据,数值全部失真)。**报告引用的一切数值必须来自带 `nModelID` 过滤的 SQL**;该接口仅用于定位节点ID与名称初判。
+8. **段数直接取 path 数组长度**:巷道段数 = `path.length`,禁止手工清点(实测手工计数出错)。
+9. **显示单位与精度**:报告中风量一律 **m³/min**(= 库值 × 60,保留 1 位小数);阻力类数值(总阻力/摩擦/调节/分段/Top10)一律**四舍五入取整**(Pa);风速保持 m/s;能耗估算用原始 m³/s 值计算后以 kW 取整呈现,禁止先舍入再相乘。
+
+## 执行流程
+
+### 第 1 步:解析意图与选定模型
+1. 从提问中解析回风系统名称(用于后续模糊匹配,如"松定霍洛")。
+2. 取平台默认模型:`get_model_param_pub_list` → `param.records[0].defaultmodelid`,报告中注明模型 ID。仅当用户点名其他模型时才切换;平台有多个模型不主动询问。
+
+### 第 2 步:确定计算对象(每套回风系统)
+1. `get_out_shafts(model_id 字符串)` → 回风井巷道 ID 列表。
+2. 对每个回风井巷道 ID:`query_wind_by_tunid(tun_id)`(小整数,安全)→ 取 `tunnelName / fromId / toId / fQ / connectType`。风硐名称通常含风井名(如"松定霍洛风井风硐"),据此匹配用户指定的系统;`toId` 即解算终点节点。
+3. 用户指定了系统但名称匹配不到 → **列出所有风硐名称让用户确认,不要猜**。
+
+### 第 3 步:解算关键阻力路线
+`get_max_resistance_path(model_id 字符串, node_id = 风硐 toId)` → `fMaxH`(Pa,含调节阻力)+ `path`(巷道 ID 有序序列,进风侧 → 风硐)。
+
+### 第 4 步:巷道属性映射与核验(SQL 优先)
+```sql
+SELECT nTunID, strName, fLength, fQ, fV, fHFric, fHTotal, nWindowID
+FROM tun
+WHERE nModelID = {模型ID字面量} AND nTunID IN ({path 逗号列表})
+ORDER BY FIELD(nTunID, {path 按路线顺序})
+```
+- 核验 ΣfHTotal ≈ fMaxH(偏差 ≤ 1%)。
+- 无 SQL 权限时退化:`query_wind_by_tunid` 逐条查小 ID 巷道;大 ID 巷道查不到时在报告中明确标注"X 条巷道名称缺失",**不得编造名称**。
+- 派生指标:路线长度 = ΣfLength;风机风量 = 风硐 fQ(库内单位 m³/s,报告显示 ×60 换算为 m³/min,保留 1 位小数);净摩擦阻力 = ΣfHFric;调节阻力 = fHTotal − fHFric(nWindowID > 0 确认为风窗)。
+
+### 第 5 步:分段构成归类(按巷道名称关键字 + 路线位置)
+
+| 段 | 归类规则 |
+|---|---|
+| 进风段 | 进风井筒(主斜井/平硐/进风立井)+ 进风大巷 + 途经硐室(变电所/水仓等) |
+| 中部大巷段 | ×水平主运/辅运/进风大巷等主干运输巷 |
+| 用风段 | 顺槽、切眼、工作面、采区内部联巷/绕道 |
+| 回风段 | 回风大巷 + 回风暗立井/回风立井 + 回风立井联络巷 + 风硐 |
+
+归类有歧义时按巷道相对工作面的位置判断;分段边界在报告中写明。四段阻力之和须等于总阻力。
+
+### 第 6 步:按固定模板输出
+严格按 `references/report-template.md` 填充,**禁止增删章节或改变顺序**。多套系统时:每套输出一个完整章节(系统名作章节标题),全部展示完后输出"全矿共性问题与建议"章节(横向对比各系统总阻力/风量/阻力构成,共性隐患合并陈述,个性问题留在各自章节内)。
+
+### 第 7 步:云通防知识库隐患分析
+按三层来源执行,判据见 `references/hazard-checklist.md`:
+1. **平台档案**(第一优先):`get_file_list_by_type` 检索文件中心,重点找与该回风井风机设备绑定的主通风机检验报告(风量/压力/效率)、外部漏风率测定、反风演习记录、通风(反风)设施检查记录。
+2. **平台库表佐证**:SQL 查 `doc_vent_report` 等(核对矿井名与当前模型是否一致,错位数据标注"待核实")。
+3. **内置规程判据兜底**:风速限值 / 漏风率 / 反风率 / 调节阻力占比 / 风机效率等。
+
+每条结论必须标注来源与数据日期;档案与模型对象错位(如报告标注的矿井/风井名与当前系统不符)时如实说明,不得张冠李戴。增强诊断(故障诊断/三区分布/实时数据)超时或不可用时,在"数据来源与局限"中声明"拓扑级诊断未执行",不得声称排除循环风、角联等问题。
+
+### 第 8 步:落盘报告
+在当前工作目录 `vent-resistance-report/` 下生成 `阻力检查报告_模型{模型ID}_{YYYY-MM-DD}.md`:
+- 正文 = 对话展示内容(与模板一致);
+- 附录 = 路线全部巷道明细表(巷道ID/名称/长度/风量/风速/摩擦阻力/总阻力/风窗标记)。
+生成后向用户报告文件路径。
+
+## 完成标准
+- 每套系统:总阻力(含/不含调节阻力双口径)、风机风量、路线长度、路线走向、Top10、分段构成、注意事项齐全;Σ核验通过。
+- 隐患分析:合规项表、问题与隐患、建议动作(按优先级)、数据来源与局限齐全,逐条可溯源。
+- 输出格式与 `references/report-template.md` 完全一致;多系统逐个完整展示、共性问题汇总在最后;报告文件已落盘。

+ 51 - 0
vent-resistance-check/references/capability-map.md

@@ -0,0 +1,51 @@
+# 能力映射层(跨工具适配指引)
+
+本 skill 默认调用 TF-MCP(通防管控平台 MCP)。在其他智能体工具(Claude Code、Cursor、Dify、Coze、自研 Agent 等)中运行时,按下表把"能力"映射为本环境的等价工具(MCP 工具、HTTP API、CLI、内部函数均可),映射不到的能力按"降级策略"处理,并在报告"数据来源与局限"中声明。
+
+## 能力清单
+
+| 编号 | 能力 | TF-MCP 工具 | 输入 → 输出契约 | 降级策略 |
+|---|---|---|---|---|
+| C1 | 取默认通风模型 | `get_model_param_pub_list` | 无入参 → `param.records[0].defaultmodelid`(字符串) | 让用户直接提供模型ID;或从平台配置项 `defaultmodelid` / `homeModelid` 读取 |
+| C2 | 取回风井巷道ID列表 | `get_out_shafts(model_id: 字符串)` | model_id → `{ cnt, ids: [巷道ID...] }` | SQL:`SELECT nTunID FROM tun WHERE nModelID={id} AND nConnectType='0'`(风硐类巷道,connectType=0 连接到地面);或人工提供 |
+| C3 | 单条巷道详情(仅用于定位节点/名称) | `query_wind_by_tunid(tun_id: 小整数)` | tun_id → `{ tunnelName, fromId, toId, fQ, connectType, ... }` | SQL:`SELECT ... FROM tun WHERE nModelID={id} AND nTunID={tunId}`。**警告:该接口不按模型过滤,返回值可能是其他模型同ID巷道的数据,报告取数一律用带 nModelID 过滤的 SQL** |
+| C4 | 最大阻力路线解算(核心,不可降级) | `get_max_resistance_path(model_id: 字符串, node_id: 字符串)` | → `{ bSucced, fMaxH: Pa, path: [巷道ID有序序列], strRemark }` | 无解算引擎时本 skill 无法完成核心计算;可对接通防平台等价 HTTP 接口(入参模型ID+终点节点ID,出参最大阻力与路线) |
+| C5 | 批量巷道属性(含大ID巷道) | `execute_sql_query(SQL)` | 见 SKILL.md 第 4 步 SQL | 逐条调 C3;> 2^53 的巷道ID经整数通道必失真,只能由 SQL/字符串通道取,取不到即在报告中标注缺失,禁止编造 |
+| C6 | 档案/知识库检索 | `get_file_list_by_type(type)` / `get_file_base64_by_id(id)` | 业务类型/文件名 → 档案列表与结构化内容 | 换用本环境文档库/知识库检索"云通防知识库";再无则仅用内置判据(hazard-checklist.md)并声明"未接入现场档案" |
+| C7 | 诊断增强(可选) | `get_model_fault_diagnosis` / `check_model_connect_status` / `check_model_one_dir_cycle` / `check_model_one_dir_node` / `check_model_diagonal_structure` / `get_three_area_distribution` / `get_key_path_decision` / `query_device_realtime_data` | 见各工具描述 | 整体跳过,报告声明"拓扑级诊断未执行",不得声称排除循环风/角联/连通性问题 |
+
+## 精度与参数红线(所有环境通用)
+
+1. **模型ID与后补录巷道ID均可能 > 2^53(JS Number.MAX_SAFE_INTEGER)**:任何经 JSON number 的通道都会舍入失真(实测 2012326636757958658 被舍为 2012326636757958700 导致查空)。
+   - 字符串型参数:传字符串;
+   - 整数型参数的工具:**不要**用来传模型ID或大巷道ID;
+   - SQL:把 ID 作为数字字面量写进 SQL 文本,无精度问题。
+2. **回风井巷道ID ≠ 节点ID**:解算终点用风硐巷道的 `toId`(connectType="0"/连接到地面一侧,主风机侧)。
+3. **SQL 保留字**:`window` 表必须写反引号。
+4. **路线顺序保持**:MySQL 用 `ORDER BY FIELD(nTunID, id1, id2, ...)`;其他数据库用 CASE WHEN 或取回后在应用层按 path 序重排。
+5. **诊断类接口可能超时**(大模型计算重,实测 30s 超时并拖垮会话):超时一次后不要无限重试,按降级策略处理并如实标注。
+6. **TF-MCP 为 HTTP(streamable http)传输**:"Session not found" 表示服务端重启导致客户端会话失效,客户端重试无效。恢复方式:宿主端重连(ZCode:Settings → MCP 重连或重启会话);应急可用本 skill 自带的 `scripts/mcp_call.js`(相对 SKILL.md 所在目录,Node.js ≥18)直连端点取数:`node scripts/mcp_call.js <工具名> "参数"`,其内部完成 initialize → notifications/initialized → tools/call 的完整握手。
+7. **数据库风量单位为 m³/s**(fQ/fFixQ):能力契约与取数保持平台原始单位;**报告呈现前 ×60 转为 m³/min(保留 1 位小数)**,阻力类数值显示取整(Pa),风速保持 m/s。转换只发生在报告呈现层。
+
+## C4 输出契约样例(自行实现解算对接时对齐此结构)
+
+```json
+{
+  "bSucced": 1,
+  "fMaxH": 1077.756085,
+  "path": [37, 41, 43, "...", 3217, 3957],
+  "strRemark": "成功获得最大阻力路线"
+}
+```
+
+- `fMaxH`:单位 Pa,**含调节(风窗)阻力**的总阻力;
+- `path`:从进风侧起点到回风井风硐的巷道ID有序序列。
+
+## vent 库核心表速查
+
+| 表 | 用途 | 关键字段 |
+|---|---|---|
+| `tun` | 巷道(属性+解算结果) | nTunID, nModelID, strName, nFromID, nToID, fLength, fQ, fV, fHFric, fHTotal, nWindowID, nConnectType, fCoff |
+| `node` | 节点 | 节点ID 与 tun.nFromID/nToID 对应 |
+| `doc_vent_report` | 测风报表主表 | mine_name, report_date, 矿井总进/排风量、有效风量率 |
+| `file_server_info`(经 MCP 文件接口访问) | 文件中心 | fileName, fileContent(结构化档案数据), deviceId 绑定 |

+ 53 - 0
vent-resistance-check/references/hazard-checklist.md

@@ -0,0 +1,53 @@
+# 隐患分析判据清单(云通防知识库判据层)
+
+隐患分析按 SKILL.md 第 7 步三层来源取数,用本清单判据逐项比对。**每条结论必须标注来源**,标签限定为以下四种之一:
+`[规程条款]`、`[工程经验]`、`[平台解算]`、`[档案: 名称 yyyy-mm-dd]`。
+
+## A. 规程类判据(《煤矿安全规程》常用条目)
+
+> 引用口径以现行《煤矿安全规程》为准;skill 不复述条文号,标注"规程条款"并在报告括注限值。
+
+### A1 井巷风速限值(m/s)
+
+| 井巷类别 | 最高允许风速 |
+|---|---|
+| 无提升设备的风井和风硐 | 15 |
+| 专用升降物料的井筒 | 12 |
+| 风桥 | 10 |
+| 升降人员和物料的井筒、主要进回风巷 | 8 |
+| 输送机巷道、采区进回风巷 | 6 |
+| 采掘工作面及其进、回风顺槽 | 4(煤巷/半煤岩巷最低 0.25) |
+
+判定:路线各段 fV 逐条比对;≥ 限值 90% 即写入"风速临界"预警;超限直接列为问题。
+
+### A2 主要通风机装置
+- 装有主要通风机的出风井口**外部漏风率**:无提升任务 ≤ 5%,有提升任务 ≤ 15%。 `[规程条款]`
+- **反风**:能在 10 min 内实现反风,反风后风量不小于正常风量的 60%。检验演习记录时两项都核。 `[规程条款]`
+- 主要通风机必须安装两套同等能力装置(一套备用)。 `[规程条款]`(档案核对项)
+
+## B. 工程经验判据(非规程强制,用于预警,报告中标 `[工程经验]`)
+
+| 编号 | 判据 | 阈值/口径 | 说明 |
+|---|---|---|---|
+| B1 | 风硐风速偏高 | > 10 m/s | 设计常用 8~10 m/s;偏高提示断面富余小,阻力 ∝ Q²,增风时先成瓶颈 |
+| B2 | 调节风窗阻力占比过大 | 风窗附加阻力 > 系统总阻力 20% | 串在关键路线上的风窗直接增加主扇负担,能耗代价 ≈ Δh × Q(m³/s)(W),报告以 kW 取整呈现(用 m³/min 口径时除以 60);评估调节点迁移至低风量分支 |
+| B3 | 主通风机运行效率偏低 | < 65% | 对旋风机高效区一般 ≥ 70%;低效率+高调节阻力相互印证工况点偏移 |
+| B4 | 两翼(多回风系统)阻力不均衡 | 最大/最小总阻力比 > 1.3 | 全矿瓶颈在高阻力侧,增风改造优先级排序依据 |
+| B5 | 回风段阻力占比偏重 | > 45% | 降阻方向应指向回风段(回风大巷/立井/风硐) |
+| B6 | 通风线路过长 | 关键路线 > 15 km | 长距离高阻力,关注采区接替后的系统简化 |
+| B7 | 摩擦阻力异常集中 | 单段 fHFric > 总阻力 12% | 断面偏小/维护差的长井筒优先核 |
+| B8 | 小开度风窗密集 | 单风窗开口面积 < 0.2 m² 且压降 > 300 Pa | 近乎关死,风流不稳定、易受扰反向;控风管理复杂度高 |
+
+## C. 数据治理核查项(平台侧隐患,标 `[平台解算]` 或 `[档案: ...]`)
+
+1. **测风报表对象一致性**:`doc_vent_report.mine_name` 与当前模型是否同一矿井;风量量级是否与解算风机风量匹配(相差数倍即错位)。错位数据只能"待核实"引用。
+2. **档案绑定一致性**:文件中心档案(检验报告/演习记录等)绑定的 deviceId 与当前回风井风机是否一致;档案内风井名称与当前系统名不符时必须声明。
+3. **实时监测断链**:主风机实时数据接口查不到该设备 → 监测盲区,列为管理隐患。
+4. **诊断接口未完成**:拓扑级诊断(连通性/循环风/单向节点/角联)超时或不可用时,声明"拓扑级诊断未执行",**不得声称排除循环风、角联问题**。
+
+## D. 分析输出规则(与固定模板对应)
+
+1. 合规项表:数值 + 判据 + 结论(合规 / 偏高 / 超限),只收有数据的项。
+2. 问题与隐患:按"现象 → 数据 → 影响"三段写,编号列出,逐条带来源标签。
+3. 建议动作:按优先级编号,必须可执行(对应到具体巷道/设施/数据项),不写空话。
+4. 免责边界:档案数据有错位嫌疑、诊断未完成、实时数据缺失等情况,逐条写入"数据来源与局限";本分析不替代现场核查。

+ 124 - 0
vent-resistance-check/references/report-template.md

@@ -0,0 +1,124 @@
+# 固定报告模板(占位符用 {} 表示,禁止增删章节、改变顺序)
+
+## 成稿规则(终版报告必须干净)
+
+落盘与展示的报告是**完善终稿**,禁止出现任何过程性/修订性痕迹:
+- 不写"更正说明""此前口径""待补"等修订语(数据没取到就在"数据来源与局限"里声明,不用修订体);
+- 不写取数过程叙述(MCP 会话、直连、补数、脚本路径等,这些只进对话不进报告);
+- 不用 Markdown 引用块(`>`)呈现说明文字(渲染带竖线,观感像划线批注),说明一律用正文段落;
+- 不罗列原始巷道ID序列,明细以附录表格呈现(表内含ID即可);
+- 不出现实现层术语(接口返回值、fMaxH、path、Redis key 等),统一表述为"解算值/路线序列/平台数据";
+- 单位与精度统一:风量 **m³/min**(库值 × 60,保留 1 位小数);阻力(总阻力/摩擦/调节/分段/Top10)**取整** Pa;风速 m/s;能耗 kW 取整。
+
+## 一、全矿井多系统包装结构
+
+```
+# 全矿井回风系统阻力检查报告({N}套回风系统)
+
+**计算依据**:通防管控平台模型 {模型ID},逐套以各回风井风硐出口节点为终点做最大阻力路线解算,各段阻力求和与解算值核验通过(偏差 ≤1%)。
+
+## 系统一:{回风系统名}
+{——完整填充"二、单系统模板",全部章节——}
+
+## 系统二:{回风系统名}
+{——完整填充"二、单系统模板"——}
+
+## 全矿共性问题与建议
+{横向对比 + 共性隐患合并陈述,格式见"三、共性汇总模板"}
+```
+
+> 只有一套系统时不使用包装结构,直接输出单系统模板。
+
+## 二、单系统模板
+
+```
+# {系统名}回风系统关键阻力路线计算结果
+
+**计算依据**:通防管控平台模型(模型ID {模型ID}),以{风硐名称}出口节点 {节点ID} 为终点做最大阻力路线解算,并逐段核验(各段阻力之和与接口返回值吻合)。
+
+## 总体结论
+
+| 指标 | 数值 |
+|---|---|
+| 关键阻力路线总阻力 | **{fMaxH 取整} Pa**(含调节阻力 {取整} Pa;净摩擦阻力约 {ΣfHFric 取整} Pa) |
+| 回风井风机风量 | {风硐fQ × 60,保留 1 位小数} m³/min |
+| 路线长度 | 约 {ΣfLength 换算km} km(共 {path条数} 段巷道) |
+
+## 路线走向
+
+**{进风起点井筒}** → {主干大巷} → …(中间节点合并同类巷道,只列转折性节点)→ **{工作面/用风地点}** → {回风大巷} → **{回风暗立井} → {回风立井} → {风硐}**(地面主风机)
+
+## 阻力最大的巷道(Top 10)
+
+| 巷道 | 长度(m) | 风量(m³/min) | 阻力(Pa,取整) | 占比 |
+|---|---|---|---|---|
+| {按fHTotal降序前10,调节风窗巷道在巷道名后标注"(含风窗调节阻力 X Pa)"} | | | | {占fMaxH百分比} |
+
+## 分段阻力构成
+
+- **进风段**({范围描述}):约 {h} Pa,占 {pct}%
+- **中部大巷段**({范围描述}):约 {h} Pa,占 {pct}%
+- **用风段**({范围描述}):约 {h} Pa,占 {pct}%
+- **回风段**({范围描述}):约 {h} Pa,占 {pct}%
+
+(四段之和 = 总阻力)
+
+## 需要注意的事项
+
+1. **调节阻力口径**:{指出路线上风窗的位置与附加阻力值,说明属主动控风措施、不是降阻对象,并给出主扇能耗代价估算(kW = Δh × Q(m³/min) ÷ 60,取整)}。
+2. **摩擦阻力最大巷道**:{按fHFric最大者,给长度/风量/风速}。
+3. **风速与瓶颈**:{风硐、回风立井风速及与限值距离;接近限值90%的巷道点名}。
+4. **对照其他系统**:{其余回风系统总阻力对比一句话}。
+(有则列出,无则省略对应条,不得虚构)
+
+## 隐患分析(云通防知识库)
+
+### 合规项
+
+| 项目 | 数值 | 判据 | 结论 |
+|---|---|---|---|
+| {回风立井风速/风硐风速/大巷风速/外部漏风率/反风率…} | {值} | {限值及出处} | 合规/偏高/超限 |
+
+### 问题与隐患
+
+**{N}. {隐患标题}**
+{现象 → 数据 → 影响,每条标注 [来源:档案名/表名 yyyy-mm-dd | 平台解算 | 规程条款 | 工程经验]}
+
+### 建议动作(按优先级)
+
+1. {可执行动作}
+2. …
+
+### 数据来源与局限
+- {来源清单与日期}
+- {档案/报表与模型对象错位情况;未完成的诊断项;实时数据缺失等,逐条声明}
+```
+
+## 三、共性汇总模板(多系统时追加在最后)
+
+```
+## 全矿共性问题与建议
+
+### 各系统横向对比
+
+| 回风系统 | 总阻力(Pa,取整) | 风机风量(m³/min) | 净摩擦阻力(Pa,取整) | 调节阻力占比 | 路线长度(km) |
+|---|---|---|---|---|---|
+| {系统名} | | | | | |
+
+### 共性问题
+1. {多系统同时存在的问题,合并陈述并注明涉及哪些系统}
+(仅单一系统存在的问题不在此重复,留在各系统章节内)
+
+### 全矿性建议
+1. {按优先级}
+```
+
+## 四、附录(落盘文件中使用,对话展示可省略)
+
+```
+## 附录:{系统名}关键阻力路线巷道明细
+
+| 序号 | 巷道ID | 巷道名称 | 长度(m) | 风量(m³/min) | 风速(m/s) | 摩擦阻力(Pa) | 总阻力(Pa) | 风窗 |
+|---|---|---|---|---|---|---|---|---|
+| {按路线顺序全量列出,风窗列填 nWindowID>0 时"是(ID)"否则"—"} |
+```

+ 54 - 0
vent-resistance-check/scripts/mcp_call.js

@@ -0,0 +1,54 @@
+// TF-MCP 直连通用调用器(客户端会话失效时的应急通道)
+// 用法:
+//   node mcp_call.js execute_sql_query "SELECT ..."
+//   node mcp_call.js get_out_shafts "model_id=2012326636757958658"
+//   node mcp_call.js get_max_resistance_path "model_id=2012326636757958658 node_id=3958"
+// key=value 参数:model_id/node_id 等保持字符串;tun_id/page_no/page_size/skip/include_diagonal 转数字
+const BASE = 'http://39.97.59.228:8071/mcp';
+const NUM_KEYS = new Set(['tun_id', 'page_no', 'page_size', 'skip', 'include_diagonal']);
+
+function buildArgs(tool, raw) {
+  if (tool === 'execute_sql_query') return { sql_query: raw };
+  const args = {};
+  for (const kv of raw.split(/\s+/)) {
+    const i = kv.indexOf('=');
+    if (i < 0) continue;
+    const k = kv.slice(0, i), v = kv.slice(i + 1);
+    args[k] = NUM_KEYS.has(k) ? Number(v) : v;
+  }
+  return args;
+}
+
+async function rpc(sid, body) {
+  const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' };
+  if (sid) headers['Mcp-Session-Id'] = sid;
+  const r = await fetch(BASE, { method: 'POST', headers, body: JSON.stringify(body) });
+  if (r.status === 202) return {};
+  if (!r.ok) throw new Error(`HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);
+  const sid2 = r.headers.get('mcp-session-id');
+  const text = await r.text();
+  const data = text.split('\n').filter(l => l.startsWith('data:')).map(l => l.slice(5).trim()).join('');
+  return sid2 ? { sid: sid2, json: JSON.parse(data) } : { json: JSON.parse(data) };
+}
+
+(async () => {
+  const tool = process.argv[2];
+  const raw = process.argv[3] || '';
+  const init = await rpc(null, { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'zcode-recover', version: '0.1' } } });
+  const sid = init.sid;
+  await rpc(sid, { jsonrpc: '2.0', method: 'notifications/initialized' });
+  const res = await rpc(sid, { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: tool, arguments: buildArgs(tool, raw) } });
+  if (res.json.error) { console.error(JSON.stringify(res.json.error)); process.exit(1); }
+  const txt = res.json.result.content.map(c => c.text).join('');
+  let payload;
+  try { payload = JSON.parse(txt); } catch { console.log(txt); return; }
+  if (payload.success === false) { console.log('TOOL_FAIL:', txt.slice(0, 500)); process.exit(1); }
+  const r = payload.result;
+  if (Array.isArray(r)) {
+    if (r.length && typeof r[0] === 'object') {
+      const keys = Object.keys(r[0]);
+      console.log(keys.join('|'));
+      for (const row of r) console.log(keys.map(k => row[k]).join('|'));
+    } else console.log(r.join(','));
+  } else console.log(typeof r === 'string' ? r : JSON.stringify(r));
+})().catch(e => { console.error('FAILED:', e.message); process.exit(1); });

+ 46 - 0
wind-hazard-diagnosis/README.md

@@ -0,0 +1,46 @@
+# wind-hazard-diagnosis(风速风量监测与隐患判识 skill)
+
+通用 AI Agent skill:查询一通三防管控平台测风装置实时数据,结合云通防知识库判识**风速超限 / 风量不足 / 重大隐患**,输出**固定格式** md 分析报告。
+
+## 触发方式
+
+用户提到以下意图时自动调用:
+- 「查询当前风速风量数据及是否存在问题/隐患」
+- 「测风数据判识」「风速风量有没有问题/隐患」
+- 手动:`/wind-hazard-diagnosis` 或 `run_skill({name: "wind-hazard-diagnosis"})`
+
+## 依赖环境
+
+| 依赖 | 说明 |
+|---|---|
+| MCP 服务器 `tf_mcp` | 一通三防管控平台数据接口(`query_device_info` / `query_device_data_by_id` / `get_needq_all_data` / `execute_sql_query` 等) |
+| 云通防知识库 | `http://39.97.59.228:8067`(Obsidian Vault Knowledge Service,`/api/search`、`/api/retrieve`) |
+
+缺少任一依赖时,skill 会明确提示缺失环节,不静默降级。
+
+## 执行流程(9 步)
+
+1. 获取测风装置全量清单(`device` 表 `windrect` 大类,标注测试/模拟设备)
+2. 逐台获取实时数据(`query_device_data_by_id`,含模拟数据识别)
+3. 判定巷道类型(**绑定信息优先** + 名称关键词推断)→ 确定风速上下限
+4. 云通防知识库佐证限值(《煤矿安全规程》2025版 表6 / AQ1028-2006 表2)
+5. 风速超限判定(超速 / 欠速 / 接近上限)
+6. **需风量关联对比**(巷道ID精确 → 名称模糊匹配 → 未匹配明示),回复逐条写明"监测风量 → 需风量位置/数值/关联依据"供人工排查
+7. 重大隐患比对(《煤矿重大事故隐患判定标准》第八条:供风量 < 需风量 × 75%)
+8. 输出整体结论(总览 + 🔴🟠🟡 分级 + 重大隐患三选一)
+9. `ask` 询问是否生成 md 报告,同意则按**固定格式**输出
+
+## 安装
+
+将 `SKILL.md` 放入 Agent 的 skills 目录(如 `~/.reasonix/skills/wind-hazard-diagnosis/SKILL.md` 或项目 `.reasonix/skills/`),或通过 `install_skill` / `/wind-hazard-diagnosis` 安装。
+
+## 报告固定格式
+
+生成的报告文件名:`测风装置实时数据分析报告_YYYY-MM-DD.md`(同日覆盖)。
+
+固定 9 章节结构(0 报告元信息 → 一 判识框架 → 二 总体结论 → 三 逐台判识明细(13列) → 四 需风量关联明细(7列) → 五 需风量数据全表(6列) → 六 隐患分级清单 → 七 整改建议 → 八 判识依据出处 → 尾部声明),表格列、判定用语(正常/接近上限/超速/欠速/疑似超速/无法判识;一致/不符/严重不符;构成/不构成/数据不足无法判定)均为固定词表,任何一次生成格式一致。
+
+## 维护
+
+- 判识依据:云通防知识库(更新限值/条款时同步知识库即可,skill 每次运行实时检索佐证)
+- 报告模板变更:需同步修改 SKILL.md「固定报告格式」章节

+ 180 - 0
wind-hazard-diagnosis/SKILL.md

@@ -0,0 +1,180 @@
+---
+name: wind-hazard-diagnosis
+description: 风速风量监测与隐患判识:查询平台测风装置实时数据,结合云通防知识库判识风速超限/风量不足/重大隐患,输出固定格式md报告。触发词:"查询当前风速风量"、"测风数据有没有问题/隐患"、"风速风量隐患判识"
+---
+
+# 风速风量监测与隐患判识(wind-hazard-diagnosis)
+
+通用 skill:当用户提到「查询当前风速风量数据及是否存在问题/隐患」「测风数据判识」「风速风量有没有问题」等意图时自动启用。**不局限于特定 agent**,任何具备以下依赖的环境均可运行。
+
+## 依赖(写死,运行前先确认可用)
+- **MCP 服务器 `tf_mcp`**:提供测风装置/实时数据/需风量查询工具(`query_device_info`、`query_device_data_by_id`、`get_needq_all_data`、`execute_sql_query`、`get_device_kind_dict` 等)。
+- **云通防知识库**:`http://39.97.59.228:8067`(Obsidian Vault Knowledge Service,`/api/search`、`/api/retrieve`、`/api/context`)。
+- 若以上不可用:明确告知用户"缺少 tf_mcp 或云知识库,无法完整执行",并说明缺哪一环。
+
+## 执行流程
+
+### Step 1 获取测风装置全量清单
+1. `mcp__tf_mcp__get_device_kind_dict` 确认测风装置大类为 `windrect`(小类:`windrect_dd/ds/ds_two/ds_two_235/ds_four/rect/normal/sensor/siyoute/test` 等)。
+2. `mcp__tf_mcp__query_device_info(device_kind="windrect")` 或 SQL `SELECT gdeviceid, strName, strInstallPos, gdevicetype, nSubStationID FROM device WHERE gdevicetype LIKE 'windrect%'` 获取全量清单。
+3. 标注设备类别:
+   - **测试/模拟设备**:类型含 `test`,或名称匹配 `测风N`、`测试`、`多参传感器`、`test`、`25xd`、`**矿井进风/回风` 等 → 标记为"测试设备"。
+   - **真实测点**:其余。
+
+### Step 2 逐台获取实时数据
+对每台设备调用 `mcp__tf_mcp__query_device_data_by_id(device_id=...)`(**必须逐台调用,不支持逗号批量**)。
+提取字段:`windSpeed`/`va`(风速 m/s)、`airVolume`/`m3`(风量 m³/min)、`temperature`、`tTime`(采样时间)、`netStatus`、`cachedAlarms`(活跃报警)、`alarmLogs`(历史报警)。
+- `netStatus` 为空或 0、无 `windSpeed` → 记录"**无实时数据/离线**"。
+- **模拟数据识别**:若多台设备数据呈高度雷同规律值(如风速≈1.3x 或 3.2x、风量≈120x、温度≈33~34℃、`sourcePressure≈8~9`),且**风量 ≠ 风速×断面×60**,判定为分站模拟/仿真数据,标注"模拟数据,不可用于生产判识"。
+
+### Step 3 判定巷道类型(确定风速上下限依据)
+优先级:
+1. **绑定信息优先**:若设备查询结果/`wind` 表/模型数据中带有明确的巷道类型(`strType`、`modeltype`、巷道属性等),直接采用并在报告中注明"绑定信息"。
+2. **名称关键词推断**(无绑定信息时),规则表:
+   - 无提升设备的风井/风硐 → 上限 15
+   - 专为升降物料的井筒 → 上限 12
+   - 风桥 → 上限 10
+   - 升降人员和物料的井筒(副井、副斜井、主斜井带提升)→ 上限 8
+   - 总进风巷/总回风巷/回风大巷/进风大巷 → 上限 8
+   - 架线电机车巷道 → 下限 1.0 / 上限 8
+   - 箕斗提升井兼作进风 → 上限 6
+   - 装有带式输送机兼作回风的井筒 → 上限 6
+   - 输送机巷、采区进/回风巷、胶运/胶带/皮带巷、顺槽(胶运/回风)→ 下限 0.25 / 上限 6
+   - 装有带式输送机兼作进风的井筒 → 上限 4
+   - 采煤工作面、掘进中的煤巷和半煤岩巷、工作面进/回风 → 下限 0.25 / 上限 4
+   - 掘进中的岩巷 → 下限 0.15 / 上限 4
+   - 其他通风人行巷道、辅运/行人巷 → 下限 0.15 / 无上限
+3. 报告中**注明每台设备的判定依据**(绑定信息 or 关键词推断),供人工核对。
+
+### Step 4 知识库佐证限值
+用 `web_fetch` 调云知识库确认限值表未过时:
+`http://39.97.59.228:8067/api/search?q=井巷允许风流速度 最低 最高 总回风巷 采煤工作面&mode=keyword&top_k=5&min_score=0.1`
+将命中片段(如《煤矿安全规程》2025版 表6 / AQ1028-2006 表2)作为引用出处写入报告。若检索不到或分过低,以 Step 3 规则表为准并注明"知识库未检索到佐证"。
+
+### Step 5 风速超限判定
+每台设备:实测风速 vs [下限,上限]:
+- `风速 > 上限` → 🔴 **超速**(写明超限百分比与限值来源)。
+- `风速 < 下限` → 🔴 **欠速/风速不足**(通风不良风险)。
+- 在区间内但贴近边界(≥85%上限)→ 提示"接近上限"。
+
+### Step 6 需风量关联与风量不足判定(核心)
+1. `mcp__tf_mcp__get_needq_all_data` 获取全量需风量:采煤面 `retWorkFaceList`、掘进面 `headingFaceList`、硐室 `roomList`、其他用风点 `otherList`、通风系统 `ventilationSystemList`。
+2. **筛选最新月份**:按 `nyear/nmonth`(及 `time`)取最新一期数据,避免多月份重复记录干扰。
+3. **关联顺序**(每个测点依次尝试,命中即停):
+   - **① 巷道 ID 精确关联**:测风装置 `wind` 表 `nTunID` 或 `query_device_data_by_id` 返回的 `tunnelId`,匹配需风量地点的 `tunIDs` 列表(逗号分隔),命中 → 关联成功,依据=巷道ID。
+   - **② 名称模糊匹配兜底**:规范化(去空格/单位/方位词)后,测点名称(或安装位置)与需风量地点 `strName` 满足"一方包含另一方 ≥1 个长度≥2 的核心词"(如"42上工作面""15211回风""充电硐室"),命中 → 关联成功,依据=名称匹配。
+   - **③ 不满足匹配条件** → 显示「**未匹配到对应用风地点**」。若该测点为系统/区段巷道(总进回风巷、大巷等),追加与所属通风系统 `ventilationSystemList` 的 `qra`(系统总需风量)做系统级对比,并注明。
+4. **判定**:监测风量 < 需风量(`fqneed` 或 `fqmax`,取 `fqmax` 为准并列出两者)→ 风量不足预警。
+5. **回复中必须逐条明示**(人工排查关联是否错误的关键):
+   > 监测风量 [数值] m³/min ←→ 需风量位置:「[地点名]」需风量 [fqneed/fqmax] m³/min(关联依据:巷道ID xxx / 名称匹配 / 未匹配到对应用风地点)
+
+### Step 7 重大隐患比对
+1. 知识库检索重大隐患条款:`/api/search?q=煤矿重大事故隐患判定标准 通风 风量不足 需风量&mode=keyword`。核心红线:**《煤矿重大事故隐患判定标准》第八条:实际供风量 < 需风量 × 75% → 通风系统重大事故隐患**。
+2. 判定:
+   - **系统级**:总进/回风测点风量(或系统进风合计)< 该系统需风量 × 75% → 🔴 构成重大事故隐患风险。
+   - **用风点级**:监测风量 < 该地点需风量 × 75% → 🔴 该地点供风严重不足(同条款精神)。
+   - 数据不可靠(模拟/缺失/异常值)无法支撑判定 → 明确写"**当前数据不足以支撑重大隐患直接判定**",并列出阻断判定的原因。
+3. 其他隐患(离线、超速、活跃报警、数据失配、测点失效)逐一与重大隐患条款比对,说明是否触碰红线。
+
+### Step 8 输出整体结论
+回复结构(会话形式即可,无需固定模板但须包含):
+- **总览**:总测点数 / 有效数据数 / 离线数 / 超速数 / 风量不足数 / 活跃报警数。
+- **分级清单**:🔴 高风险(离线、超速、活跃报警、风量严重不足)→ 🟠 中风险(数据失配、接近限值)→ 🟡 提示(模拟数据、测试设备、台账问题)。
+- **重大隐患结论**:构成 / 不构成 / 数据不足无法判定(三者取一,给出依据)。
+- **Step 6 的关联明细表**(监测风量→需风量位置/数值/关联依据)必须完整呈现。
+
+### Step 9 询问并生成报告
+1. 流程结束用 `ask` 工具询问(无 ask 环境用文字问):「是否生成 md 分析报告?」
+   - 选项:生成(默认,含完整数据表)/ 不生成(仅会话结论)。
+2. 用户同意 → 生成 md 文件:
+   - **文件名固定**:`测风装置实时数据分析报告_YYYY-MM-DD.md`(YYYY-MM-DD = 数据采样日期;同日重复生成直接覆盖旧文件)。
+   - **路径固定**:当前工作区根目录(用户另行指定路径时除外)。
+   - **格式必须严格按下方「固定报告格式」章节执行,不得自行增删章节/表格列/判定用语。**
+3. 用户拒绝 → 仅输出会话结论,不写文件。
+
+---
+
+## 固定报告格式(必须严格遵守,每次输出一致)
+
+> 规则:章节顺序固定;无内容章节保留标题并写"无";表格列名固定不可增减;判定用语使用固定词表。**任何一次生成都不得改变本格式。**
+
+### 文件
+```
+测风装置实时数据分析报告_YYYY-MM-DD.md
+```
+
+### 报告骨架(章节顺序固定,编号 0 至 八 不可变)
+
+```markdown
+# 测风装置实时数据分析报告(风速风量监测与隐患判识)
+
+## 0. 报告元信息
+| 字段 | 内容 |
+|---|---|
+| 报告名称 | 测风装置实时数据分析报告 |
+| 生成时间 | <本地时间> |
+| 数据采样时间 | <平台实时数据 tTime 起止范围> |
+| 数据来源 | tf_mcp:device表 / query_device_data_by_id / get_needq_all_data |
+| 判识知识库 | 云通防知识库 http://39.97.59.228:8067 |
+| 判识标准 | 《煤矿安全规程》2025版表6 / AQ1028-2006表2 / 《煤矿重大事故隐患判定标准》第八条 |
+| 测风装置总数 | <N> 台(真实测点 <X> / 测试设备 <Y>) |
+| 生成方式 | wind-hazard-diagnosis skill 自动生成 |
+
+## 一、判识框架与标准
+(固定四要素:①速域判识 ②量纲判识 ③红线判识 ④测风管理;附《煤矿安全规程》2025版表6 允许风流速度全表,每次照抄不省略行)
+
+## 二、总体结论
+### 2.1 总览表(固定行)
+| 指标 | 数值 |
+|---|---|
+| 测风装置总数 | |
+| 有实时数据测点 | |
+| 离线/无数据 | |
+| 超速(含疑似) | |
+| 风量不足 | |
+| 活跃报警 | |
+| 数据失配/模拟 | |
+| 重大隐患结论 | 构成 / 不构成 / 数据不足无法判定 |
+
+### 2.2 重大隐患判定(固定格式)
+**结论:<三选一>**
+判定依据:……
+阻断原因(仅"数据不足无法判定"时列出,编号列表):
+
+## 三、逐台判识明细(固定 13 列;测试设备并入表底并标注)
+| 序号 | 设备ID | 测点名称 | 设备类型 | 巷道类型(判定依据) | 风速(m/s) | 风量(m³/min) | 限值(最低~最高) | 风速判定 | 风量校验 | 需风量关联 | 报警/状态 | 问题分级 |
+
+## 四、需风量关联明细(固定 7 列,供人工排查关联)
+| 测点名称 | 监测风量(m³/min) | 需风量位置 | 需风量fqneed | 需风量fqmax | 关联依据 | 对比判定 |
+
+## 五、需风量数据全表(平台最新月份,固定 6 列)
+| 用风地点 | 类型 | fqneed(m³/min) | fqmax(m³/min) | 所属通风系统 | 系统总需风qra(m³/min) |
+
+## 六、隐患分级清单(固定三级,编号格式固定)
+### 🔴 高风险
+1. <测点>:<问题描述>(<判定依据>)
+### 🟠 中风险
+1. <测点>:<问题描述>(<判定依据>)
+### 🟡 提示
+1. <测点>:<问题描述>(<判定依据>)
+
+## 七、整改建议(固定编号列表,按 🔴🟠🟡 顺序归类)
+
+## 八、判识依据出处(固定列表:note_id / 标准号 / 条款)
+
+> 尾部声明(固定段落,不可省略):
+> 本报告基于 <采样时间> 平台实时快照,由 wind-hazard-diagnosis skill 自动生成。测风装置数据质量参差(模拟/失配占比 <N>%),判定以"需核查项"为主;重大隐患结论受数据可靠性限制未直接认定时,建议结合每旬人工测风数据复核后形成正式结论。
+```
+
+### 固定用语词表(判识/填写时只允许使用下列用语)
+- **风速判定**:`正常` / `接近上限` / `超速` / `欠速` / `疑似超速` / `无法判识`(超上限/低于下限时附"超/欠 X%")
+- **风量校验**(风速×断面×60 vs 上报风量):`一致`(±10%内)/ `不符`(10%~50%)/ `严重不符`(>50%)/ `—`(无断面或数据)
+- **需风量关联依据**:`巷道ID匹配` / `名称匹配` / `未命中`
+- **需风量位置**(未命中时固定写):`未匹配到对应用风地点`
+- **问题分级**:`高风险` / `中风险` / `提示`(对应 🔴🟠🟡)
+- **重大隐患结论**:`构成` / `不构成` / `数据不足无法判定`(三选一)
+
+### 填充规则
+- 每章内容为空时:保留该章标题,正文写"无"。
+- 表格无数据行时:保留表头,正文写"无数据"。
+- 判定用语必须取自词表;限值、依据、出处必须与知识库检索结果一致。