Quellcode durchsuchen

Merge pull request #645 from monkeyhlj/feature/monkeyhlj-NetworkHealthReportAgent

[毕业设计] NetworkHealthReportAgent - 多智能体网络健康报告助手
Sizhou Chen vor 3 Wochen
Ursprung
Commit
10369f5532
36 geänderte Dateien mit 5104 neuen und 0 gelöschten Zeilen
  1. 10 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/.env.example
  2. 3 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/.gitignore
  3. 327 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/README.md
  4. 27 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/device_inventory.json
  5. 33 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/device_status_timeseries.json
  6. 12 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/network_logs.json
  7. 68 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/sites.json
  8. 8 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/terminal_compliance.json
  9. 20 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/test_cases.json
  10. 12 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/index.html
  11. 1536 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/package-lock.json
  12. 20 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/package.json
  13. 583 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/App.vue
  14. 123 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/api.js
  15. 71 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/components/ReportPanel.vue
  16. 95 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/components/SiteMap.vue
  17. 6 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/main.js
  18. 302 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/styles.css
  19. 10 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/vite.config.js
  20. 553 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/main.ipynb
  21. 3 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/outputs/review_report.md
  22. 45 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/outputs/site-cd-west_2026-05-25_2026-05-31_20260531_191908.md
  23. 9 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/requirements.txt
  24. 4 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/run_api.py
  25. 5 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/__init__.py
  26. 124 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/base.py
  27. 60 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/device_status_agent.py
  28. 47 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/log_analysis_agent.py
  29. 102 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/network_health_report_agent.py
  30. 140 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/orchestrator.py
  31. 387 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/site_qa_agent.py
  32. 50 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/user_status_agent.py
  33. 172 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/api/main.py
  34. 44 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/tools/data_mcp_server.py
  35. 63 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/tools/data_repository.py
  36. 30 0
      Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/utils/date_utils.py

+ 10 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/.env.example

@@ -0,0 +1,10 @@
+# HelloAgents LLM configuration
+LLM_MODEL_ID=gpt-4o-mini
+LLM_API_KEY=
+LLM_BASE_URL=https://api.openai.com/v1
+LLM_TIMEOUT=60
+
+# Optional OpenAI-compatible fallback
+OPENAI_API_KEY=
+OPENAI_BASE_URL=https://api.openai.com/v1
+OPENAI_MODEL=gpt-4o-mini

+ 3 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/.gitignore

@@ -0,0 +1,3 @@
+node_modules/
+dist/
+build/

+ 327 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/README.md

@@ -0,0 +1,327 @@
+# NetworkHealthReportAgent
+
+基于 Hello-Agents 的多智能体网络健康报告系统,支持地图展示站点、点击查看站点健康报告,并支持默认最近一周与自定义时间窗口查询。
+
+## 📝 项目简介
+
+本项目面向企业网络运维场景,演示如何用多智能体协作生成可读、可执行的网络健康报告。
+
+- 前端(Vue + Leaflet):展示各站点地理位置,点击站点拉取报告
+- 后端(FastAPI):提供站点、报告、全局问答、流式输出与报告下载 API
+- 智能体层(HelloAgents):
+
+1. 日志分析 Agent
+2. 网络设备状态 Agent
+3. 网络用户状态分析 Agent
+4. 网络健康报告 Agent(综合前 3 个结果)
+5. 全局问答 Agent(支持站点问答与报告导出意图)
+
+- MCP 层(FastMCP):从 data 目录开放站点、设备、日志、终端合规数据读取接口
+
+## ✨ 核心功能
+
+- 地图可视化站点分布,点击站点查看报告
+- 默认时间窗口为最近 7 天
+- 支持自定义 `start_date` 和 `end_date`
+- 假数据覆盖:站点、设备台账、设备状态时序、网络日志、终端合规
+- 全局问答界面:可直接提问,例如“有几个site?”“上海有哪些site?”“某个site的设备情况如何?”“帮我生成成都site近一周的报告”
+- 问答导出报告:生成 `outputs/*.md`,并返回可下载链接
+
+## 🛠️ 技术栈
+
+- HelloAgents (hello-agents)
+- FastAPI
+- FastMCP
+- Vue 3 + Vite + Leaflet
+
+## 📂 目录结构
+
+```text
+monkeyhlj-NetworkHealthReportAgent/
+├── README.md                               # 项目说明文档
+├── requirements.txt                        # Python 依赖
+├── .env.example                            # LLM/模型环境变量模板
+├── main.ipynb                              # Notebook 演示入口(7部分 walkthrough)
+├── run_api.py                              # FastAPI 启动入口(uvicorn 指向此文件)
+├── data/
+│   ├── sites.json                          # 站点地理信息(经纬度、区域、等级)
+│   ├── device_inventory.json               # 设备台账(交换机/WLC/AP 等)
+│   ├── device_status_timeseries.json       # 设备状态时序(在线率、时延、丢包等)
+│   ├── network_logs.json                   # 网络日志(告警事件)
+│   ├── terminal_compliance.json            # 终端接入与合规数据
+│   └── test_cases.json                     # 报告接口测试样例
+├── outputs/
+│   ├── review_report.md                    # 示例报告
+│   └── site-*_YYYY-MM-DD_*.md             # 问答导出的站点周报文件(动态生成)
+├── src/
+│   ├── __init__.py
+│   ├── api/
+│   │   └── main.py                         # API 路由(/api/sites /api/reports /api/chat /api/chat/stream /api/outputs)
+│   ├── agents/
+│   │   ├── base.py                         # Agent 基类,负责 LLM 与 MCPTool 装载
+│   │   ├── log_analysis_agent.py           # 日志分析 Agent
+│   │   ├── device_status_agent.py          # 设备状态 Agent
+│   │   ├── user_status_agent.py            # 用户状态 Agent
+│   │   ├── network_health_report_agent.py  # 综合报告 Agent
+│   │   ├── site_qa_agent.py                # 全局问答/报告导出 Agent
+│   │   └── orchestrator.py                 # 多 Agent 编排与总流程
+│   ├── tools/
+│   │   ├── data_repository.py              # 数据访问层(读取 data/*.json)
+│   │   └── data_mcp_server.py              # MCP 服务,向 Agent 暴露数据工具
+│   └── utils/
+│       └── date_utils.py                   # 日期窗口处理(默认最近 7 天)
+└── frontend/
+    ├── package.json                         # 前端依赖和脚本
+    ├── vite.config.js                       # Vite 开发配置
+    ├── index.html
+    └── src/
+        ├── main.js                          # 前端入口
+        ├── App.vue                          # 页面主视图(左地图+下报告+右问答)
+        ├── api.js                           # 后端 API 调用与地址探测
+        ├── styles.css                       # 页面样式
+        └── components/
+            ├── SiteMap.vue                  # 地图组件
+            └── ReportPanel.vue              # 报告展示组件
+```
+
+说明:目录树中省略了 `venv/`、`__pycache__/`、`.idea/`、`memory/` 等运行时或本地环境目录。
+
+## 🤖 运行模式说明
+
+系统支持两种模式:
+
+1. 无 LLM 模式(可直接演示)
+
+- 不配置 LLM_API_KEY / OPENAI_API_KEY 也能运行。
+- 报告由本地规则逻辑生成,适合离线演示和开发联调。
+
+2. LLM 模式(推荐)
+
+- 需要配置 API Key。
+- Agent 会实例化 HelloAgentsLLM,并尝试挂载 MCP 数据工具(不同 hello-agents 版本自动兼容)。
+
+## 🔐 LLM 启动方式(重点)
+
+### 1) 准备环境变量
+
+先复制模板:
+
+```bash
+cp .env.example .env
+```
+
+Windows PowerShell 可使用:
+
+```powershell
+Copy-Item .env.example .env
+```
+
+然后至少配置以下任意一组:
+
+- 统一配置(推荐)
+
+```env
+LLM_MODEL_ID=gpt-4o-mini
+LLM_API_KEY=你的密钥
+LLM_BASE_URL=https://api.openai.com/v1
+LLM_TIMEOUT=60
+```
+
+- OpenAI 兼容配置(备选)
+
+```env
+OPENAI_API_KEY=你的密钥
+OPENAI_BASE_URL=https://api.openai.com/v1
+OPENAI_MODEL=gpt-4o-mini
+```
+
+说明:当前代码在 agents/base.py 中通过以下逻辑判断是否启用 LLM:
+
+- 读取 LLM_API_KEY 或 OPENAI_API_KEY
+- 有 Key:创建 HelloAgentsLLM
+- 无 Key:降级到无 LLM 模式
+
+### 2) 启动后端
+
+```bash
+uvicorn run_api:app --reload --port 8000
+```
+
+### 3) 验证 LLM 是否生效
+
+- 可先访问健康检查:
+
+```bash
+curl "http://localhost:8000/api/health"
+```
+
+- 再请求任意站点报告,确认接口返回正常:
+
+```bash
+curl "http://localhost:8000/api/reports/site-sh-fin"
+```
+
+## 🚀 快速开始
+
+### 1) Python 环境和依赖
+
+```bash
+pip install -r requirements.txt
+```
+
+### 2) 配置环境变量
+
+```bash
+cp .env.example .env
+```
+
+Windows PowerShell 可使用:
+
+```powershell
+Copy-Item .env.example .env
+```
+
+如果要启用 LLM 模式,请在 .env 配置 LLM_API_KEY(或 OPENAI_API_KEY)。
+
+### 3) 启动后端 API
+
+```bash
+uvicorn run_api:app --reload --port 8000
+```
+
+API 入口:`http://localhost:8000`
+
+- 站点列表:`GET /api/sites`
+- 单站报告:`GET /api/reports/{site_id}`
+- 参数:`start_date`、`end_date`(格式 `YYYY-MM-DD`)
+- 全站报告:`GET /api/reports`
+- 问答接口:`POST /api/chat`
+- 流式问答接口:`POST /api/chat/stream`
+- 报告文件下载:`GET /api/outputs/{filename}`
+- 运行时状态:`GET /api/runtime`
+
+### 4) 启动前端
+
+```bash
+cd frontend
+npm install
+npm run dev
+```
+
+前端默认地址:`http://localhost:5173`
+
+## 📖 使用示例
+
+查询上海站点最近一周报告:
+
+```bash
+curl "http://localhost:8000/api/reports/site-sh-fin"
+```
+
+查询深圳站点自定义时间窗口:
+
+```bash
+curl "http://localhost:8000/api/reports/site-sz-ops?start_date=2026-05-23&end_date=2026-05-29"
+```
+
+查询全部站点报告:
+
+```bash
+curl "http://localhost:8000/api/reports"
+```
+
+让问答 Agent 生成某个站点近一周报告,并返回可下载文件:
+
+```bash
+curl -X POST "http://localhost:8000/api/chat" \
+	-H "Content-Type: application/json" \
+	-d '{
+		"question": "帮我生成 site-sh-fin 近一周的报告",
+		"site_id": "site-sh-fin",
+		"start_date": "2026-05-23",
+		"end_date": "2026-05-29"
+	}'
+```
+
+返回结果里会包含 `artifact.download_url`,前端会把它展示成下载链接,生成文件默认保存在 `outputs/` 目录下。
+
+全局问答示例:
+
+```bash
+curl -X POST "http://localhost:8000/api/chat/stream" \
+	-H "Content-Type: application/json" \
+	-d '{
+		"question": "上海有哪些 site?",
+		"start_date": "2026-05-23",
+		"end_date": "2026-05-29"
+	}'
+```
+
+如果当前选中了某个站点,问题里写“帮我生成当前站点近一周报告”也可以直接触发文件生成。
+
+## 🧪 Notebook 使用
+
+```bash
+jupyter lab
+```
+
+打开 main.ipynb:
+
+- 第 1 部分:项目介绍
+- 第 2 部分:环境配置
+- 第 3 部分:工具定义(`SiteQuickLookupTool`)
+- 第 4 部分:智能体构建(`NetworkHealthOrchestrator` + 可选 `demo_agent`)
+- 第 5 部分:功能演示(问答、报告、导出下载链接)
+- 第 6 部分:性能评估(可选)
+- 第 7 部分:总结与展望
+
+## 🎯 项目亮点
+
+- 从数据层到 Agent 层可追溯:每个结论都有原始指标依据
+- MCP 抽象数据访问接口,便于后续替换真实数据源
+- 前后端解耦,便于扩展告警中心、工单系统和趋势大屏
+
+## 🔮 下一步建议
+
+- 接入真实设备数据源(SNMP/Telemetry/NetFlow)
+- 引入时序库存储并做趋势预测
+- 加入报告导出(PDF/Markdown)和自动推送(邮件/飞书)
+
+## 🧰 常见问题
+
+1. 报错 cannot import name MCPTool
+
+- 原因:hello-agents 不同版本导出路径不一致。
+- 现状:base.py 已做兼容探测,找不到会自动降级,系统仍可运行。
+
+2. 接口能访问但不是 LLM 风格输出
+
+- 原因:未配置 LLM_API_KEY/OPENAI_API_KEY,系统在无 LLM 模式下运行。
+- 处理:补齐 .env 后重启 uvicorn。
+
+3. 问答能回答,但“导出站点周报”失败
+
+- 原因:导出周报场景要求 LLM 可用;若 LLM 未启用或调用失败,会返回提示且不生成文件。
+- 处理:确认 `.env` 中 `LLM_API_KEY`(或 `OPENAI_API_KEY`)有效,并检查模型可访问性。
+
+4. 前端请求失败(CORS 或连接失败)
+
+- 检查后端是否已启动在 8000 端口。
+- 检查前端是否在 5173 端口运行。
+
+5. MCP Server 需要单独启动吗
+
+- 一般不需要。当前项目通过 `MCPTool(server_command=[...])` 由 Agent 在调用时自动拉起。
+- 仅在你要单独调试 MCP 服务时,才手动运行:
+
+```bash
+python -m src.tools.data_mcp_server
+```
+
+## 📄 许可证
+
+MIT License
+
+## 🙏 致谢
+
+感谢Datawhale社区和Hello-Agents项目!

+ 27 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/device_inventory.json

@@ -0,0 +1,27 @@
+[
+  {"device_id":"bj-core-sw-01","site_id":"site-bj-hq","device_type":"wired_switch","model":"Cisco C9500","vendor":"Cisco","firmware":"17.9.4","status":"online"},
+  {"device_id":"bj-access-sw-17","site_id":"site-bj-hq","device_type":"wired_switch","model":"H3C S5560X","vendor":"H3C","firmware":"R7510P10","status":"online"},
+  {"device_id":"bj-wlc-01","site_id":"site-bj-hq","device_type":"wireless_controller","model":"Cisco 9800-40","vendor":"Cisco","firmware":"17.12.2","status":"online"},
+  {"device_id":"bj-ap-f7-03","site_id":"site-bj-hq","device_type":"wireless_ap","model":"Aironet 9130","vendor":"Cisco","firmware":"8.10.185","status":"online"},
+
+  {"device_id":"sh-core-sw-01","site_id":"site-sh-fin","device_type":"wired_switch","model":"Aruba 8325","vendor":"Aruba","firmware":"10.12.1020","status":"online"},
+  {"device_id":"sh-wlc-01","site_id":"site-sh-fin","device_type":"wireless_controller","model":"Aruba 7210","vendor":"Aruba","firmware":"8.11.2.3","status":"online"},
+  {"device_id":"sh-ap-f12-08","site_id":"site-sh-fin","device_type":"wireless_ap","model":"Aruba AP-635","vendor":"Aruba","firmware":"10.6.0.2","status":"online"},
+
+  {"device_id":"gz-core-sw-01","site_id":"site-gz-rd","device_type":"wired_switch","model":"Huawei S6730","vendor":"Huawei","firmware":"V200R022","status":"online"},
+  {"device_id":"gz-wlc-01","site_id":"site-gz-rd","device_type":"wireless_controller","model":"Huawei AC6508","vendor":"Huawei","firmware":"V200R021","status":"online"},
+  {"device_id":"gz-ap-lab-04","site_id":"site-gz-rd","device_type":"wireless_ap","model":"Huawei AP7060","vendor":"Huawei","firmware":"V200R021","status":"online"},
+
+  {"device_id":"sz-core-sw-01","site_id":"site-sz-ops","device_type":"wired_switch","model":"Juniper EX4650","vendor":"Juniper","firmware":"23.4R1","status":"online"},
+  {"device_id":"sz-edge-sw-03","site_id":"site-sz-ops","device_type":"wired_switch","model":"Juniper EX4400","vendor":"Juniper","firmware":"23.2R2","status":"degraded"},
+  {"device_id":"sz-wlc-01","site_id":"site-sz-ops","device_type":"wireless_controller","model":"Mist Edge","vendor":"Juniper","firmware":"1.9.2","status":"online"},
+  {"device_id":"sz-ap-c7-12","site_id":"site-sz-ops","device_type":"wireless_ap","model":"Juniper AP45","vendor":"Juniper","firmware":"0.11.31680","status":"online"},
+
+  {"device_id":"cd-core-sw-01","site_id":"site-cd-west","device_type":"wired_switch","model":"Ruijie RG-S6220","vendor":"Ruijie","firmware":"11.4(1)","status":"online"},
+  {"device_id":"cd-wlc-01","site_id":"site-cd-west","device_type":"wireless_controller","model":"Ruijie WS6008","vendor":"Ruijie","firmware":"3.5.1","status":"online"},
+  {"device_id":"cd-ap-a3-02","site_id":"site-cd-west","device_type":"wireless_ap","model":"Ruijie AP820-I","vendor":"Ruijie","firmware":"3.5.1","status":"online"},
+
+  {"device_id":"wh-core-sw-01","site_id":"site-wh-delivery","device_type":"wired_switch","model":"Cisco C9300","vendor":"Cisco","firmware":"17.6.5","status":"online"},
+  {"device_id":"wh-wlc-01","site_id":"site-wh-delivery","device_type":"wireless_controller","model":"Cisco 9800-L","vendor":"Cisco","firmware":"17.9.3","status":"online"},
+  {"device_id":"wh-ap-e2-06","site_id":"site-wh-delivery","device_type":"wireless_ap","model":"Cisco 9120","vendor":"Cisco","firmware":"8.10.183","status":"offline"}
+]

+ 33 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/device_status_timeseries.json

@@ -0,0 +1,33 @@
+[
+  {"date":"2026-05-23","site_id":"site-bj-hq","wired_switch_online_rate":0.998,"wireless_controller_online_rate":1.0,"wireless_ap_online_rate":0.993,"cpu_avg":41.2,"mem_avg":58.1,"packet_loss":0.002,"latency_ms":11.5,"critical_alarm_count":1},
+  {"date":"2026-05-24","site_id":"site-bj-hq","wired_switch_online_rate":0.997,"wireless_controller_online_rate":1.0,"wireless_ap_online_rate":0.991,"cpu_avg":44.8,"mem_avg":59.3,"packet_loss":0.003,"latency_ms":12.1,"critical_alarm_count":1},
+  {"date":"2026-05-25","site_id":"site-bj-hq","wired_switch_online_rate":0.994,"wireless_controller_online_rate":1.0,"wireless_ap_online_rate":0.988,"cpu_avg":50.7,"mem_avg":61.5,"packet_loss":0.005,"latency_ms":13.8,"critical_alarm_count":2},
+  {"date":"2026-05-26","site_id":"site-bj-hq","wired_switch_online_rate":0.998,"wireless_controller_online_rate":1.0,"wireless_ap_online_rate":0.995,"cpu_avg":43.1,"mem_avg":56.7,"packet_loss":0.002,"latency_ms":11.2,"critical_alarm_count":0},
+  {"date":"2026-05-27","site_id":"site-bj-hq","wired_switch_online_rate":0.999,"wireless_controller_online_rate":1.0,"wireless_ap_online_rate":0.996,"cpu_avg":39.5,"mem_avg":54.2,"packet_loss":0.001,"latency_ms":10.5,"critical_alarm_count":0},
+  {"date":"2026-05-28","site_id":"site-bj-hq","wired_switch_online_rate":0.997,"wireless_controller_online_rate":1.0,"wireless_ap_online_rate":0.994,"cpu_avg":42.4,"mem_avg":57.8,"packet_loss":0.002,"latency_ms":11.0,"critical_alarm_count":1},
+  {"date":"2026-05-29","site_id":"site-bj-hq","wired_switch_online_rate":0.996,"wireless_controller_online_rate":1.0,"wireless_ap_online_rate":0.992,"cpu_avg":45.3,"mem_avg":60.1,"packet_loss":0.003,"latency_ms":12.0,"critical_alarm_count":1},
+
+  {"date":"2026-05-23","site_id":"site-sh-fin","wired_switch_online_rate":0.995,"wireless_controller_online_rate":0.998,"wireless_ap_online_rate":0.982,"cpu_avg":47.2,"mem_avg":63.3,"packet_loss":0.006,"latency_ms":16.8,"critical_alarm_count":3},
+  {"date":"2026-05-24","site_id":"site-sh-fin","wired_switch_online_rate":0.993,"wireless_controller_online_rate":0.997,"wireless_ap_online_rate":0.979,"cpu_avg":52.9,"mem_avg":66.2,"packet_loss":0.009,"latency_ms":18.2,"critical_alarm_count":4},
+  {"date":"2026-05-25","site_id":"site-sh-fin","wired_switch_online_rate":0.992,"wireless_controller_online_rate":0.996,"wireless_ap_online_rate":0.975,"cpu_avg":56.4,"mem_avg":68.5,"packet_loss":0.011,"latency_ms":19.4,"critical_alarm_count":5},
+  {"date":"2026-05-26","site_id":"site-sh-fin","wired_switch_online_rate":0.994,"wireless_controller_online_rate":0.997,"wireless_ap_online_rate":0.980,"cpu_avg":49.1,"mem_avg":64.8,"packet_loss":0.007,"latency_ms":17.5,"critical_alarm_count":3},
+  {"date":"2026-05-27","site_id":"site-sh-fin","wired_switch_online_rate":0.995,"wireless_controller_online_rate":0.998,"wireless_ap_online_rate":0.983,"cpu_avg":46.5,"mem_avg":61.7,"packet_loss":0.006,"latency_ms":16.3,"critical_alarm_count":2},
+  {"date":"2026-05-28","site_id":"site-sh-fin","wired_switch_online_rate":0.994,"wireless_controller_online_rate":0.998,"wireless_ap_online_rate":0.981,"cpu_avg":50.2,"mem_avg":63.9,"packet_loss":0.008,"latency_ms":17.0,"critical_alarm_count":3},
+  {"date":"2026-05-29","site_id":"site-sh-fin","wired_switch_online_rate":0.993,"wireless_controller_online_rate":0.997,"wireless_ap_online_rate":0.978,"cpu_avg":54.0,"mem_avg":67.1,"packet_loss":0.010,"latency_ms":18.9,"critical_alarm_count":4},
+
+  {"date":"2026-05-23","site_id":"site-gz-rd","wired_switch_online_rate":0.996,"wireless_controller_online_rate":0.999,"wireless_ap_online_rate":0.987,"cpu_avg":45.1,"mem_avg":59.8,"packet_loss":0.004,"latency_ms":14.2,"critical_alarm_count":2},
+  {"date":"2026-05-26","site_id":"site-gz-rd","wired_switch_online_rate":0.994,"wireless_controller_online_rate":0.998,"wireless_ap_online_rate":0.984,"cpu_avg":48.8,"mem_avg":62.6,"packet_loss":0.006,"latency_ms":15.6,"critical_alarm_count":3},
+  {"date":"2026-05-29","site_id":"site-gz-rd","wired_switch_online_rate":0.995,"wireless_controller_online_rate":0.999,"wireless_ap_online_rate":0.986,"cpu_avg":46.3,"mem_avg":60.7,"packet_loss":0.005,"latency_ms":14.7,"critical_alarm_count":2},
+
+  {"date":"2026-05-23","site_id":"site-sz-ops","wired_switch_online_rate":0.991,"wireless_controller_online_rate":0.997,"wireless_ap_online_rate":0.981,"cpu_avg":58.2,"mem_avg":70.4,"packet_loss":0.014,"latency_ms":22.1,"critical_alarm_count":6},
+  {"date":"2026-05-26","site_id":"site-sz-ops","wired_switch_online_rate":0.989,"wireless_controller_online_rate":0.996,"wireless_ap_online_rate":0.978,"cpu_avg":61.5,"mem_avg":72.8,"packet_loss":0.016,"latency_ms":23.5,"critical_alarm_count":7},
+  {"date":"2026-05-29","site_id":"site-sz-ops","wired_switch_online_rate":0.990,"wireless_controller_online_rate":0.997,"wireless_ap_online_rate":0.979,"cpu_avg":59.4,"mem_avg":71.1,"packet_loss":0.015,"latency_ms":22.9,"critical_alarm_count":6},
+
+  {"date":"2026-05-23","site_id":"site-cd-west","wired_switch_online_rate":0.998,"wireless_controller_online_rate":1.0,"wireless_ap_online_rate":0.992,"cpu_avg":37.9,"mem_avg":50.3,"packet_loss":0.002,"latency_ms":10.7,"critical_alarm_count":1},
+  {"date":"2026-05-26","site_id":"site-cd-west","wired_switch_online_rate":0.997,"wireless_controller_online_rate":1.0,"wireless_ap_online_rate":0.991,"cpu_avg":39.2,"mem_avg":52.1,"packet_loss":0.003,"latency_ms":11.2,"critical_alarm_count":1},
+  {"date":"2026-05-29","site_id":"site-cd-west","wired_switch_online_rate":0.998,"wireless_controller_online_rate":1.0,"wireless_ap_online_rate":0.993,"cpu_avg":38.6,"mem_avg":51.7,"packet_loss":0.002,"latency_ms":10.9,"critical_alarm_count":1},
+
+  {"date":"2026-05-23","site_id":"site-wh-delivery","wired_switch_online_rate":0.994,"wireless_controller_online_rate":0.998,"wireless_ap_online_rate":0.977,"cpu_avg":53.2,"mem_avg":66.8,"packet_loss":0.010,"latency_ms":18.3,"critical_alarm_count":4},
+  {"date":"2026-05-26","site_id":"site-wh-delivery","wired_switch_online_rate":0.992,"wireless_controller_online_rate":0.997,"wireless_ap_online_rate":0.973,"cpu_avg":56.8,"mem_avg":69.2,"packet_loss":0.013,"latency_ms":20.4,"critical_alarm_count":5},
+  {"date":"2026-05-29","site_id":"site-wh-delivery","wired_switch_online_rate":0.991,"wireless_controller_online_rate":0.996,"wireless_ap_online_rate":0.970,"cpu_avg":58.0,"mem_avg":70.5,"packet_loss":0.014,"latency_ms":21.1,"critical_alarm_count":6}
+]

+ 12 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/network_logs.json

@@ -0,0 +1,12 @@
+[
+  {"timestamp":"2026-05-29T08:11:23+08:00","site_id":"site-sh-fin","device_id":"sh-wlc-01","severity":"critical","event_type":"auth_failure_spike","message":"802.1X authentication failures exceeded threshold: 176 events/5min"},
+  {"timestamp":"2026-05-29T08:13:10+08:00","site_id":"site-sh-fin","device_id":"sh-ap-f12-08","severity":"warning","event_type":"channel_interference","message":"Detected high 5GHz co-channel interference, channel utilization 86%"},
+  {"timestamp":"2026-05-29T09:42:54+08:00","site_id":"site-sz-ops","device_id":"sz-edge-sw-03","severity":"critical","event_type":"uplink_flap","message":"TenGig uplink flapping detected on xe-0/0/47, 9 flaps in 1 hour"},
+  {"timestamp":"2026-05-29T09:58:03+08:00","site_id":"site-wh-delivery","device_id":"wh-ap-e2-06","severity":"critical","event_type":"ap_offline","message":"AP disconnected from controller for 23 minutes"},
+  {"timestamp":"2026-05-29T10:21:16+08:00","site_id":"site-bj-hq","device_id":"bj-core-sw-01","severity":"warning","event_type":"high_cpu","message":"CPU utilization above 80% for 7 minutes due to backup window"},
+  {"timestamp":"2026-05-29T11:30:44+08:00","site_id":"site-gz-rd","device_id":"gz-wlc-01","severity":"info","event_type":"config_change","message":"Wireless profile updated by netops user: tx-power policy adjusted"},
+  {"timestamp":"2026-05-29T12:17:52+08:00","site_id":"site-cd-west","device_id":"cd-core-sw-01","severity":"warning","event_type":"crc_errors","message":"CRC errors observed on access port ge1/0/23, cable quality suspected"},
+  {"timestamp":"2026-05-29T13:04:11+08:00","site_id":"site-bj-hq","device_id":"bj-ap-f7-03","severity":"info","event_type":"roaming_peak","message":"Client roaming peak reached 154 transitions/hour, within normal range"},
+  {"timestamp":"2026-05-29T14:25:39+08:00","site_id":"site-sz-ops","device_id":"sz-wlc-01","severity":"warning","event_type":"dhcp_latency","message":"Average DHCP ACK delay increased to 410ms"},
+  {"timestamp":"2026-05-29T15:08:27+08:00","site_id":"site-wh-delivery","device_id":"wh-core-sw-01","severity":"critical","event_type":"port_security_violation","message":"Multiple MAC violation events on port Gi1/0/34"}
+]

+ 68 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/sites.json

@@ -0,0 +1,68 @@
+[
+  {
+    "site_id": "site-bj-hq",
+    "site_name": "北京总部园区",
+    "city": "北京",
+    "province": "北京市",
+    "latitude": 39.9042,
+    "longitude": 116.4074,
+    "region": "north",
+    "site_type": "hq",
+    "criticality": "high"
+  },
+  {
+    "site_id": "site-sh-fin",
+    "site_name": "上海金融中心",
+    "city": "上海",
+    "province": "上海市",
+    "latitude": 31.2304,
+    "longitude": 121.4737,
+    "region": "east",
+    "site_type": "branch",
+    "criticality": "high"
+  },
+  {
+    "site_id": "site-gz-rd",
+    "site_name": "广州研发中心",
+    "city": "广州",
+    "province": "广东省",
+    "latitude": 23.1291,
+    "longitude": 113.2644,
+    "region": "south",
+    "site_type": "rd",
+    "criticality": "medium"
+  },
+  {
+    "site_id": "site-sz-ops",
+    "site_name": "深圳运营中心",
+    "city": "深圳",
+    "province": "广东省",
+    "latitude": 22.5431,
+    "longitude": 114.0579,
+    "region": "south",
+    "site_type": "ops",
+    "criticality": "medium"
+  },
+  {
+    "site_id": "site-cd-west",
+    "site_name": "成都西区办公室",
+    "city": "成都",
+    "province": "四川省",
+    "latitude": 30.5728,
+    "longitude": 104.0668,
+    "region": "west",
+    "site_type": "branch",
+    "criticality": "medium"
+  },
+  {
+    "site_id": "site-wh-delivery",
+    "site_name": "武汉交付中心",
+    "city": "武汉",
+    "province": "湖北省",
+    "latitude": 30.5928,
+    "longitude": 114.3055,
+    "region": "central",
+    "site_type": "delivery",
+    "criticality": "low"
+  }
+]

+ 8 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/terminal_compliance.json

@@ -0,0 +1,8 @@
+[
+  {"date":"2026-05-29","site_id":"site-bj-hq","wired_clients":812,"wireless_clients":1260,"unknown_clients":13,"compliant_rate":0.982,"high_risk_terminals":9,"guest_network_ratio":0.134},
+  {"date":"2026-05-29","site_id":"site-sh-fin","wired_clients":645,"wireless_clients":1045,"unknown_clients":27,"compliant_rate":0.946,"high_risk_terminals":21,"guest_network_ratio":0.178},
+  {"date":"2026-05-29","site_id":"site-gz-rd","wired_clients":523,"wireless_clients":738,"unknown_clients":14,"compliant_rate":0.967,"high_risk_terminals":12,"guest_network_ratio":0.152},
+  {"date":"2026-05-29","site_id":"site-sz-ops","wired_clients":701,"wireless_clients":998,"unknown_clients":33,"compliant_rate":0.931,"high_risk_terminals":28,"guest_network_ratio":0.194},
+  {"date":"2026-05-29","site_id":"site-cd-west","wired_clients":420,"wireless_clients":612,"unknown_clients":8,"compliant_rate":0.977,"high_risk_terminals":7,"guest_network_ratio":0.121},
+  {"date":"2026-05-29","site_id":"site-wh-delivery","wired_clients":510,"wireless_clients":690,"unknown_clients":19,"compliant_rate":0.952,"high_risk_terminals":16,"guest_network_ratio":0.169}
+]

+ 20 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/data/test_cases.json

@@ -0,0 +1,20 @@
+[
+  {
+    "name": "default_week_shanghai",
+    "site_id": "site-sh-fin",
+    "start_date": "2026-05-23",
+    "end_date": "2026-05-29",
+    "expected": {
+      "health_level": "warning"
+    }
+  },
+  {
+    "name": "default_week_beijing",
+    "site_id": "site-bj-hq",
+    "start_date": "2026-05-23",
+    "end_date": "2026-05-29",
+    "expected": {
+      "health_level": "healthy"
+    }
+  }
+]

+ 12 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/index.html

@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Network Health Report Agent</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 1536 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/package-lock.json

@@ -0,0 +1,1536 @@
+{
+  "name": "network-health-report-agent-ui",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "network-health-report-agent-ui",
+      "version": "0.1.0",
+      "dependencies": {
+        "axios": "^1.7.7",
+        "leaflet": "^1.9.4",
+        "vue": "^3.5.13"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "^5.1.4",
+        "vite": "^5.4.10"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.7",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@babel/parser/-/parser-7.29.7.tgz",
+      "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.7"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.7",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@babel/types/-/types-7.29.7.tgz",
+      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
+      "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
+      "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
+      "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
+      "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
+      "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
+      "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
+      "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
+      "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
+      "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
+      "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
+      "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
+      "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
+      "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
+      "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
+      "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
+      "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
+      "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
+      "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
+      "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
+      "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
+      "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
+      "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
+      "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
+      "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
+      "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "5.2.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+      "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@vue/compiler-core/-/compiler-core-3.5.35.tgz",
+      "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.3",
+        "@vue/shared": "3.5.35",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz",
+      "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.35",
+        "@vue/shared": "3.5.35"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz",
+      "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.3",
+        "@vue/compiler-core": "3.5.35",
+        "@vue/compiler-dom": "3.5.35",
+        "@vue/compiler-ssr": "3.5.35",
+        "@vue/shared": "3.5.35",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.15",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz",
+      "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.35",
+        "@vue/shared": "3.5.35"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@vue/reactivity/-/reactivity-3.5.35.tgz",
+      "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.35"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@vue/runtime-core/-/runtime-core-3.5.35.tgz",
+      "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.35",
+        "@vue/shared": "3.5.35"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz",
+      "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.35",
+        "@vue/runtime-core": "3.5.35",
+        "@vue/shared": "3.5.35",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@vue/server-renderer/-/server-renderer-3.5.35.tgz",
+      "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.35",
+        "@vue/shared": "3.5.35"
+      },
+      "peerDependencies": {
+        "vue": "3.5.35"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/@vue/shared/-/shared-3.5.35.tgz",
+      "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==",
+      "license": "MIT"
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.16.1",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/axios/-/axios-1.16.1.tgz",
+      "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.5",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/leaflet": {
+      "version": "1.9.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/leaflet/-/leaflet-1.9.4.tgz",
+      "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.12",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/nanoid/-/nanoid-3.3.12.tgz",
+      "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/postcss": {
+      "version": "8.5.15",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/postcss/-/postcss-8.5.15.tgz",
+      "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.12",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.60.4",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/rollup/-/rollup-4.60.4.tgz",
+      "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.60.4",
+        "@rollup/rollup-android-arm64": "4.60.4",
+        "@rollup/rollup-darwin-arm64": "4.60.4",
+        "@rollup/rollup-darwin-x64": "4.60.4",
+        "@rollup/rollup-freebsd-arm64": "4.60.4",
+        "@rollup/rollup-freebsd-x64": "4.60.4",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
+        "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
+        "@rollup/rollup-linux-arm64-gnu": "4.60.4",
+        "@rollup/rollup-linux-arm64-musl": "4.60.4",
+        "@rollup/rollup-linux-loong64-gnu": "4.60.4",
+        "@rollup/rollup-linux-loong64-musl": "4.60.4",
+        "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
+        "@rollup/rollup-linux-ppc64-musl": "4.60.4",
+        "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
+        "@rollup/rollup-linux-riscv64-musl": "4.60.4",
+        "@rollup/rollup-linux-s390x-gnu": "4.60.4",
+        "@rollup/rollup-linux-x64-gnu": "4.60.4",
+        "@rollup/rollup-linux-x64-musl": "4.60.4",
+        "@rollup/rollup-openbsd-x64": "4.60.4",
+        "@rollup/rollup-openharmony-arm64": "4.60.4",
+        "@rollup/rollup-win32-arm64-msvc": "4.60.4",
+        "@rollup/rollup-win32-ia32-msvc": "4.60.4",
+        "@rollup/rollup-win32-x64-gnu": "4.60.4",
+        "@rollup/rollup-win32-x64-msvc": "4.60.4",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "5.4.21",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/vite/-/vite-5.4.21.tgz",
+      "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.35",
+      "resolved": "https://maven.xpaas.lenovo.com/nexus/repository/npm/vue/-/vue-3.5.35.tgz",
+      "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.35",
+        "@vue/compiler-sfc": "3.5.35",
+        "@vue/runtime-dom": "3.5.35",
+        "@vue/server-renderer": "3.5.35",
+        "@vue/shared": "3.5.35"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    }
+  }
+}

+ 20 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/package.json

@@ -0,0 +1,20 @@
+{
+  "name": "network-health-report-agent-ui",
+  "version": "0.1.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "axios": "^1.7.7",
+    "leaflet": "^1.9.4",
+    "vue": "^3.5.13"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.1.4",
+    "vite": "^5.4.10"
+  }
+}

+ 583 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/App.vue

@@ -0,0 +1,583 @@
+<template>
+  <div class="page-shell">
+    <header class="hero-panel">
+      <div class="hero-copy">
+        <p class="eyebrow">Network Health Report Agent</p>
+        <h1>站点地图、健康报告、智能问答一屏联动</h1>
+        <p class="hero-text">基于 HelloAgents + FastAPI + MCP 构建的多站点网络健康演示,支持周报生成、文件下载和全局问答。</p>
+      </div>
+
+      <div class="hero-status" v-if="runtime">
+        <span class="runtime-pill" :class="runtime.qa_agent?.llm_enabled ? 'on' : 'off'">
+          LLM {{ runtime.qa_agent?.llm_enabled ? 'ON' : 'OFF' }}
+        </span>
+        <span class="runtime-pill" :class="runtime.qa_agent?.mcp_enabled ? 'on' : 'off'">
+          MCP {{ runtime.qa_agent?.mcp_enabled ? 'ON' : 'OFF' }}
+        </span>
+        <span class="runtime-desc">
+          {{ runtime.qa_agent?.mcp_enabled ? 'MCP 工具由 Agent 通过 server_command 自动拉起' : '当前环境未启用 MCPTool,采用上下文问答回退模式' }}
+        </span>
+      </div>
+    </header>
+
+    <div class="toolbar panel-surface">
+      <div class="field">
+        <label>当前站点</label>
+        <input :value="selectedSite ? selectedSite.site_name : '未选择'" disabled />
+      </div>
+
+      <div class="field">
+        <label>开始日期</label>
+        <input type="date" v-model="startDate" />
+      </div>
+
+      <div class="field">
+        <label>结束日期</label>
+        <input type="date" v-model="endDate" />
+      </div>
+
+      <button class="primary-button" @click="reloadReport" :disabled="!selectedSite || loading">
+        {{ loading ? '加载中...' : '刷新报告' }}
+      </button>
+    </div>
+
+    <div v-if="reportError" class="error-banner panel-surface">报告请求失败:{{ reportError }}</div>
+
+    <main class="layout-grid">
+      <section class="left-column">
+        <div class="panel-surface map-panel">
+          <div class="section-header">
+            <div>
+              <strong>站点地理分布</strong>
+              <small>点击站点查看健康报告</small>
+            </div>
+            <span class="section-hint">地图在左,报告在下</span>
+          </div>
+          <SiteMap :sites="sites" :selected-site-id="selectedSite?.site_id || ''" @select="handleSelectSite" />
+          <div class="site-list">
+            <button
+              v-for="site in sites"
+              :key="site.site_id"
+              class="site-chip"
+              :class="{ active: site.site_id === selectedSite?.site_id }"
+              @click="handleSelectSite(site)"
+            >
+              {{ site.city }} · {{ site.site_name }}
+            </button>
+          </div>
+        </div>
+
+        <ReportPanel :title="panelTitle" :report="report" />
+      </section>
+
+      <aside class="panel-surface qa-panel">
+        <div class="section-header">
+          <div>
+            <strong>全局网络问答</strong>
+            <small>示例:有几个 site?上海有哪些 site?帮我生成当前站点近一周报告</small>
+          </div>
+        </div>
+
+        <div class="qa-body">
+          <div class="qa-input-row">
+            <input
+              v-model="question"
+              type="text"
+              class="qa-input"
+              placeholder="请输入问题,例如:帮我生成当前站点近一周的报告"
+              @keyup.enter="ask"
+            />
+            <button class="primary-button" @click="ask" :disabled="!question.trim() || qaLoading">
+              {{ qaLoading ? '回答中...' : '提问' }}
+            </button>
+          </div>
+
+          <div class="qa-answer" v-if="qaAnswer">
+            <h3>Agent 回答</h3>
+            <pre>{{ qaAnswer }}</pre>
+          </div>
+
+          <div v-if="qaArtifact" class="qa-artifact">
+            <div class="artifact-title">已生成文件</div>
+            <a :href="qaArtifact.downloadUrl" target="_blank" rel="noreferrer" download>
+              下载 {{ qaArtifact.fileName }}
+            </a>
+          </div>
+
+          <div v-if="qaError" class="error-banner">问答失败:{{ qaError }}</div>
+
+          <div class="qa-debug" v-if="qaDebug">
+            调试信息:LLM={{ qaDebug.llmEnabled ? 'ON' : 'OFF' }}, MCP={{ qaDebug.mcpEnabled ? 'ON' : 'OFF' }}
+            <span v-if="qaDebug.reportIntent">,已触发报告生成</span>
+          </div>
+        </div>
+      </aside>
+    </main>
+  </div>
+</template>
+
+<script setup>
+import { computed, onMounted, ref } from 'vue'
+import { askQuestionStream, fetchReport, fetchRuntime, fetchSites } from './api'
+import SiteMap from './components/SiteMap.vue'
+import ReportPanel from './components/ReportPanel.vue'
+
+const sites = ref([])
+const selectedSite = ref(null)
+const report = ref(null)
+const loading = ref(false)
+const runtime = ref(null)
+const qaLoading = ref(false)
+const question = ref('')
+const qaAnswer = ref('')
+const qaArtifact = ref(null)
+const reportError = ref('')
+const qaError = ref('')
+const qaDebug = ref(null)
+
+const today = new Date()
+const endDate = ref(today.toISOString().slice(0, 10))
+const startDateObj = new Date(today)
+startDateObj.setDate(today.getDate() - 6)
+const startDate = ref(startDateObj.toISOString().slice(0, 10))
+
+const panelTitle = computed(() => {
+  if (!selectedSite.value) {
+    return '网络健康报告'
+  }
+  return `${selectedSite.value.site_name} - ${startDate.value} 至 ${endDate.value}`
+})
+
+async function loadSites() {
+  try {
+    sites.value = await fetchSites()
+    runtime.value = await fetchRuntime()
+    if (sites.value.length > 0) {
+      await handleSelectSite(sites.value[0])
+    }
+  } catch (err) {
+    reportError.value = err?.message || '初始化失败,请检查后端服务是否启动。'
+  }
+}
+
+async function handleSelectSite(site) {
+  selectedSite.value = site
+  await reloadReport()
+}
+
+async function reloadReport() {
+  if (!selectedSite.value) return
+  loading.value = true
+  reportError.value = ''
+  try {
+    report.value = await fetchReport(selectedSite.value.site_id, startDate.value, endDate.value)
+  } catch (err) {
+    report.value = null
+    reportError.value = err?.message || '报告加载失败'
+  } finally {
+    loading.value = false
+  }
+}
+
+async function ask() {
+  if (!question.value.trim()) return
+  qaLoading.value = true
+  qaAnswer.value = ''
+  qaArtifact.value = null
+  qaError.value = ''
+  qaDebug.value = null
+  try {
+    const meta = await askQuestionStream(
+      question.value.trim(),
+      startDate.value,
+      endDate.value,
+      selectedSite.value?.site_id || null,
+      (chunk, streamMeta) => {
+        qaAnswer.value += chunk
+        qaDebug.value = streamMeta
+      }
+    )
+    qaDebug.value = meta
+    if (meta.artifactUrl) {
+      qaArtifact.value = {
+        fileName: meta.artifactName || 'report.md',
+        downloadUrl: meta.artifactUrl
+      }
+    }
+    if (!qaAnswer.value.trim()) {
+      qaError.value = '接口已返回,但内容为空。请检查 LLM 配置或查看后端日志。'
+    }
+  } catch (err) {
+    qaError.value = err?.message || '问答失败'
+  } finally {
+    qaLoading.value = false
+  }
+}
+
+onMounted(loadSites)
+</script>
+
+<style scoped>
+:global(body) {
+  margin: 0;
+  background:
+    radial-gradient(circle at top left, rgba(14, 165, 164, 0.12), transparent 28%),
+    radial-gradient(circle at top right, rgba(59, 130, 246, 0.12), transparent 24%),
+    linear-gradient(180deg, #eef6f7 0%, #f7fafc 38%, #edf2f7 100%);
+  color: #0f172a;
+}
+
+:global(*) {
+  box-sizing: border-box;
+}
+
+.page-shell {
+  min-height: 100vh;
+  padding: 24px;
+  display: flex;
+  flex-direction: column;
+  gap: 18px;
+}
+
+.hero-panel,
+.panel-surface {
+  border: 1px solid rgba(15, 23, 42, 0.08);
+  border-radius: 24px;
+  box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
+  backdrop-filter: blur(14px);
+}
+
+.hero-panel {
+  padding: 28px 30px;
+  color: #eff6ff;
+  background:
+    linear-gradient(135deg, rgba(15, 23, 42, 0.98), rgba(15, 118, 110, 0.94) 58%, rgba(14, 165, 164, 0.9));
+  display: flex;
+  justify-content: space-between;
+  gap: 20px;
+  align-items: flex-end;
+}
+
+.hero-copy {
+  max-width: 860px;
+}
+
+.eyebrow {
+  margin: 0 0 10px;
+  text-transform: uppercase;
+  letter-spacing: 0.14em;
+  font-size: 12px;
+  color: rgba(226, 232, 240, 0.8);
+}
+
+.hero-copy h1 {
+  margin: 0;
+  font-size: clamp(28px, 4vw, 46px);
+  line-height: 1.08;
+}
+
+.hero-text {
+  margin: 12px 0 0;
+  max-width: 72ch;
+  color: rgba(226, 232, 240, 0.88);
+  line-height: 1.7;
+}
+
+.hero-status {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+.runtime-pill {
+  display: inline-flex;
+  align-items: center;
+  padding: 8px 12px;
+  border-radius: 999px;
+  font-size: 12px;
+  font-weight: 700;
+  letter-spacing: 0.04em;
+  border: 1px solid rgba(255, 255, 255, 0.16);
+  background: rgba(255, 255, 255, 0.1);
+}
+
+.runtime-pill.on {
+  color: #dcfce7;
+}
+
+.runtime-pill.off {
+  color: #fecaca;
+}
+
+.runtime-desc {
+  max-width: 320px;
+  color: rgba(226, 232, 240, 0.84);
+  font-size: 13px;
+  line-height: 1.6;
+}
+
+.toolbar {
+  display: grid;
+  grid-template-columns: minmax(0, 1.2fr) repeat(2, minmax(0, 1fr)) auto;
+  gap: 14px;
+  padding: 18px;
+  align-items: end;
+}
+
+.field {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.field label {
+  font-size: 12px;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+  color: #475569;
+  font-weight: 700;
+}
+
+.field input,
+.qa-input {
+  width: 100%;
+  border: 1px solid rgba(148, 163, 184, 0.32);
+  border-radius: 16px;
+  background: rgba(255, 255, 255, 0.88);
+  color: #0f172a;
+  padding: 14px 16px;
+  font-size: 14px;
+  outline: none;
+  transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
+}
+
+.field input:focus,
+.qa-input:focus {
+  border-color: rgba(20, 184, 166, 0.55);
+  box-shadow: 0 0 0 4px rgba(20, 184, 166, 0.12);
+}
+
+.primary-button,
+.site-chip {
+  border: none;
+  cursor: pointer;
+  transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
+}
+
+.primary-button {
+  padding: 14px 18px;
+  border-radius: 16px;
+  font-weight: 700;
+  color: white;
+  background: linear-gradient(135deg, #0f766e, #14b8a6);
+  box-shadow: 0 12px 24px rgba(15, 118, 110, 0.24);
+}
+
+.primary-button:hover:not(:disabled),
+.site-chip:hover:not(:disabled) {
+  transform: translateY(-1px);
+}
+
+.primary-button:disabled,
+.site-chip:disabled {
+  opacity: 0.55;
+  cursor: not-allowed;
+}
+
+.error-banner {
+  padding: 14px 16px;
+  border-radius: 16px;
+  background: rgba(254, 226, 226, 0.9);
+  color: #991b1b;
+  border: 1px solid rgba(248, 113, 113, 0.18);
+}
+
+.layout-grid {
+  display: grid;
+  grid-template-columns: minmax(0, 1.45fr) minmax(340px, 0.92fr);
+  gap: 20px;
+  align-items: start;
+}
+
+.left-column {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.map-panel,
+.qa-panel {
+  padding: 20px;
+}
+
+.section-header {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 14px;
+  margin-bottom: 16px;
+}
+
+.section-header strong {
+  display: block;
+  font-size: 18px;
+  color: #0f172a;
+}
+
+.section-header small,
+.section-hint {
+  color: #64748b;
+}
+
+.section-hint {
+  white-space: nowrap;
+  font-size: 12px;
+  padding: 6px 10px;
+  border-radius: 999px;
+  background: rgba(15, 23, 42, 0.05);
+}
+
+.site-list {
+  margin-top: 14px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.site-chip {
+  padding: 10px 14px;
+  border-radius: 999px;
+  background: rgba(15, 23, 42, 0.06);
+  color: #0f172a;
+  font-size: 13px;
+  font-weight: 600;
+}
+
+.site-chip.active {
+  color: #ecfeff;
+  background: linear-gradient(135deg, #0f766e, #14b8a6);
+  box-shadow: 0 10px 24px rgba(15, 118, 110, 0.26);
+}
+
+.qa-panel {
+  position: sticky;
+  top: 18px;
+}
+
+.qa-body {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.qa-input-row {
+  display: flex;
+  gap: 12px;
+}
+
+.qa-answer {
+  border-radius: 20px;
+  padding: 18px;
+  background: linear-gradient(180deg, rgba(15, 23, 42, 0.03), rgba(15, 23, 42, 0.015));
+  border: 1px solid rgba(148, 163, 184, 0.18);
+}
+
+.qa-answer h3 {
+  margin: 0 0 12px;
+  font-size: 15px;
+  color: #0f172a;
+}
+
+.qa-answer pre {
+  margin: 0;
+  white-space: pre-wrap;
+  word-break: break-word;
+  line-height: 1.75;
+  color: #1e293b;
+  font-family: inherit;
+  font-size: 14px;
+}
+
+.qa-artifact {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 14px 16px;
+  border-radius: 18px;
+  background: rgba(236, 253, 245, 0.95);
+  border: 1px solid rgba(16, 185, 129, 0.18);
+}
+
+.artifact-title {
+  font-size: 13px;
+  font-weight: 700;
+  color: #065f46;
+}
+
+.qa-artifact a {
+  color: #0f766e;
+  font-weight: 700;
+  text-decoration: none;
+}
+
+.qa-artifact a:hover {
+  text-decoration: underline;
+}
+
+.qa-debug {
+  font-size: 12px;
+  color: #64748b;
+  padding: 4px 2px 0;
+}
+
+@media (max-width: 1160px) {
+  .toolbar {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .layout-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .qa-panel {
+    position: static;
+  }
+}
+
+@media (max-width: 720px) {
+  .page-shell {
+    padding: 14px;
+  }
+
+  .hero-panel,
+  .map-panel,
+  .qa-panel,
+  .panel-surface,
+  .toolbar {
+    border-radius: 18px;
+  }
+
+  .hero-panel {
+    padding: 20px;
+    align-items: flex-start;
+    flex-direction: column;
+  }
+
+  .toolbar,
+  .qa-input-row {
+    grid-template-columns: 1fr;
+    flex-direction: column;
+  }
+
+  .section-header {
+    flex-direction: column;
+  }
+
+  .qa-artifact {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+}
+</style>

+ 123 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/api.js

@@ -0,0 +1,123 @@
+import axios from 'axios'
+
+const API_TIMEOUT = 15000
+
+const candidateBases = (() => {
+  const envBase = import.meta.env.VITE_API_BASE_URL
+  const currentHost = typeof window !== 'undefined' ? window.location.hostname : 'localhost'
+  const protocol = typeof window !== 'undefined' ? window.location.protocol : 'http:'
+
+  const bases = []
+  if (envBase) {
+    bases.push(envBase.replace(/\/$/, ''))
+  }
+  bases.push('http://127.0.0.1:8000/api')
+  bases.push('http://localhost:8000/api')
+  bases.push(`${protocol}//${currentHost}:8000/api`)
+
+  return [...new Set(bases)]
+})()
+
+let activeBase = null
+
+const api = axios.create({ timeout: API_TIMEOUT })
+
+async function resolveBase() {
+  if (activeBase) {
+    return activeBase
+  }
+
+  for (const base of candidateBases) {
+    try {
+      await axios.get(`${base}/health`, { timeout: 3000 })
+      activeBase = base
+      return base
+    } catch (_) {
+      // Try next candidate
+    }
+  }
+
+  throw new Error(`No reachable backend API. Tried: ${candidateBases.join(', ')}`)
+}
+
+async function apiGet(path, config = {}) {
+  const base = await resolveBase()
+  return api.get(`${base}${path}`, config)
+}
+
+async function apiPost(path, body = {}, config = {}) {
+  const base = await resolveBase()
+  return api.post(`${base}${path}`, body, config)
+}
+
+export async function fetchSites() {
+  const { data } = await apiGet('/sites')
+  return data.sites
+}
+
+export async function fetchReport(siteId, startDate, endDate) {
+  const { data } = await apiGet(`/reports/${siteId}`, {
+    params: {
+      start_date: startDate,
+      end_date: endDate
+    }
+  })
+  return data.report
+}
+
+export async function fetchRuntime() {
+  const { data } = await apiGet('/runtime')
+  return data.runtime
+}
+
+export async function askQuestion(question, startDate, endDate, siteId = null) {
+  const { data } = await apiPost('/chat', {
+    question,
+    start_date: startDate,
+    end_date: endDate,
+    site_id: siteId
+  })
+  return data
+}
+
+export async function askQuestionStream(question, startDate, endDate, siteId = null, onChunk) {
+  const base = await resolveBase()
+  const resp = await fetch(`${base}/chat/stream`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json'
+    },
+    body: JSON.stringify({
+      question,
+      start_date: startDate,
+      end_date: endDate,
+      site_id: siteId
+    })
+  })
+
+  if (!resp.ok || !resp.body) {
+    const message = await resp.text()
+    throw new Error(`stream request failed: ${resp.status} ${message}`)
+  }
+
+  const meta = {
+    llmEnabled: resp.headers.get('X-Agent-LLM') === 'true',
+    mcpEnabled: resp.headers.get('X-Agent-MCP') === 'true',
+    reportIntent: resp.headers.get('X-Agent-Report-Intent') === 'true',
+    artifactUrl: resp.headers.get('X-Agent-Artifact-Url') || '',
+    artifactName: resp.headers.get('X-Agent-Artifact-Name') || ''
+  }
+
+  const reader = resp.body.getReader()
+  const decoder = new TextDecoder('utf-8')
+
+  while (true) {
+    const { done, value } = await reader.read()
+    if (done) {
+      break
+    }
+    onChunk(decoder.decode(value, { stream: true }), meta)
+  }
+
+  return meta
+}

+ 71 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/components/ReportPanel.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="panel">
+    <div class="report-header">
+      <div>
+        <strong>{{ title }}</strong>
+      </div>
+      <span v-if="report" class="badge" :class="report.health_level">{{ report.health_level }}</span>
+    </div>
+
+    <div v-if="!report" class="empty">请选择一个站点查看健康报告</div>
+
+    <div v-else class="report-body">
+      <div class="kpis">
+        <div class="kpi">
+          <div class="label">健康评分</div>
+          <div class="value">{{ report.health_score }}</div>
+        </div>
+        <div class="kpi">
+          <div class="label">设备在线率</div>
+          <div class="value">{{ (report.sections.device_status.online_rate * 100).toFixed(2) }}%</div>
+        </div>
+        <div class="kpi">
+          <div class="label">终端合规率</div>
+          <div class="value">{{ (report.sections.user_status.compliant_rate * 100).toFixed(2) }}%</div>
+        </div>
+      </div>
+
+      <div class="section">
+        <h3>日志分析 Agent</h3>
+        <p>{{ report.sections.log_analysis.summary }}</p>
+      </div>
+
+      <div class="section">
+        <h3>设备状态 Agent</h3>
+        <p>{{ report.sections.device_status.summary }}</p>
+      </div>
+
+      <div class="section">
+        <h3>用户状态 Agent</h3>
+        <p>{{ report.sections.user_status.summary }}</p>
+      </div>
+
+      <div class="section">
+        <h3>网络健康报告 Agent 建议</h3>
+        <ul class="recommendation-list">
+          <li v-for="(item, idx) in report.recommendations" :key="idx">{{ item }}</li>
+        </ul>
+      </div>
+
+      <div class="section" v-if="report.llm_insight">
+        <h3>大模型综合研判</h3>
+        <p>{{ report.llm_insight }}</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'ReportPanel' })
+
+defineProps({
+  title: {
+    type: String,
+    default: '网络健康报告'
+  },
+  report: {
+    type: Object,
+    default: null
+  }
+})
+</script>

+ 95 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/components/SiteMap.vue

@@ -0,0 +1,95 @@
+<template>
+  <div ref="mapEl" class="map-container"></div>
+</template>
+
+<script setup>
+import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
+import L from 'leaflet'
+
+defineOptions({ name: 'SiteMap' })
+
+const props = defineProps({
+  sites: {
+    type: Array,
+    default: () => []
+  },
+  selectedSiteId: {
+    type: String,
+    default: ''
+  }
+})
+
+const emit = defineEmits(['select'])
+const mapEl = ref(null)
+let map = null
+let markers = []
+let labels = []
+
+function markerColor(siteId) {
+  return siteId === props.selectedSiteId ? '#c7382f' : '#1864ab'
+}
+
+function drawMarkers() {
+  markers.forEach((m) => m.remove())
+  labels.forEach((l) => l.remove())
+  markers = []
+  labels = []
+
+  props.sites.forEach((site) => {
+    const isSelected = site.site_id === props.selectedSiteId
+    const marker = L.circleMarker([site.latitude, site.longitude], {
+      radius: isSelected ? 11 : 8,
+      color: markerColor(site.site_id),
+      weight: 2,
+      fillOpacity: 0.8
+    })
+    marker.bindPopup(
+      `<b>${site.site_name}</b><br/>${site.province}${site.city}<br/>` +
+      `经纬度: ${site.latitude.toFixed(2)}, ${site.longitude.toFixed(2)}`
+    )
+    marker.on('click', () => emit('select', site))
+    marker.addTo(map)
+    markers.push(marker)
+
+    const label = L.marker([site.latitude + 0.18, site.longitude + 0.22], {
+      interactive: false,
+      icon: L.divIcon({
+        className: 'site-label',
+        html: `<span>${site.city}</span>`
+      })
+    })
+    label.addTo(map)
+    labels.push(label)
+
+    if (isSelected) {
+      marker.openPopup()
+      map.flyTo([site.latitude, site.longitude], Math.max(map.getZoom(), 6), { duration: 0.6 })
+    }
+  })
+
+  if (props.sites.length > 1 && !props.selectedSiteId) {
+    const bounds = L.latLngBounds(props.sites.map((s) => [s.latitude, s.longitude]))
+    map.fitBounds(bounds.pad(0.45))
+  }
+}
+
+onMounted(() => {
+  map = L.map(mapEl.value).setView([35.5, 104.5], 4.2)
+  L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
+    maxZoom: 18,
+    attribution: '&copy; OpenStreetMap contributors &copy; CARTO'
+  }).addTo(map)
+  L.control.scale({ imperial: false }).addTo(map)
+  drawMarkers()
+})
+
+watch(() => props.sites, drawMarkers, { deep: true })
+watch(() => props.selectedSiteId, drawMarkers)
+
+onBeforeUnmount(() => {
+  if (map) {
+    map.remove()
+    map = null
+  }
+})
+</script>

+ 6 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/main.js

@@ -0,0 +1,6 @@
+import { createApp } from 'vue'
+import 'leaflet/dist/leaflet.css'
+import App from './App.vue'
+import './styles.css'
+
+createApp(App).mount('#app')

+ 302 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/src/styles.css

@@ -0,0 +1,302 @@
+:root {
+  color-scheme: light;
+  --bg: #f4f7fb;
+  --card: #ffffff;
+  --line: #d9e0eb;
+  --text: #0f172a;
+  --sub: #4b5563;
+  --ok: #1f8f4d;
+  --warn: #e0a100;
+  --bad: #c7382f;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+  background: radial-gradient(circle at 0% 0%, #ebf3ff 0%, var(--bg) 40%, #edf3f8 100%);
+  color: var(--text);
+}
+
+.page {
+  max-width: 1300px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.page h1 {
+  margin: 0 0 10px;
+  font-size: 28px;
+}
+
+.runtime-strip {
+  margin: 10px 0 0;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.runtime-pill {
+  border-radius: 999px;
+  padding: 4px 10px;
+  font-size: 12px;
+  border: 1px solid var(--line);
+}
+
+.runtime-pill.on {
+  color: #0a7f3f;
+  background: #e9f7ef;
+  border-color: #bde5cb;
+}
+
+.runtime-pill.off {
+  color: #8a5b00;
+  background: #fff6e0;
+  border-color: #f6db9c;
+}
+
+.runtime-desc {
+  color: var(--sub);
+  font-size: 13px;
+}
+
+.toolbar {
+  display: grid;
+  grid-template-columns: 1fr auto auto auto;
+  gap: 12px;
+  align-items: end;
+  margin: 14px 0 16px;
+}
+
+.field {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.field label {
+  color: var(--sub);
+  font-size: 13px;
+}
+
+input[type='date'],
+button {
+  border: 1px solid var(--line);
+  border-radius: 10px;
+  height: 38px;
+  padding: 0 12px;
+  background: var(--card);
+}
+
+button {
+  cursor: pointer;
+  font-weight: 600;
+}
+
+.layout {
+  display: grid;
+  grid-template-columns: 1.1fr 1fr;
+  gap: 16px;
+}
+
+.panel {
+  background: var(--card);
+  border: 1px solid var(--line);
+  border-radius: 14px;
+  box-shadow: 0 6px 24px rgba(26, 50, 80, 0.08);
+}
+
+.map-panel {
+  overflow: hidden;
+}
+
+.map-header,
+.report-header {
+  padding: 14px 16px;
+  border-bottom: 1px solid var(--line);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.badge {
+  border-radius: 999px;
+  padding: 4px 10px;
+  font-size: 12px;
+  color: #fff;
+}
+
+.badge.healthy { background: var(--ok); }
+.badge.warning { background: var(--warn); }
+.badge.critical { background: var(--bad); }
+
+.map-container {
+  height: 560px;
+}
+
+.site-list {
+  padding: 12px;
+  border-top: 1px solid var(--line);
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.site-chip {
+  height: 32px;
+  border-radius: 999px;
+  border: 1px solid var(--line);
+  background: #f8fbff;
+  padding: 0 12px;
+  font-size: 12px;
+}
+
+.site-chip.active {
+  background: #e6f1ff;
+  border-color: #7aa7de;
+  color: #11467a;
+}
+
+.qa-panel {
+  margin-top: 16px;
+}
+
+.qa-body {
+  padding: 14px 16px;
+  display: grid;
+  gap: 12px;
+}
+
+.qa-input-row {
+  display: grid;
+  grid-template-columns: 1fr auto;
+  gap: 10px;
+}
+
+.qa-input {
+  border: 1px solid var(--line);
+  border-radius: 10px;
+  height: 38px;
+  padding: 0 12px;
+  background: var(--card);
+}
+
+.qa-answer h3 {
+  margin: 0 0 8px;
+  font-size: 15px;
+}
+
+.qa-answer pre {
+  white-space: pre-wrap;
+  margin: 0;
+  padding: 10px;
+  border: 1px solid var(--line);
+  border-radius: 10px;
+  background: #f8fafc;
+  color: var(--text);
+  font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+}
+
+.error-banner {
+  margin-top: 10px;
+  border: 1px solid #f3b3b0;
+  background: #fff1f0;
+  color: #9d2b22;
+  border-radius: 10px;
+  padding: 10px 12px;
+  font-size: 13px;
+}
+
+.qa-debug {
+  margin-top: 6px;
+  color: var(--sub);
+  font-size: 12px;
+}
+
+.site-label span {
+  display: inline-block;
+  padding: 1px 6px;
+  border-radius: 8px;
+  background: rgba(15, 23, 42, 0.78);
+  color: #fff;
+  font-size: 11px;
+  font-weight: 600;
+}
+
+.report-body {
+  padding: 14px 16px;
+  display: grid;
+  gap: 12px;
+}
+
+.kpis {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 10px;
+}
+
+.kpi {
+  padding: 10px;
+  border: 1px solid var(--line);
+  border-radius: 10px;
+}
+
+.kpi .label {
+  color: var(--sub);
+  font-size: 12px;
+}
+
+.kpi .value {
+  margin-top: 4px;
+  font-size: 21px;
+  font-weight: 700;
+}
+
+.section {
+  border: 1px solid var(--line);
+  border-radius: 10px;
+  padding: 10px;
+}
+
+.section h3 {
+  margin: 0 0 8px;
+  font-size: 15px;
+}
+
+.section p {
+  margin: 0;
+  color: var(--sub);
+}
+
+.recommendation-list {
+  margin: 0;
+  padding-left: 18px;
+}
+
+.empty {
+  padding: 24px;
+  text-align: center;
+  color: var(--sub);
+}
+
+@media (max-width: 980px) {
+  .toolbar {
+    grid-template-columns: 1fr 1fr;
+  }
+
+  .layout {
+    grid-template-columns: 1fr;
+  }
+
+  .map-container {
+    height: 420px;
+  }
+
+  .qa-input-row {
+    grid-template-columns: 1fr;
+  }
+}

+ 10 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/frontend/vite.config.js

@@ -0,0 +1,10 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+  plugins: [vue()],
+  server: {
+    port: 5173,
+    host: '0.0.0.0'
+  }
+})

+ 553 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/main.ipynb

@@ -0,0 +1,553 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "cb4209e5",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'\\n## 项目名称\\nNetworkHealthReportAgent\\n\\n> 基于 Hello-Agents + FastAPI + MCP 的企业多站点网络健康分析与问答系统\\n> 说明:本 Notebook 按“项目介绍 -> 环境配置 -> 工具定义 -> 智能体构建 -> 功能演示 -> 性能评估 -> 总结展望”组织,便于演示与复现。\\n\\n## 作者信息\\n- 姓名:monkeyhlj\\n- GitHub:@monkeyhlj\\n- 日期:2026-05-31\\n'"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# ========================================\n",
+    "# 第1部分: 项目介绍\n",
+    "# ========================================\n",
+    "\"\"\"\n",
+    "## 项目名称\n",
+    "NetworkHealthReportAgent\n",
+    "\n",
+    "> 基于 Hello-Agents + FastAPI + MCP 的企业多站点网络健康分析与问答系统\n",
+    "> 说明:本 Notebook 按“项目介绍 -> 环境配置 -> 工具定义 -> 智能体构建 -> 功能演示 -> 性能评估 -> 总结展望”组织,便于演示与复现。\n",
+    "\n",
+    "## 作者信息\n",
+    "- 姓名:monkeyhlj\n",
+    "- GitHub:@monkeyhlj\n",
+    "- 日期:2026-05-31\n",
+    "\"\"\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "3060d3be",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "当前工作目录: c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\n"
+     ]
+    }
+   ],
+   "source": [
+    "# ========================================\n",
+    "# 第2部分: 环境配置\n",
+    "# ========================================\n",
+    "\n",
+    "## 可选:在首次运行时安装依赖(已安装可跳过)\n",
+    "# !pip install -q hello-agents[all]\n",
+    "# !pip install -q -r requirements.txt\n",
+    "\n",
+    "# 导入必要库\n",
+    "from datetime import date, timedelta\n",
+    "from pathlib import Path\n",
+    "import os\n",
+    "import json\n",
+    "import time\n",
+    "\n",
+    "from dotenv import load_dotenv\n",
+    "\n",
+    "# 加载环境变量\n",
+    "load_dotenv()\n",
+    "\n",
+    "# 切换到项目根目录(保证相对路径可用)\n",
+    "PROJECT_ROOT = Path.cwd()\n",
+    "if not (PROJECT_ROOT / \"src\").exists():\n",
+    "    # Notebook 可能从仓库根目录打开,自动切换到项目目录\n",
+    "    PROJECT_ROOT = Path.cwd() / \"Co-creation-projects\" / \"monkeyhlj-NetworkHealthReportAgent\"\n",
+    "os.chdir(PROJECT_ROOT)\n",
+    "\n",
+    "print(\"当前工作目录:\", Path.cwd())"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "0fb88e3a",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "工具类定义完成:SiteQuickLookupTool\n"
+     ]
+    }
+   ],
+   "source": [
+    "# ========================================\n",
+    "# 第3部分: 工具定义\n",
+    "# ========================================\n",
+    "\n",
+    "from hello_agents.tools.base import Tool, ToolParameter\n",
+    "\n",
+    "class SiteQuickLookupTool(Tool):\n",
+    "    \"\"\"示例工具:根据城市关键字在站点列表中快速检索。\"\"\"\n",
+    "\n",
+    "    def __init__(self, sites):\n",
+    "        super().__init__(\n",
+    "            name=\"site_quick_lookup\",\n",
+    "            description=\"根据城市或关键字快速查找站点信息\",\n",
+    "            expandable=False,\n",
+    "        )\n",
+    "        self._sites = sites\n",
+    "\n",
+    "    def get_parameters(self):\n",
+    "        return [\n",
+    "            ToolParameter(\n",
+    "                name=\"query\",\n",
+    "                type=\"string\",\n",
+    "                description=\"城市名、site_id 或站点名关键字\",\n",
+    "                required=True,\n",
+    "            )\n",
+    "        ]\n",
+    "\n",
+    "    def run(self, parameters):\n",
+    "        q = (parameters or {}).get(\"query\", \"\").strip()\n",
+    "        if not q:\n",
+    "            return \"请输入城市或站点关键字。\"\n",
+    "        matched = [\n",
+    "            s for s in self._sites\n",
+    "            if q in s.get(\"city\", \"\") or q.lower() in s.get(\"site_id\", \"\").lower() or q in s.get(\"site_name\", \"\")\n",
+    "        ]\n",
+    "        if not matched:\n",
+    "            return f\"未匹配到与 {q} 相关的站点。\"\n",
+    "        return json.dumps(matched, ensure_ascii=False, indent=2)\n",
+    "\n",
+    "print(\"工具类定义完成:SiteQuickLookupTool\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "6d41ae13",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "📝 使用 Stdio 传输 (命令): c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Scripts\\python.exe -m src.tools.data_mcp_server\n",
+      "🔗 连接到 MCP 服务器...\n",
+      "✅ 工具 'network_data' 已注册。\n",
+      "📝 使用 Stdio 传输 (命令): c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Scripts\\python.exe -m src.tools.data_mcp_server\n",
+      "🔗 连接到 MCP 服务器...\n",
+      "✅ 工具 'network_data' 已注册。\n",
+      "📝 使用 Stdio 传输 (命令): c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Scripts\\python.exe -m src.tools.data_mcp_server\n",
+      "🔗 连接到 MCP 服务器...\n",
+      "✅ 工具 'network_data' 已注册。\n",
+      "📝 使用 Stdio 传输 (命令): c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Scripts\\python.exe -m src.tools.data_mcp_server\n",
+      "🔗 连接到 MCP 服务器...\n",
+      "✅ 工具 'network_data' 已注册。\n",
+      "📝 使用 Stdio 传输 (命令): c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Scripts\\python.exe -m src.tools.data_mcp_server\n",
+      "🔗 连接到 MCP 服务器...\n",
+      "✅ 工具 'network_data' 已注册。\n",
+      "站点数量: 6\n",
+      "QA Agent 运行态: {'llm_enabled': True, 'mcp_enabled': True}\n",
+      "前3个站点:\n",
+      "- site-bj-hq | 北京总部园区 | 北京\n",
+      "- site-sh-fin | 上海金融中心 | 上海\n",
+      "- site-gz-rd | 广州研发中心 | 广州\n",
+      "✅ 工具 'site_quick_lookup' 已注册。\n",
+      "demo_agent 已创建,可用于演示。\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Lib\\site-packages\\hello_agents\\tools\\builtin\\protocol_tools.py:278: RuntimeWarning: coroutine 'MCPTool._discover_tools.<locals>.discover' was never awaited\n",
+      "  self._available_tools = []\n",
+      "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n"
+     ]
+    }
+   ],
+   "source": [
+    "# ========================================\n",
+    "# 第4部分: 智能体构建\n",
+    "# ========================================\n",
+    "# 这里使用项目内置编排器 `NetworkHealthOrchestrator` 作为主入口,同时演示一个可选的 `SimpleAgent + 自定义工具` 组合。\n",
+    "# 项目编排器(推荐主入口)\n",
+    "from src.agents.orchestrator import NetworkHealthOrchestrator\n",
+    "\n",
+    "orchestrator = NetworkHealthOrchestrator()\n",
+    "sites = orchestrator.list_sites()\n",
+    "runtime = orchestrator.runtime_status()\n",
+    "\n",
+    "print(\"站点数量:\", len(sites))\n",
+    "print(\"QA Agent 运行态:\", runtime.get(\"qa_agent\", {}))\n",
+    "print(\"前3个站点:\")\n",
+    "for s in sites[:3]:\n",
+    "    print(f\"- {s['site_id']} | {s['site_name']} | {s['city']}\")\n",
+    "\n",
+    "# 可选:SimpleAgent + 自定义工具(展示 hello-agents 原生能力)\n",
+    "from hello_agents import HelloAgentsLLM, SimpleAgent\n",
+    "from dotenv import load_dotenv\n",
+    "load_dotenv()\n",
+    "api_key = os.getenv(\"LLM_API_KEY\") or os.getenv(\"OPENAI_API_KEY\")\n",
+    "demo_agent = None\n",
+    "if api_key:\n",
+    "    llm_kwargs = {\"api_key\": api_key}\n",
+    "    base_url = os.getenv(\"LLM_BASE_URL\") or os.getenv(\"OPENAI_BASE_URL\")\n",
+    "    model = os.getenv(\"LLM_MODEL_ID\") or os.getenv(\"OPENAI_MODEL\")\n",
+    "    if base_url:\n",
+    "        llm_kwargs[\"base_url\"] = base_url\n",
+    "    if model:\n",
+    "        llm_kwargs[\"model\"] = model\n",
+    "    llm = HelloAgentsLLM(**llm_kwargs)\n",
+    "    demo_agent = SimpleAgent(\n",
+    "        name=\"NotebookDemoAgent\",\n",
+    "        llm=llm,\n",
+    "        system_prompt=\"你是网络运维助手,请基于工具输出提供简洁建议。\",\n",
+    "    )\n",
+    "    demo_agent.add_tool(SiteQuickLookupTool(sites))\n",
+    "    print(\"demo_agent 已创建,可用于演示。\")\n",
+    "else:\n",
+    "    print(\"未检测到 LLM_API_KEY/OPENAI_API_KEY,跳过 demo_agent 构建。\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "id": "aec87975",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "=== 示例1: demo_agent + 自定义工具问答 ===\n",
+      "question: 请查询成都站点信息,并给出一句运维建议。\n",
+      "tool_result_preview:\n",
+      " [\n",
+      "  {\n",
+      "    \"site_id\": \"site-cd-west\",\n",
+      "    \"site_name\": \"成都西区办公室\",\n",
+      "    \"city\": \"成都\",\n",
+      "    \"province\": \"四川省\",\n",
+      "    \"latitude\": 30.5728,\n",
+      "    \"longitude\": 104.0668,\n",
+      "    \"region\": \"west\",\n",
+      "    \"site_type\": \"branch\",\n",
+      "    \"criticality\": \"medium\"\n",
+      "  }\n",
+      "]\n",
+      "answer:\n",
+      " 针对该站点为**中等重要级别**的**分支机构**,建议重点关注其广域网链路的稳定性与带宽利用率,并做好出口设备的日常巡检与配置备份,以保障日常办公业务的网络顺畅。\n",
+      "\n",
+      "=== 示例2: 单站点基础报告 ===\n",
+      "site: 北京总部园区 site-bj-hq\n",
+      "score: 90.8 level: healthy\n",
+      "summary: 北京总部园区在统计周期内网络健康评分为90.8,等级为healthy。\n",
+      "\n",
+      "=== 示例3: 全局问答 ===\n",
+      "answer:\n",
+      " 1) **结论**\n",
+      "上海目前共有 1 个站点:**上海金融中心**(站点ID: site-sh-fin)。该站点当前网络健康状态处于告警水平。\n",
+      "\n",
+      "2) **关键证据**\n",
+      "- **基本信息**:根据全局网络站点列表,上海地区仅有“上海金融中心”这一个分支站点,且属于高重要性(criticality: high)节点。\n",
+      "- **健康状态**:在统计周期内(2026-05-25 至 2026-05-31),该站点健康评分为 **72.9**,整体等级为 **warning**。\n",
+      "- **异常指标**:\n",
+      "  - **设备告警**:存在高达 17 条严重告警,且捕获到 1 次认证失败激增(auth_failure_spike)和 1 次信道干扰事件。\n",
+      "  - **终端合规**:终端合规率仅为 94.6%,存在 21 台高风险终端和 27 台未知终端,用户状态评级为 degraded。\n",
+      "\n",
+      "3) **建议动作**\n",
+      "- **紧急排查**:针对上海金融中心的 critical 日志对应链路与设备进行根因定位,检查近期是否有异常变更并执行回滚。\n",
+      "- **终端准入收严**:立即提升终端准入合规率,排查并隔离\n",
+      "debug: {'agent': 'SiteQAAgent', 'llm_enabled': True, 'mcp_enabled': True, 'tools': ['network_data'], 'flow': ['collect_global_context', 'build_prompt', 'try_llm_or_stream', 'fallback_if_needed'], 'intent': 'general_qa', 'target_site_id': 'site-sh-fin', 'site_count': 6, 'report_count': 6}\n",
+      "\n",
+      "=== 示例4: 成都站点近一周报告导出 ===\n",
+      "intent: site_report_export\n",
+      "target_site_id: site-cd-west\n",
+      "artifact: {'file_name': 'site-cd-west_2026-05-25_2026-05-31_20260531_192804.md', 'file_path': 'C:\\\\Users\\\\houlj12\\\\Desktop\\\\work\\\\test\\\\hello-agents\\\\Co-creation-projects\\\\monkeyhlj-NetworkHealthReportAgent\\\\outputs\\\\site-cd-west_2026-05-25_2026-05-31_20260531_192804.md'}\n",
+      "answer_preview:\n",
+      " # 成都西区办公室 近一周网络健康报告\n",
+      "\n",
+      "- 生成时间:2026-05-31 19:28:04\n",
+      "- 统计窗口:2026-05-25 至 2026-05-31\n",
+      "- 站点编号:site-cd-west\n",
+      "- 站点城市:成都\n",
+      "- 健康评分:90.8\n",
+      "- 健康等级:healthy\n",
+      "\n",
+      "## 大模型摘要\n",
+      "**【成都西区办公室近一周网络健康报告摘要(2026.05.25 - 2026.05.31)】**\n",
+      "\n",
+      "*   **总体结论**\n",
+      "    成都西区办公室近一周网络整体运行平稳,健康评分达 **90.8**(状态:**健康**)。核心网络指标表现优良,3台网络设备平均在线率高达99.65%,平均时延仅11.05ms;终端准入合规率保持在97.7%的高位水平,基础网络架构与业务承载状态良好。\n",
+      "\n",
+      "*   **主要风险**\n",
+      "    当前全局风险为低,但存在两项需关注的局部隐患:\n",
+      "    1. **底层链路隐患**:日志监测到1起 `crc_errors`(CRC校验错误)警告,且伴随2条设备级告警,提示可能存在物理链路质量抖动或接口/线缆硬件老化风险。\n",
+      "    2. **终端接入盲区**:内网中仍存活有 **7台高风险终端** 和 **8台未知终端**,存在一定的边界安全隐患。\n",
+      "\n",
+      "*   **优先动作**\n",
+      "    1. **硬件与链路排查(建议优先)**:针对产生CRC错误的设备端口进行误码率检查,巡检光模块或网线,必要时更换线缆,将隐患扼杀在摇篮中。\n",
+      "    2. **终端准入收严**:核查并清理现网中的7台高风险与8台未知终端,进一步收严网络准入控制(NAC)策略。\n",
+      "    3. **日常维护**:当前网络性能稳定,建议维持现网路由与QoS策略不变,持续监测业务高峰期的带宽容量,并按周度进行常态化复盘。\n",
+      "\n",
+      "## 核心指标\n",
+      "- 设备在线率:99.65%\n",
+      "- 平均时延:11.05 ms\n",
+      "- 平均丢\n"
+     ]
+    }
+   ],
+   "source": [
+    "# ========================================\n",
+    "# 第5部分: 功能演示\n",
+    "# ========================================\n",
+    "\"\"\"\n",
+    "下面给出 4 个演示:\n",
+    "1. `demo_agent` + 自定义工具问答(如果已配置 LLM)\n",
+    "2. 单站点近一周健康报告\n",
+    "3. 全局问答\n",
+    "4. 生成“成都站点近一周报告”并展示下载链接信息(后端接口模式)\n",
+    "\"\"\"\n",
+    "\n",
+    "print(\"=== 示例1: demo_agent + 自定义工具问答 ===\")\n",
+    "# 为了避免模型未触发工具调用导致空结果,这里先直接调用工具拿到站点上下文\n",
+    "lookup_tool = SiteQuickLookupTool(sites)\n",
+    "cd_context = lookup_tool.run({\"query\": \"成都\"})\n",
+    "\n",
+    "if demo_agent is None:\n",
+    "    print(\"未创建 demo_agent(通常是因为未配置 LLM_API_KEY/OPENAI_API_KEY),跳过该示例。\")\n",
+    "    print(\"工具直查结果:\\n\", cd_context)\n",
+    "else:\n",
+    "    try:\n",
+    "        demo_question = \"请基于以下成都站点信息,给出一句运维建议。\\n\\n\" + cd_context\n",
+    "        demo_result = demo_agent.run(demo_question)\n",
+    "        print(\"question: 请查询成都站点信息,并给出一句运维建议。\")\n",
+    "        print(\"tool_result_preview:\\n\", cd_context[:300])\n",
+    "        print(\"answer:\\n\", str(demo_result)[:600])\n",
+    "    except Exception as e:\n",
+    "        print(\"demo_agent 运行失败:\", e)\n",
+    "        print(\"工具直查结果:\\n\", cd_context)\n",
+    "\n",
+    "print(\"\\n=== 示例2: 单站点基础报告 ===\")\n",
+    "target_site = sites[0][\"site_id\"]\n",
+    "end = date.today()\n",
+    "start = end - timedelta(days=6)\n",
+    "report = orchestrator.build_report(site_id=target_site, start=start, end=end)\n",
+    "print(\"site:\", report[\"site\"][\"site_name\"], report[\"site\"][\"site_id\"])\n",
+    "print(\"score:\", report[\"health_score\"], \"level:\", report[\"health_level\"])\n",
+    "print(\"summary:\", report[\"summary\"])\n",
+    "\n",
+    "print(\"\\n=== 示例3: 全局问答 ===\")\n",
+    "qa_resp = orchestrator.ask_global_question(\n",
+    "    question=\"上海有哪些site?\",\n",
+    "    start=start,\n",
+    "    end=end,\n",
+    "    site_id=None,\n",
+    ")\n",
+    "print(\"answer:\\n\", qa_resp[\"answer\"][:500])\n",
+    "print(\"debug:\", qa_resp.get(\"debug\", {}))\n",
+    "\n",
+    "print(\"\\n=== 示例4: 成都站点近一周报告导出 ===\")\n",
+    "export_resp = orchestrator.ask_global_question(\n",
+    "    question=\"帮我生成成都site近一周的报告\",\n",
+    "    start=start,\n",
+    "    end=end,\n",
+    "    site_id=target_site,  # 即便传入其他站点,系统也会优先按问题匹配“成都”\n",
+    ")\n",
+    "print(\"intent:\", export_resp.get(\"debug\", {}).get(\"intent\"))\n",
+    "print(\"target_site_id:\", export_resp.get(\"debug\", {}).get(\"target_site_id\"))\n",
+    "print(\"artifact:\", export_resp.get(\"artifact\"))\n",
+    "print(\"answer_preview:\\n\", (export_resp.get(\"answer\") or \"\")[:800])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "id": "9d776024",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Lib\\site-packages\\fastapi\\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.\n",
+      "  from starlette.testclient import TestClient as TestClient  # noqa\n",
+      "c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Lib\\site-packages\\hello_agents\\tools\\builtin\\protocol_tools.py:278: RuntimeWarning: coroutine 'MCPTool._discover_tools.<locals>.discover' was never awaited\n",
+      "  self._available_tools = []\n",
+      "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "📝 使用 Stdio 传输 (命令): c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Scripts\\python.exe -m src.tools.data_mcp_server\n",
+      "🔗 连接到 MCP 服务器...\n",
+      "✅ 工具 'network_data' 已注册。\n",
+      "📝 使用 Stdio 传输 (命令): c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Scripts\\python.exe -m src.tools.data_mcp_server\n",
+      "🔗 连接到 MCP 服务器...\n",
+      "✅ 工具 'network_data' 已注册。\n",
+      "📝 使用 Stdio 传输 (命令): c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Scripts\\python.exe -m src.tools.data_mcp_server\n",
+      "🔗 连接到 MCP 服务器...\n",
+      "✅ 工具 'network_data' 已注册。\n",
+      "📝 使用 Stdio 传输 (命令): c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Scripts\\python.exe -m src.tools.data_mcp_server\n",
+      "🔗 连接到 MCP 服务器...\n",
+      "✅ 工具 'network_data' 已注册。\n",
+      "📝 使用 Stdio 传输 (命令): c:\\Users\\houlj12\\Desktop\\work\\test\\hello-agents\\Co-creation-projects\\monkeyhlj-NetworkHealthReportAgent\\venv\\Scripts\\python.exe -m src.tools.data_mcp_server\n",
+      "🔗 连接到 MCP 服务器...\n",
+      "✅ 工具 'network_data' 已注册。\n",
+      "status: 200\n",
+      "intent: site_report_export\n",
+      "target_site_id: site-cd-west\n",
+      "download_url: http://testserver/api/outputs/site-cd-west_2026-05-25_2026-05-31_20260531_191908.md\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 通过 FastAPI TestClient 演示“可下载链接”字段(模拟前端调用 /api/chat)\n",
+    "from fastapi.testclient import TestClient\n",
+    "from src.api.main import app\n",
+    "\n",
+    "client = TestClient(app)\n",
+    "payload = {\n",
+    "    \"question\": \"帮我生成成都site近一周的报告\",\n",
+    "    \"site_id\": \"site-bj-hq\",\n",
+    "    \"start_date\": start.strftime(\"%Y-%m-%d\"),\n",
+    "    \"end_date\": end.strftime(\"%Y-%m-%d\"),\n",
+    "}\n",
+    "resp = client.post(\"/api/chat\", json=payload)\n",
+    "print(\"status:\", resp.status_code)\n",
+    "resp_json = resp.json()\n",
+    "print(\"intent:\", resp_json.get(\"debug\", {}).get(\"intent\"))\n",
+    "print(\"target_site_id:\", resp_json.get(\"debug\", {}).get(\"target_site_id\"))\n",
+    "print(\"download_url:\", resp_json.get(\"artifact\", {}).get(\"download_url\"))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "bf1accb7",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "build_report 耗时: 0.002s\n",
+      "ask_global_question 耗时: 34.930s\n"
+     ]
+    }
+   ],
+   "source": [
+    "# ========================================\n",
+    "# 第6部分: 性能评估(可选)\n",
+    "# ========================================\n",
+    "\"\"\"\n",
+    "通过简单计时观察两类核心路径耗时:\n",
+    "- 单站点报告生成 `build_report`\n",
+    "- 全局问答 `ask_global_question`\n",
+    "\"\"\"\n",
+    "def benchmark_once(fn, name: str):\n",
+    "    t0 = time.perf_counter()\n",
+    "    result = fn()\n",
+    "    t1 = time.perf_counter()\n",
+    "    print(f\"{name} 耗时: {(t1 - t0):.3f}s\")\n",
+    "    return result\n",
+    "\n",
+    "_ = benchmark_once(\n",
+    "    lambda: orchestrator.build_report(site_id=target_site, start=start, end=end),\n",
+    "    \"build_report\",\n",
+    ")\n",
+    "\n",
+    "_ = benchmark_once(\n",
+    "    lambda: orchestrator.ask_global_question(\n",
+    "        question=\"请给出全站风险最高的两个站点并说明原因\",\n",
+    "        start=start,\n",
+    "        end=end,\n",
+    "        site_id=None,\n",
+    "    ),\n",
+    "    \"ask_global_question\",\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "id": "389e3ce6",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'\\n## 项目总结\\n\\n### 已实现功能\\n- 多站点网络健康报告(日志、设备、终端多维聚合)\\n- 站点地图联动 + 全局问答\\n- 问答触发报告导出(输出到 `outputs/`,并可通过 API 下载)\\n\\n### 关键挑战与解决方案\\n- 站点识别优先级:由“当前选中”调整为“优先按问题文本匹配”\\n- 下载链接不可见:通过 CORS `expose_headers` 暴露自定义响应头\\n- 报告真实性要求:报告导出场景要求 LLM 可用,避免纯模板拼接\\n\\n### 后续优化方向\\n- 增加更细粒度的报表格式(HTML/PDF)\\n- 引入历史趋势对比(环比、同比)\\n- 增加问答结果可解释性与工具调用轨迹展示\\n'"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# ========================================\n",
+    "# 第7部分: 总结与展望\n",
+    "# ========================================\n",
+    "\"\"\"\n",
+    "## 项目总结\n",
+    "\n",
+    "### 已实现功能\n",
+    "- 多站点网络健康报告(日志、设备、终端多维聚合)\n",
+    "- 站点地图联动 + 全局问答\n",
+    "- 问答触发报告导出(输出到 `outputs/`,并可通过 API 下载)\n",
+    "\n",
+    "### 关键挑战与解决方案\n",
+    "- 站点识别优先级:由“当前选中”调整为“优先按问题文本匹配”\n",
+    "- 下载链接不可见:通过 CORS `expose_headers` 暴露自定义响应头\n",
+    "- 报告真实性要求:报告导出场景要求 LLM 可用,避免纯模板拼接\n",
+    "\n",
+    "### 后续优化方向\n",
+    "- 增加更细粒度的报表格式(HTML/PDF)\n",
+    "- 引入历史趋势对比(环比、同比)\n",
+    "- 增加问答结果可解释性与工具调用轨迹展示\n",
+    "\"\"\""
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "venv (3.11.4)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.4"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}

+ 3 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/outputs/review_report.md

@@ -0,0 +1,3 @@
+# Network Health Report Demo Output
+
+Generated reports will be exported here in future iterations.

+ 45 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/outputs/site-cd-west_2026-05-25_2026-05-31_20260531_191908.md

@@ -0,0 +1,45 @@
+# 成都西区办公室 近一周网络健康报告
+
+- 生成时间:2026-05-31 19:19:08
+- 统计窗口:2026-05-25 至 2026-05-31
+- 站点编号:site-cd-west
+- 站点城市:成都
+- 健康评分:90.8
+- 健康等级:healthy
+
+## 大模型摘要
+📄 **成都西区办公室网络健康周报摘要**(2026.05.25 - 2026.05.31)
+
+**1. 总体结论**
+成都站点近一周网络整体运行平稳,综合健康评分高达 **90.8(健康)**。核心网络指标优异,设备平均在线率达99.65%,平均时延仅11.1ms,丢包率低至0.25%,终端准入合规率稳定在97.7%,业务承载状态良好。
+
+**2. 主要风险**
+* **物理链路隐患**:日志监测到1起CRC错误告警,且设备存在2条严重级别告警,提示可能存在底层接口老化或光模块/线缆物理故障风险。
+* **终端安全盲区**:内网中仍存活7台高风险终端与8台未知终端,面临一定的边界安全管控压力。
+
+**3. 优先动作(可执行建议)**
+* **排查链路异常(优先级:高)**:针对产生CRC错误的端口进行硬件巡检,重点检查光衰情况及网线/模块状态,必要时进行更换,防止隐患放大导致业务中断。
+* **清理违规终端(优先级:中)**:盘点并排查接入网络的未知终端与高风险终端,严格执行准入控制策略,隔离或阻断违规设备的网络访问权限。
+* **常态化容量观察(优先级:常规)**:维持当前网络与QoS策略,持续监控业务高峰期的带宽容量,按计划进行周度运维复盘。
+
+## 核心指标
+- 设备在线率:99.65%
+- 平均时延:11.05 ms
+- 平均丢包率:0.25%
+- 终端合规率:97.70%
+- 高风险终端数:7
+
+## 日志分析
+最近日志共1条,critical=0,warning=1。
+
+## 设备状态
+设备总数3,平均在线率99.65%,平均时延11.1ms。
+
+## 用户状态
+终端合规率97.70%,高风险终端7台,未知终端8台。
+
+## 推荐动作
+1. 维持现网策略并持续观察峰值时段容量,建议周度复盘。
+
+## 用户提问
+帮我生成成都site近一周的报告

+ 9 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/requirements.txt

@@ -0,0 +1,9 @@
+fastapi>=0.115.0
+uvicorn[standard]>=0.32.0
+pydantic>=2.9.0
+python-dotenv>=1.0.1
+hello-agents>=0.2.4,<=0.2.9
+fastmcp>=2.0.0
+python-dateutil>=2.9.0
+huggingface_hub>=0.25.0
+ipykernel==7.2.0

+ 4 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/run_api.py

@@ -0,0 +1,4 @@
+from src.api.main import app
+
+# Launch command:
+# uvicorn run_api:app --reload --port 8000

+ 5 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/__init__.py

@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# @Time    : 2026/5/29 17:09
+# @Author  : houlj12
+# @File    : __init__.py
+# @Description :

+ 124 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/base.py

@@ -0,0 +1,124 @@
+from __future__ import annotations
+
+import importlib
+import os
+import sys
+from dataclasses import dataclass
+from typing import Any, Iterator, Optional
+
+from hello_agents import HelloAgentsLLM, SimpleAgent
+
+
+def _resolve_mcp_tool_class() -> Optional[type]:
+    """Best-effort resolve for MCP tool class across hello-agents versions."""
+    candidates = [
+        ("hello_agents.tools", "MCPTool"),
+        ("hello_agents.tools.mcp", "MCPTool"),
+    ]
+    for module_name, class_name in candidates:
+        try:
+            module = importlib.import_module(module_name)
+            cls = getattr(module, class_name, None)
+            if cls is not None:
+                return cls
+        except Exception:
+            continue
+    return None
+
+
+MCPToolClass = _resolve_mcp_tool_class()
+
+
+@dataclass
+class AgentRuntime:
+    llm: Optional[HelloAgentsLLM]
+    mcp_tool: Optional[Any]
+
+
+class BaseNetworkAgent:
+    def __init__(self, name: str, system_prompt: str) -> None:
+        self.name = name
+        self.system_prompt = system_prompt
+        self.runtime = self._bootstrap_runtime()
+
+        self.agent: Optional[SimpleAgent] = None
+        if self.runtime.llm is not None:
+            self.agent = SimpleAgent(name=name, llm=self.runtime.llm, system_prompt=system_prompt)
+            if self.runtime.mcp_tool is not None:
+                self.agent.add_tool(self.runtime.mcp_tool)
+
+    def _bootstrap_runtime(self) -> AgentRuntime:
+        from dotenv import load_dotenv
+        load_dotenv()
+        api_key = os.getenv("LLM_API_KEY") or os.getenv("OPENAI_API_KEY")
+        if api_key and api_key.startswith("your-"):
+            api_key = None
+        if not api_key:
+            return AgentRuntime(llm=None, mcp_tool=None)
+
+        base_url = os.getenv("LLM_BASE_URL") or os.getenv("OPENAI_BASE_URL")
+        model = os.getenv("LLM_MODEL_ID") or os.getenv("OPENAI_MODEL")
+        timeout_str = os.getenv("LLM_TIMEOUT", "15")
+        try:
+            timeout = int(timeout_str)
+        except ValueError:
+            timeout = 15
+
+        llm_kwargs: dict[str, Any] = {"api_key": api_key, "timeout": timeout}
+        if base_url:
+            llm_kwargs["base_url"] = base_url
+        if model:
+            llm_kwargs["model"] = model
+
+        llm = HelloAgentsLLM(**llm_kwargs)
+        mcp_tool = None
+        if MCPToolClass is not None:
+            mcp_tool = MCPToolClass(
+                name="network_data",
+                description="Network health data service",
+                server_command=[sys.executable, "-m", "src.tools.data_mcp_server"],
+                auto_expand=True,
+            )
+        return AgentRuntime(llm=llm, mcp_tool=mcp_tool)
+
+    @property
+    def enabled(self) -> bool:
+        return self.agent is not None
+
+    @property
+    def llm_enabled(self) -> bool:
+        return self.runtime.llm is not None
+
+    @property
+    def mcp_enabled(self) -> bool:
+        return self.runtime.mcp_tool is not None
+
+    def run_llm(self, prompt: str) -> Optional[str]:
+        if self.agent is None:
+            return None
+        try:
+            return self.agent.run(prompt)
+        except Exception:
+            return None
+
+    def stream_llm(self, prompt: str) -> Optional[Iterator[str]]:
+        if self.agent is None:
+            return None
+        try:
+            return self.agent.stream_run(prompt)
+        except Exception:
+            return None
+
+    def list_tool_names(self) -> list[str]:
+        if self.agent is None:
+            return []
+        if not hasattr(self.agent, "list_tools"):
+            return []
+        try:
+            tools = self.agent.list_tools()
+        except Exception:
+            return []
+        names: list[str] = []
+        for t in tools or []:
+            names.append(getattr(t, "name", str(t)))
+        return names

+ 60 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/device_status_agent.py

@@ -0,0 +1,60 @@
+from __future__ import annotations
+
+from statistics import mean
+from typing import Dict, List
+
+from src.agents.base import BaseNetworkAgent
+
+
+class DeviceStatusAgent(BaseNetworkAgent):
+    def __init__(self) -> None:
+        prompt = (
+            "你是网络设备状态Agent。请调用network_data工具读取设备和状态,"
+            "评估在线率、性能瓶颈和告警趋势。"
+        )
+        super().__init__(name="DeviceStatusAgent", system_prompt=prompt)
+
+    def analyze(self, inventory: List[Dict], status_series: List[Dict]) -> Dict:
+        if not status_series:
+            return {
+                "device_count": len(inventory),
+                "online_rate": 0.0,
+                "avg_packet_loss": 0.0,
+                "avg_latency_ms": 0.0,
+                "avg_cpu": 0.0,
+                "avg_mem": 0.0,
+                "status": "unknown",
+                "summary": "无状态数据,无法评估设备健康。",
+            }
+
+        online_rates = [
+            (x["wired_switch_online_rate"] + x["wireless_controller_online_rate"] + x["wireless_ap_online_rate"]) / 3
+            for x in status_series
+        ]
+        avg_online_rate = mean(online_rates)
+        avg_packet_loss = mean(x["packet_loss"] for x in status_series)
+        avg_latency = mean(x["latency_ms"] for x in status_series)
+        avg_cpu = mean(x["cpu_avg"] for x in status_series)
+        avg_mem = mean(x["mem_avg"] for x in status_series)
+        critical_alarms = sum(x["critical_alarm_count"] for x in status_series)
+
+        status = "healthy"
+        if avg_online_rate < 0.985 or avg_packet_loss > 0.01 or avg_latency > 20:
+            status = "degraded"
+        if avg_online_rate < 0.975 or avg_packet_loss > 0.015 or critical_alarms >= 12:
+            status = "critical"
+
+        return {
+            "device_count": len(inventory),
+            "online_rate": round(avg_online_rate, 4),
+            "avg_packet_loss": round(avg_packet_loss, 4),
+            "avg_latency_ms": round(avg_latency, 2),
+            "avg_cpu": round(avg_cpu, 2),
+            "avg_mem": round(avg_mem, 2),
+            "critical_alarm_total": critical_alarms,
+            "status": status,
+            "summary": (
+                f"设备总数{len(inventory)},平均在线率{avg_online_rate:.2%},"
+                f"平均时延{avg_latency:.1f}ms。"
+            ),
+        }

+ 47 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/log_analysis_agent.py

@@ -0,0 +1,47 @@
+from __future__ import annotations
+
+from collections import Counter
+from typing import Dict, List
+
+from src.agents.base import BaseNetworkAgent
+
+
+class LogAnalysisAgent(BaseNetworkAgent):
+    def __init__(self) -> None:
+        prompt = (
+            "你是网络日志分析Agent。请调用network_data工具读取日志,"
+            "输出关键告警、根因线索和风险等级。"
+        )
+        super().__init__(name="LogAnalysisAgent", system_prompt=prompt)
+
+    def analyze(self, logs: List[Dict]) -> Dict:
+        if not logs:
+            return {
+                "total_events": 0,
+                "critical_events": 0,
+                "warning_events": 0,
+                "top_event_types": [],
+                "summary": "未发现日志事件,建议确认日志采集链路。",
+            }
+
+        severity_counter = Counter(log["severity"] for log in logs)
+        type_counter = Counter(log["event_type"] for log in logs)
+        top_types = [{"event_type": k, "count": v} for k, v in type_counter.most_common(5)]
+
+        risk = "low"
+        if severity_counter.get("critical", 0) >= 3:
+            risk = "high"
+        elif severity_counter.get("warning", 0) >= 3:
+            risk = "medium"
+
+        return {
+            "total_events": len(logs),
+            "critical_events": severity_counter.get("critical", 0),
+            "warning_events": severity_counter.get("warning", 0),
+            "top_event_types": top_types,
+            "risk": risk,
+            "summary": (
+                f"最近日志共{len(logs)}条,critical={severity_counter.get('critical', 0)},"
+                f"warning={severity_counter.get('warning', 0)}。"
+            ),
+        }

+ 102 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/network_health_report_agent.py

@@ -0,0 +1,102 @@
+from __future__ import annotations
+
+import json
+import os
+from typing import Dict, List
+
+from src.agents.base import BaseNetworkAgent
+
+
+class NetworkHealthReportAgent(BaseNetworkAgent):
+    def __init__(self) -> None:
+        prompt = (
+            "你是网络健康报告Agent。请综合日志分析、设备状态、终端状态,"
+            "给出健康分、风险等级和可执行建议。"
+        )
+        super().__init__(name="NetworkHealthReportAgent", system_prompt=prompt)
+
+    def synthesize(
+        self,
+        site: Dict,
+        log_result: Dict,
+        device_result: Dict,
+        user_result: Dict,
+        start_date: str,
+        end_date: str,
+    ) -> Dict:
+        score = 100.0
+
+        score -= min(20.0, log_result.get("critical_events", 0) * 3.0)
+        score -= min(10.0, log_result.get("warning_events", 0) * 1.0)
+        score -= max(0.0, (1.0 - device_result.get("online_rate", 1.0)) * 200)
+        score -= max(0.0, device_result.get("avg_packet_loss", 0.0) * 800)
+        score -= max(0.0, (1.0 - user_result.get("compliant_rate", 1.0)) * 150)
+        score -= min(10.0, user_result.get("high_risk_terminals", 0) * 0.3)
+
+        score = max(0.0, min(100.0, round(score, 1)))
+
+        if score >= 85:
+            level = "healthy"
+        elif score >= 70:
+            level = "warning"
+        else:
+            level = "critical"
+
+        recommendations: List[str] = []
+        if log_result.get("critical_events", 0) > 0:
+            recommendations.append("优先排查critical日志对应链路与设备,执行根因定位和变更回滚检查。")
+        if device_result.get("avg_latency_ms", 0) > 18:
+            recommendations.append("针对高时延站点执行链路压测和QoS策略复核,优化关键业务队列。")
+        if device_result.get("avg_packet_loss", 0) > 0.01:
+            recommendations.append("对丢包异常端口进行误码与光模块巡检,必要时更换线缆或模块。")
+        if user_result.get("compliant_rate", 1.0) < 0.96:
+            recommendations.append("提升终端准入合规率,收敛未知终端与高风险终端访问权限。")
+        if not recommendations:
+            recommendations.append("维持现网策略并持续观察峰值时段容量,建议周度复盘。")
+
+        llm_insight = None
+        llm_insight_enabled = os.getenv("ENABLE_REPORT_LLM_INSIGHT", "false").lower() in {"1", "true", "yes"}
+        if llm_insight_enabled:
+            llm_prompt = (
+                "请作为网络健康报告专家,总结该站点网络健康情况。"
+                "输出三段:总体判断、主要风险、本周优先动作。"
+                "\n\n输入JSON:\n"
+                + json.dumps(
+                    {
+                        "site": site,
+                        "window": {"start_date": start_date, "end_date": end_date},
+                        "score": score,
+                        "level": level,
+                        "log_analysis": log_result,
+                        "device_status": device_result,
+                        "user_status": user_result,
+                        "recommendations": recommendations,
+                    },
+                    ensure_ascii=False,
+                )
+            )
+            llm_result = self.run_llm(llm_prompt)
+            if llm_result:
+                llm_insight = llm_result
+
+        return {
+            "site": site,
+            "window": {"start_date": start_date, "end_date": end_date},
+            "health_score": score,
+            "health_level": level,
+            "summary": f"{site['site_name']}在统计周期内网络健康评分为{score},等级为{level}。",
+            "llm_insight": llm_insight,
+            "debug": {
+                "llm_enabled": self.llm_enabled,
+                "mcp_enabled": self.mcp_enabled,
+                "tools": self.list_tool_names(),
+                "llm_insight_enabled": llm_insight_enabled,
+                "llm_insight_used": llm_insight is not None,
+            },
+            "sections": {
+                "log_analysis": log_result,
+                "device_status": device_result,
+                "user_status": user_result,
+            },
+            "recommendations": recommendations,
+        }

+ 140 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/orchestrator.py

@@ -0,0 +1,140 @@
+from __future__ import annotations
+
+from datetime import date
+from typing import Dict, Iterator, List
+
+from src.agents.device_status_agent import DeviceStatusAgent
+from src.agents.log_analysis_agent import LogAnalysisAgent
+from src.agents.network_health_report_agent import NetworkHealthReportAgent
+from src.agents.site_qa_agent import SiteQAAgent
+from src.agents.user_status_agent import UserStatusAgent
+from src.tools.data_repository import DataRepository
+
+
+class NetworkHealthOrchestrator:
+    def __init__(self) -> None:
+        self.repo = DataRepository()
+        self.log_agent = LogAnalysisAgent()
+        self.device_agent = DeviceStatusAgent()
+        self.user_agent = UserStatusAgent()
+        self.report_agent = NetworkHealthReportAgent()
+        self.qa_agent = SiteQAAgent()
+
+    def list_sites(self) -> List[Dict]:
+        return self.repo.list_sites()
+
+    def get_site(self, site_id: str) -> Dict:
+        site = self.repo.get_site(site_id)
+        if not site:
+            raise ValueError(f"site_id not found: {site_id}")
+        return site
+
+    def build_report(self, site_id: str, start: date, end: date) -> Dict:
+        site = self.get_site(site_id)
+        start_str = start.strftime("%Y-%m-%d")
+        end_str = end.strftime("%Y-%m-%d")
+
+        logs = self.repo.list_logs(site_id=site_id)
+        inventory = self.repo.list_device_inventory(site_id=site_id)
+        status_series = self.repo.list_device_status(site_id=site_id, start_date=start_str, end_date=end_str)
+        compliance = self.repo.latest_terminal_compliance(site_id=site_id)
+
+        log_result = self.log_agent.analyze(logs)
+        device_result = self.device_agent.analyze(inventory=inventory, status_series=status_series)
+        user_result = self.user_agent.analyze(terminal_row=compliance)
+
+        return self.report_agent.synthesize(
+            site=site,
+            log_result=log_result,
+            device_result=device_result,
+            user_result=user_result,
+            start_date=start_str,
+            end_date=end_str,
+        )
+
+    def ask_site_question(self, site_id: str, question: str, start: date, end: date) -> Dict:
+        site = self.get_site(site_id)
+        report = self.build_report(site_id=site_id, start=start, end=end)
+        start_str = start.strftime("%Y-%m-%d")
+        end_str = end.strftime("%Y-%m-%d")
+        payload = self.qa_agent.answer_global_payload(
+            question=question,
+            sites=[site],
+            site_reports=[report],
+            start_date=start_str,
+            end_date=end_str,
+            selected_site_id=site_id,
+        )
+        return {
+            "site": site,
+            "window": {"start_date": start_str, "end_date": end_str},
+            "question": question,
+            "answer": payload["answer"],
+            "artifact": payload.get("artifact"),
+            "debug": payload.get("debug"),
+        }
+
+    def ask_global_question(self, question: str, start: date, end: date, site_id: str | None = None) -> Dict:
+        start_str = start.strftime("%Y-%m-%d")
+        end_str = end.strftime("%Y-%m-%d")
+        sites = self.list_sites()
+        reports = [self.build_report(site_id=s["site_id"], start=start, end=end) for s in sites]
+
+        payload = self.qa_agent.answer_global_payload(
+            question=question,
+            sites=sites,
+            site_reports=reports,
+            start_date=start_str,
+            end_date=end_str,
+            selected_site_id=site_id,
+        )
+        return {
+            "window": {"start_date": start_str, "end_date": end_str},
+            "question": question,
+            "answer": payload["answer"],
+            "artifact": payload.get("artifact"),
+            "debug": {
+                **payload.get("debug", self.qa_agent.debug_meta()),
+                "site_count": len(sites),
+                "report_count": len(reports),
+            },
+        }
+
+    def stream_global_question(self, question: str, start: date, end: date, site_id: str | None = None) -> Iterator[str]:
+        start_str = start.strftime("%Y-%m-%d")
+        end_str = end.strftime("%Y-%m-%d")
+        sites = self.list_sites()
+        reports = [self.build_report(site_id=s["site_id"], start=start, end=end) for s in sites]
+
+        yield from self.qa_agent.stream_answer_global(
+            question=question,
+            sites=sites,
+            site_reports=reports,
+            start_date=start_str,
+            end_date=end_str,
+            selected_site_id=site_id,
+        )
+
+    def runtime_status(self) -> Dict:
+        return {
+            "log_agent": {
+                "llm_enabled": self.log_agent.llm_enabled,
+                "mcp_enabled": self.log_agent.mcp_enabled,
+            },
+            "device_agent": {
+                "llm_enabled": self.device_agent.llm_enabled,
+                "mcp_enabled": self.device_agent.mcp_enabled,
+            },
+            "user_agent": {
+                "llm_enabled": self.user_agent.llm_enabled,
+                "mcp_enabled": self.user_agent.mcp_enabled,
+            },
+            "report_agent": {
+                "llm_enabled": self.report_agent.llm_enabled,
+                "mcp_enabled": self.report_agent.mcp_enabled,
+            },
+            "qa_agent": {
+                "llm_enabled": self.qa_agent.llm_enabled,
+                "mcp_enabled": self.qa_agent.mcp_enabled,
+            },
+        }

+ 387 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/site_qa_agent.py

@@ -0,0 +1,387 @@
+from __future__ import annotations
+
+import json
+import re
+from datetime import datetime
+from pathlib import Path
+from typing import Dict, Iterator, List, Optional
+
+from src.agents.base import BaseNetworkAgent
+
+
+PROJECT_ROOT = Path(__file__).resolve().parents[2]
+OUTPUT_DIR = PROJECT_ROOT / "outputs"
+
+
+class SiteQAAgent(BaseNetworkAgent):
+    def __init__(self) -> None:
+        prompt = (
+            "你是企业网络运维全局问答助手。请优先根据给定的全局上下文回答。"
+            "如果可以使用network_data工具,请调用工具核对数据后再回答。"
+            "回答要简洁、专业、可执行。"
+        )
+        super().__init__(name="SiteQAAgent", system_prompt=prompt)
+
+    def _build_prompt(
+        self,
+        question: str,
+        context: Dict,
+    ) -> str:
+        return (
+            "请回答用户关于企业网络健康的提问。"
+            "\n\n[用户问题]\n"
+            f"{question}"
+            "\n\n[上下文JSON]\n"
+            f"{json.dumps(context, ensure_ascii=False)}"
+            "\n\n请用中文回答,输出结构:1)结论 2)关键证据 3)建议动作。"
+        )
+
+    def _looks_like_report_request(self, question: str) -> bool:
+        q = question.lower()
+        return any(keyword in q for keyword in ["生成", "导出", "下载", "报告", "周报", "一周", "近一周"])
+
+    def _find_target_site(
+        self,
+        question: str,
+        sites: List[Dict],
+        selected_site_id: Optional[str] = None,
+    ) -> Optional[Dict]:
+        lowered = question.lower()
+        for site in sites:
+            site_id = str(site.get("site_id", "")).lower()
+            site_name = str(site.get("site_name", "")).lower()
+            city = str(site.get("city", "")).lower()
+            province = str(site.get("province", "")).lower()
+            if site_id and site_id in lowered:
+                return site
+            if site_name and site_name in lowered:
+                return site
+            if city and city in question:
+                return site
+            if province and province in question:
+                return site
+
+        candidates = re.findall(r"site[-_a-zA-Z0-9]+", lowered)
+        for candidate in candidates:
+            match = next((site for site in sites if str(site.get("site_id", "")).lower() == candidate), None)
+            if match is not None:
+                return match
+
+        if selected_site_id:
+            selected = next((site for site in sites if site.get("site_id") == selected_site_id), None)
+            if selected is not None:
+                return selected
+
+        if len(sites) == 1 and self._looks_like_report_request(question):
+            return sites[0]
+
+        return None
+
+    def _fallback_answer(
+        self,
+        question: str,
+        sites: List[Dict],
+        site_reports: List[Dict],
+    ) -> str:
+        q = question.strip().lower()
+        if "几个site" in q or "多少site" in q or "多少个站点" in q:
+            names = "、".join([s["site_name"] for s in sites])
+            return f"当前共有 {len(sites)} 个 site,分别是:{names}。"
+
+        if "上海" in question and ("site" in q or "站点" in question):
+            sh_sites = [s for s in sites if s.get("city") == "上海" or s.get("province") == "上海市"]
+            if not sh_sites:
+                return "当前没有位于上海的 site。"
+            details = ";".join([f"{s['site_name']}({s['site_id']})" for s in sh_sites])
+            return f"上海相关 site 有 {len(sh_sites)} 个:{details}。"
+
+        target = None
+        for s in sites:
+            if s["site_id"] in question or s["site_name"] in question or s.get("city", "") in question:
+                target = s
+                break
+
+        if target:
+            target_report = next((r for r in site_reports if r.get("site", {}).get("site_id") == target["site_id"]), None)
+            if target_report:
+                device = target_report.get("sections", {}).get("device_status", {})
+                return (
+                    f"{target['site_name']} 详细信息:城市{target.get('city')},类型{target.get('site_type')},"
+                    f"关键等级{target.get('criticality')}。"
+                    f"设备在线率{device.get('online_rate', 0) * 100:.2f}%,"
+                    f"平均时延{device.get('avg_latency_ms', 0)}ms,"
+                    f"平均丢包{device.get('avg_packet_loss', 0) * 100:.2f}%。"
+                )
+
+        levels = [r.get("health_level", "unknown") for r in site_reports]
+        healthy = len([x for x in levels if x == "healthy"])
+        warning = len([x for x in levels if x == "warning"])
+        critical = len([x for x in levels if x == "critical"])
+        return (
+            f"全局概览:共{len(sites)}个site,healthy={healthy},warning={warning},critical={critical}。"
+            "你可以继续问:上海有哪些site、某个site的详细信息、某个site的网络设备情况如何。"
+        )
+
+    def _summarize_report(self, report: Dict, question: str) -> Optional[str]:
+        llm_prompt = (
+            "你是企业网络健康报告的摘要撰写助手。"
+            "请基于输入 JSON 生成一段中文摘要,适合直接放在报告首页。"
+            "要求:简洁、专业、可执行,覆盖总体结论、主要风险、优先动作。"
+            "如果用户问题里有明确的关注点,也要顺带回应。"
+            "\n\n[用户问题]\n"
+            f"{question}"
+            "\n\n[报告JSON]\n"
+            f"{json.dumps(report, ensure_ascii=False)}"
+        )
+        return self.run_llm(llm_prompt)
+
+    def _build_report_markdown(
+        self,
+        site: Dict,
+        report: Dict,
+        question: str,
+        summary: Optional[str],
+    ) -> str:
+        window = report.get("window", {})
+        sections = report.get("sections", {})
+        device = sections.get("device_status", {})
+        user_status = sections.get("user_status", {})
+        log_analysis = sections.get("log_analysis", {})
+        recommendations = report.get("recommendations", [])
+
+        lines = [
+            f"# {site.get('site_name', site.get('site_id', 'site'))} 近一周网络健康报告",
+            "",
+            f"- 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
+            f"- 统计窗口:{window.get('start_date', '')} 至 {window.get('end_date', '')}",
+            f"- 站点编号:{site.get('site_id', '')}",
+            f"- 站点城市:{site.get('city', '')}",
+            f"- 健康评分:{report.get('health_score', 'N/A')}",
+            f"- 健康等级:{report.get('health_level', 'N/A')}",
+            "",
+        ]
+
+        if summary:
+            lines.extend([
+                "## 大模型摘要",
+                summary.strip(),
+                "",
+            ])
+
+        lines.extend([
+            "## 核心指标",
+            f"- 设备在线率:{device.get('online_rate', 0) * 100:.2f}%",
+            f"- 平均时延:{device.get('avg_latency_ms', 0)} ms",
+            f"- 平均丢包率:{device.get('avg_packet_loss', 0) * 100:.2f}%",
+            f"- 终端合规率:{user_status.get('compliant_rate', 0) * 100:.2f}%",
+            f"- 高风险终端数:{user_status.get('high_risk_terminals', 0)}",
+            "",
+            "## 日志分析",
+            log_analysis.get("summary", "暂无日志摘要。"),
+            "",
+            "## 设备状态",
+            device.get("summary", "暂无设备状态摘要。"),
+            "",
+            "## 用户状态",
+            user_status.get("summary", "暂无用户状态摘要。"),
+            "",
+            "## 推荐动作",
+        ])
+
+        if recommendations:
+            lines.extend([f"{idx + 1}. {item}" for idx, item in enumerate(recommendations)])
+        else:
+            lines.append("1. 继续观察站点运行状态,暂未发现明显风险。")
+
+        lines.extend([
+            "",
+            "## 用户提问",
+            question,
+        ])
+        return "\n".join(lines).strip() + "\n"
+
+    def _save_report_artifact(self, site: Dict, start_date: str, end_date: str, content: str) -> Dict:
+        OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
+        safe_site_id = re.sub(r"[^0-9A-Za-z_-]+", "_", str(site.get("site_id", "site")))
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        filename = f"{safe_site_id}_{start_date}_{end_date}_{timestamp}.md"
+        file_path = OUTPUT_DIR / filename
+        file_path.write_text(content, encoding="utf-8")
+        return {
+            "file_name": filename,
+            "file_path": str(file_path),
+        }
+
+    def _build_answer_payload(
+        self,
+        question: str,
+        sites: List[Dict],
+        site_reports: List[Dict],
+        start_date: str,
+        end_date: str,
+        selected_site_id: Optional[str] = None,
+    ) -> Dict:
+        context = {
+            "scope": "global",
+            "window": {"start_date": start_date, "end_date": end_date},
+            "site_count": len(sites),
+            "sites": sites,
+            "reports": site_reports,
+            "selected_site_id": selected_site_id,
+        }
+
+        prompt = self._build_prompt(question=question, context=context)
+        llm_answer: Optional[str] = self.run_llm(prompt)
+
+        target_site = self._find_target_site(question, sites, selected_site_id=selected_site_id)
+        is_report_request = self._looks_like_report_request(question)
+
+        if is_report_request and target_site is not None:
+            if not self.llm_enabled:
+                return {
+                    "answer": (
+                        "检测到你在请求生成站点周报,但当前 LLM 未启用,无法按要求生成大模型报告。"
+                        "请先在 .env 配置 LLM_API_KEY(或 OPENAI_API_KEY)后重试。"
+                    ),
+                    "artifact": None,
+                    "debug": {
+                        **self.debug_meta(),
+                        "intent": "site_report_requires_llm",
+                        "target_site_id": target_site.get("site_id"),
+                    },
+                }
+
+            target_report = next((r for r in site_reports if r.get("site", {}).get("site_id") == target_site["site_id"]), None)
+            if target_report is not None:
+                report_summary = self._summarize_report(target_report, question)
+                if not report_summary:
+                    return {
+                        "answer": (
+                            "已识别到报告生成请求,但本次大模型调用失败,未生成周报文件。"
+                            "请检查 LLM 配置和网络连通性后重试。"
+                        ),
+                        "artifact": None,
+                        "debug": {
+                            **self.debug_meta(),
+                            "intent": "site_report_llm_failed",
+                            "target_site_id": target_site.get("site_id"),
+                        },
+                    }
+                answer_text = self._build_report_markdown(
+                    site=target_site,
+                    report=target_report,
+                    question=question,
+                    summary=report_summary,
+                )
+                artifact = self._save_report_artifact(target_site, start_date, end_date, answer_text)
+                return {
+                    "answer": answer_text,
+                    "artifact": artifact,
+                    "debug": {
+                        **self.debug_meta(),
+                        "intent": "site_report_export",
+                        "target_site_id": target_site.get("site_id"),
+                    },
+                }
+
+        answer_text = llm_answer or self._fallback_answer(question=question, sites=sites, site_reports=site_reports)
+        return {
+            "answer": answer_text,
+            "artifact": None,
+            "debug": {
+                **self.debug_meta(),
+                "intent": "general_qa",
+                "target_site_id": target_site.get("site_id") if target_site else None,
+            },
+        }
+
+    def answer_global(
+        self,
+        question: str,
+        sites: List[Dict],
+        site_reports: List[Dict],
+        start_date: str,
+        end_date: str,
+    ) -> str:
+        return self._build_answer_payload(
+            question=question,
+            sites=sites,
+            site_reports=site_reports,
+            start_date=start_date,
+            end_date=end_date,
+        )["answer"]
+
+    def answer_global_payload(
+        self,
+        question: str,
+        sites: List[Dict],
+        site_reports: List[Dict],
+        start_date: str,
+        end_date: str,
+        selected_site_id: Optional[str] = None,
+    ) -> Dict:
+        return self._build_answer_payload(
+            question=question,
+            sites=sites,
+            site_reports=site_reports,
+            start_date=start_date,
+            end_date=end_date,
+            selected_site_id=selected_site_id,
+        )
+
+    def debug_meta(self) -> Dict:
+        return {
+            "agent": "SiteQAAgent",
+            "llm_enabled": self.llm_enabled,
+            "mcp_enabled": self.mcp_enabled,
+            "tools": self.list_tool_names(),
+            "flow": [
+                "collect_global_context",
+                "build_prompt",
+                "try_llm_or_stream",
+                "fallback_if_needed",
+            ],
+        }
+
+    def stream_answer_global(
+        self,
+        question: str,
+        sites: List[Dict],
+        site_reports: List[Dict],
+        start_date: str,
+        end_date: str,
+        selected_site_id: Optional[str] = None,
+    ) -> Iterator[str]:
+        target_site = self._find_target_site(question, sites, selected_site_id=selected_site_id)
+        if self._looks_like_report_request(question) and target_site is not None:
+            payload = self._build_answer_payload(
+                question=question,
+                sites=sites,
+                site_reports=site_reports,
+                start_date=start_date,
+                end_date=end_date,
+                selected_site_id=selected_site_id,
+            )
+            yield payload["answer"]
+            return
+
+        context = {
+            "scope": "global",
+            "window": {"start_date": start_date, "end_date": end_date},
+            "site_count": len(sites),
+            "sites": sites,
+            "reports": site_reports,
+            "selected_site_id": selected_site_id,
+        }
+        prompt = self._build_prompt(question=question, context=context)
+        stream_iter = self.stream_llm(prompt)
+        if stream_iter is not None:
+            try:
+                for chunk in stream_iter:
+                    yield chunk
+                return
+            except Exception:
+                pass
+
+        prompt = self._build_prompt(question=question, context=context)
+        yield self.run_llm(prompt) or self._fallback_answer(question=question, sites=sites, site_reports=site_reports)

+ 50 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/agents/user_status_agent.py

@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+from typing import Dict, Optional
+
+from src.agents.base import BaseNetworkAgent
+
+
+class UserStatusAgent(BaseNetworkAgent):
+    def __init__(self) -> None:
+        prompt = (
+            "你是网络用户状态分析Agent。请调用network_data工具读取终端接入与合规数据,"
+            "输出终端风险和访问健康度。"
+        )
+        super().__init__(name="UserStatusAgent", system_prompt=prompt)
+
+    def analyze(self, terminal_row: Optional[Dict]) -> Dict:
+        if not terminal_row:
+            return {
+                "wired_clients": 0,
+                "wireless_clients": 0,
+                "unknown_clients": 0,
+                "compliant_rate": 0.0,
+                "high_risk_terminals": 0,
+                "status": "unknown",
+                "summary": "无终端合规数据。",
+            }
+
+        compliant_rate = terminal_row["compliant_rate"]
+        unknown_clients = terminal_row["unknown_clients"]
+        high_risk = terminal_row["high_risk_terminals"]
+
+        status = "healthy"
+        if compliant_rate < 0.96 or unknown_clients > 20:
+            status = "degraded"
+        if compliant_rate < 0.94 or high_risk > 25:
+            status = "critical"
+
+        return {
+            "wired_clients": terminal_row["wired_clients"],
+            "wireless_clients": terminal_row["wireless_clients"],
+            "unknown_clients": unknown_clients,
+            "compliant_rate": compliant_rate,
+            "high_risk_terminals": high_risk,
+            "guest_network_ratio": terminal_row["guest_network_ratio"],
+            "status": status,
+            "summary": (
+                f"终端合规率{compliant_rate:.2%},高风险终端{high_risk}台,"
+                f"未知终端{unknown_clients}台。"
+            ),
+        }

+ 172 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/api/main.py

@@ -0,0 +1,172 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Iterator
+
+from fastapi import FastAPI, HTTPException, Query, Request
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import FileResponse, StreamingResponse
+from pydantic import BaseModel
+
+from src.agents.orchestrator import NetworkHealthOrchestrator
+from src.utils.date_utils import default_date_window
+
+app = FastAPI(title="Network Health Report Agent API", version="0.1.0")
+
+PROJECT_ROOT = Path(__file__).resolve().parents[2]
+OUTPUTS_DIR = PROJECT_ROOT / "outputs"
+OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
+
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+    expose_headers=[
+        "X-Agent-LLM",
+        "X-Agent-MCP",
+        "X-Agent-Report-Intent",
+        "X-Agent-Artifact-Url",
+        "X-Agent-Artifact-Name",
+    ],
+)
+
+orchestrator = NetworkHealthOrchestrator()
+
+
+class AskRequest(BaseModel):
+    question: str
+    start_date: str | None = None
+    end_date: str | None = None
+    site_id: str | None = None
+
+
+@app.get("/api/health")
+def health_check() -> dict:
+    return {"status": "ok"}
+
+
+@app.get("/api/runtime")
+def runtime_status() -> dict:
+    return {"runtime": orchestrator.runtime_status()}
+
+
+@app.get("/api/sites")
+def list_sites() -> dict:
+    return {"sites": orchestrator.list_sites()}
+
+
+@app.get("/api/sites/{site_id}")
+def get_site(site_id: str) -> dict:
+    try:
+        return {"site": orchestrator.get_site(site_id)}
+    except ValueError as e:
+        raise HTTPException(status_code=404, detail=str(e)) from e
+
+
+@app.get("/api/reports/{site_id}")
+def get_site_report(
+    site_id: str,
+    start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
+    end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
+) -> dict:
+    start, end = default_date_window(start_date=start_date, end_date=end_date, days=7)
+    try:
+        report = orchestrator.build_report(site_id=site_id, start=start, end=end)
+        return {"report": report}
+    except ValueError as e:
+        raise HTTPException(status_code=404, detail=str(e)) from e
+
+
+@app.get("/api/reports")
+def get_all_site_reports(
+    start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
+    end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
+) -> dict:
+    start, end = default_date_window(start_date=start_date, end_date=end_date, days=7)
+    reports = []
+    for site in orchestrator.list_sites():
+        reports.append(orchestrator.build_report(site_id=site["site_id"], start=start, end=end))
+
+    return {"count": len(reports), "reports": reports}
+
+
+@app.get("/api/outputs/{filename}", name="download_generated_report")
+def download_generated_report(filename: str) -> FileResponse:
+    file_path = OUTPUTS_DIR / filename
+    if not file_path.exists() or not file_path.is_file():
+        raise HTTPException(status_code=404, detail="report artifact not found")
+    return FileResponse(file_path, filename=filename, media_type="text/markdown; charset=utf-8")
+
+
+@app.post("/api/chat")
+def ask_global_question(payload: AskRequest, request: Request) -> dict:
+    start, end = default_date_window(
+        start_date=payload.start_date,
+        end_date=payload.end_date,
+        days=7,
+    )
+    try:
+        answer = orchestrator.ask_global_question(
+            question=payload.question,
+            start=start,
+            end=end,
+            site_id=payload.site_id,
+        )
+        artifact = answer.get("artifact")
+        if artifact:
+            artifact["download_url"] = str(request.url_for("download_generated_report", filename=artifact["file_name"]))
+        return answer
+    except ValueError as e:
+        raise HTTPException(status_code=404, detail=str(e)) from e
+
+
+@app.post("/api/chat/stream")
+def stream_global_question(payload: AskRequest, request: Request):
+    start, end = default_date_window(
+        start_date=payload.start_date,
+        end_date=payload.end_date,
+        days=7,
+    )
+
+    report_intent = any(keyword in payload.question for keyword in ["生成", "导出", "下载", "报告", "周报", "近一周", "一周"])
+    answer_payload = None
+    artifact = None
+    debug = orchestrator.runtime_status().get("qa_agent", {})
+
+    if report_intent:
+        answer_payload = orchestrator.ask_global_question(
+            question=payload.question,
+            start=start,
+            end=end,
+            site_id=payload.site_id,
+        )
+        artifact = answer_payload.get("artifact")
+        debug = answer_payload.get("debug", debug)
+
+    headers = {
+        "X-Agent-LLM": str(debug.get("llm_enabled", False)).lower(),
+        "X-Agent-MCP": str(debug.get("mcp_enabled", False)).lower(),
+        "X-Agent-Report-Intent": str(debug.get("intent") == "site_report_export").lower(),
+    }
+    if artifact:
+        artifact_url = str(request.url_for("download_generated_report", filename=artifact["file_name"]))
+        headers["X-Agent-Artifact-Url"] = artifact_url
+        headers["X-Agent-Artifact-Name"] = artifact["file_name"]
+
+    def generator() -> Iterator[str]:
+        if answer_payload is not None and (artifact or debug.get("intent") == "site_report_export"):
+            yield answer_payload.get("answer", "")
+            return
+
+        for chunk in orchestrator.stream_global_question(
+            question=payload.question,
+            start=start,
+            end=end,
+            site_id=payload.site_id,
+        ):
+            if chunk:
+                yield chunk
+
+    return StreamingResponse(generator(), media_type="text/plain; charset=utf-8", headers=headers)

+ 44 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/tools/data_mcp_server.py

@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+from typing import Optional
+
+from fastmcp import FastMCP
+
+from src.tools.data_repository import DataRepository
+
+mcp = FastMCP("network-health-data")
+repo = DataRepository()
+
+
+@mcp.tool()
+def list_sites() -> list:
+    return repo.list_sites()
+
+
+@mcp.tool()
+def get_site(site_id: str) -> Optional[dict]:
+    return repo.get_site(site_id)
+
+
+@mcp.tool()
+def list_device_inventory(site_id: str) -> list:
+    return repo.list_device_inventory(site_id)
+
+
+@mcp.tool()
+def list_device_status(site_id: str, start_date: str, end_date: str) -> list:
+    return repo.list_device_status(site_id=site_id, start_date=start_date, end_date=end_date)
+
+
+@mcp.tool()
+def list_logs(site_id: str, limit: int = 100) -> list:
+    return repo.list_logs(site_id=site_id, limit=limit)
+
+
+@mcp.tool()
+def latest_terminal_compliance(site_id: str) -> Optional[dict]:
+    return repo.latest_terminal_compliance(site_id=site_id)
+
+
+if __name__ == "__main__":
+    mcp.run()

+ 63 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/tools/data_repository.py

@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from src.utils.date_utils import in_range
+
+
+class DataRepository:
+    def __init__(self, data_dir: Optional[Path] = None) -> None:
+        project_root = Path(__file__).resolve().parents[2]
+        self.data_dir = data_dir or (project_root / "data")
+
+    def _load_json(self, filename: str) -> List[Dict[str, Any]]:
+        path = self.data_dir / filename
+        with path.open("r", encoding="utf-8") as f:
+            return json.load(f)
+
+    def list_sites(self) -> List[Dict[str, Any]]:
+        return self._load_json("sites.json")
+
+    def get_site(self, site_id: str) -> Optional[Dict[str, Any]]:
+        return next((s for s in self.list_sites() if s["site_id"] == site_id), None)
+
+    def list_device_inventory(self, site_id: Optional[str] = None) -> List[Dict[str, Any]]:
+        records = self._load_json("device_inventory.json")
+        if site_id:
+            records = [r for r in records if r["site_id"] == site_id]
+        return records
+
+    def list_device_status(self, site_id: str, start_date: str, end_date: str) -> List[Dict[str, Any]]:
+        records = self._load_json("device_status_timeseries.json")
+        return [
+            r
+            for r in records
+            if r["site_id"] == site_id and in_range(r["date"], start=start_date_obj(start_date), end=end_date_obj(end_date))
+        ]
+
+    def list_logs(self, site_id: str, limit: int = 100) -> List[Dict[str, Any]]:
+        records = self._load_json("network_logs.json")
+        filtered = [r for r in records if r["site_id"] == site_id]
+        return filtered[:limit]
+
+    def latest_terminal_compliance(self, site_id: str) -> Optional[Dict[str, Any]]:
+        records = self._load_json("terminal_compliance.json")
+        by_site = [r for r in records if r["site_id"] == site_id]
+        if not by_site:
+            return None
+        by_site.sort(key=lambda x: x["date"], reverse=True)
+        return by_site[0]
+
+
+def start_date_obj(value: str):
+    from datetime import datetime
+
+    return datetime.strptime(value, "%Y-%m-%d").date()
+
+
+def end_date_obj(value: str):
+    from datetime import datetime
+
+    return datetime.strptime(value, "%Y-%m-%d").date()

+ 30 - 0
Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/utils/date_utils.py

@@ -0,0 +1,30 @@
+from __future__ import annotations
+
+from datetime import date, datetime, timedelta
+from typing import Optional, Tuple
+
+
+def parse_date(value: Optional[str]) -> Optional[date]:
+    if not value:
+        return None
+    return datetime.strptime(value, "%Y-%m-%d").date()
+
+
+def default_date_window(start_date: Optional[str], end_date: Optional[str], days: int = 7) -> Tuple[date, date]:
+    parsed_start = parse_date(start_date)
+    parsed_end = parse_date(end_date)
+
+    if parsed_end is None:
+        parsed_end = date.today()
+    if parsed_start is None:
+        parsed_start = parsed_end - timedelta(days=days - 1)
+
+    if parsed_start > parsed_end:
+        parsed_start, parsed_end = parsed_end, parsed_start
+
+    return parsed_start, parsed_end
+
+
+def in_range(date_str: str, start: date, end: date) -> bool:
+    current = datetime.strptime(date_str, "%Y-%m-%d").date()
+    return start <= current <= end