diff --git a/.agents/skills/analyze-questions/SKILL.md b/.agents/skills/analyze-questions/SKILL.md new file mode 100644 index 0000000..d716bfc --- /dev/null +++ b/.agents/skills/analyze-questions/SKILL.md @@ -0,0 +1,31 @@ +--- +name: analyze-questions +description: 问题分析规范。用于当用户使用疑问句提问时,约束模型行为:优先分析问题并给出解答或排查思路,不要直接进行自动解决或修改代码。 +--- + +# 问题分析规范 + +当用户的提问是疑问句(例如:"为什么..."、"是什么..."、"怎么排查..."、"这个命令的作用是?"等)时,这通常意味着用户正在进行探索、学习、调试或排查,他们更需要理解现象和背后的原理,而不是立刻执行修改。 + +为了提供更好的交互体验,请严格遵守以下行为准则: + +## 1. 优先只读与收集信息 + +- 可以并且鼓励使用**只读工具**(如 `view_file`, `grep_search`, `list_dir`,以及通过 `run_command` 执行 `cat`、`ls`、查看日志或状态的命令)来获取上下文和现场信息。 +- 在收集到充分信息后,进行逻辑分析。 + +## 2. 深入分析与耐心解答 + +- 基于收集到的信息,直接回答用户的疑问,指出问题所在、原因、或者代码/命令的实际作用。 +- 保证解答的逻辑清晰,让用户能够理解你的推导过程。 + +## 3. 严禁自动修改或“自作主张” + +- **绝对不要**直接调用修改文件的工具(如 `write_to_file`, `multi_replace_file_content`, `replace_file_content`)。 +- **绝对不要**通过 `run_command` 自动执行具有副作用的命令(如安装依赖、修改配置、重启服务、删除文件等)。 +- 即使你已经找到了问题的解决方案,也不要“自作主张”地直接实施。 + +## 4. 提供建议并等待确认 + +- 在解答完疑问后,你可以提供解决该问题的候选方案或建议(例如:“如果您希望修复此问题,我建议修改文件 X 的 Y 行”或“您可以执行以下命令:...”)。 +- 最后,必须将控制权交还给用户,询问用户:“您希望我帮您进行这些修改吗?”或者“是否需要我帮您执行?”。只有在用户给予明确的指令(如“好的”、“修改吧”、“执行”)后,才能进行实际的操作。 diff --git a/.agents/skills/backend-conventions/SKILL.md b/.agents/skills/backend-conventions/SKILL.md new file mode 100644 index 0000000..64f1ade --- /dev/null +++ b/.agents/skills/backend-conventions/SKILL.md @@ -0,0 +1,84 @@ +--- +name: backend-conventions +description: 后端 C++ 规范。用于修改本仓库后端接口、Controller、Manager、配置文件与接口文档时,统一 JSON 处理、字段命名、响应结构与文档同步要求。 +--- + +# 后端规范 + +## JSON 规则 + +- 统一使用 `nlohmann/json` +- 在实现文件中统一写:`using json = nlohmann::json;` +- 禁止新增 `jsoncpp` 依赖 +- 解析请求体时必须处理非法 JSON 分支 + +## Drogon 响应构建 + +- 优先复用 `ResponseUtil`,避免每个接口自行拼装响应 +- 明确设置 `Content-Type: application/json` +- 可预期失败走业务错误响应,不直接向前端暴露底层异常细节 + +示例: + +```cpp +auto resp = HttpResponse::newHttpResponse(); +resp->setContentTypeCode(CT_APPLICATION_JSON); +resp->setStatusCode(k200OK); +resp->setBody(ResponseUtil::GenerateSuccessResponse(data).dump()); +callback(resp); +``` + +## 字段命名 + +- 请求体、响应体、配置文件统一 `snake_case` +- 禁止在 JSON 中混用 `camelCase` + +## 响应约定 + +- 响应结构和状态码语义以当前模块现状为准(不要凭空定义新格式) +- 错误码与错误文案保持稳定,避免前后端契约漂移 + +## 日志与错误信息 + +- 先复用同模块既有日志前缀和措辞 +- 对外错误信息默认中文(除非该接口已约定英文) +- 不要无故把已有中文日志改成英文 + +## 文档同步 + +- 改后端接口时,必须同步更新该模块 `docs/接口文档.md` +- 若前端有 API 封装,同提交同步更新封装层 +- 新增接口时补齐:路径、方法、参数、成功/失败示例 +- **每次新增或修改协议时,必须同步更新 `collector/docs/协议支持清单.md` 文档** +- **每次新增协议或修改协议细节时,必须在 `collector/docs/protocols/` 下新增或更新对应的协议实现文档,并且文档内强制要求写入底层依赖库来源及其具体安装/编译方式** + +## 协议开发规范 + +- **新增协议驱动时,严禁在应用层类中直接调用底层的原生 API(如原生 Socket API、原生串口操作函数等),除非有特殊需求需要和我确认。** +- **必须注入并使用通用的公共类进行通信,例如:** + - TCP 连接使用 `TcpTransport` + - UDP 连接使用 `UdpTransport` + - 串口连接使用 `SerialTransport` + - 这些类均应继承自核心抽象接口 `ITransport`。 + +## 协议配置规范 + +> 详细规则参见 `collector/docs/协议支持清单.md` 的"更新规范"章节。 + +### protocol_name + +- 全大写 + 下划线:`MODBUS_TCP`、`FINS_TCP` +- 同一协议不同连接方式**拆分为独立条目**,后缀:`_TCP`/`_RTU`/`_SERIAL`/`_OVERTCP` +- 驱动注册标识必须与 `protocol_name` 完全一致 + +### brand + +- 有品牌协议:使用品牌原名,不附加连接方式或描述 +- 通用协议(Modbus/OPC UA):使用标准协议名 +- 无品牌行业标准:使用**应用领域**(如 `"电力仪表"`, `"水气仪表"`),避免与 protocol_name 重复 +- ✅ `"西门子"`, `"电力仪表"` / ❌ `"哈斯串口"`, `"IEC104"`(与 protocol_name 冗余) + +### connection_type + +- 每个协议条目只允许一种 `connection_type`(`"ethernet"` 或 `"serial"`) +- 需要同时支持串口和以太网时,新增独立协议条目 diff --git a/.agents/skills/cloud-deploy-verify/SKILL.md b/.agents/skills/cloud-deploy-verify/SKILL.md new file mode 100644 index 0000000..4d45d92 --- /dev/null +++ b/.agents/skills/cloud-deploy-verify/SKILL.md @@ -0,0 +1,62 @@ +--- +name: cloud-deploy-verify +description: 云平台部署与验证流程。用于用户说“部署云平台”“用 deploy_cloud.sh 部署”“部署到云服务器”“云端部署并验证”“发布云平台前端/后端”等场景,默认使用 deploy_cloud.sh 部署到 ubuntu@119.45.4.75 并验证 cloud-server 与关键接口。 +--- + +# 云平台部署与验证 + +## 何时使用 + +- 部署云平台 +- 用 `deploy_cloud.sh` 部署 +- 部署到云服务器 +- 云端部署并验证 +- 发布云平台前端或后端 + +## 固定约定 + +- 部署脚本:`./deploy_cloud.sh` +- 默认目标:`ubuntu@119.45.4.75` +- 默认云平台地址:`http://119.45.4.75` +- systemd 服务:`cloud-server` +- 默认不要加 `--init`;只有用户明确要求初始化、清库、重置云端状态时才使用 `--init` + +## 执行流程 + +1. 在仓库根目录执行部署: + +```bash +./deploy_cloud.sh +``` + +2. 确认脚本完成并输出: + +```text +[OK] cloud-server is running +``` + +3. 验证远端服务状态: + +```bash +ssh ubuntu@119.45.4.75 'sudo systemctl is-active cloud-server' +``` + +4. 验证云平台登录与关键接口: + - 登录接口:`POST http://119.45.4.75/api/auth/login` + - OTA 包列表:`GET http://119.45.4.75/api/admin/edge-upgrades/packages` + - 如果本次改动涉及 OTA 包字段,确认响应包含预期字段,例如 `release_type`、`description`、`release_notes` + +5. 如果本次改动影响边缘侧 OTA 查询,再验证边缘侧代理透传: + - 先登录边缘侧,例如 87:`POST http://192.168.40.87/api/login` + - 再调用:`POST http://192.168.40.87/api/ota/cloud/packages` + - 确认云端字段能透传到边缘侧响应 + +## 注意 + +- `deploy_cloud.sh` 会构建云端前端和 `cloud_server`,同步 `runtime/cloud_server/`,迁移/校验 Mosquitto Dynamic Security,并重启 `cloud-server`。 +- 不要手写云端 rsync/scp/systemctl 流程,优先使用 `deploy_cloud.sh`。 +- 如果部署失败,先看脚本输出;服务启动失败再查: + +```bash +ssh ubuntu@119.45.4.75 'sudo journalctl -u cloud-server --since "5 min ago" --no-pager' +``` diff --git a/.agents/skills/cloud-deploy-verify/agents/openai.yaml b/.agents/skills/cloud-deploy-verify/agents/openai.yaml new file mode 100644 index 0000000..fc739c3 --- /dev/null +++ b/.agents/skills/cloud-deploy-verify/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: 云平台部署验证 +short_description: 使用 deploy_cloud.sh 部署云平台并验证关键接口 +default_prompt: Use this skill when the user asks to deploy the cloud platform, deploy to the cloud server, run deploy_cloud.sh, or verify a cloud frontend/backend release. From the repo root, run ./deploy_cloud.sh by default, targeting ubuntu@119.45.4.75. Do not pass --init unless explicitly requested. After deployment, verify cloud-server is running, test /api/auth/login and relevant cloud APIs, and if OTA package fields changed, also verify the 87 edge proxy /api/ota/cloud/packages returns those fields. diff --git a/.agents/skills/cloud-public-deploy/SKILL.md b/.agents/skills/cloud-public-deploy/SKILL.md new file mode 100644 index 0000000..3cbd341 --- /dev/null +++ b/.agents/skills/cloud-public-deploy/SKILL.md @@ -0,0 +1,120 @@ +--- +name: cloud-public-deploy +description: edge_collector 云平台公网部署流程规范。用于整理、审查或执行云平台公网部署方案时参考 deploy_cloud.sh,覆盖 package.sh --cloud-only、runtime/cloud_server 同步、远端配置保护、Mosquitto Dynamic Security、systemd/nginx 初始化、cloud-server 重启和公网接口验证。 +--- + +# 云平台公网部署 + +## 固定约定 + +- 部署脚本:`./deploy_cloud.sh` +- 默认目标:`ubuntu@119.45.4.75` +- 远端目录:`~/cloud_server` +- systemd 服务:`cloud-server` +- 公网入口:`http://119.45.4.75` +- 本地构建输出:`runtime/cloud_server/` + +## 使用边界 + +- 常规部署使用 `./deploy_cloud.sh`。 +- 只看流程或生成文档时可以参考本 skill,不直接执行。 +- 只有用户明确要求“初始化、清库、重置云端状态”时才允许加 `--init`。 +- 不要手写 rsync、scp、systemctl 流程替代 `deploy_cloud.sh`。 + +## deploy_cloud.sh 实际流程 + +```text +1. bash ./package.sh --cloud-only +2. 校验 runtime/cloud_server 和 cloud_server/config/server_config.json +3. 读取 MQTT dynsec、PostgreSQL、TDengine 配置 +4. 检查 SSH 连通性 +5. 备份远端 server_config.json 和 ai_config.json +6. rsync runtime/cloud_server/ 到 ~/cloud_server/ +7. 恢复/生成远端运行密钥,保留远端 AI 配置 +8. 迁移并校验 Mosquitto Dynamic Security +9. --init 模式下安装 systemd 服务和 nginx +10. 重启 cloud-server 并输出公网 URL +``` + +## 运行配置保护 + +部署脚本会保护: + +- `~/cloud_server/config/server_config.json` 中的 `jwt_secret`。 +- `custom_config.terminal.credential_key`。 +- `~/cloud_server/config/ai_config.json`。 + +审查或修改部署逻辑时,必须确认这些运行态配置不会被打包产物覆盖。 + +## MQTT Dynamic Security + +脚本会根据 `server_config.json` 配置: + +- 禁用旧的 Mosquitto 静态账号/ACL 配置。 +- 初始化或更新 `/var/lib/mosquitto/dynamic-security.json`。 +- 创建 gateway/cloud 角色和 cloud MQTT client。 +- 设置 `/data/#`、`/status/#`、`/ack/#`、`/cmd/#` 相关权限。 + +如果部署失败,先查 `mosquitto_ctrl`、`mosquitto_dynamic_security.so` 和 Mosquitto 服务状态。 + +## 初始化模式 + +`--init` 会执行高风险动作: + +- 停止 `cloud-server` 和 `mosquitto`。 +- 重置 PostgreSQL 数据库。 +- 重置 TDengine 数据库。 +- 清理 MQTT dynsec 状态。 +- 安装/覆盖 systemd service。 +- 配置 nginx 80 端口反代到 8081,443 自签名证书重定向到 HTTP。 + +未获用户明确确认时禁止使用 `--init`。 + +## 验证步骤 + +部署完成后至少验证: + +```bash +ssh ubuntu@119.45.4.75 'sudo systemctl is-active cloud-server' +ssh ubuntu@119.45.4.75 'sudo systemctl is-active mosquitto' +curl -s http://119.45.4.75/api/health +``` + +按改动范围补充: + +- 登录接口:`POST /api/auth/login` +- AI 配置/分析接口。 +- OTA 包列表接口。 +- MQTT 网关连接和设备在线状态。 +- 前端页面静态资源是否刷新。 + +## 故障排查 + +服务启动失败: + +```bash +ssh ubuntu@119.45.4.75 'sudo journalctl -u cloud-server --since "10 min ago" --no-pager' +``` + +nginx 异常: + +```bash +ssh ubuntu@119.45.4.75 'sudo nginx -t && sudo systemctl status nginx --no-pager' +``` + +MQTT dynsec 异常: + +```bash +ssh ubuntu@119.45.4.75 'sudo systemctl status mosquitto --no-pager' +``` + +## 文档输出 + +整理公网部署文档时必须写清: + +- 目标主机和远端目录。 +- 是否使用 `--init`。 +- 会保留哪些远端配置。 +- 会重启哪些服务。 +- 公网访问入口和验证接口。 +- 回滚方式和日志位置。 diff --git a/.agents/skills/cpp-coding-style/SKILL.md b/.agents/skills/cpp-coding-style/SKILL.md new file mode 100644 index 0000000..42a44ab --- /dev/null +++ b/.agents/skills/cpp-coding-style/SKILL.md @@ -0,0 +1,57 @@ +--- +name: cpp-coding-style +description: C/C++ 代码风格规范。用于本仓库 C/C++ 新增、修改、重构时,统一格式、命名、include 顺序和现代 C++ 用法。 +--- + +# C/C++ 风格规范 + +## 基础约束 + +- 使用 `C++17` +- 4 空格缩进 +- 大括号使用 K&R 风格 +- 单行长度尽量不超过 100 + +## 命名规则 + +- 类型(`class/struct/enum`):`PascalCase` +- 函数/方法:`PascalCase` +- 普通变量:`snake_case` +- 成员变量:`snake_case_` +- 常量:`kPascalCase` +- 宏:`ALL_CAPS` +- 命名空间:小写 + +## include 顺序 + +1. 对应头文件 +2. C 系统头 +3. C++ 标准库 +4. 第三方库 +5. 项目内头文件 + +组间空一行。 + +## 代码习惯 + +- 单行 `if/for/while` 也加大括号 +- 不使用 `using namespace std;` +- 优先 `constexpr`、`[[nodiscard]]`、RAII +- 优先 `static_cast`,避免 C 风格强转 +- `auto` 只在不降低可读性时使用 + +## 注释与日志 + +- 注释优先说明"为什么",不是"做了什么" +- 保留高价值中文注释,不随手删除 +- 强制包含 `#include "logger/Logger.hpp"`(或其相对路径) +- **统一使用流式日志宏**:`LOG_DEBUGS()`、`LOG_INFOS()`、`LOG_WARNS()`、`LOG_ERRORS()`,通过 `<<` 拼接内容。严禁使用 `std::cerr`/`std::cout`、第三方日志原生接口或混用其他宏。 + ```cpp + // 正确:流式 + LOG_ERRORS() << "SerialTransport: 无法打开串口 " << port_ << ": " << strerror(errno); + // 错误:传参格式 + LOG_ERRORS("SerialTransport: 无法打开串口 {}: {}", port_, strerror(errno)); + // 错误:原生输出 + std::cerr << "SerialTransport: 无法打开串口" << std::endl; + ``` +- 日志风格沿用同模块既有前缀和语气 diff --git a/.agents/skills/edge-82-release/SKILL.md b/.agents/skills/edge-82-release/SKILL.md new file mode 100644 index 0000000..ff5126a --- /dev/null +++ b/.agents/skills/edge-82-release/SKILL.md @@ -0,0 +1,48 @@ +--- +name: edge-82-release +description: 82主机发布流程。用于用户说“去82主机编译”“去82主机编译代码”“去82发布xxx版本”“发布并上传版本”时,默认到 192.168.40.82 的 /home/cat/code/edge_collector 执行 git pull 与打包;发布并上传时再把产物上传到云平台 admin 账号。 +--- + +# 82 主机发布流程 + +## 何时使用 + +- 去82主机编译代码 +- 去82主机编译 +- 去82发布 xxx 版本 +- 发布并上传版本 + +## 固定环境 + +- 主机:`cat@192.168.40.82` +- 代码根目录:`/home/cat/code/edge_collector` +- 构建目标:`arm64` +- 云平台:`http://119.45.4.75:8081` +- 云端账号:`admin` + +## 执行顺序 + +1. 去82主机编译代码 + - `cd /home/cat/code/edge_collector` + - `git pull` + - `./package.sh --edge` + +2. 去82发布 xxx 版本 + - `cd /home/cat/code/edge_collector` + - `git pull` + - `./package.sh --publish --version xxx` + +3. 发布并上传版本 + - 先按“去82发布 xxx 版本”执行 + - 再把 `publish/edge__arm64.tar.gz` 上传到云平台 + - 使用云平台默认 `admin` 账号登录 + +## 上传字段 + +- `file` +- `package_type=edge` +- `version=<版本号>` +- `target_arch=arm64` +- `release_type=stable` +- `visibility=platform` +- `enabled=true` diff --git a/.agents/skills/edge-82-release/agents/openai.yaml b/.agents/skills/edge-82-release/agents/openai.yaml new file mode 100644 index 0000000..370ca71 --- /dev/null +++ b/.agents/skills/edge-82-release/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: 82发布流程 +short_description: 82主机编译、发布和上传规则 +default_prompt: Use this skill when the user asks "去82主机编译", "去82主机编译代码", or asks to publish/publish-and-upload from host 82. For compile requests, go to /home/cat/code/edge_collector on 192.168.40.82, run git pull first, then run ./package.sh --edge. For publish requests, run ./package.sh --publish --version . Upload to the cloud admin account only when the user explicitly asks to publish and upload. diff --git a/.agents/skills/edge-bug-lessons/SKILL.md b/.agents/skills/edge-bug-lessons/SKILL.md new file mode 100644 index 0000000..a638618 --- /dev/null +++ b/.agents/skills/edge-bug-lessons/SKILL.md @@ -0,0 +1,91 @@ +--- +name: edge-bug-lessons +description: edge_collector Bug 经验库沉淀规范。用于用户要求 bug 教训、故障复盘、为什么流出、总结经验、沉淀规则时,把边缘侧、云平台、协议采集、部署同步、前端、AI、网络等问题整理为可检索的历史 lesson 和预防规则。 +--- + +# edge_collector Bug 经验库 + +## 目标 + +把一次故障从“修好了”沉淀为“以后能提前拦住”。重点记录根因链路、漏检点、验证方式和反哺动作。 + +## 触发场景 + +- “总结这次 bug” +- “为什么会流出” +- “写一个复盘” +- “沉淀经验” +- “以后怎么避免” +- 修复完成后需要补长期规则 + +## 默认落点 + +```text +docs/bugfix/BugLesson-YYYYMMDD-简述.md +docs/bugfix/BugLesson-index.md +``` + +如果已有更合适的专题目录,可放到: + +- `docs/鲁班猫*/` +- `collector/docs/protocols/` +- `docs/ops/` + +但索引仍建议保留在 `docs/bugfix/BugLesson-index.md`。 + +## Lesson 结构 + +```markdown +# 标题 + +**日期**: +**模块**: +**影响范围**: + +## 1. 问题现象 + +## 2. 根因链路 + +## 3. 流出路径 / 漏检点 + +## 4. 修复内容 + +## 5. 验证结果 + +## 6. 本可在哪一步拦住 + +## 7. 预防措施 + +## 8. 可复用规则 + +## 9. 反哺动作 + +## 10. 相关文件 +``` + +## 当前项目重点 + +优先沉淀以下类型: + +- 打包或同步覆盖运行态动态配置。 +- 97/94/82 等主机系统差异导致运行异常。 +- FANUC/西门子协议库、架构、链接方式问题。 +- 前端白屏、按钮无反馈、错误提示过泛。 +- AI 分析超时、空内容、内部配置泄露。 +- WiFi/4G/frpc/端口转发独立 agent 异常。 +- 云端设备在线状态、历史趋势、数据不连续误判。 + +## 写法要求 + +- 区分“已确认事实”和“推断”。 +- 根因必须落到文件、配置、命令、日志或环境差异。 +- 不写“加强测试”这类空话,要写可执行拦截点。 +- 反哺动作要明确更新哪个 skill、测试清单、文档或脚本检查项。 +- 涉及密钥、密码、Token 时必须脱敏。 + +## 索引格式 + +```markdown +| 日期 | 标题 | 模块 | 核心根因 | 漏检点 | 预防规则 | 文件 | +|------|------|------|----------|--------|----------|------| +``` diff --git a/.agents/skills/edge-bugfix/SKILL.md b/.agents/skills/edge-bugfix/SKILL.md new file mode 100644 index 0000000..f1db40f --- /dev/null +++ b/.agents/skills/edge-bugfix/SKILL.md @@ -0,0 +1,86 @@ +--- +name: edge-bugfix +description: edge_collector 缺陷排查与根因修复流程。用于用户报告边缘侧、云平台、协议采集、前端白屏、部署同步、远程主机 CPU/内存异常、脚本失败、接口失败等 Bug 或异常时,按读取证据、根因定位、最小修复、定向验证和报告沉淀推进。 +--- + +# edge_collector Bug 修复流程 + +## 适用范围 + +- 边缘服务:`collector`、`configurator`、`edge` systemd 服务。 +- 云平台:`cloud_server`、`frontend/cloud_app`、`deploy_cloud.sh`。 +- 前端:`frontend/config_app`、`frontend/cloud_app`。 +- 协议采集:FANUC、西门子、Modbus、OPC UA、传感器等。 +- 脚本/部署:`scripts/`、`package.sh`、`scripts/migrate_edge.sh`。 +- 远程主机:82/87/94/97、云服务器 `119.45.4.75`。 + +## 核心原则 + +1. 先只读取证据,后修改。 +2. 必须定位根因,禁止只修表面症状。 +3. 不回滚用户改动,不清空运行配置。 +4. 涉及远程同步默认使用既有项目脚本,不手写替代流程。 +5. 修改后必须给出定向验证命令和关键结果。 + +## 排查流程 + +### 1. 收集现场 + +按问题类型优先读取: + +- Git 状态:`git status --short` +- 相关日志:`logs/`、`journalctl -u edge`、`journalctl -u cloud-server` +- 配置:`runtime/edge/config/`、`collector/config/`、`configurator/config/` +- 前端:浏览器错误、接口响应、构建产物、路由 +- 远程主机:`uptime`、`free -h`、`df -h`、`systemctl status` + +远程数字主机遵循 `host-connection-defaults`;边缘同步遵循 `edge-sync-host`。 + +### 2. 定位根因 + +优先沿真实链路追踪: + +```text +用户现象 + -> 前端页面 / API + -> configurator 或 cloud_server + -> collector / agent / 脚本 + -> 配置文件 / SQLite / 网络 / systemd +``` + +典型链路: + +- 前端白屏:CSS -> DOM -> JS -> API -> 构建产物。 +- 云端接口失败:前端代理 -> cloud_server 路由 -> 数据库/外部服务。 +- 采集异常:设备配置 -> DriverRegistry -> 驱动日志 -> 协议依赖库。 +- 同步后异常:构建主机架构 -> 打包产物 -> runtime 配置排除 -> systemd 重启。 + +### 3. 修复策略 + +- 小范围修改,不做无关重构。 +- C++ 遵循 `cpp-coding-style`。 +- 后端接口/配置遵循 `backend-conventions`。 +- 前端遵循 `frontend-ui-conventions`、`frontend-debug`、`frontend-dialog`。 +- Shell 遵循 `shell-scripting`。 +- 第三方库遵循 `third-party-libs`。 + +### 4. 验证要求 + +按改动选择最小但可信的验证: + +- JSON 配置:`jq empty ` +- C++ collector:`cmake --build build --target collector -j2` +- configurator/cloud_server:对应 target 或项目测试脚本。 +- 前端:能运行 npm 的环境执行 `npm run build`。 +- 边缘打包:`./package.sh --edge-only` +- 远程部署:按用户明确要求再同步/重启。 + +### 5. 报告沉淀 + +复杂 Bug 或远程事故修复后,在 `docs/` 下写简短报告,建议位置: + +- 远程主机/设备类:`docs/鲁班猫*/` +- 协议类:`collector/docs/protocols/` +- 通用事故:`docs/` + +报告至少包含:现象、根因、修复、验证、后续预防。 diff --git a/.agents/skills/edge-business-rule-extractor/SKILL.md b/.agents/skills/edge-business-rule-extractor/SKILL.md new file mode 100644 index 0000000..976bef9 --- /dev/null +++ b/.agents/skills/edge-business-rule-extractor/SKILL.md @@ -0,0 +1,54 @@ +--- +name: edge-business-rule-extractor +description: edge_collector 业务与技术规则提取规范。用于从用户需求、讨论、故障复盘和实现方案中提取稳定规则,维护云边采集、上传策略、动态配置保护、权限、前端交互、部署同步和协议模板等项目规则。 +--- + +# edge_collector 规则提取 + +## 适用场景 + +- 用户反复强调某个约束。 +- 某个事故暴露出需要长期遵守的规则。 +- 方案中出现“必须、不能、只允许、默认、除非明确要求”等表述。 +- 需要把对话中的口头规范沉淀到文档或 skill。 + +## 规则类型 + +- `BR-COLLECT`:采集与上传规则。 +- `BR-CONFIG`:配置和动态文件保护规则。 +- `BR-DEPLOY`:打包、同步、部署规则。 +- `BR-UI`:前端交互和用户可见文案规则。 +- `BR-PERM`:权限和安全规则。 +- `BR-PROTOCOL`:协议模板和驱动规则。 +- `BR-AI`:AI 分析和模型配置规则。 + +## 当前项目典型规则 + +- 相同数据不上传,5 分钟强制上传;短时间点位不连续可能是正常现象。 +- 打包或同步不能携带目标主机运行态动态配置。 +- 同主机编译部署也要使用 `scripts/migrate_edge.sh`。 +- `install_all.sh` 只有用户明确要求时才执行。 +- 用户可见协议介绍不透露 helper、SDK、库路径等技术细节。 +- AI 分析报告不展示内部 AI 配置名、Provider 名称或模型细节。 + +## 输出格式 + +```markdown +| 编号 | 类型 | 规则 | 来源 | 影响范围 | 验证方式 | +|------|------|------|------|----------|----------| +| BR-DEPLOY-001 | 部署 | ... | 用户确认 | package/sync | ... | +``` + +## 执行流程 + +1. 从需求、对话或文档中提取候选规则。 +2. 去重,避免把同一规则写成多个版本。 +3. 判断规则是否长期有效,临时现场处理不沉淀为规则。 +4. 写明影响范围和验证方式。 +5. 如需落盘,优先更新 `docs/` 下已有规则/概览文档;没有则建议新增规则表。 + +## 注意 + +- 不把猜测写成规则。 +- 不把一次性临时命令写成规则。 +- 规则变更会影响部署或运行安全时,先让用户确认。 diff --git a/.agents/skills/edge-code-review/SKILL.md b/.agents/skills/edge-code-review/SKILL.md new file mode 100644 index 0000000..b6fcb7e --- /dev/null +++ b/.agents/skills/edge-code-review/SKILL.md @@ -0,0 +1,70 @@ +--- +name: edge-code-review +description: edge_collector 代码评审流程。用于用户要求 review、代码审查、提交前检查、质量审核时,按严重程度输出问题,覆盖 C++ 采集驱动、Drogon 接口、React 前端、脚本、打包部署、运行配置和测试缺口。 +--- + +# edge_collector 代码评审 + +## 输出规则 + +评审必须 findings first: + +1. 先列问题,按严重程度排序。 +2. 每条问题包含文件与行号。 +3. 没有问题时明确说明,并列出剩余风险或测试缺口。 +4. 摘要放在问题之后。 + +## 评审维度 + +### C++/采集端 + +- 是否破坏 `DriverRegistry` 注册名与协议配置一致性。 +- 是否错误链接第三方库或跨架构库。 +- 是否直接调用原生通信 API,绕过 `TcpTransport`/`UdpTransport`/`SerialTransport`。 +- 是否遵循 `PointData::UpdateValue` 类型约束。 +- 是否使用流式日志宏。 +- 是否存在线程、生命周期、子进程回收、fd 泄漏风险。 + +### 后端接口 + +- JSON 字段是否 `snake_case`。 +- 是否处理非法 JSON。 +- 是否复用 `ResponseUtil`。 +- 错误响应是否稳定且不暴露底层敏感细节。 +- 配置写入是否会覆盖运行态动态配置。 + +### 前端 + +- 是否复用现有组件。 +- 是否符合 CSS Modules 和暗色主题。 +- 弹窗是否使用统一对话框,不用原生 alert/confirm/prompt。 +- 交互失败是否给出清晰反馈。 +- 移动/窄屏是否溢出或遮挡。 + +### 脚本与部署 + +- 是否使用 `set -euo pipefail`。 +- 路径是否从脚本位置推导。 +- 是否误覆盖 `runtime/edge/config` 中动态配置。 +- 同步部署是否遵循 `scripts/migrate_edge.sh`。 +- 新增常驻服务是否独立,不耦合 edge 主服务。 + +### 测试与验证 + +- 是否有定向单元测试或脚本验证。 +- 协议改动是否更新协议文档。 +- 前端改动是否能构建或说明未构建原因。 +- 远程问题是否给出服务状态或接口验证。 + +## 高风险信号 + +命中以下内容需重点审查: + +- `collector/CMakeLists.txt` +- `package.sh`、`scripts/migrate_edge.sh` +- `collector/src/driver/` +- `configurator/config/*.json` +- `runtime/`、`data/`、动态配置文件处理 +- systemd 安装脚本 +- 远程同步/重启逻辑 + diff --git a/.agents/skills/edge-codex-automation/SKILL.md b/.agents/skills/edge-codex-automation/SKILL.md new file mode 100644 index 0000000..9cb402d --- /dev/null +++ b/.agents/skills/edge-codex-automation/SKILL.md @@ -0,0 +1,84 @@ +--- +name: edge-codex-automation +description: edge_collector Codex 自动化任务建设规范。用于新增、修改或评审自动化任务、定时检查、自动部署验证、远程主机巡检、日志汇总、报告生成等流程时,明确执行边界、调度来源、脚本位置、通知、手工验证和安全限制。 +--- + +# edge_collector Codex 自动化 + +## 适用场景 + +- 定时检查云平台或边缘主机状态。 +- 自动生成巡检报告。 +- 自动构建或验证,但不自动发布。 +- 自动拉取日志、磁盘、CPU、内存信息。 +- 自动检查 docs、skills、配置格式。 + +## 设计原则 + +- 自动化只能做边界清晰、可回滚、可验证的任务。 +- 涉及部署、重启、清库、删除、覆盖配置时必须有人确认。 +- 自动化脚本要独立,不能和 `edge` 主服务强耦合。 +- 运行日志必须可追溯。 +- 失败要有明确提示和下一步处理建议。 + +## 建设流程 + +1. 明确目标: + - 自动化要解决什么问题。 + - 成功标准和失败标准。 + - 运行在哪台主机、哪个目录。 + +2. 明确调度: + - 一次性、定时还是手动触发。 + - cron、systemd timer、CI 或其他调度器。 + - 时区和执行频率。 + +3. 明确权限: + - 是否需要 SSH。 + - 是否需要 sudo。 + - 是否会修改远程状态。 + - 是否访问密钥或配置文件。 + +4. 落地脚本: + - 脚本放到 `scripts/` 或 `.agents/` 约定目录。 + - Shell 遵循 `shell-scripting`。 + - Python 脚本保持独立、参数清晰、日志明确。 + +5. 手工验证一次: + - 先 `--dry-run` 或只读模式。 + - 再执行真实任务。 + - 检查退出码、日志、输出文件。 + +## 当前项目自动化边界 + +允许默认自动化: + +- 只读巡检。 +- 构建验证。 +- 文档/skill 校验。 +- 日志采集和摘要。 +- 接口健康检查。 + +必须确认后才执行: + +- `deploy_cloud.sh` +- `scripts/migrate_edge.sh` +- `install_all.sh` +- systemd restart/stop。 +- 数据库写入、清理、重置。 +- 删除文件、清理 `/tmp`、覆盖运行配置。 + +## 输出格式 + +```text +自动化任务: +- 名称: +- 目标: +- 执行脚本: +- 调度方式: +- 运行主机: +- 权限需求: +- 日志位置: +- 手工验证: +- 风险: +``` diff --git a/.agents/skills/edge-config-lifecycle/SKILL.md b/.agents/skills/edge-config-lifecycle/SKILL.md new file mode 100644 index 0000000..12c3567 --- /dev/null +++ b/.agents/skills/edge-config-lifecycle/SKILL.md @@ -0,0 +1,96 @@ +--- +name: edge-config-lifecycle +description: edge_collector 配置生命周期管理规范。用于新增、修改、打包、同步、动态生成或排除配置文件时,明确默认配置、运行态配置、用户保存配置、密钥配置、迁移保留、备份恢复和前端保存行为,防止覆盖现场配置。 +--- + +# edge_collector 配置生命周期 + +## 适用配置 + +- 边缘运行配置:`runtime/edge/config/` +- 云端运行配置:`~/cloud_server/config/` +- AI 配置:`ai_config.json` +- 端口转发:`port_forward.json` +- frpc/内网穿透配置。 +- 协议设备配置。 +- WiFi/4G 辅助配置。 +- 默认模板:`configurator/config/templates/` +- 用户可见协议描述:`configurator/config/protocols/` + +## 配置分类 + +### 默认配置 + +随代码发布,提供初始结构和默认值。 + +### 运行态配置 + +目标主机运行后由用户、前端或服务生成。打包和同步不能覆盖。 + +### 密钥配置 + +包含 key、secret、password、token。必须脱敏、禁止提交真实值。 + +### 模板配置 + +协议模板、默认点位、用户可选参数。可随版本更新,但要考虑兼容已有设备。 + +## 新增配置文件检查 + +新增配置时必须回答: + +- 默认文件放在哪里。 +- 运行态文件放在哪里。 +- 如果文件不存在,谁负责动态生成。 +- 打包是否包含。 +- 同步是否排除。 +- 前端保存是否会覆盖其他字段。 +- 是否包含密钥。 +- 是否需要备份和迁移。 + +## 打包与同步 + +修改以下脚本时必须检查配置影响: + +- `package.sh` +- `scripts/migrate_edge.sh` +- `deploy_cloud.sh` +- `scripts/install_all.sh` + +原则: + +- 默认配置可以进入包。 +- 运行态配置不能被 `--delete` 同步清掉。 +- 远端已有密钥配置必须保留。 +- 删除配置文件前必须确认是否会自动再生成。 + +## 前端保存 + +- 保存配置时只更新相关字段。 +- 不要用空对象覆盖整个配置文件。 +- 保存失败要显示具体原因。 +- 权限不足要按已有权限体系处理。 + +## 验证 + +至少验证: + +```bash +jq empty +``` + +同步/部署后验证: + +- 目标主机已有配置仍存在。 +- 新增默认配置可生成。 +- 服务重启后能读取配置。 +- 前端读取和保存正常。 + +## 风险信号 + +- `rsync --delete` +- `cp -r config` +- `cat > config.json` +- 前端保存整个 JSON。 +- 后端启动时无条件重写配置。 +- 示例配置中出现真实 key。 diff --git a/.agents/skills/edge-data-quality-analyzer/SKILL.md b/.agents/skills/edge-data-quality-analyzer/SKILL.md new file mode 100644 index 0000000..d83e6e7 --- /dev/null +++ b/.agents/skills/edge-data-quality-analyzer/SKILL.md @@ -0,0 +1,93 @@ +--- +name: edge-data-quality-analyzer +description: edge_collector 采集与上传数据质量分析规范。用于分析历史趋势、AI 分析输入、网关/设备/点位数据缺失、断点、不连续、重复值、时间戳异常、上传策略影响、离线缓存重传和云端展示差异,并输出可验证的数据质量结论。 +--- + +# edge_collector 数据质量分析 + +## 适用问题 + +- 云平台历史趋势看起来断续。 +- AI 分析使用点数明显少于原始点数。 +- 设备在线但云端显示离线。 +- 点位长时间不变化、重复上传或缺失。 +- 离线缓存重传后数据仍不完整。 +- 用户质疑采集频率、上传策略或降采样结果。 + +## 分析维度 + +1. 数据完整性:应有点数、实际点数、缺口时间段。 +2. 时间连续性:相邻时间间隔、断点、乱序、重复时间戳。 +3. 值质量:重复值、常量段、异常突变、空值、类型异常。 +4. 上传策略影响:相同数据不上传、5 分钟强制上传导致的短时不连续。 +5. 降采样影响:原始点数、展示点数、AI 分析点数、是否保留极值。 +6. 云边一致性:边缘本地数据、上传队列、云端历史数据是否一致。 + +## 排查流程 + +### 1. 确认对象 + +明确: + +- 网关名称和 ID。 +- 设备名称和 ID。 +- 点位名称和 ID。 +- 时间范围。 +- 页面或接口来源。 + +### 2. 查询链路 + +按真实链路分析: + +```text +设备采集 + -> collector 点位值 + -> 边缘本地缓存/上传队列 + -> 云端入库 + -> 历史趋势接口 + -> 图表降采样 / AI 分析输入 +``` + +### 3. 统计指标 + +输出至少包含: + +- 原始记录数。 +- 有效记录数。 +- 展示/分析使用记录数。 +- 最大采样间隔。 +- P50/P95 采样间隔。 +- 重复值比例。 +- 缺口时间段 Top N。 + +### 4. 解释结论 + +结论必须区分: + +- 正常策略导致:例如相同数据不上传、5 分钟强制上传。 +- 展示降采样导致:图表为了性能减少点数。 +- 采集异常导致:设备离线、驱动读失败、点位配置错误。 +- 上传异常导致:网络断开、离线缓存未重传、云端接口失败。 + +## AI 分析专项 + +当分析 AI 输入数据时: + +- 必须带上网关名称、设备名称、点位名称。 +- 必须说明原始点数和用于 AI 分析点数的区别。 +- 深度分析应提高采样点数、异常片段数量和上下文摘要,不只改变提示词。 +- 给 AI 的提示词要说明上传策略:相同数据不上传,5 分钟强制上传,因此短时间不连续可能是正常现象。 + +## 报告格式 + +```markdown +## 数据范围 + +## 关键统计 + +## 异常片段 + +## 原因判断 + +## 建议动作 +``` diff --git a/.agents/skills/edge-database-ops/SKILL.md b/.agents/skills/edge-database-ops/SKILL.md new file mode 100644 index 0000000..0828443 --- /dev/null +++ b/.agents/skills/edge-database-ops/SKILL.md @@ -0,0 +1,104 @@ +--- +name: edge-database-ops +description: edge_collector 数据库查询与安全操作规范。用于查询或排查云平台 PostgreSQL、TDengine、边缘 SQLite/本地数据、历史趋势、网关设备点位、用户权限和 AI 分析数据时,按只读优先、脱敏、备份、写操作确认和结果可追溯执行。 +--- + +# edge_collector 数据库操作 + +## 适用场景 + +- 查询云端网关、设备、点位、用户、权限数据。 +- 排查历史趋势、AI 分析输入、设备在线状态。 +- 验证离线缓存、上传结果、配置是否入库。 +- 对比边缘本地数据和云端数据。 +- 需要执行 SQL 修复或清理数据。 + +## 基本原则 + +- 默认只读。 +- 写操作必须用户明确确认。 +- 生产或云端写操作前必须说明影响范围和回滚方案。 +- 查询结果默认脱敏。 +- 不在回复中输出数据库密码、Token、Key。 + +## 先确认环境 + +执行前确认: + +- 目标:本机、边缘主机、云服务器。 +- 数据库类型:PostgreSQL、TDengine、SQLite 或文件型数据。 +- 数据库来源:配置文件、服务环境变量、用户提供。 +- 操作类型:查询、导出、修复、删除。 + +优先读取配置: + +- `cloud_server/config/server_config.json` +- `runtime/cloud_server/config/server_config.json` +- `runtime/edge/config/` +- 部署脚本和 systemd 环境。 + +## 查询流程 + +1. 先定位表和字段来源。 +2. 写出 SQL 或命令。 +3. 只读执行。 +4. 汇总关键结果,不粘贴大量原始数据。 +5. 对涉及用户、密钥、地址的数据脱敏。 + +## 写操作流程 + +写操作前必须给用户确认: + +```text +将执行: +- 数据库: +- 表: +- 条件: +- 影响行数预估: +- 回滚方式: +``` + +执行前建议备份受影响数据: + +```sql +SELECT * FROM WHERE ; +``` + +必要时导出为临时文件,并说明路径。 + +## 常用只读检查 + +PostgreSQL: + +```sql +SELECT now(); +SELECT version(); +``` + +TDengine: + +```sql +SHOW DATABASES; +SHOW STABLES; +``` + +SQLite: + +```bash +sqlite3 ".tables" +sqlite3 "PRAGMA integrity_check;" +``` + +## 输出要求 + +- 说明数据来源。 +- 说明查询条件和时间范围。 +- 说明结论是事实还是推断。 +- 给出下一步建议。 + +## 禁止事项 + +- 未确认就执行 `UPDATE`、`DELETE`、`DROP`、`TRUNCATE`。 +- 把配置中的数据库密码打印到回复。 +- 用线上写操作验证猜测。 +- 将大量敏感原始数据贴到对话中。 diff --git a/.agents/skills/edge-deployment-writer/SKILL.md b/.agents/skills/edge-deployment-writer/SKILL.md new file mode 100644 index 0000000..3b1f860 --- /dev/null +++ b/.agents/skills/edge-deployment-writer/SKILL.md @@ -0,0 +1,68 @@ +--- +name: edge-deployment-writer +description: edge_collector 部署手册与上线方案编写规范。用于整理边缘侧、云平台、82/97/94/87 主机、runtime 同步、systemd 服务、回滚和验证步骤时,生成可执行部署文档。 +--- + +# edge_collector 部署文档 + +## 固定项目约定 + +- 云平台部署优先使用 `deploy_cloud.sh`。 +- 边缘打包使用 `package.sh`。 +- 边缘同步使用 `scripts/migrate_edge.sh`。 +- 用户明确要求执行 `install_all.sh` 时才执行;不要每次同步都运行。 +- 同主机编译部署也要使用 `scripts/migrate_edge.sh`。 + +## 部署文档结构 + +```text +目标与范围 +目标主机与账号 +前置条件 +构建步骤 +同步/部署步骤 +服务重启步骤 +验证步骤 +回滚方案 +风险与注意事项 +``` + +## 必须写清 + +- 源主机、目标主机、目标目录。 +- 是否会覆盖运行配置。 +- 是否需要重启 `edge`、`cloud-server` 或独立 agent。 +- 是否需要执行 `install_all.sh`。 +- 验证命令和预期输出。 + +## 常用验证 + +边缘: + +```bash +systemctl is-active edge +curl -s http://127.0.0.1/api/status +``` + +云端: + +```bash +systemctl is-active cloud-server +curl -s http://127.0.0.1:/api/health +``` + +脚本: + +```bash +bash -n scripts/.sh +``` + +## 回滚说明 + +文档必须说明: + +- 上一个 runtime/edge 或发布包位置。 +- 如何恢复二进制和 web 资源。 +- 哪些配置不能回滚覆盖。 +- 回滚后如何重启服务和验证。 + diff --git a/.agents/skills/edge-design-doc-writer/SKILL.md b/.agents/skills/edge-design-doc-writer/SKILL.md new file mode 100644 index 0000000..526019a --- /dev/null +++ b/.agents/skills/edge-design-doc-writer/SKILL.md @@ -0,0 +1,64 @@ +--- +name: edge-design-doc-writer +description: edge_collector 详细设计与方案文档编写规范。用于新增功能、协议适配、AI 功能、边缘 agent、云端功能、前端页面或部署机制前,输出适合本仓库 docs 结构的设计文档、接口草案、数据流、验证计划和实施拆分。 +--- + +# edge_collector 设计文档编写 + +## 适用文档 + +- 功能方案设计 +- 详细设计 +- 协议适配方案 +- 本地模型部署方案 +- 云边协同方案 +- 边缘 agent 方案 +- 前端页面方案 + +## 文档落点 + +- 通用方案:`docs/` +- 本地模型:`docs/本地模型/` +- 鲁班猫专题:`docs/鲁班猫*/` +- 协议实现:`collector/docs/protocols/` +- 采集架构:`collector/docs/` + +## 推荐内容 + +```text +背景与目标 +现状与问题 +设计原则 +总体架构 +目录与配置 +接口/API +数据流/状态流 +权限与安全 +实施步骤 +验证计划 +风险与对策 +``` + +## 本项目必须考虑 + +- 边缘侧和云端职责是否清晰。 +- 是否影响 `collector` 采集稳定性。 +- 是否需要新增独立 agent 或 systemd 服务。 +- 是否会覆盖运行时动态配置。 +- 是否需要 82/97/94/87 主机验证。 +- 是否需要 `package.sh` 或 `scripts/migrate_edge.sh` 改动。 +- 是否需要协议模板、用户可见介绍和技术文档分开。 + +## 图示 + +流程或状态变化可用 ASCII 图;复杂架构图使用 `edge-svg-diagram`。 + +## 方案验证 + +文档结尾必须写: + +- 单元测试或脚本验证。 +- 构建验证。 +- 远程部署验证(如需要)。 +- 回滚或降级策略。 + diff --git a/.agents/skills/edge-design-reviewer/SKILL.md b/.agents/skills/edge-design-reviewer/SKILL.md new file mode 100644 index 0000000..7cf3d99 --- /dev/null +++ b/.agents/skills/edge-design-reviewer/SKILL.md @@ -0,0 +1,91 @@ +--- +name: edge-design-reviewer +description: edge_collector 详细设计与方案评审规范。用于评审协议适配、云平台功能、边缘 agent、AI 分析、本地模型、前端页面、部署机制等设计文档,检查结构完整性、边界、接口、配置、数据流、前端可实现性、部署影响、测试和回滚。 +--- + +# edge_collector 设计评审 + +## 评审目标 + +确认设计文档足够指导实现、测试和部署,不留下关键歧义。 + +## 结论级别 + +- `[严重]`:会导致无法实现、运行风险或数据/配置损坏。 +- `[警告]`:可实现但存在质量、可维护性或验证缺口。 +- `[建议]`:改进项,不阻塞。 + +通过标准:无 `[严重]`,关键 `[警告]` 有明确处理计划。 + +## 评审维度 + +### 1. 文档结构 + +- 背景、目标、范围、不做什么是否明确。 +- 是否有现状分析和约束。 +- 是否有实施步骤和验证计划。 +- 是否写清假设和待确认项。 + +### 2. 云边职责 + +- 边缘侧、云平台、前端、独立 agent 职责是否清晰。 +- 是否把高风险或长耗时任务放到合适进程。 +- 新增常驻进程是否独立,不耦合 `edge` 主服务。 + +### 3. 接口与配置 + +- API 路径、方法、请求、响应、错误码是否完整。 +- JSON 字段是否符合当前后端约定。 +- 配置文件路径、默认值、动态生成规则是否明确。 +- 是否会覆盖运行态动态配置。 + +### 4. 数据流与状态流 + +- 采集、缓存、上传、云端入库、展示、AI 分析链路是否完整。 +- 状态机是否覆盖成功、失败、超时、重试、停止。 +- 离线、断网、重启、服务异常是否有处理。 + +### 5. 前端可实现性 + +- 页面布局、主要状态、按钮反馈、错误提示是否明确。 +- 用户可见文案是否隐藏内部技术细节。 +- 权限、空状态、loading、长内容滚动是否覆盖。 +- 复杂页面是否需要原型或图示。 + +### 6. 部署与运维 + +- 是否影响 `package.sh`、`deploy_cloud.sh`、`scripts/migrate_edge.sh`。 +- 是否需要 `install_all.sh`,是否明确执行条件。 +- 是否需要 systemd 服务、日志路径、重启策略。 +- 是否考虑 82/97/94/87 和云服务器差异。 + +### 7. 测试与回滚 + +- 是否有单元、构建、接口、前端、设备或远程验证。 +- 是否覆盖异常场景。 +- 是否有回滚或降级策略。 +- 是否能验证“不覆盖运行配置”。 + +## 输出格式 + +```markdown +## 评审结论 + +通过 / 不通过 / 有条件通过 + +## 问题列表 + +| 级别 | 位置 | 问题 | 影响 | 建议 | +|------|------|------|------|------| + +## 待确认项 + +## 建议补充验证 +``` + +## 注意 + +- 评审先列问题,再写总结。 +- 文件和行号尽量具体。 +- 不把个人偏好当成缺陷。 +- 如果设计引用官方能力或第三方 SDK,拿不准时要联网查证。 diff --git a/.agents/skills/edge-doc-coauthoring/SKILL.md b/.agents/skills/edge-doc-coauthoring/SKILL.md new file mode 100644 index 0000000..f45323d --- /dev/null +++ b/.agents/skills/edge-doc-coauthoring/SKILL.md @@ -0,0 +1,68 @@ +--- +name: edge-doc-coauthoring +description: edge_collector 文档协作规范。用于编写或更新方案设计、部署说明、协议文档、事故分析、本地模型方案、用户手册等 docs 文档时,帮助确定读者、落点、结构、验证依据和后续实施清单。 +--- + +# edge_collector 文档协作 + +## 文档落点 + +- 协议实现:`collector/docs/protocols/` +- 协议清单:`collector/docs/协议支持清单.md` +- 边缘/云端通用方案:`docs/` +- 鲁班猫设备问题:`docs/鲁班猫1/`、`docs/鲁班猫3/` +- 本地模型方案:`docs/本地模型/` +- 部署/同步/运维:`docs/` 或 `docs/ops/` + +## 写作流程 + +1. 明确读者:开发、运维、现场用户、管理后台用户。 +2. 明确目标:评估、实施、排障、交付说明、用户操作。 +3. 收集依据:代码路径、配置文件、脚本、远程验证、官方文档链接。 +4. 写清边界:第一版做什么、不做什么、风险和前置条件。 +5. 给出可执行步骤:命令、目录、配置示例、验证方法。 + +## 推荐结构 + +技术方案: + +```text +背景与目标 +当前现状 +方案设计 +目录/配置/API +实施步骤 +验证计划 +风险与对策 +参考资料 +``` + +事故分析: + +```text +问题现象 +影响范围 +现场证据 +根因分析 +修复方案 +验证结果 +预防措施 +``` + +用户说明: + +```text +功能用途 +使用步骤 +参数解释 +常见问题 +注意事项 +``` + +## 约束 + +- 给用户看的协议介绍不透露内部技术细节。 +- 技术方案可写实现细节,但要标注假设和验证状态。 +- 引用外部信息时提供链接。 +- 不把未经验证的能力写成已完成。 + diff --git a/.agents/skills/edge-framework-learner/SKILL.md b/.agents/skills/edge-framework-learner/SKILL.md new file mode 100644 index 0000000..3944483 --- /dev/null +++ b/.agents/skills/edge-framework-learner/SKILL.md @@ -0,0 +1,63 @@ +--- +name: edge-framework-learner +description: edge_collector 框架、SDK、协议库和工具链学习沉淀规范。用于需要学习并沉淀 ONNX Runtime、RKNN、FOCAS SDK、Snap7、Drogon、React/Vite、Playwright、交叉编译工具链、AI Provider SDK 等新技术,并生成适合本仓库使用的 skill 或技术笔记。 +--- + +# edge_collector 技术学习沉淀 + +## 适用场景 + +- 用户要求“学习某框架并生成 skill”。 +- 新接入第三方 SDK、协议库、AI Provider 或模型推理框架。 +- 当前知识可能过期,需要联网查官方文档。 +- 需要把一次调研变成后续可复用的项目规则。 + +## 信息来源 + +优先级: + +1. 官方文档、官方仓库、官方示例。 +2. 当前仓库已有实现和构建脚本。 +3. 设备或 SDK 随包文档。 +4. 社区资料,仅用于补充,并标注来源。 + +涉及外部技术版本、接口或模型能力时必须联网确认,避免凭记忆。 + +## 学习输出 + +```text +技术定位 +适用版本 +当前项目使用场景 +安装与依赖 +最小可用示例 +项目集成方式 +构建/部署注意事项 +常见错误 +验证命令 +是否需要新增 skill +``` + +## 生成 skill 时 + +- 名称使用小写短横线。 +- 放到 `.agents/skills//SKILL.md`。 +- frontmatter 只保留 `name` 和 `description`。 +- 内容必须面向 `edge_collector`,不要生成通用教程。 +- 复杂资料可放 `references/`,但优先保持 SKILL.md 简洁。 +- 用 `skill-creator` 的 `quick_validate.py` 校验。 + +## 本项目集成检查 + +新增技术必须检查: + +- 是否影响 `collector` 稳定性。 +- 是否需要新增第三方库目录和架构分层。 +- 是否需要修改 `package.sh` 或 `scripts/migrate_edge.sh`。 +- 是否会引入运行配置覆盖风险。 +- 是否需要 82/97/94/87 或云平台验证。 +- 是否需要文档区分用户说明和技术细节。 + +## 输出语气 + +给用户的是选型和落地建议,不堆砌官方概念;每条建议都要说明对当前工程的影响。 diff --git a/.agents/skills/edge-frontend-design/SKILL.md b/.agents/skills/edge-frontend-design/SKILL.md new file mode 100644 index 0000000..cc7861e --- /dev/null +++ b/.agents/skills/edge-frontend-design/SKILL.md @@ -0,0 +1,84 @@ +--- +name: edge-frontend-design +description: edge_collector 前端页面设计与 UI 落地规范。用于设计或优化边缘侧 frontend/config_app、云平台 frontend/cloud_app 页面、组件、布局、交互、按钮状态、弹窗、图表、AI 分析、WiFi、端口转发、内网穿透等用户界面时,结合当前 React/Vite/CSS Modules 暗色主题输出可落地设计。 +--- + +# edge_collector 前端设计 + +## 适用前端 + +- 边缘侧:`frontend/config_app` +- 云平台:`frontend/cloud_app` + +## 设计原则 + +- 先阅读相邻页面和 CSS Modules,沿用当前视觉语言。 +- 首屏直接呈现可用工具,不做营销式 landing page。 +- 工业/运维页面要安静、清晰、密集但不拥挤。 +- 不引入新的 UI 框架。 +- 不把内部技术细节展示给最终用户。 +- 卡片、按钮、启停、危险操作样式要与已有模块一致。 + +## 视觉基线 + +后续前端设计按以下口径走: + +- 暗色底。 +- 细边框。 +- 蓝紫作为主操作色。 +- 状态色克制使用,只用于表达成功、警告、错误、运行中等明确状态。 +- 避免营销页式大渐变。 +- 避免装饰感过强的科技视觉,如大面积霓虹、发光线框、玻璃拟态、纯装饰光效。 +- 页面应像工业网关/运维工具,而不是宣传页或展示大屏。 + +现有颜色基线: + +```text +背景:#0d0d14 / #14141e +面板:#1e1e2e +边框:#2a2a3a +主文字:#e0e0e0 +标题文字:#ffffff +主操作色:#6366f1 +``` + +## 工作流 + +1. 明确目标用户:现场用户、运维、管理员、开发。 +2. 梳理核心任务:用户进页面后最需要完成什么。 +3. 阅读现有页面,提取布局、按钮、表格、弹窗和状态样式。 +4. 先给信息架构,再给具体组件布局。 +5. 覆盖加载、空状态、错误、保存中、权限不足、操作成功。 +6. 实现时遵循 `frontend-ui-conventions`、`frontend-conventions`、`frontend-dialog`。 + +## 当前项目常见布局 + +- 高级功能:模块标签页 + 左右均分列表/配置区 + 底部操作区靠右。 +- 数据查看/AI 分析:图表区域与报告区域独立,报告内容向下延展,不向上挤占图表。 +- 配置页:表单和列表并排,避免单列垂直堆叠导致右侧空白。 +- 规则列表:单条启用/停用放操作列,不使用复选框表达启停。 + +## 交互要求 + +- 点击连接、扫描、保存、分析、启停等耗时操作,按钮必须出现 loading 或局部状态反馈。 +- 危险操作必须二次确认。 +- 失败提示要说明可执行下一步,不只显示接口失败。 +- 普通按钮、危险按钮、启停按钮风格要统一。 +- 长文本和长报告必须支持滚动查看,不能遮挡上方关键内容。 + +## 文案规则 + +- 用户可见文案使用业务语言。 +- 不展示 AI Provider 名称、内部模型名、helper、SDK 路径、接口路径、堆栈、SQL。 +- 参数说明写影响和建议值。 +- 空状态告诉用户下一步操作。 + +## 设计检查 + +- 是否有大片空白。 +- 文本是否溢出。 +- 窄屏是否可用。 +- 操作后是否有反馈。 +- 图表、表格、报告是否互相遮挡。 +- 权限不足是否有清晰状态。 +- 与相邻模块按钮和标签风格是否一致。 diff --git a/.agents/skills/edge-frontend-testing/SKILL.md b/.agents/skills/edge-frontend-testing/SKILL.md new file mode 100644 index 0000000..dd4a1c2 --- /dev/null +++ b/.agents/skills/edge-frontend-testing/SKILL.md @@ -0,0 +1,104 @@ +--- +name: edge-frontend-testing +description: edge_collector 前端测试与浏览器验证规范。用于边缘侧或云平台 React/Vite 页面白屏、布局错乱、按钮无反馈、弹窗异常、图表遮挡、接口失败、构建后验证时,使用 npm build、浏览器控制台、Network、Playwright 截图和交互脚本进行验证。 +--- + +# edge_collector 前端测试 + +## 适用范围 + +- `frontend/config_app` +- `frontend/cloud_app` +- 云平台公网页面 `http://119.45.4.75` +- 边缘网关页面,如 `http://192.168.40./` + +## 验证顺序 + +1. 构建验证。 +2. 页面加载验证。 +3. 控制台错误检查。 +4. Network 接口响应检查。 +5. 关键交互点击。 +6. 布局截图和窄屏检查。 +7. 状态反馈检查。 + +## 构建命令 + +按实际目录执行: + +```bash +npm run build +``` + +如果不能构建,最终说明原因,例如缺少依赖、Node 版本不对或远程主机不可用。 + +## Playwright 验证流程 + +使用浏览器验证时: + +```text +打开目标 URL + -> wait networkidle + -> 收集 console error + -> 截图 + -> 定位关键按钮/输入框 + -> 执行操作 + -> 检查 loading/toast/dialog/network + -> 再截图 +``` + +优先使用稳定选择器: + +- 可见文本。 +- button role/name。 +- 表单 label。 +- 现有 data 属性。 +- 必要时再用 CSS selector。 + +## 重点页面检查 + +- 高级功能:WiFi、内网穿透、端口转发、硬件控制。 +- 数据查看:历史趋势、AI 分析弹窗、AI 分析报告。 +- 离线缓存:参数默认值、保存反馈、状态展示。 +- OTA:包列表、升级确认、进度和失败提示。 +- 管理后台:AI 配置、权限、启用配置唯一性。 + +## 失败提示检查 + +前端不能只显示: + +```text +failed to request cloud config +AI 服务请求失败 +操作失败 +``` + +应尽量展示后端或 agent 给出的具体原因,并转成用户可理解文案: + +- 连接失败。 +- 请求超时。 +- 权限不足。 +- 配置缺失。 +- 服务未运行。 +- 返回格式异常。 + +## 截图要求 + +复杂 UI 改动至少检查: + +- 桌面宽度。 +- 窄屏或移动宽度。 +- 长内容状态。 +- 操作中状态。 +- 错误状态。 + +最终说明截图路径或验证 URL。 + +## 回归重点 + +- 页面不白屏。 +- 无严重 console error。 +- 按钮 loading 不导致布局抖动。 +- 报告和表格区域可滚动。 +- 文案不泄露内部实现。 +- 动态配置不会因为前端保存被清空。 diff --git a/.agents/skills/edge-local-dev-services/SKILL.md b/.agents/skills/edge-local-dev-services/SKILL.md new file mode 100644 index 0000000..e132130 --- /dev/null +++ b/.agents/skills/edge-local-dev-services/SKILL.md @@ -0,0 +1,94 @@ +--- +name: edge-local-dev-services +description: edge_collector 本地开发与联调服务管理规范。用于在本机或远程开发主机启动、检查、停止 edge/cloud 前后端开发服务和依赖服务,包含 collector、configurator、cloud_server、React/Vite 前端、PostgreSQL、TDengine、Mosquitto、端口占用和日志验证。 +--- + +# edge_collector 本地开发服务 + +## 适用场景 + +- 本机启动边缘侧或云平台开发环境。 +- 前端页面需要 dev server 联调。 +- 后端接口需要本地验证。 +- E2E 前需要确认依赖服务。 +- 端口冲突、服务没起来、接口连接失败。 + +## 先读配置 + +不要假设服务和端口,优先读取: + +- `docs/project-overview.md` +- `cloud_server/config/server_config.json` +- `configurator/config/` +- `frontend/*/package.json` +- `package.sh` +- `deploy_cloud.sh` +- `scripts/install_all.sh` +- systemd service 安装脚本 + +## 常见服务 + +- 边缘:`collector`、`configurator`、`edge` systemd 服务。 +- 云端:`cloud_server`、`cloud-server` systemd 服务。 +- 前端:`frontend/config_app`、`frontend/cloud_app`。 +- 依赖:PostgreSQL、TDengine、Mosquitto。 +- 独立 agent:frpc agent、port forward agent、4G/WiFi 相关脚本。 + +## 检查流程 + +1. 查看端口占用: + +```bash +ss -lntp +``` + +2. 查看服务状态: + +```bash +systemctl status edge --no-pager +systemctl status cloud-server --no-pager +``` + +3. 查看最近日志: + +```bash +journalctl -u edge --since "10 min ago" --no-pager +journalctl -u cloud-server --since "10 min ago" --no-pager +``` + +4. 验证接口: + +```bash +curl -s http://127.0.0.1/api/status +curl -s http://127.0.0.1:8081/api/health +``` + +## 前端开发 + +进入对应目录后: + +```bash +npm install +npm run dev +npm run build +``` + +如果 Node 环境在 97/ARM64 主机异常,优先参考 `scripts/set_env/install_nvm_npm.sh` 和本地 nvm 离线安装规则。 + +## 禁止事项 + +- 不要直接连接生产库做写操作。 +- 不要随意 kill 非本次启动的进程。 +- 不要删除用户已有容器、数据库或运行配置。 +- 不要把本地端口和临时密码写死进代码。 +- 不要每次同步后都执行 `install_all.sh`,除非用户明确要求。 + +## 输出 + +最终说明: + +- 启动或检查了哪些服务。 +- 使用了哪些端口。 +- 哪些接口验证通过。 +- 日志里是否有错误。 +- 如何停止本次启动的临时服务。 diff --git a/.agents/skills/edge-markdown-docs/SKILL.md b/.agents/skills/edge-markdown-docs/SKILL.md new file mode 100644 index 0000000..2fc0df1 --- /dev/null +++ b/.agents/skills/edge-markdown-docs/SKILL.md @@ -0,0 +1,91 @@ +--- +name: edge-markdown-docs +description: edge_collector Markdown 文档编写与处理规范。用于创建、更新、拆分、合并、校对 docs、collector/docs、协议文档、部署说明、测试报告、故障报告、本地模型方案等 Markdown 文档,并维护目录、链接、图片引用和读者边界。 +--- + +# edge_collector Markdown 文档处理 + +## 适用范围 + +- `docs/` +- `collector/docs/` +- `collector/docs/protocols/` +- `docs/本地模型/` +- `docs/鲁班猫*/` +- `.agents/skills/` + +## 工作流 + +1. 先确认读者:用户、运维、开发、客户交付。 +2. 选择落点:优先更新已有文档,避免重复文档。 +3. 读取相邻文档,保持标题层级和术语一致。 +4. 写完后检查链接、图片路径、代码块语言和表格可读性。 +5. 技术文档标注假设和验证状态,用户文档隐藏内部实现细节。 + +## 推荐结构 + +技术方案: + +```text +背景与目标 +现状与问题 +方案设计 +接口/配置/目录 +实施步骤 +验证计划 +风险与回滚 +``` + +操作说明: + +```text +功能用途 +使用前准备 +操作步骤 +参数说明 +常见问题 +注意事项 +``` + +故障报告: + +```text +问题现象 +影响范围 +证据 +根因 +修复 +验证 +预防措施 +``` + +## 格式规则 + +- 标题层级从 `#` 开始,不跳级。 +- 命令使用 `bash` 代码块。 +- JSON 使用 `json` 代码块。 +- 表格列不要过宽,必要时拆成列表。 +- 图片放到文档同级 `assets/` 或专题目录下,并用相对路径引用。 +- 引用外部资料时给链接。 + +## 当前项目特别规则 + +- 协议用户介绍不要透露 helper、SDK、库路径、内部进程。 +- 部署文档要写清是否会覆盖运行配置。 +- 云平台 AI 文档不要展示真实 key。 +- 远程主机文档不要写明密码。 +- 同主机编译部署仍要使用 `scripts/migrate_edge.sh`。 + +## 校验 + +至少执行: + +```bash +rg -n "TODO|待确认|change_me|password|secret" docs collector/docs .agents/skills +``` + +按文档类型补充: + +- JSON 示例:`jq empty` +- Shell 示例:`bash -n` +- SVG 引用:浏览器或图片查看器打开检查 diff --git a/.agents/skills/edge-mcp-tools/SKILL.md b/.agents/skills/edge-mcp-tools/SKILL.md new file mode 100644 index 0000000..9883ff2 --- /dev/null +++ b/.agents/skills/edge-mcp-tools/SKILL.md @@ -0,0 +1,182 @@ +--- +name: edge-mcp-tools +description: edge_collector 边缘侧本地模型与 MCP 工具接入规范。用于在鲁班猫/RK3566/RK3576/RK3588 等边缘设备部署本地模型后,设计或实现 MCP 工具服务,让模型安全调用网关状态、设备点位、历史数据、诊断、配置查询和运维只读能力。 +--- + +# edge_collector MCP 工具接入 + +## 目标 + +让边缘侧本地模型可以通过受控工具访问网关能力,而不是直接读取任意文件、执行任意命令或绕过现有服务。 + +典型链路: + +```text +本地模型 + -> MCP Client + -> edge MCP tools + -> configurator / collector / 本地数据库 / 只读诊断命令 +``` + +## 适用场景 + +- 在 RK3566/RK3576/RK3588 鲁班猫上部署 Qwen 等本地模型。 +- 给本地模型增加“查询设备状态”“分析点位趋势”“解释报警”“读取网关状态”等工具。 +- 把边缘侧诊断能力封装成 AI 可调用工具。 +- 设计 MCP 工具权限、输入输出和安全边界。 + +## 设计原则 + +- 默认只读。 +- 工具服务独立运行,不耦合 `edge` 主服务。 +- 本地模型不直接访问数据库文件、配置文件和 shell。 +- 所有工具必须有明确输入 schema、输出 schema 和错误语义。 +- 写配置、重启服务、删除数据等高风险动作第一版不开放。 +- 工具返回用户可理解信息,不泄露密钥、路径、Token、内部模型配置。 + +## 推荐第一版工具 + +优先做只读工具: + +- `edge_get_gateway_status`:读取网关状态、版本、运行时间。 +- `edge_list_devices`:列出设备名称、协议、在线状态。 +- `edge_list_points`:列出某设备点位名称、类型、单位。 +- `edge_read_latest_values`:读取指定设备/点位最新值。 +- `edge_query_history_summary`:查询历史数据摘要,不返回超大原始数据。 +- `edge_get_alarm_summary`:读取报警或异常摘要。 +- `edge_get_network_status`:读取网络、WiFi、4G、端口转发只读状态。 +- `edge_get_service_health`:读取 `edge`、独立 agent 状态。 + +暂不开放: + +- 修改协议配置。 +- 保存 AI Key。 +- 重启服务。 +- 删除缓存或历史数据。 +- 执行任意 shell。 +- 读取任意文件。 + +## 工具命名 + +- 使用 `edge_` 前缀。 +- 动词清晰:`get`、`list`、`query`、`analyze`。 +- 避免泛化工具名,例如 `run_command`、`read_file`。 + +## 输入输出 + +输入必须限制范围: + +```text +gateway_id +device_id 或 device_name +point_id 或 point_name +time_range +limit +``` + +输出建议结构: + +```json +{ + "ok": true, + "data": {}, + "warnings": [], + "source": "configurator", + "timestamp": "2026-06-16T00:00:00+08:00" +} +``` + +错误要可行动: + +```json +{ + "ok": false, + "error_code": "DEVICE_NOT_FOUND", + "message": "未找到指定设备,请确认设备名称或 ID", + "suggestion": "可先调用 edge_list_devices 查看可用设备" +} +``` + +## 与现有服务集成 + +优先通过现有 API 或受控本地接口访问: + +- `configurator` API。 +- `collector` 状态接口或已有数据接口。 +- 本地只读数据库查询。 +- systemd 只读状态命令。 + +不要绕过业务逻辑直接修改配置文件。 + +## 本地模型注意 + +参考已有本地模型文档: + +- `docs/本地模型/Qwen2.5-0.6B-Instruct在RK3566本地部署方案.md` +- `docs/本地模型/Qwen2.5-VL-3B-Instruct在RK3576鲁班猫3边缘图文模型部署方案.md` +- `docs/本地模型/Qwen2.5-14B-Instruct在16G_RK3588鲁班猫5部署方案.md` + +设计工具时必须考虑: + +- 模型上下文有限,工具返回要摘要化。 +- RK3566/RK3576 资源有限,工具查询要分页、限流。 +- 大历史数据先聚合摘要,再按需返回异常片段。 +- 离线运行时不要依赖云端 AI Provider。 + +## 安全边界 + +结合 `edge-security-secrets`: + +- 不返回 API Key、JWT、MQTT 密码、SSH 密码。 +- 不暴露真实配置文件完整内容。 +- 不开放任意命令执行。 +- 日志中记录工具名、参数摘要、耗时、结果状态,不记录敏感值。 +- 对外接口只监听本机或受控内网,默认不暴露公网。 + +## 部署方式 + +第一版建议使用 Python 独立 agent: + +- 遵循 `edge-python-agent`。 +- 使用 systemd 独立托管。 +- 配置文件动态生成但不覆盖已有配置。 +- 打包和同步遵循 `edge-config-lifecycle`。 + +服务名建议: + +```text +edge-mcp-tools +``` + +## 验证计划 + +至少验证: + +- 工具列表可发现。 +- 每个工具 schema 正确。 +- 正常查询返回结构化数据。 +- 设备不存在、点位不存在、时间范围过大时错误可理解。 +- 返回内容脱敏。 +- 大数据查询有 limit 或摘要。 +- 服务重启后配置保留。 +- 本地模型能完成一个端到端问题,例如“分析最近 1 小时某设备是否异常”。 + +## 文档输出 + +设计 MCP 工具时输出: + +```markdown +## 工具清单 + +## 权限边界 + +## 输入输出 schema + +## 数据来源 + +## 部署方式 + +## 安全与脱敏 + +## 验证计划 +``` diff --git a/.agents/skills/edge-observability/SKILL.md b/.agents/skills/edge-observability/SKILL.md new file mode 100644 index 0000000..095ef08 --- /dev/null +++ b/.agents/skills/edge-observability/SKILL.md @@ -0,0 +1,116 @@ +--- +name: edge-observability +description: edge_collector 运行观测与资源诊断规范。用于排查边缘主机或云服务器 CPU 高、内存占用、磁盘空间、/tmp 清理、进程数量、jq/python/agent 异常、服务日志、端口监听和系统负载时,按只读证据链输出分析结论。 +--- + +# edge_collector 运行观测 + +## 适用场景 + +- CPU 占用高。 +- 内存比其他主机高。 +- `/tmp` 或工程目录占用大。 +- `jq`、Python、agent 进程很多。 +- 服务频繁重启。 +- 网关在线状态异常。 +- 前端或云端接口偶发失败。 + +## 排查顺序 + +1. 系统概况。 +2. CPU 和进程。 +3. 内存。 +4. 磁盘。 +5. systemd 服务。 +6. 应用日志。 +7. 网络端口。 +8. 与对照主机比较。 + +## 常用命令 + +系统: + +```bash +uptime +free -h +df -h +uname -a +date +``` + +CPU/进程: + +```bash +ps -eo pid,ppid,user,stat,pcpu,pmem,rss,etime,cmd --sort=-pcpu | head -30 +ps -eo pid,ppid,user,stat,pcpu,pmem,rss,etime,cmd --sort=-rss | head -30 +``` + +进程树: + +```bash +pstree -ap +``` + +磁盘: + +```bash +du -h --max-depth=1 /home/cat 2>/dev/null | sort -h +du -h --max-depth=1 /tmp 2>/dev/null | sort -h +``` + +服务: + +```bash +systemctl status edge --no-pager +journalctl -u edge --since "30 min ago" --no-pager +systemctl list-units --type=service --state=running +``` + +端口: + +```bash +ss -lntp +``` + +## 分析规则 + +- 短时尖峰和持续高占用分开判断。 +- 先找父进程,再判断是脚本循环、服务重启还是用户命令。 +- 内存分析区分 RSS、缓存和可用内存。 +- 磁盘清理只给建议,删除必须等用户确认。 +- 与 119.45.4.75 或其他主机对比时,列出相同指标。 + +## 高风险操作 + +以下操作必须用户明确同意: + +- 删除文件或目录。 +- kill 进程。 +- 重启服务。 +- 清理日志。 +- apt 安装诊断工具。 + +## 报告格式 + +```text +结论: + +证据: +- CPU: +- 内存: +- 磁盘: +- 进程: +- 日志: + +判断: + +建议: +``` + +## 当前项目常见根因 + +- shell + `jq` 高频轮询导致短时 CPU 尖峰。 +- 未插 SIM/设备缺失导致 4G 脚本重复探测。 +- 前端构建产物或代码仓库占用较大。 +- `/tmp` 离线安装包、构建缓存未清理。 +- 独立 agent 异常退出后被 systemd 频繁拉起。 diff --git a/.agents/skills/edge-pdf-docs/SKILL.md b/.agents/skills/edge-pdf-docs/SKILL.md new file mode 100644 index 0000000..d13c49a --- /dev/null +++ b/.agents/skills/edge-pdf-docs/SKILL.md @@ -0,0 +1,76 @@ +--- +name: edge-pdf-docs +description: edge_collector PDF 文档读取、提取、转换和交付检查规范。用于处理客户 PDF、导出报告、部署手册、测试报告、扫描件、表格提取、PDF 转图片预览,以及从 DOCX/Markdown 生成 PDF 交付件。 +--- + +# edge_collector PDF 处理 + +## 适用场景 + +- 阅读客户 PDF 需求、手册、协议资料。 +- 从 PDF 提取文字或表格。 +- 把 Word/Markdown 报告转 PDF。 +- 将 PDF 页面转图片用于视觉检查。 +- 合并、拆分或旋转 PDF。 + +## 文本提取 + +优先使用: + +```bash +pdftotext -layout input.pdf output.txt +``` + +需要表格时使用 `pdfplumber`: + +```python +import pdfplumber + +with pdfplumber.open("input.pdf") as pdf: + for page in pdf.pages: + print(page.extract_text()) + print(page.extract_tables()) +``` + +扫描件需要 OCR 时,先说明 OCR 可能有识别误差,并保留人工复核步骤。 + +## PDF 转图片 + +用于检查版式、截图或报告附件: + +```bash +pdftoppm -png -r 150 input.pdf page +``` + +只转指定页: + +```bash +pdftoppm -png -r 150 -f 1 -l 3 input.pdf page +``` + +## 合并与拆分 + +优先使用 `qpdf`: + +```bash +qpdf --empty --pages a.pdf b.pdf -- merged.pdf +qpdf input.pdf --pages . 1-5 -- part.pdf +``` + +## 交付检查 + +生成 PDF 后检查: + +- 页面是否缺失。 +- 中文是否乱码。 +- 表格是否截断。 +- 图片是否模糊。 +- 页眉页脚和页码是否正确。 +- 是否包含未脱敏的密钥、账号、密码、内网地址。 + +## 当前项目注意 + +- 用户手册 PDF 不写内部技术细节。 +- 故障报告 PDF 要保留证据截图和验证命令摘要。 +- 部署报告 PDF 要写清目标主机但隐藏敏感凭据。 +- AI 分析报告 PDF 不展示内部 AI Provider 和模型配置。 diff --git a/.agents/skills/edge-presentation-docs/SKILL.md b/.agents/skills/edge-presentation-docs/SKILL.md new file mode 100644 index 0000000..bc12b62 --- /dev/null +++ b/.agents/skills/edge-presentation-docs/SKILL.md @@ -0,0 +1,70 @@ +--- +name: edge-presentation-docs +description: edge_collector PPT/汇报材料编写与处理规范。用于把方案设计、部署方案、测试结果、故障复盘、AI/本地模型方案、协议适配方案整理成汇报型 PPT 或演示大纲,并可读取、检查、转换已有 .pptx。 +--- + +# edge_collector 汇报材料处理 + +## 适用场景 + +- 方案汇报。 +- 项目进展汇报。 +- 故障复盘汇报。 +- 部署上线说明。 +- 本地模型或 AI 功能方案展示。 +- 协议适配方案展示。 + +## 默认结构 + +```text +1. 背景与目标 +2. 当前现状/问题 +3. 方案总览 +4. 核心设计或流程 +5. 实施计划 +6. 验证结果 +7. 风险与对策 +8. 下一步 +``` + +## 设计口径 + +- 面向工业网关和云边协同场景,风格稳重、清晰、克制。 +- 优先用流程图、架构图、对比表,而不是大段文字。 +- 一页只表达一个核心结论。 +- 保留必要证据:截图、日志摘要、测试结果、关键指标。 +- 不展示密钥、密码、真实 Token。 + +## 内容转换 + +从 Markdown 方案转 PPT 时: + +- 每个二级标题通常对应 1 页或 1 组页。 +- 长表格改成摘要表 + 附录。 +- 命令行只保留关键命令和结果,不放完整日志。 +- 复杂架构图优先使用 `edge-svg-diagram` 生成 SVG 后嵌入。 + +## 读取 PPTX + +提取文本: + +```bash +python3 -m markitdown input.pptx > output.md +``` + +没有 `markitdown` 时,先说明无法直接提取,改用 LibreOffice 或解包 XML。 + +检查结构: + +```bash +unzip -l input.pptx | rg "ppt/slides/slide|ppt/media|ppt/theme" +``` + +## 交付检查 + +- 标题是否能单独表达结论。 +- 字体和颜色是否统一。 +- 截图是否清晰。 +- 图表文字是否不截断。 +- 每页是否有明确层级。 +- 是否隐藏内部 AI 配置、密钥和调试信息。 diff --git a/.agents/skills/edge-project-overview/SKILL.md b/.agents/skills/edge-project-overview/SKILL.md new file mode 100644 index 0000000..c3cc18d --- /dev/null +++ b/.agents/skills/edge-project-overview/SKILL.md @@ -0,0 +1,72 @@ +--- +name: edge-project-overview +description: edge_collector 项目概览维护规范。用于读取、生成或更新本仓库项目总览,沉淀边缘侧、云平台、前端、协议采集、脚本部署、远程主机、运行目录、动态配置保护和常用验证命令,帮助新任务快速建立上下文。 +--- + +# edge_collector 项目概览 + +## 何时使用 + +- 用户要求“整理项目概览”“说明当前工程结构”。 +- 新增较大功能前需要建立上下文。 +- 部署、协议、前端、云端多模块同时涉及。 +- 文档或 skill 需要引用项目约定。 + +## 建议落点 + +默认维护: + +```text +docs/project-overview.md +``` + +如果已有同类文档,优先更新已有文档,不新增重复总览。 + +## 必须覆盖 + +```text +项目定位 +模块结构 +边缘侧服务 +云平台服务 +前端应用 +协议采集架构 +运行目录 runtime/edge +配置文件与动态配置保护 +构建与打包脚本 +部署与同步脚本 +常用远程主机 +常用验证命令 +风险与注意事项 +推荐阅读路径 +``` + +## 事实来源 + +生成或更新概览时优先读取: + +- `CMakeLists.txt`、`collector/CMakeLists.txt` +- `package.sh` +- `deploy_cloud.sh` +- `scripts/migrate_edge.sh` +- `scripts/install_all.sh` +- `frontend/*/package.json` +- `configurator/config/` +- `collector/docs/` +- `.agents/skills/` + +## 环境与主机 + +概览可记录常用主机,但不要写敏感密钥: + +- 82:常用 arm64 发布构建主机。 +- 97:arm64 编译/同步验证主机。 +- 94、87:边缘运行验证主机。 +- 云平台:`119.45.4.75`。 + +## 输出要求 + +- 明确“已确认事实”和“从文件推断”。 +- 不把历史临时问题写成永久事实。 +- 动态配置保护规则要写清楚,避免打包/同步覆盖运行配置。 +- 同主机编译部署也要使用 `scripts/migrate_edge.sh` 的规范需要写入。 diff --git a/.agents/skills/edge-project-plan-writer/SKILL.md b/.agents/skills/edge-project-plan-writer/SKILL.md new file mode 100644 index 0000000..bb71831 --- /dev/null +++ b/.agents/skills/edge-project-plan-writer/SKILL.md @@ -0,0 +1,80 @@ +--- +name: edge-project-plan-writer +description: edge_collector 项目计划与实施拆分编写规范。用于为协议适配、云平台功能、边缘 agent、AI 分析、本地模型、前端页面、部署机制、故障治理等工作编写项目计划、里程碑、任务拆分、风险清单和验证排期。 +--- + +# edge_collector 项目计划编写 + +## 适用场景 + +- 新协议适配计划。 +- 云平台功能迭代计划。 +- 边缘独立 agent 实施计划。 +- AI 分析或本地模型接入计划。 +- 前端复杂页面改造计划。 +- 部署/打包/同步机制优化计划。 +- 故障治理和稳定性专项计划。 + +## 推荐结构 + +```text +目标与范围 +现状与约束 +阶段划分 +里程碑 +任务拆分 +依赖关系 +验证计划 +部署计划 +风险与缓解 +交付物 +``` + +## 阶段模板 + +```text +阶段 1:调研与方案 +阶段 2:最小可用实现 +阶段 3:联调与异常场景 +阶段 4:部署验证 +阶段 5:文档与交付 +``` + +按任务实际裁剪,不要机械套用。 + +## 任务拆分要求 + +每个任务写清: + +- 目标。 +- 涉及目录。 +- 负责人或执行对象。 +- 前置依赖。 +- 验收标准。 +- 验证命令或验证页面。 + +## 当前项目必须考虑 + +- 是否影响 `collector` 稳定性。 +- 是否影响 `runtime/edge` 或 `runtime/cloud_server` 动态配置。 +- 是否需要 82/97/94/87 或云服务器验证。 +- 是否需要修改 `package.sh`、`deploy_cloud.sh`、`scripts/migrate_edge.sh`。 +- 是否需要新增 systemd 服务或独立 agent。 +- 是否需要用户文档和技术文档分开。 + +## 风险清单 + +常见风险: + +- 跨架构第三方库不可用。 +- 目标主机系统版本差异。 +- 前端构建环境不一致。 +- 配置同步覆盖运行态文件。 +- AI Provider 超时、费用或响应格式差异。 +- 真实设备不可用导致只能 mock 验证。 + +## 输出要求 + +- 计划要能直接转成执行清单。 +- 不确定项标为“待确认”,不要伪装成已完成。 +- 时间排期必须留出联调、回归和远程部署验证。 diff --git a/.agents/skills/edge-protocol-research/SKILL.md b/.agents/skills/edge-protocol-research/SKILL.md new file mode 100644 index 0000000..c98fa4e --- /dev/null +++ b/.agents/skills/edge-protocol-research/SKILL.md @@ -0,0 +1,81 @@ +--- +name: edge-protocol-research +description: edge_collector 协议调研与适配评估规范。用于调研 FANUC、西门子 CNC、PLC、传感器、第三方 SDK 或参考仓实现时,按当前协议模板、驱动代码、参考实现、官方资料和验证计划输出适配差异、点位补充和实现建议。 +--- + +# edge_collector 协议调研 + +## 适用场景 + +- 新增协议驱动。 +- 完善 FANUC/西门子 CNC 点位。 +- 参考外部仓采集程序。 +- 判断第三方 SDK 架构和库是否可用。 +- 协议模板是否合理、是否缺常用点位。 + +## 调研顺序 + +1. 读取当前协议模板和用户可见描述。 +2. 读取当前驱动实现和文档。 +3. 对比参考仓或历史实现。 +4. 拿不准的协议语义联网查官方资料或 SDK 文档。 +5. 输出差异、风险、实施建议和验证计划。 + +## 当前项目路径 + +优先查看: + +- `configurator/config/templates/` +- `configurator/config/protocols/` +- `collector/src/driver/` +- `collector/docs/protocols/` +- `third_party/` +- `collector/CMakeLists.txt` + +## 对比重点 + +- 连接参数是否够用。 +- 点位名称、类型、单位、默认采集周期是否合理。 +- 模板点位和驱动读取逻辑是否一致。 +- 是否保留现有 `PointData::UpdateValue` 行为。 +- 用户可见协议介绍是否隐藏内部技术细节。 +- 第三方库是否按架构分层放置。 +- ARM64/ARM32/x64 构建模式是否明确。 + +## 联网规则 + +遇到以下情况必须联网查证: + +- 协议函数含义不确定。 +- SDK 架构、库名、系统依赖不确定。 +- 西门子/FANUC 指标语义不确定。 +- 第三方资料可能过期。 + +优先官方文档、SDK 手册、厂商资料;社区资料只能作为补充。 + +## 输出格式 + +```markdown +## 当前现状 + +## 参考实现差异 + +## 点位/参数建议 + +## 驱动实现建议 + +## 构建与第三方库影响 + +## 用户文档影响 + +## 验证计划 + +## 风险与待确认 +``` + +## 禁止事项 + +- 不凭猜测写协议语义。 +- 不提交未知来源二进制库。 +- 不把参考仓问题照搬进当前工程。 +- 不在用户可见协议介绍中写 helper、SDK 路径、库文件细节。 diff --git a/.agents/skills/edge-prototype-design/SKILL.md b/.agents/skills/edge-prototype-design/SKILL.md new file mode 100644 index 0000000..5f81703 --- /dev/null +++ b/.agents/skills/edge-prototype-design/SKILL.md @@ -0,0 +1,50 @@ +--- +name: edge-prototype-design +description: edge_collector 高保真原型设计规范。用于设计边缘侧或云平台前端页面、复杂交互、管理后台页面、AI 分析、WiFi、端口转发、OTA、数据趋势等功能原型时,先分析现有 React/CSS Modules 风格,再输出适合当前项目落地的原型和实现建议。 +--- + +# edge_collector 原型设计 + +## 适用范围 + +- `frontend/config_app` 边缘侧页面。 +- `frontend/cloud_app` 云平台页面。 +- AI 分析、WiFi 管理、端口转发、内网穿透、OTA、离线缓存、数据趋势等复杂页面。 + +## 工作流 + +1. 阅读现有页面和 CSS Modules,提取当前视觉语言。 +2. 明确目标用户和核心任务。 +3. 先画信息架构和布局,不急着写代码。 +4. 给出关键状态:加载、空状态、失败、保存中、禁用、权限不足。 +5. 再进入实现,遵循 `frontend-ui-conventions`。 + +## 原型输出形式 + +按任务选择: + +- 文档内 ASCII 线框:适合接口/流程方案。 +- HTML 静态原型:适合复杂页面评估。 +- React 组件草案:适合直接落地到现有前端。 +- SVG 交互说明图:适合文档配图。 + +## 本项目 UI 约束 + +- 使用 React + Vite + CSS Modules。 +- 保持现有暗色主题。 +- 暗色底、细边框、蓝紫主操作色、状态色克制使用。 +- 避免营销页式大渐变和装饰感过强的科技视觉。 +- 原型应呈现工业网关/运维工具气质,优先清晰、稳定、可操作。 +- 不引入 Ant Design、Tailwind 或新的 UI 框架。 +- 按 `frontend-dialog` 使用统一弹窗。 +- 普通操作按钮、危险按钮、启停按钮样式要与现有模块一致。 +- 页面首屏应是可用工具,不做营销式 landing page。 + +## 设计检查 + +- 右侧是否有大片空白。 +- 文本是否溢出或遮挡。 +- 操作后是否有 loading/反馈。 +- 是否支持窄屏。 +- 危险操作是否二次确认。 +- 用户可见文案是否隐藏内部实现细节。 diff --git a/.agents/skills/edge-python-agent/SKILL.md b/.agents/skills/edge-python-agent/SKILL.md new file mode 100644 index 0000000..f6a1aa7 --- /dev/null +++ b/.agents/skills/edge-python-agent/SKILL.md @@ -0,0 +1,88 @@ +--- +name: edge-python-agent +description: edge_collector Python 独立 agent 开发规范。用于新增或修改 frpc agent、port forward agent、WiFi/4G 辅助进程、巡检脚本、数据分析脚本等 Python3 常驻或命令行工具时,统一配置、日志、systemd、资源占用、退出码和与 edge 主服务解耦要求。 +--- + +# edge_collector Python Agent + +## 适用范围 + +- `scripts/frp/start_*.py` +- `scripts/port_forward/start_*.py` +- WiFi/4G 辅助脚本。 +- 远程巡检和数据分析脚本。 +- 需要 systemd 托管的 Python 常驻进程。 + +## 命名与位置 + +- 常驻启动脚本使用 `start_` 前缀,保持现有风格。 +- 按功能放到独立目录,例如 `scripts/frp/`、`scripts/port_forward/`。 +- 不要把独立 agent 代码塞进 `collector` 或 `configurator`。 + +## 配置 + +- 配置文件放到运行目录的 `config/` 或功能子目录。 +- 支持配置缺失时生成默认文件,但不得覆盖已有配置。 +- 动态配置必须被打包和同步规则排除,避免目标主机运行配置被覆盖。 +- 密钥、Token、URL 不写死在代码中。 + +## 日志 + +- 使用 Python `logging`。 +- 日志包含时间、级别、模块、关键状态。 +- 不打印密钥、密码、Token。 +- 高频循环日志要限流,避免 CPU/磁盘压力。 +- 错误日志要保留具体原因,供前端展示更明确错误。 + +## 常驻进程要求 + +- 支持优雅退出 `SIGTERM`/`SIGINT`。 +- 主循环有固定 sleep 或事件等待,禁止无休眠空转。 +- 外部命令调用设置 timeout。 +- 子进程必须回收。 +- 网络请求必须设置连接和读取超时。 +- 异常后退避重试,不要短时间无限重启。 + +## systemd + +服务文件应明确: + +```text +WorkingDirectory +ExecStart +Restart=on-failure +RestartSec +User +Environment +``` + +新增服务应独立,不替代已有 `edge`、`frpc_agent` 或其他服务,除非用户明确要求迁移。 + +## CLI + +建议支持: + +```bash +--config +--log-level +--once +--dry-run +``` + +`--once` 适合调试和安装后验证。 + +## 验证 + +至少验证: + +```bash +python3 -m py_compile scripts//.py +python3 scripts//.py --help +``` + +常驻服务验证: + +```bash +systemctl status --no-pager +journalctl -u --since "5 min ago" --no-pager +``` diff --git a/.agents/skills/edge-release-notes/SKILL.md b/.agents/skills/edge-release-notes/SKILL.md new file mode 100644 index 0000000..444b019 --- /dev/null +++ b/.agents/skills/edge-release-notes/SKILL.md @@ -0,0 +1,90 @@ +--- +name: edge-release-notes +description: edge_collector 版本说明与变更日志编写规范。用于提交、push、边缘包发布、云平台部署、OTA 发布、阶段交付时,生成面向用户、运维和开发的 release notes,区分用户可见变化、部署影响、配置影响、验证结果和回滚说明。 +--- + +# edge_collector 版本说明 + +## 适用场景 + +- 提交前整理变更。 +- push 后总结。 +- 边缘版本发布。 +- 云平台部署。 +- OTA 包说明。 +- 阶段交付说明。 + +## 读者分层 + +- 用户可见:功能变化、操作入口、体验优化。 +- 运维可见:部署步骤、配置变化、服务重启、回滚。 +- 开发可见:代码结构、接口、测试、技术债。 + +不要把内部 helper、SDK 路径、密钥、模型配置写进用户可见说明。 + +## 推荐结构 + +```markdown +## 版本信息 + +- 版本: +- 日期: +- 范围: + +## 用户可见变化 + +## 运维与部署影响 + +## 配置变化 + +## 修复问题 + +## 验证结果 + +## 已知风险 + +## 回滚说明 +``` + +## 从 Git 生成摘要 + +可参考: + +```bash +git log --oneline -10 +git diff --stat HEAD~1..HEAD +git status --short +``` + +只总结和本次发布相关内容,不把无关工作区改动写进版本说明。 + +## 边缘发布说明 + +必须说明: + +- 目标架构。 +- 是否需要执行 `install_all.sh`。 +- 是否需要重启 `edge` 或独立 agent。 +- 是否影响运行态动态配置。 +- 是否通过目标主机验证。 + +## 云平台发布说明 + +必须说明: + +- 是否执行 `deploy_cloud.sh`。 +- 是否使用 `--init`。 +- 是否影响 `ai_config.json`、`server_config.json`。 +- 是否重启 `cloud-server`、Mosquitto、nginx。 +- 公网接口验证结果。 + +## OTA 包说明 + +面向用户时写: + +- 新增能力。 +- 修复问题。 +- 升级注意事项。 +- 回滚建议。 + +避免写内部提交号、代码路径和密钥。 diff --git a/.agents/skills/edge-release-prepare/SKILL.md b/.agents/skills/edge-release-prepare/SKILL.md new file mode 100644 index 0000000..f2d5b05 --- /dev/null +++ b/.agents/skills/edge-release-prepare/SKILL.md @@ -0,0 +1,67 @@ +--- +name: edge-release-prepare +description: 边缘侧版本发布确认流程。用于用户说“发布边缘侧版本”“准备发布边缘侧版本”“发布边缘侧版本 版本号 xxx”等场景:先去 82 主机拉取最新代码,整理版本信息和版本说明,等待用户确认后才允许执行 package.sh --publish。 +--- + +# 边缘侧版本发布确认 + +## 何时使用 + +- 发布边缘侧版本 +- 准备发布边缘侧版本 +- 发布边缘侧版本 版本号 xxx +- 先整理边缘侧发布说明 + +## 固定环境 + +- 82 主机:`cat@192.168.40.82` +- 82 代码根目录:`/home/cat/code/edge_collector` +- 构建目标:`arm64` +- 默认云平台:`http://119.45.4.75` +- 默认云平台账号:`admin` + +## 第一阶段:只准备,不发布 + +用户说“发布边缘侧版本”时,先执行: + +```bash +sshpass -p 'i7568737i' ssh -o StrictHostKeyChecking=no cat@192.168.40.82 \ + 'cd /home/cat/code/edge_collector && git pull && git status --short && git log -5 --oneline' +``` + +然后根据用户输入和最新代码状态整理发布草案,至少包含: + +- 目标版本号:用户已给则使用;未给则请用户确认版本号 +- 目标架构:`arm64` +- 发布类型:用户已给则使用;未给则建议 `release` 或请用户选择 +- 产物名称:`publish/edge__arm64.tar.gz` +- 版本信息:一句话概括本次发布 +- 版本说明:多行列点,来自用户说明、最近提交、已完成改动和验证结果 +- 后续动作预览:确认后将在 82 执行 `./package.sh --publish --version `,必要时上传云平台 + +## 确认边界 + +- 在用户明确确认前,禁止执行 `./package.sh --publish --version ...` +- 在用户明确确认前,禁止上传云平台 +- 用户确认后,再按 `edge-82-release` 的发布流程执行 + +## 建议输出格式 + +```text +发布草案: +- 版本号: +- 架构:arm64 +- 发布类型:release +- 产物:publish/edge__arm64.tar.gz + +版本信息: +<一句话说明> + +版本说明: +- <说明 1> +- <说明 2> + +确认后执行: +1. 82: ./package.sh --publish --version +2. 如需上传云平台,使用上述版本说明和发布类型 +``` diff --git a/.agents/skills/edge-release-prepare/agents/openai.yaml b/.agents/skills/edge-release-prepare/agents/openai.yaml new file mode 100644 index 0000000..46465f8 --- /dev/null +++ b/.agents/skills/edge-release-prepare/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: 边缘发布确认 +short_description: 去82拉最新代码,整理版本信息和说明,确认后才发布 +default_prompt: Use this skill when the user says "发布边缘侧版本" or asks to prepare an edge-side release. First SSH to cat@192.168.40.82, cd /home/cat/code/edge_collector, run git pull, inspect git status and recent commits, then produce a release draft with version, arm64 architecture, release type, artifact name, version summary, and release notes. Do not run ./package.sh --publish or upload to the cloud until the user explicitly confirms. diff --git a/.agents/skills/edge-remote-access/SKILL.md b/.agents/skills/edge-remote-access/SKILL.md new file mode 100644 index 0000000..3a6f9df --- /dev/null +++ b/.agents/skills/edge-remote-access/SKILL.md @@ -0,0 +1,110 @@ +--- +name: edge-remote-access +description: edge_collector 远程主机访问与操作规范。用于 SSH 到 82/87/94/97 等边缘主机或云服务器执行命令、采集日志、传文件、检查端口、做临时隧道和远程排障时,约束只读优先、命令安全、敏感信息脱敏和避免误操作。 +--- + +# edge_collector 远程访问 + +## 主机解析 + +数字主机连接规则由 `host-connection-defaults` 提供: + +```text +87 -> cat@192.168.40.87 +97 -> cat@192.168.40.97 +``` + +云服务器常用: + +```text +ubuntu@119.45.4.75 +``` + +如果用户明确给出账号、IP、密码或端口,以用户本次说明为准。 + +## 操作原则 + +- 只读排查优先。 +- 多条只读命令可以合并一次 SSH 执行。 +- 写操作、重启、删除、同步、清库必须有用户明确要求。 +- 不要在最终回复中暴露密码、Token、Key。 +- 不要手写替代项目已有部署脚本。 + +## 常用只读命令 + +```bash +hostname +uptime +date +uname -a +df -h +free -h +ss -lntp +systemctl status edge --no-pager +journalctl -u edge --since "10 min ago" --no-pager +``` + +云端: + +```bash +systemctl status cloud-server --no-pager +journalctl -u cloud-server --since "10 min ago" --no-pager +systemctl status mosquitto --no-pager +``` + +## 传文件 + +优先使用项目脚本: + +- 边缘同步:`scripts/migrate_edge.sh` +- 云平台部署:`deploy_cloud.sh` + +只有用户要求临时取日志、截图或单个文件时,才使用 `scp`/`rsync`。传输前说明源路径、目标路径和是否覆盖。 + +## sudo + +使用 sudo 前先确认是否必要。常见只读 sudo: + +```bash +sudo journalctl -u edge --since "10 min ago" --no-pager +sudo systemctl status edge --no-pager +``` + +避免执行: + +```bash +sudo rm -rf +sudo systemctl restart +sudo apt install +``` + +除非用户明确要求。 + +## 端口和网络 + +检查端口: + +```bash +ss -lntp +curl -s http://127.0.0.1/api/status +curl -s http://127.0.0.1:8081/api/health +``` + +排查同网段设备时: + +```bash +ip addr +ip route +ip neigh show +arp -an | grep +``` + +## 输出要求 + +最终说明: + +- 连接的主机。 +- 执行的关键只读检查。 +- 发现的异常证据。 +- 未执行的高风险动作。 +- 建议下一步。 diff --git a/.agents/skills/edge-requirement-interview/SKILL.md b/.agents/skills/edge-requirement-interview/SKILL.md new file mode 100644 index 0000000..78f5d0c --- /dev/null +++ b/.agents/skills/edge-requirement-interview/SKILL.md @@ -0,0 +1,60 @@ +--- +name: edge-requirement-interview +description: edge_collector 需求采访规范。用于较大功能、跨模块改造、协议适配、云端 AI、边缘 agent、前端复杂页面、部署机制调整或重要文档编写前,通过少量关键问题澄清目标、范围、成功标准、约束和交付物。 +--- + +# edge_collector 需求采访 + +## 触发场景 + +- 新增协议或重构协议采集。 +- 新增边缘独立 agent 或 systemd 服务。 +- 云平台新增 AI、数据分析、设备管理能力。 +- 前端新增复杂页面或复杂交互。 +- 修改打包、同步、部署、运行目录规则。 +- 编写重要方案、详细设计或交付文档。 + +## 原则 + +- 一次只问一个关键问题。 +- 优先问影响方案方向的问题。 +- 最多 8 个问题;需求很明确时可以少问或不问。 +- 用户已经给出明确实施指令时,不用采访拖延,直接执行并在关键假设处说明。 + +## 标准问题池 + +按需要选择: + +1. 核心目标是什么,完成后用户能做什么? +2. 涉及哪些模块,哪些明确不包含? +3. 成功标准是什么,如何验证? +4. 目标用户是谁,是现场用户、运维还是开发? +5. 是否需要兼容已有配置、协议模板或运行数据? +6. 是否涉及 82/97/94/87 或云服务器部署验证? +7. 是否允许新增独立进程、配置文件或 systemd 服务? +8. 文档需要写给谁看,放到哪个目录? + +## 输出摘要 + +采访结束或信息足够时,输出: + +```markdown +## 需求摘要 + +- 目标: +- 范围: +- 不包含: +- 成功标准: +- 关键约束: +- 交付物: +- 验证方式: +- 待确认: +``` + +## 本项目特别关注 + +- 不能覆盖运行时动态配置。 +- 用户可见协议介绍不暴露内部技术细节。 +- 边缘采集稳定性优先于 UI 或辅助功能。 +- 新增常驻进程应独立,不耦合 `edge` 主服务。 +- 同步部署遵循 `scripts/migrate_edge.sh`。 diff --git a/.agents/skills/edge-security-secrets/SKILL.md b/.agents/skills/edge-security-secrets/SKILL.md new file mode 100644 index 0000000..576b284 --- /dev/null +++ b/.agents/skills/edge-security-secrets/SKILL.md @@ -0,0 +1,96 @@ +--- +name: edge-security-secrets +description: edge_collector 密钥、权限和敏感配置处理规范。用于处理 AI Key、JWT secret、MQTT dynsec、SSH 密码、数据库密码、Token、配置同步、日志脱敏、提交检查和用户可见文档时,防止泄露、覆盖运行密钥或把敏感信息提交到仓库。 +--- + +# edge_collector 敏感配置安全 + +## 敏感信息范围 + +- AI Provider API Key。 +- `jwt_secret`。 +- terminal `credential_key`。 +- MQTT dynsec 管理员和客户端密码。 +- 数据库账号密码。 +- SSH 密码和私钥。 +- Token、Cookie、Session。 +- 内网穿透访问密钥。 +- 客户设备真实敏感地址。 + +## 基本原则 + +- 不在最终回复中打印完整密钥。 +- 不把密钥写死进代码。 +- 不提交真实配置。 +- 不用打包产物覆盖远端运行密钥。 +- 日志和前端错误提示要脱敏。 +- 用户文档隐藏内部模型、Provider、Key、URL 中的敏感部分。 + +## 配置文件 + +运行态配置优先保存在目标主机 `runtime` 或服务目录下: + +- `runtime/edge/config/` +- `~/cloud_server/config/server_config.json` +- `~/cloud_server/config/ai_config.json` + +打包和同步脚本必须保护动态配置。修改以下脚本时要特别检查: + +- `package.sh` +- `deploy_cloud.sh` +- `scripts/migrate_edge.sh` +- `scripts/install_all.sh` + +## 脱敏规则 + +展示时保留前后少量字符: + +```text +sk-abc...xyz +``` + +URL 中如包含 key、token、password 参数,必须隐藏参数值。 + +日志中禁止输出: + +```text +Authorization +api_key +password +secret +token +credential_key +``` + +## 提交前检查 + +提交前建议: + +```bash +git status --short +git diff --cached +rg -n "api[_-]?key|password|secret|token|credential_key|Authorization" . +``` + +发现真实密钥时: + +1. 不提交。 +2. 改为配置文件或环境变量。 +3. 如已暴露,提醒用户轮换密钥。 + +## 云平台 AI 配置 + +- 后端配置可保存 Provider、Base URL、模型名、思考模式等。 +- 前端和报告不展示内部 Provider 名称、模型细节和 Key。 +- AI 请求失败日志可以记录错误类型和状态码,但不要记录 Key。 + +## 远程操作 + +- SSH 命令中可使用既有默认连接规则,但最终回复不要打印密码。 +- 采集远程配置时,输出前先脱敏。 +- 复制配置文件前确认是否包含密钥。 + +## 用户可见文档 + +- 协议介绍、用户手册、AI 报告、导出报告不写内部技术细节和密钥。 +- 运维文档可以写配置路径和字段含义,但示例值必须使用占位符。 diff --git a/.agents/skills/edge-skill-builder/SKILL.md b/.agents/skills/edge-skill-builder/SKILL.md new file mode 100644 index 0000000..e554a7f --- /dev/null +++ b/.agents/skills/edge-skill-builder/SKILL.md @@ -0,0 +1,81 @@ +--- +name: edge-skill-builder +description: edge_collector 技能建设规范。用于从外部仓库、已有流程、项目经验、框架/SDK 学习结果中创建或改写 .agents/skills 下的 Codex skills,要求结合当前 C++/Drogon/React/Vite/云边部署/现场主机实际情况,避免原封不动照搬无关技术栈。 +--- + +# edge_collector 技能建设 + +## 目标 + +把项目中重复出现的流程和判断沉淀为 `.agents/skills//SKILL.md`,让后续任务能稳定复用。 + +## 适用来源 + +- 当前仓库脚本,如 `deploy_cloud.sh`、`package.sh`、`scripts/migrate_edge.sh`。 +- 已完成的故障排查和现场经验。 +- 外部仓库中的通用 skill。 +- 官方文档或 SDK 调研结果。 +- 用户明确确认的长期规则。 + +## 命名规则 + +- 使用小写短横线。 +- 本项目专用优先加 `edge-` 前缀。 +- 云平台专用可用 `cloud-` 前缀。 +- 名称要表达动作或场景,例如 `edge-frontend-testing`。 + +## frontmatter + +只写: + +```yaml +--- +name: +description: <做什么 + 什么时候使用 + 当前项目关键上下文> +--- +``` + +`description` 必须包含触发词,例如“部署云平台”“前端测试”“协议适配”“同步到97”。 + +## 改写原则 + +- 先读当前仓库真实文件,再写 skill。 +- 保留流程骨架,替换成当前工程技术栈。 +- 删除 Java、Spring、K3s、Ant Design、Umi、ClickHouse 等与当前工程不匹配的固定假设,除非当前文件真实使用。 +- 不写通用教程,只写能指导本仓库工作的规则。 +- 不把临时现场处理写成永久规则。 + +## 当前项目必须体现 + +- C++ collector 和 Drogon 后端。 +- React/Vite/CSS Modules 前端。 +- `runtime/edge` 与 `runtime/cloud_server`。 +- `package.sh`、`deploy_cloud.sh`、`scripts/migrate_edge.sh`。 +- 82/97/94/87 边缘主机和云服务器 `119.45.4.75`。 +- 运行态动态配置不能被打包或同步覆盖。 +- 用户可见文档不能透露内部技术细节。 + +## 校验 + +新增或修改 skill 后执行: + +```bash +python3 /home/cloud/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/ +``` + +同时检查: + +```bash +grep -R "Java\\|Spring\\|K3s\\|Ant Design\\|Umi\\|one_person" -n .agents/skills/ || true +``` + +如果出现这些词,要确认是项目真实需要,还是外部 skill 残留。 + +## 输出 + +最终向用户说明: + +- 新增或修改了哪些 skill。 +- 每个 skill 覆盖什么场景。 +- 是否通过校验。 +- 是否只改了 skill 文件,是否未提交。 diff --git a/.agents/skills/edge-spreadsheet-docs/SKILL.md b/.agents/skills/edge-spreadsheet-docs/SKILL.md new file mode 100644 index 0000000..98887a1 --- /dev/null +++ b/.agents/skills/edge-spreadsheet-docs/SKILL.md @@ -0,0 +1,83 @@ +--- +name: edge-spreadsheet-docs +description: edge_collector 表格、CSV、XLSX 文档处理规范。用于整理点位清单、协议模板、测试用例、数据质量统计、AI 分析数据摘要、设备清单、问题回溯表和导入导出表格,支持读取、生成、校验 CSV/XLSX。 +--- + +# edge_collector 表格文档处理 + +## 适用场景 + +- 点位清单和协议模板对照。 +- 系统测试用例表。 +- 数据质量统计表。 +- 设备/网关清单。 +- 故障问题回溯表。 +- AI 分析输入/输出摘要。 +- CSV/XLSX 导入导出检查。 + +## 工具选择 + +- 简单 CSV:优先用 Python `csv` 或 `pandas`。 +- XLSX 格式和样式:使用 `openpyxl`。 +- 需要公式:使用 Excel 公式,不在 Python 中硬编码计算结果。 +- 大文件分析:分块读取,避免一次性加载导致内存过高。 + +## 表格设计 + +每张表应明确: + +- 表名。 +- 数据来源。 +- 时间范围。 +- 字段含义。 +- 单位。 +- 是否脱敏。 +- 生成时间。 + +## 当前项目常用列 + +点位/设备: + +```text +网关名称, 网关ID, 设备名称, 设备ID, 点位名称, 点位ID, 协议, 数据类型, 单位, 说明 +``` + +测试用例: + +```text +编号, 模块, 场景, 前置条件, 操作步骤, 预期结果, 实际结果, 状态, 问题记录 +``` + +数据质量: + +```text +对象, 时间范围, 原始点数, 有效点数, 分析点数, 最大间隔, 缺口数量, 重复值比例, 结论 +``` + +## 校验 + +CSV: + +```bash +python3 - <<'PY' +import csv +with open("file.csv", newline="", encoding="utf-8-sig") as f: + rows = list(csv.reader(f)) +print(len(rows), rows[0] if rows else []) +PY +``` + +XLSX: + +```python +from openpyxl import load_workbook +wb = load_workbook("file.xlsx", data_only=False) +print(wb.sheetnames) +``` + +## 注意 + +- 中文 CSV 优先使用 `utf-8-sig`,方便 Excel 打开。 +- 导出给用户的表格不要出现内部字段名、接口路径或密钥。 +- 公式表必须检查 `#REF!`、`#DIV/0!`、`#VALUE!`、`#NAME?`。 +- 修改既有模板时保留原列顺序和样式,除非用户明确要求调整。 diff --git a/.agents/skills/edge-svg-diagram/SKILL.md b/.agents/skills/edge-svg-diagram/SKILL.md new file mode 100644 index 0000000..47e2c02 --- /dev/null +++ b/.agents/skills/edge-svg-diagram/SKILL.md @@ -0,0 +1,59 @@ +--- +name: edge-svg-diagram +description: edge_collector 可编辑 SVG 图示规范。用于用户要求画架构图、部署拓扑图、协议链路图、流程图、方案配图、报告示意图时,生成可提交到 docs 的静态 SVG,并结合当前边缘/云平台/鲁班猫/协议采集场景设计。 +--- + +# edge_collector SVG 图示 + +## 适用场景 + +- 云边架构图 +- 边缘 runtime 目录结构图 +- 协议采集链路图 +- FANUC helper / proxy 架构图 +- OTA 升级流程图 +- AI 本地模型部署图 +- WiFi/4G/frpc/端口转发 agent 关系图 + +## 输出位置 + +- 文档配图优先放在对应文档旁边的子目录,例如 `docs/assets/` 或专题目录下。 +- 文件名使用清晰中文或 `snake_case`,扩展名 `.svg`。 +- Markdown 中使用相对路径引用。 + +## 设计要求 + +- SVG 必须可编辑、可 diff。 +- 使用真实项目元素命名:`collector`、`configurator`、`cloud_server`、`runtime/edge`、`scripts/migrate_edge.sh`。 +- 不使用复杂渐变和难维护滤镜。 +- 字号、间距、线条保持清晰,适合 Markdown 预览。 +- 区域超过 3 个或节点超过 8 个时,先做布局骨架,再补细节。 + +## 风格规则 + +按用途选择风格,不要混用: + +- 文档/方案/报告配图:优先浅色、打印友好,白色或近白背景,深色文字,少量蓝/绿/橙用于区分云端、边缘、设备、风险。 +- 前端原型/交互说明图:应贴近当前前端暗色风格,参考 `frontend/config_app` 和 `frontend/cloud_app` 的视觉基线: + - 背景:`#0d0d14`、`#14141e` + - 边框:`#2a2a3a` + - 主文字:`#e0e0e0` + - 标题/高亮文字:`#ffffff` + - 强调色:`#6366f1` + - 状态色按现有页面语义选择,避免一整张图只有紫蓝色 + - 避免营销页式大渐变和装饰感过强的科技视觉 +- 用户故事中的业务场景图:优先清晰、业务化,不必强行模拟前端 UI;如果故事本身是前端页面或交互改造,再使用暗色项目风格。 + +## 推荐布局 + +- 云边拓扑:左边缘、右云端,中间网络/隧道。 +- 进程架构:上层 UI/API,中层服务,底层配置/数据库/设备。 +- 部署流程:从构建主机到 runtime 到目标主机。 + +## 验证 + +- 用浏览器或图片查看工具打开 SVG。 +- 确认文字不重叠、不截断。 +- 确认中文显示正常。 +- 确认风格与用途匹配:文档图可打印,前端原型图与项目暗色主题一致。 +- 文档引用路径有效。 diff --git a/.agents/skills/edge-sync-host/SKILL.md b/.agents/skills/edge-sync-host/SKILL.md new file mode 100644 index 0000000..ad9d699 --- /dev/null +++ b/.agents/skills/edge-sync-host/SKILL.md @@ -0,0 +1,73 @@ +--- +name: edge-sync-host +description: 边缘运行目录同步流程。用于用户说“同步到87主机”“同步到85主机”“把边缘包同步到某主机”等场景,默认执行 scripts/migrate_edge.sh,并把用户说出的数字主机作为 --dst_host;87 只是示例,其他数字主机同理。 +--- + +# 边缘运行目录同步 + +## 何时使用 + +- 同步到87主机 +- 同步到 85 主机 +- 把边缘包同步到某主机 +- 将 runtime/edge 部署到目标主机 + +## 固定规则 + +- 脚本:`scripts/migrate_edge.sh` +- 默认源:82 主机的 `/home/cat/code/edge_collector/runtime/edge` +- 默认目标目录:`/home/cat/edge` +- 数字主机解析:`87` -> `192.168.40.87` +- 默认用户:`cat` +- 默认密码:`i7568737i` +- 如果用户要求“在某台主机编译,再同步到同一台主机”,也必须使用 `scripts/migrate_edge.sh` 的同步方式;不要改成手写 `scp`、`rsync` 或本机 `cp`。 +- 同主机编译部署时,显式传入相同的源和目标主机,例如在 97 编译并同步到 97: + +```bash +bash scripts/migrate_edge.sh --src_host 97 --dst_host 97 +``` + +## 执行方式 + +用户说“同步到87主机”时,在本仓库根目录执行: + +```bash +bash scripts/migrate_edge.sh --dst_host 87 +``` + +同步完成后,必须在目标主机重启边缘服务并验证状态: + +```bash +sshpass -p 'i7568737i' ssh -o StrictHostKeyChecking=no cat@192.168.40.87 \ + 'echo i7568737i | sudo -S systemctl restart edge && systemctl is-active edge' +``` + +用户说其他数字主机时,把数字替换到 `--dst_host`: + +```bash +bash scripts/migrate_edge.sh --dst_host +``` + +随后也要把重启命令中的目标地址替换为 `192.168.40.`,执行 `sudo systemctl restart edge` 并确认 `systemctl is-active edge` 返回 `active`。 + +用户说“在 97 编译,同步到 97”这类同主机编译部署时,应先在对应主机完成构建: + +```bash +sshpass -p 'i7568737i' ssh -o StrictHostKeyChecking=no cat@192.168.40.97 \ + 'cd /home/cat/code/edge_collector && git pull && ./package.sh --edge-only' +``` + +然后仍然通过迁移脚本同步,源和目标主机保持一致: + +```bash +bash scripts/migrate_edge.sh --src_host 97 --dst_host 97 +``` + +最后重启同一台目标主机的 `edge` 服务并验证状态。 + +## 注意 + +- 87 只是示例,不是固定目标。 +- 不要手写 scp/rsync 流程,优先使用 `scripts/migrate_edge.sh`。 +- 同步成功后必须重启目标主机的 `edge` 服务;不要只同步文件就结束。 +- 如果用户明确指定源主机、目标用户、目标目录或密码,以用户本次明确值为准,并透传给脚本参数。 diff --git a/.agents/skills/edge-sync-host/agents/openai.yaml b/.agents/skills/edge-sync-host/agents/openai.yaml new file mode 100644 index 0000000..1595869 --- /dev/null +++ b/.agents/skills/edge-sync-host/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: 边缘同步主机 +short_description: 使用 migrate_edge.sh 同步 runtime/edge 并重启目标 edge 服务 +default_prompt: Use this skill when the user says "同步到87主机", "同步到85主机", or asks to sync the edge runtime package to a numbered host. Run bash scripts/migrate_edge.sh --dst_host from the repo root. Treat 87 only as an example; other numbered hosts map to 192.168.40.. After sync succeeds, SSH to cat@192.168.40. with password i7568737i, run sudo systemctl restart edge, and verify systemctl is-active edge returns active. Prefer the script over hand-written scp or rsync commands. diff --git a/.agents/skills/edge-system-test-writer/SKILL.md b/.agents/skills/edge-system-test-writer/SKILL.md new file mode 100644 index 0000000..f0af846 --- /dev/null +++ b/.agents/skills/edge-system-test-writer/SKILL.md @@ -0,0 +1,89 @@ +--- +name: edge-system-test-writer +description: edge_collector 系统测试文档编写规范。用于为协议采集、边缘前端、云平台 AI 分析、WiFi/4G、内网穿透、端口转发、OTA、离线缓存、部署同步等功能编写验收测试、系统测试、测试评审清单和问题回溯记录。 +--- + +# edge_collector 系统测试编写 + +## 适用范围 + +- 协议采集:FANUC、西门子、Modbus、OPC UA、传感器等。 +- 边缘功能:WiFi、4G、内网穿透、端口转发、离线缓存、OTA、高级功能页面。 +- 云平台:历史趋势、AI 分析、设备状态、配置管理。 +- 部署:`package.sh`、`deploy_cloud.sh`、`scripts/migrate_edge.sh`、systemd 服务。 + +## 文档落点 + +- 通用测试方案:`docs/` +- 协议测试:`collector/docs/protocols/` +- 鲁班猫/设备测试:`docs/鲁班猫*/` +- 本地模型测试:`docs/本地模型/` + +## 输出结构 + +```text +测试目标 +测试范围 +测试环境 +测试数据 +前置条件 +测试场景 +测试步骤与预期结果 +异常与恢复场景 +问题记录与回溯 +通过标准 +``` + +## 测试场景要求 + +每个功能至少覆盖: + +- 正常路径。 +- 参数非法或配置缺失。 +- 网络断开、服务重启、进程异常退出。 +- 同步/打包后动态配置是否被保留。 +- 前端操作反馈、失败提示、权限控制。 +- 远程目标主机差异,如 82/97/94/87 的架构和系统环境。 + +## 协议采集专项 + +测试点包括: + +- 驱动能否按协议模板加载。 +- 连接、读取、断线重连、设备离线恢复。 +- 点位值类型是否符合 `PointData::UpdateValue` 预期。 +- 用户可见协议介绍不暴露内部实现。 +- ARM64/ARM32 helper 或第三方库场景要覆盖构建和运行验证。 + +## 前端专项 + +测试点包括: + +- 页面不白屏。 +- 按钮有 loading、成功、失败反馈。 +- 弹窗使用项目统一对话框。 +- 窄屏和长内容不遮挡、不溢出。 +- 接口失败时展示可理解错误,不只显示通用失败。 + +## 验证命令 + +按实际改动选择: + +```bash +npm run build +cmake --build build --target collector -j2 +./package.sh --edge-only +bash -n scripts/.sh +jq empty +``` + +远程同步或重启必须等用户明确要求,并遵循对应部署 skill。 + +## 问题回溯 + +测试文档应保留问题回溯表: + +```markdown +| 问题 | 影响场景 | 根因位置 | 修复提交/文件 | 回归结果 | +|------|----------|----------|----------------|----------| +``` diff --git a/.agents/skills/edge-technical-zeroing-report/SKILL.md b/.agents/skills/edge-technical-zeroing-report/SKILL.md new file mode 100644 index 0000000..da0efc2 --- /dev/null +++ b/.agents/skills/edge-technical-zeroing-report/SKILL.md @@ -0,0 +1,80 @@ +--- +name: edge-technical-zeroing-report +description: edge_collector 技术归零与现场故障报告编写规范。用于边缘网关、协议采集、云平台、网络、4G/WiFi、内网穿透、端口转发、AI 分析、部署同步等故障需要形成正式根因报告、归零报告、事故复盘或客户交付说明时使用。 +--- + +# edge_collector 技术归零报告 + +## 目标 + +把现场故障从“现象描述”整理为证据闭环: + +```text +现象 + -> 影响范围 + -> 现场证据 + -> 排查路径 + -> 根因 + -> 修复 + -> 验证 + -> 预防措施 +``` + +## 适用故障 + +- 边缘服务异常、CPU/内存/磁盘异常。 +- 云端设备离线、历史数据缺失、AI 接口失败。 +- 协议采集失败、第三方库或跨架构运行问题。 +- 4G/WiFi、frpc、端口转发等独立 agent 异常。 +- 打包同步后运行异常、动态配置被覆盖。 + +## 报告结构 + +```text +问题概述 +影响范围 +现场环境 +现象与时间线 +证据清单 +排查过程 +根因分析 +修复措施 +验证结果 +预防措施 +结论 +``` + +## 证据要求 + +优先收集: + +- `git log`、`git status`、构建主机信息。 +- `journalctl`、应用日志、浏览器 console、接口响应。 +- `systemctl status`、进程、端口、CPU、内存、磁盘。 +- 配置文件差异,但注意隐藏密钥。 +- 远程主机系统版本和架构。 + +## 根因表达 + +结论必须具体到可操作层级: + +- 不写“网络问题”,要写是哪段链路、哪个接口、什么失败。 +- 不写“部署问题”,要写是哪个脚本、哪个文件、哪个动态配置规则。 +- 不写“兼容问题”,要写构建系统、库版本、架构或符号冲突证据。 + +## 归零判定 + +只有同时满足以下条件才写“已归零”: + +- 根因有证据支撑。 +- 修复已实施。 +- 回归验证通过。 +- 已说明预防同类问题的规则或检查项。 + +否则写“暂不具备归零条件”,并列出缺失证据。 + +## 文档落点 + +- 通用事故:`docs/` +- 鲁班猫设备:`docs/鲁班猫*/` +- 协议故障:`collector/docs/protocols/` diff --git a/.agents/skills/edge-user-manual-writer/SKILL.md b/.agents/skills/edge-user-manual-writer/SKILL.md new file mode 100644 index 0000000..f40c90d --- /dev/null +++ b/.agents/skills/edge-user-manual-writer/SKILL.md @@ -0,0 +1,56 @@ +--- +name: edge-user-manual-writer +description: edge_collector 用户手册与交付说明编写规范。用于为边缘侧前端、云平台、协议配置、AI 分析、WiFi、端口转发、内网穿透、OTA、离线缓存等用户可见功能编写操作说明、培训材料、交付文档和常见问题,避免暴露内部技术细节。 +--- + +# edge_collector 用户手册编写 + +## 读者 + +- 现场实施人员。 +- 运维人员。 +- 管理后台用户。 +- 客户侧使用人员。 + +## 文档落点 + +- 用户手册:`docs/` 或对应专题目录。 +- 协议用户说明:优先与协议文档分开,用户可见介绍不能写内部实现细节。 +- 鲁班猫设备操作:`docs/鲁班猫*/`。 + +## 推荐结构 + +```text +功能用途 +适用场景 +使用前准备 +操作步骤 +参数说明 +状态说明 +常见问题 +注意事项 +``` + +## 写作规则 + +- 面向用户目标写,不按代码模块写。 +- 只写用户能看到、能操作、能验证的内容。 +- 隐藏内部模型名、AI Provider 名称、helper、进程、库路径等技术细节,除非读者是运维人员且文档明确为运维手册。 +- 参数说明要写“影响和建议值”,不要只复述字段名。 +- 错误说明要写用户下一步可以怎么处理。 + +## 当前项目常见功能口径 + +- AI 分析:说明分析深度、提示词、数据不连续的业务原因,不显示内部 AI 配置。 +- WiFi 管理:说明扫描、刷新、加入隐藏网络、已保存网络连接、自动连接。 +- 内网穿透:说明映射启停、保存配置、云端配置失败提示。 +- 端口转发:说明规则启停、监听地址、目标地址、冲突端口。 +- 离线缓存:说明最大缓存、保留天数、重传批次、重传速率的影响。 + +## 检查清单 + +- 功能名称和界面文案一致。 +- 操作步骤能被现场用户照着完成。 +- 参数默认值和当前代码/配置一致。 +- 没有泄露内部接口、密钥、模型、库路径。 +- 有失败场景和恢复建议。 diff --git a/.agents/skills/edge-user-story-reviewer/SKILL.md b/.agents/skills/edge-user-story-reviewer/SKILL.md new file mode 100644 index 0000000..a2d0d9b --- /dev/null +++ b/.agents/skills/edge-user-story-reviewer/SKILL.md @@ -0,0 +1,108 @@ +--- +name: edge-user-story-reviewer +description: edge_collector 用户故事评审规范。用于审查协议适配、边缘功能、云平台功能、AI 分析、本地模型、前端页面、部署运维等用户故事是否清晰、可测、范围合适、验收标准完整,并识别拆分建议和风险。 +--- + +# edge_collector 用户故事评审 + +## 目标 + +确认用户故事能进入方案设计或实现阶段,避免范围不清、验收不可测、实现边界混乱。 + +## 评审结论 + +- 通过:可进入设计或实现。 +- 有条件通过:小问题已列出,可同步修正。 +- 不通过:存在严重范围、验收或安全风险。 + +## 检查维度 + +### 1. 价值清晰 + +- 是否写清角色、动作、价值。 +- 是否能说明“不做有什么影响”。 +- 是否避免只写“实现某接口/改某文件”。 + +### 2. 范围合适 + +- 一个故事是否只交付一个清晰能力。 +- 是否混入多个独立功能。 +- 是否写清不包含范围。 +- 是否能在一次迭代中完成验证。 + +### 3. 验收可测 + +- 每个验收场景是否有 Given/When/Then 或等价描述。 +- 是否覆盖正常路径、异常路径、边界条件。 +- 是否写明验证方式。 +- 是否能通过页面、接口、日志、构建、远程主机或设备验证。 + +### 4. 项目约束 + +- 是否会覆盖运行时动态配置。 +- 是否需要 `install_all.sh`,是否明确触发条件。 +- 是否涉及 82/97/94/87 或云服务器验证。 +- 是否需要新增 systemd 服务或独立 agent。 +- 是否影响 `collector` 稳定性。 + +### 5. 用户可见信息 + +- 是否泄露 helper、SDK、库路径、AI Provider、模型内部配置、密钥。 +- 用户文案是否面向现场用户或运维人员。 +- 错误提示是否可理解。 + +### 6. 拆分建议 + +遇到以下情况建议拆分: + +- 一个故事包含 4 个以上主要验收场景。 +- 同时改边缘、云端、前端、部署且无法独立验证。 +- 同时包含功能开发和大规模重构。 +- 同时包含用户功能和运维自动化。 +- 协议适配同时覆盖多个设备族或多个 SDK 运行方式。 + +### 7. 业务场景图 + +- 复杂流程、云边链路、协议采集链路、部署流程、AI 分析数据流、前端多区域交互是否提供 SVG。 +- SVG 是否放在用户故事文档旁边的 `assets/` 并被 Markdown 正文引用。 +- 图中是否只表达用户、业务对象、流程、状态和结果。 +- 是否泄露 helper、SDK、库路径、AI Key、内部模型配置、接口路径或调试信息。 +- 文档/方案型故事的图是否适合 Markdown 和打印预览。 +- 前端交互型故事的图是否贴近当前暗色前端风格。 + +## 输出格式 + +```markdown +## 评审结论 + +通过 / 有条件通过 / 不通过 + +## 问题列表 + +| 级别 | 位置 | 问题 | 影响 | 建议 | +|------|------|------|------|------| + +## 拆分建议 + +## 需要补充的验收标准 + +## 业务场景图检查 + +## 风险与待确认 +``` + +## 严重问题示例 + +- 没有验收标准。 +- 验收标准无法验证。 +- 没有写不包含范围,导致明显范围膨胀。 +- 涉及部署同步但未说明运行配置保护。 +- 涉及 AI Key、密码、Token 却没有安全边界。 +- 协议适配没有真实设备或 mock 验证方案。 +- 复杂用户故事缺少业务场景 SVG,导致流程和边界无法直观看清。 + +## 与其他 skill 协作 + +- 发现需求不清:转 `edge-requirement-interview`。 +- 发现规则未沉淀:转 `edge-business-rule-extractor`。 +- 发现故事过大:建议拆分后再进入 `edge-design-doc-writer`。 diff --git a/.agents/skills/edge-user-story-writer/SKILL.md b/.agents/skills/edge-user-story-writer/SKILL.md new file mode 100644 index 0000000..3b0a325 --- /dev/null +++ b/.agents/skills/edge-user-story-writer/SKILL.md @@ -0,0 +1,151 @@ +--- +name: edge-user-story-writer +description: edge_collector 用户故事编写规范。用于将协议适配、边缘功能、云平台功能、AI 分析、本地模型、前端页面、部署运维、稳定性治理等需求整理为用户故事、验收标准、边界、不包含范围和验证方式。 +--- + +# edge_collector 用户故事编写 + +## 适用场景 + +- 协议适配:FANUC、西门子、Modbus、OPC UA 等。 +- 边缘功能:WiFi、4G、内网穿透、端口转发、离线缓存、OTA。 +- 云平台:设备管理、历史趋势、AI 分析、版本发布。 +- 本地模型和 AI Provider 接入。 +- 部署运维、远程同步、故障治理。 +- 前端复杂页面或交互改造。 + +## 编写原则 + +- 面向用户价值,不从代码模块倒推需求。 +- 保持故事小而可测,一个故事只交付一个清晰能力。 +- 写清“不包含什么”,避免范围膨胀。 +- 验收标准必须能通过接口、页面、日志、构建或远程验证证明。 +- 对用户可见能力隐藏内部实现细节。 + +## 编号建议 + +```text +US-EDGE-001 边缘运行能力 +US-CLOUD-001 云平台能力 +US-PROTO-001 协议适配 +US-AI-001 AI 分析 +US-OPS-001 部署运维 +US-UI-001 前端交互 +``` + +如果项目已有编号体系,优先沿用已有体系。 + +## 标准模板 + +```markdown +### US--: <简短标题> + +**角色**: <现场用户/运维人员/平台管理员/开发人员> +**优先级**: High/Medium/Low +**状态**: Draft/Ready/Done + +#### 1. 用户故事 + +作为 <角色>, +我希望 <完成的动作或能力>, +以便 <获得的价值或解决的问题>。 + +#### 2. 背景与问题 + +- 当前现象: +- 影响: +- 触发场景: + +#### 3. 范围 + +包含: +- + +不包含: +- + +#### 4. 业务场景图 + +> 简单配置项或单点文案修改可省略;复杂流程、云边链路、协议采集链路、部署流程、AI 分析数据流、前端多区域交互必须提供 SVG。 + +![业务场景图](./assets/<用户故事ID>-<简短标题>-业务场景图.svg) + +#### 5. 验收标准 + +场景 1:<正常路径> +- Given: +- When: +- Then: +- 验证方式: + +场景 2:<异常或边界路径> +- Given: +- When: +- Then: +- 验证方式: + +#### 6. 规则与约束 + +- + +#### 7. 相关模块 + +- 前端: +- 后端: +- 边缘: +- 云端: +- 脚本/部署: + +#### 8. 待确认 + +- [ ] +``` + +## 业务场景图规则 + +以下用户故事必须生成 SVG,并在正文引用: + +- 云边链路:边缘采集、上传、云端入库、云端展示。 +- 协议链路:设备、驱动、点位、采集结果、异常恢复。 +- 部署流程:构建主机、runtime、目标主机、服务重启、配置保护。 +- AI 分析:数据选择、降采样、提示词、AI 调用、报告展示/导出。 +- 前端复杂交互:多区域联动、弹窗流程、图表与报告、长任务状态。 +- 稳定性治理:问题发现、排查、修复、验证、预防规则。 + +可省略 SVG 的场景: + +- 单个字段默认值调整。 +- 单个按钮文案或样式调整。 +- 不涉及流程的简单配置说明。 + +SVG 生成要求: + +- 使用 `edge-svg-diagram`。 +- 放到用户故事文档同级或专题目录下的 `assets/`。 +- 文件名建议:`US---<简短标题>-业务场景图.svg`。 +- 图中只写用户、业务对象、流程、状态和结果;不写 helper、SDK、库路径、AI Key、内部模型配置。 +- 文档/方案型用户故事使用清晰、打印友好的图示风格;前端交互型用户故事可使用项目暗色 UI 风格。 + +## 当前项目常用验收方式 + +- 前端:页面操作、按钮 loading、错误提示、截图。 +- 后端:接口请求/响应、权限、配置文件。 +- 边缘:`systemctl status edge`、日志、设备采集点位。 +- 云端:`cloud-server` 状态、历史数据、AI 分析接口。 +- 部署:`package.sh`、`deploy_cloud.sh`、`scripts/migrate_edge.sh`。 +- 数据:原始点数、降采样点数、上传策略解释。 + +## 与其他 skill 协作 + +- 需求不清时先用 `edge-requirement-interview`。 +- 规则较多时用 `edge-business-rule-extractor`。 +- 复杂交互先用 `edge-prototype-design`。 +- 复杂流程或链路图用 `edge-svg-diagram`,并把 SVG 引用进用户故事。 +- 写完后用 `edge-user-story-reviewer`。 +- 后续详细设计用 `edge-design-doc-writer`。 + +## 注意 + +- 不把实现方案写成用户故事正文,可放到“相关模块”或后续详细设计。 +- 不把 helper、SDK、库路径、AI Key、内部模型配置写进用户可见故事。 +- 对部署类故事,必须写清是否会重启服务、是否影响运行配置。 diff --git a/.agents/skills/edge-webapp-testing/SKILL.md b/.agents/skills/edge-webapp-testing/SKILL.md new file mode 100644 index 0000000..a6ebc9d --- /dev/null +++ b/.agents/skills/edge-webapp-testing/SKILL.md @@ -0,0 +1,48 @@ +--- +name: edge-webapp-testing +description: edge_collector 前端 Web 测试与 Playwright 验证规范。用于边缘侧或云端前端白屏、布局错乱、交互失败、按钮无反馈、图表遮挡、页面构建后验证时,指导使用浏览器检查、截图、接口和构建验证。 +--- + +# edge_collector Web 测试 + +## 适用前端 + +- 边缘侧:`frontend/config_app` +- 云端:`frontend/cloud_app` + +## 排查顺序 + +1. 构建是否成功:`npm run build` +2. 页面是否白屏:检查控制台错误和路由。 +3. API 是否失败:检查 Network、状态码、响应体。 +4. CSS 是否遮挡/溢出:检查 DOM 和 computed style。 +5. 交互状态是否正确:按钮 loading、禁用、toast、dialog。 + +## Playwright 验证建议 + +需要浏览器验证时: + +- 先确认 dev server 或目标地址。 +- 访问用户指定 URL。 +- 截图 desktop 和必要的 mobile 宽度。 +- 检查 console error。 +- 点击关键按钮并观察 DOM/网络反馈。 + +## 本项目重点页面 + +- 边缘高级功能:WiFi、内网穿透、端口转发、硬件控制。 +- 离线缓存页面。 +- AI 分析页面。 +- OTA 升级页面。 +- 云端历史趋势和 AI 分析。 + +## 验证输出 + +最终说明要包含: + +- 访问 URL。 +- 验证的页面/操作。 +- 是否有 console error。 +- 构建命令结果。 +- 发现的问题和截图路径(如有)。 + diff --git a/.agents/skills/edge-word-docx/SKILL.md b/.agents/skills/edge-word-docx/SKILL.md new file mode 100644 index 0000000..533b758 --- /dev/null +++ b/.agents/skills/edge-word-docx/SKILL.md @@ -0,0 +1,70 @@ +--- +name: edge-word-docx +description: edge_collector Word/DOCX 文档生成、转换和格式检查规范。用于把 Markdown 方案、部署手册、测试报告、故障报告、用户手册转换为 .docx,或读取、检查、整理已有 DOCX 文档,保持中文字体、标题、表格和验证记录规范。 +--- + +# edge_collector Word/DOCX 处理 + +## 适用场景 + +- 将 `docs/*.md` 转成客户可交付 `.docx`。 +- 生成测试报告、部署手册、故障报告 Word 版。 +- 读取客户提供的 DOCX 模板或说明。 +- 检查 DOCX 中的文字、表格、图片和格式。 + +## 默认中文格式 + +| 内容 | 字体 | 字号 | 行距 | +|------|------|------|------| +| 正文 | 宋体 | 小四 12pt | 1.5 倍 | +| 表格 | 宋体 | 小四 12pt | 1.2 倍 | +| 一级标题 | 黑体 | 小三 15pt | 1.5 倍 | +| 二级标题 | 黑体 | 四号 14pt | 1.5 倍 | +| 三级标题 | 宋体 | 小四 12pt,加粗 | 1.5 倍 | + +用户提供模板时,模板优先。 + +## 生成流程 + +1. 确认源文档、输出路径、标题、是否需要封面/目录/页码。 +2. 优先从 Markdown 生成结构化 DOCX。 +3. 表格单元格显式设置中文字体和行距。 +4. 图片保留清晰度,图题和正文引用一致。 +5. 生成后解包或转换检查关键格式。 + +## 读取 DOCX + +优先: + +```bash +pandoc --track-changes=all input.docx -o output.md +``` + +需要检查图片、批注、复杂格式时,再解包查看 OOXML: + +```bash +unzip -l input.docx +unzip -p input.docx word/document.xml +``` + +## 验证 + +生成后至少检查: + +```bash +unzip -p output.docx word/styles.xml | rg "宋体|黑体|w:sz" +unzip -p output.docx word/document.xml | rg "w:line" +``` + +如果安装 LibreOffice,可转换 PDF 抽查版式: + +```bash +soffice --headless --convert-to pdf output.docx +``` + +## 注意 + +- 不要把中文正文默认成 Calibri、Arial 或微软雅黑。 +- 不要只检查文件存在,要检查格式和内容。 +- 修改客户提供的 DOCX 时,尽量保留原模板样式。 +- 涉及密钥、账号、内网地址时,交付版要脱敏。 diff --git a/.agents/skills/frontend-conventions/SKILL.md b/.agents/skills/frontend-conventions/SKILL.md new file mode 100644 index 0000000..a97d986 --- /dev/null +++ b/.agents/skills/frontend-conventions/SKILL.md @@ -0,0 +1,50 @@ +--- +name: frontend-conventions +description: 前端 UI 组件使用规范。用于本仓库前端页面开发时,统一下拉组件、只读字段展示方式,避免原生控件导致交互和样式不一致。 +--- + +# 前端组件规范 + +## 下拉控件 + +- 必须使用 `CustomSelect` +- 禁止直接写原生 `` 或是手写带放大镜图标的输入框 +- `SearchInput` 组件需统一具备清除按钮和 `onChange` 的直接值映射 +- 已集成在 `src/components/common/` 目录下 + +## 组件复用优先级 + +- 页面开发前先检查现有组件:`frontend/config_app/src/components/common/`、`frontend/cloud_app/src/components/common/` +- 已有自定义组件必须优先复用,禁止在页面中重复实现同类 UI 逻辑 +- 仅当现有组件无法满足需求时才允许新增组件,并优先沉淀到各自工程的 `src/components/common/` +- 新增或改造组件时,保持 API 向后兼容,避免一次改动引发多页面回归 + +示例: + +```jsx + +``` + +## 只读字段 + +- 禁止使用 `` 伪装只读 +- 使用 `` 或 `
` + 只读样式类 + +## 设计目标 + +- 保持交互行为一致 +- 保持视觉样式一致 +- 降低页面间重复实现 diff --git a/.agents/skills/frontend-debug/SKILL.md b/.agents/skills/frontend-debug/SKILL.md new file mode 100644 index 0000000..5f8013f --- /dev/null +++ b/.agents/skills/frontend-debug/SKILL.md @@ -0,0 +1,31 @@ +--- +name: frontend-debug +description: 前端 bug 排查规范。用于样式异常、交互异常或状态不一致问题,按 CSS、DOM、JS 的顺序定位,避免盲改逻辑代码。 +--- + +# 前端排查规范 + +## 排查顺序(必须按序) + +1. CSS 层:检查 Computed Style,确认是否被覆盖 +2. DOM 层:检查 class 是否正确挂载 +3. JS 层:确认 state/ref 与渲染流程是否正确 + +## 常见坑 + +- CSS Modules 中,复合选择器优先级高于单类选择器 +- “样式不生效”大多数是优先级和覆盖问题,不是 JS 问题 + +示例: + +```css +.table td { color: #ccc; } /* (0,1,1) */ +.valueChanged { color: #22c55e; } /* (0,1,0) -> 会被覆盖 */ +.table td.valueChanged { color: #22c55e; } /* 修复 */ +``` + +## 禁止项 + +- 未验证 CSS 前反复改 JS +- 一次性改多层(CSS/DOM/JS)导致回归难定位 +- 凭感觉改,不看 DevTools 实际结果 diff --git a/.agents/skills/frontend-dialog/SKILL.md b/.agents/skills/frontend-dialog/SKILL.md new file mode 100644 index 0000000..29172de --- /dev/null +++ b/.agents/skills/frontend-dialog/SKILL.md @@ -0,0 +1,29 @@ +--- +name: frontend-dialog +description: 前端弹窗规范。用于需要确认、提醒、提示输入的交互场景,统一使用 GlobalDialog(或项目统一对话框 Hook),禁止浏览器原生弹窗。 +--- + +# 弹窗规范 + +## 核心规则 + +- 禁止使用 `window.alert` / `window.confirm` / `window.prompt` +- 必须使用项目统一弹窗能力(`GlobalDialog` 或统一 `useDialog`) + +## 使用要求 + +- 确认型操作使用异步确认 API(返回 `Promise`) +- 错误提示使用统一 `alert/toast` 能力 +- 成功反馈使用统一 `toast`,避免散落样式 + +## 最小流程 + +1. 在应用根节点挂载 `DialogProvider` +2. 页面内通过 Hook 调 `confirm/alert/toast` +3. 危险操作必须先 `await confirm(...)` 再执行 + +## 设计目标 + +- 统一视觉风格与交互行为 +- 避免原生弹窗破坏主题与体验 +- 降低重复实现和状态不一致 diff --git a/.agents/skills/frontend-ui-conventions/SKILL.md b/.agents/skills/frontend-ui-conventions/SKILL.md new file mode 100644 index 0000000..873d509 --- /dev/null +++ b/.agents/skills/frontend-ui-conventions/SKILL.md @@ -0,0 +1,47 @@ +--- +name: frontend-ui-conventions +description: 前端 UI 通用规范。用于本仓库 React 前端开发时,统一技术栈、暗色主题、CSS Modules、对话框与 API 层约束。 +--- + +# 前端通用规范 + +## 技术栈约束 + +- React + Vite +- 路由:React Router +- 状态:Zustand(如现有模块已采用) +- 样式:CSS Modules +- 网络:原生 `fetch`(统一走 `utils/api.js`) + +## 主题与视觉 + +- 保持暗色主题一致,不引入突兀浅色块 +- 颜色、边框、文字层级沿用现有页面设计 +- 新页面优先复用已有组件和样式变量 + +## 组件复用规则 + +- 改页面前先检索现有自定义组件,优先复用,不重复造轮子 +- 公共可复用组件统一放 `src/components/common/`,页面私有组件再放业务目录 +- 若必须新增组件,先评估是否可扩展已有组件(例如通过 `props`/插槽)而不是复制一份新实现 +- `frontend/config_app` 与 `frontend/cloud_app` 各自维护本工程公共组件目录,但复用策略保持一致 + +## 交互规则 + +- 对话框统一走 `frontend-dialog` 规范 +- 只读展示、下拉控件统一走 `frontend-conventions` 规范 +- 需要确认的危险操作必须二次确认 + +## API 约束 + +- 统一封装在 `utils/api.js` +- 请求/响应字段统一 `snake_case` +- 错误提示统一入口,不在页面散写不同风格处理 + +## 文件组织 + +- 页面放 `pages/` +- 组件放 `components/` +- 公共组件放 `components/common/` +- Hook 与状态放 `hooks/` +- 工具与 API 放 `utils/` diff --git a/.agents/skills/git-commit/SKILL.md b/.agents/skills/git-commit/SKILL.md new file mode 100644 index 0000000..8ed7050 --- /dev/null +++ b/.agents/skills/git-commit/SKILL.md @@ -0,0 +1,36 @@ +--- +name: git-commit +description: Git 提交规范。用于需要提交代码时,统一中文提交信息格式、禁用表情符号、禁止未授权自动提交。 +--- + +# Git 提交规范 + +## 提交格式 + +```text +<类型>: <简要描述> + +<详细说明(可选)> +``` + +## 类型(中文) + +- 功能 +- 修复 +- 重构 +- 优化 +- 样式 +- 文档 +- 测试 +- 构建 +- 清理 + +## 规则 + +- 提交信息全中文 +- 禁止使用 emoji +- 简要描述尽量 <= 50 字 +- 详细说明用 `- ` 列点 +- 默认独立提交;仅在用户明确要求时 `--amend` +- 禁止未授权自动执行 `git commit` +- 如需提交命令,使用:`git -c commit.gpgsign=false commit ...` diff --git a/.agents/skills/host-connection-defaults/SKILL.md b/.agents/skills/host-connection-defaults/SKILL.md new file mode 100644 index 0000000..8323542 --- /dev/null +++ b/.agents/skills/host-connection-defaults/SKILL.md @@ -0,0 +1,26 @@ +--- +name: host-connection-defaults +description: 主机连接默认规则。用于用户说“连接99主机”“连接 85 主机”“登录117”“ssh到其他数字主机”等数字主机连接请求时,默认使用 cat 用户连接 192.168.40 加数字主机号,密码 i7568737i。 +--- + +# 主机连接默认规则 + +当用户要求连接某个数字主机时,例如“连接99主机”“连接 85 主机”“登录117”“ssh 到 192”,默认解析为: + +```text +用户: cat +地址: 192.168.40.<数字> +密码: i7568737i +``` + +示例: + +- “连接99主机” -> `cat@192.168.40.99` +- “连接117主机” -> `cat@192.168.40.117` + +## 执行规则 + +- 如需运行命令,默认使用 `sshpass -p 'i7568737i' ssh -o StrictHostKeyChecking=no cat@192.168.40.<数字> ''`。 +- 如用户只要求连接或排查连接,优先执行无破坏的只读命令,如 `hostname`、`uptime`、`ip addr`。 +- 不要把“其他数字主机”固定成 99;数字以用户本次说出的主机号为准。 +- 如果用户明确给出不同用户名、IP 或密码,以用户本次明确值为准。 diff --git a/.agents/skills/ota-e2e-release/SKILL.md b/.agents/skills/ota-e2e-release/SKILL.md new file mode 100644 index 0000000..d334aba --- /dev/null +++ b/.agents/skills/ota-e2e-release/SKILL.md @@ -0,0 +1,51 @@ +--- +name: ota-e2e-release +description: OTA升级端到端测试。用于用户说“ota升级端到端测试 新版本号xxx”“去87测试OTA”“发布并上传后去87升级”等场景,默认走 82 打包发布、云平台上传、87 前端OTA验证的一整套流程。 +--- + +# OTA 升级端到端测试 + +## 触发词 + +- `ota升级端到端测试` +- `ota升级端到端测试 新版本号 xxx` +- `发布并上传后去87测试OTA` +- `去87测试OTA` + +## 固定约定 + +- 82 主机:`cat@192.168.40.82` +- 82 代码根目录:`/home/cat/code/edge_collector` +- 云平台账号:`admin` +- 目标版本由用户在“新版本号 xxx”里指定 +- 如果本次包含云平台前端或 `cloud_server` 代码改动,先使用 `cloud-deploy-verify` 流程部署云平台并验证关键接口 + +## 执行流程 + +0. 可选:部署云平台 + - 仅当本次改动影响云平台前端、云端后端、OTA 包上传/查询接口时执行 + - 在仓库根目录运行 `./deploy_cloud.sh` + - 确认 `cloud-server` 运行,并验证相关云端接口 + +1. 82 主机发布新版本 + - `cd /home/cat/code/edge_collector` + - `git pull` + - `./package.sh --publish --version ` + +2. 上传到云平台 + - 使用云平台 `admin` 账号登录 + - 上传 `publish/edge__arm64.tar.gz` + - 确认版本号、架构、发布状态与产物一致 + +3. 87 主机 OTA 验证 + - 打开边缘侧前端 + - 查询云平台可用版本,确认 `` 可见 + - 通过前端接口触发 OTA 升级 + - 检查 `/api/ota/status`,确认当前版本已变成 `` + +## 失败时优先排查 + +- 云端字段或页面不生效:先确认已走 `deploy_cloud.sh`,再查 `cloud-server` 状态和云端接口响应 +- 87 上看不到版本:先确认 82 包已上传成功,再查 87 的 OTA 配置 +- 87 OTA 起不来:先执行 `sudo bash /home/cat/edge/scripts/ota/install_ota_service.sh` +- 前端升级失败:先看后端 OTA 接口返回,再看服务日志 diff --git a/.agents/skills/ota-e2e-release/agents/openai.yaml b/.agents/skills/ota-e2e-release/agents/openai.yaml new file mode 100644 index 0000000..f831197 --- /dev/null +++ b/.agents/skills/ota-e2e-release/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: OTA端到端测试 +short_description: 云端可选部署、82打包发布、云平台上传、87 OTA验证 +default_prompt: Use this skill when the user says "ota升级端到端测试 新版本号 xxx" or asks to run the full release-to-87 OTA validation flow. If the current changes affect cloud_app, cloud_server, or OTA cloud upload/query APIs, first use ./deploy_cloud.sh and verify cloud-server plus relevant cloud APIs. Then go to 82 at /home/cat/code/edge_collector, git pull first, run ./package.sh --publish --version , upload publish/edge__arm64.tar.gz to the cloud admin account, validate OTA from the 87-side frontend, and verify the current version becomes . diff --git a/.agents/skills/project-structure/SKILL.md b/.agents/skills/project-structure/SKILL.md new file mode 100644 index 0000000..af9fa57 --- /dev/null +++ b/.agents/skills/project-structure/SKILL.md @@ -0,0 +1,53 @@ +--- +name: project-structure +description: 工程目录与构建规范。用于本仓库跨模块改动时,保证边缘端与云端目录职责清晰、构建产物结构一致、部署方式一致。 +--- + +# 工程结构规范 + +## 顶层职责 + +- `collector/`:边缘采集进程 +- `configurator/`:边缘配置服务 +- `cloud_server/`:云端服务 +- `frontend/config_app/`:边缘前端 +- `frontend/cloud_app/`:云端前端 +- `foundation/`:共享 C++ 基础库 +- `docs/`:通用技术文档 + +## 构建与产物 + +- 各后端模块使用 `build/` 作为构建目录 +- 打包输出到 `runtime/` +- 原则:`build/` 与对应 `runtime/` 目录结构保持一致,避免运行时路径偏差 + +## 脚本职责 + +- `build.sh`:编译 + 前端构建 + 资源同步 +- `run.sh`:构建后启动(必要时先停旧进程) +- `package.sh`:整体打包到 `runtime/` +- `run_collector_tests.sh`:采集端 collector 测试入口(unit / ci / all) +- `run_configurator_tests.sh`:配置端 configurator 测试入口(unit / ci / all) +- `run_cloud_tests.sh`:云端 cloud_server 测试入口(unit / 集成 / 压测 / 长稳) + +## 部署约束 + +- 开发环境:模块独立运行、独立调试 +- 生产环境:使用打包产物 + systemd 管理 +- 不引入额外“总控进程”替代现有部署方式 + +## 命名约定 + +- 目录:`snake_case` +- 前端组件目录:`PascalCase` +- 文档:业务文档可中文命名,标准文件按通用约定 + +## 测试约定 + +- 每个后端子工程有独立的 `run_<子工程>_tests.sh` 脚本入口 +- 各后端子工程的单元测试放在 `子工程/tests/unit/` +- 子工程相关的集成测试/压测/E2E 放在 `子工程/tests/{integration,benchmark,e2e}/` +- GTest 公共基础设施位于 `foundation/cmake/EdgeCollectorTesting.cmake` +- 测试概览文档位于 `docs/testing.md` +- 详细测试文档位于各子工程 `tests/` 目录下 + diff --git a/.agents/skills/protocol-e2e-testing/SKILL.md b/.agents/skills/protocol-e2e-testing/SKILL.md new file mode 100644 index 0000000..8b9c99b --- /dev/null +++ b/.agents/skills/protocol-e2e-testing/SKILL.md @@ -0,0 +1,22 @@ +--- +name: protocol-e2e-testing +description: 协议端到端 (E2E) 测试规范。用于指导编写和修改协议 E2E 测试框架、Mock Server 以及测试报告生成器。 +--- + +# 协议 E2E 测试规范 + +## 测试报告原则:展示真实原始数据 + +在协议的 E2E 测试报告中,**必须展示正确的、真实的原始网络报文数据(Raw Packet),而不是仅展示经过业务逻辑加工或提取后的字段**。 + +- **原始报文格式**:在记录 Mock Server 下发或接收的数据时,需要直接呈现抓包层面的完整原始报文。根据协议的实际类型选择合适的呈现方式(对于二进制协议,建议使用十六进制 Hex 格式;对于基于文本的协议,如原生支持 JSON/XML 的协议,则直接保留其原始文本或字符串形态),确保展示的是网络线缆上传输的真实数据。 +- **附加结构说明**:在原始报文下方,应当简要附上该协议报文结构的文字说明,以帮助阅读者对照报文内容(例如说明报文头部、指令码、负载、校验码的具体位置,或 JSON 协议的根节点结构)。 +- **禁止二次加工**:切忌在面向用户的测试日志/测试报告的 Mock Server 响应示例中,仅打印出协议负载内部提取出来的单一业务字段(如只打印解析后的电压值)。必须保留完整的通信底层原始数据,方便直观验证通信协议。 + +## 测试数据对比原则 + +- 驱动解析和抛出的业务层数据(Probe 采集的 JSON/格式化数据)应当与 Mock Server 预期发送的业务数据在内部对比工具(如 Data Comparator)中进行数值对比和断言。 +- 最终生成的 Markdown 测试报告中: + 1. 必须包含 **Mock Server 原始响应报文示例**(呈现其 Raw Packet 形态)。 + 2. 包含具体的比对结果(精确匹配、容差范围等)。 + 3. 可保留 Probe 采集输出的格式化首帧作为对比参考,但不可用其替代 Mock Server 的原始报文。 diff --git a/.agents/skills/shell-scripting/SKILL.md b/.agents/skills/shell-scripting/SKILL.md new file mode 100644 index 0000000..423c25a --- /dev/null +++ b/.agents/skills/shell-scripting/SKILL.md @@ -0,0 +1,38 @@ +--- +name: shell-scripting +description: Shell 脚本规范。用于本仓库脚本新增或修改时,保证路径安全、错误处理健壮、run.sh 行为一致。 +--- + +# Shell 脚本规范 + +## 基础安全 + +- 脚本头使用:`#!/bin/bash` +- 默认开启:`set -euo pipefail` +- 错误输出到 `stderr` + +## 路径规则 + +- 路径从脚本自身位置推导: + +```bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +``` + +- 禁止依赖执行时 `pwd` +- 禁止硬编码脆弱相对路径(如裸 `../build`) +- 变量引用统一加双引号 + +## run.sh 约束 + +- 标准流程:构建 -> 停旧进程 -> 启动新进程 +- 杀进程优先 `pgrep -x ` 精确匹配 +- 禁止默认使用 `pgrep -f` 误杀 +- `kill` 后短暂等待再启动,避免端口未释放 + +## 编写习惯 + +- 变量名使用大写下划线(如 `BUILD_DIR`) +- 函数内部变量用 `local` +- 新脚本创建后补 `chmod +x` diff --git a/.agents/skills/third-party-libs/SKILL.md b/.agents/skills/third-party-libs/SKILL.md new file mode 100644 index 0000000..f7d176b --- /dev/null +++ b/.agents/skills/third-party-libs/SKILL.md @@ -0,0 +1,136 @@ +--- +name: third-party-libs +description: 第三方编译库管理规范。用于新增、修改或引用 third_party 下需编译的第三方库时,统一目录结构、架构分层与 CMake 链接方式。 +--- + +# 第三方编译库管理规范 + +## 适用范围 + +`third_party/` 目录下所有**需要编译**的 C/C++ 第三方库。 +纯头文件库(如 `nlohmann`、`spdlog`)不受此规范约束。 + +## 编译架构原则 + +- 默认只编译、整理**当前运行机器架构**的库或工具文件。 +- 禁止在未被明确要求时自动交叉编译其他架构产物。 +- 需要 arm64/x64 等非当前架构产物时,必须由用户明确要求或提供已编译产物,再按对应架构目录放置。 +- 同一次任务中不要为了“完整性”主动补齐所有架构;以当前部署目标为准。 + +## 目录结构 + +每个需要编译的第三方库拆分为两个目录: + +``` +third_party/ +├── <库名>/ # 编译产物(头文件 + 静态/动态库) +│ ├── include/ # 公开头文件 +│ └── libs/ # 编译后的库文件,按架构分层 +│ ├── x64/ +│ ├── arm32/ +│ └── arm64/ +└── <库名>_repo/ # 源码仓库(带 _repo 后缀标识) +``` + +### 示例 + +``` +third_party/ +├── fwlib/ # Fanuc SDK 编译产物 +│ ├── include/ +│ └── libs/{x64,arm32,arm64}/ +├── fwlib_repo/ # Fanuc SDK 源码 +├── lib60870/ # IEC 60870 编译产物 +│ ├── include/ +│ └── libs/{x64,arm32,arm64}/ +├── lib60870_repo/ # IEC 60870 源码 +├── libplctag/ # CIP/EtherNet/IP 编译产物 +│ ├── include/ +│ └── libs/{x64,...}/ +├── paho-mqtt/ # MQTT 编译产物 +│ ├── include/ +│ └── libs/{x64,...}/ +├── nlohmann/ # 纯头文件库(不受此规范约束) +└── spdlog/ # 纯头文件库(不受此规范约束) +``` + +## 命名规则 + +| 目录 | 用途 | 示例 | +|------|------|------| +| `<库名>/` | 编译产物(include + libs) | `fwlib/`、`libplctag/` | +| `<库名>_repo/` | 源码仓库,用 `_repo` 后缀区分 | `fwlib_repo/`、`lib60870_repo/` | + +## 架构标识 + +库文件必须放在 `libs//` 子目录下,`` 取值: + +| 架构标识 | 对应处理器 | +|----------|-----------| +| `x64` | x86_64 | +| `arm32` | armv7l / arm | +| `arm64` | aarch64 / arm64 | + +## CMake 链接规范 + +### 架构检测(统一写法) + +```cmake +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7.*|arm)$") + set(TARGET_ARCH "arm32") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(TARGET_ARCH "arm64") +else() + set(TARGET_ARCH "x64") +endif() +``` + +### 引用编译产物 + +```cmake +set(XXX_DIR "${REPO_ROOT}/third_party/<库名>") + +# 头文件 +target_include_directories(target PRIVATE ${XXX_DIR}/include) + +# 链接库(使用 TARGET_ARCH 定位架构) +target_link_libraries(target ${XXX_DIR}/libs/${TARGET_ARCH}/libxxx.a) + +# 或通过 link_directories +target_link_directories(target PRIVATE ${XXX_DIR}/libs/${TARGET_ARCH}) +target_link_libraries(target xxx) +``` + +### 条件编译(可选库) + +对于非必须的协议库,使用 `EXISTS` 检测并控制编译: + +```cmake +set(XXX_DIR "${REPO_ROOT}/third_party/<库名>") +if(EXISTS "${XXX_DIR}/libs/${TARGET_ARCH}/libxxx.a") + target_include_directories(target PRIVATE ${XXX_DIR}/include) + target_link_libraries(target ${XXX_DIR}/libs/${TARGET_ARCH}/libxxx.a) + target_compile_definitions(target PRIVATE HAS_XXX=1) + message(STATUS "<库名> found — XXX driver enabled") +else() + get_target_property(_sources target SOURCES) + list(FILTER _sources EXCLUDE REGEX ".*driver/xxx/.*") + set_target_properties(target PROPERTIES SOURCES "${_sources}") + message(STATUS "<库名> NOT found — XXX driver disabled") +endif() +``` + +## 禁止事项 + +- **禁止** 将编译后的库文件直接放在 `lib/` 而不分架构 +- **禁止** 在 CMake 中硬编码 `lib/` 路径,必须使用 `libs/${TARGET_ARCH}/` +- **禁止** 将源码和编译产物混放在同一目录 +- **禁止** 将 `third_party` 改名为 `third_partys`(`third_party` 是业界标准命名) + +## 新增第三方库流程 + +1. 将源码克隆到 `third_party/<库名>_repo/` +2. 编译出目标架构的库文件 +3. 创建 `third_party/<库名>/include/`,放入公开头文件 +4. 创建 `third_party/<库名>/libs//`,放入编译产物 +5. 在 CMakeLists.txt 中按上述规范引用 diff --git a/.agents/workflows/git-push.md b/.agents/workflows/git-push.md new file mode 100644 index 0000000..eb87840 --- /dev/null +++ b/.agents/workflows/git-push.md @@ -0,0 +1,29 @@ +--- +description: 提交代码并推送 - 只输出命令,不执行 +--- + +# Git 提交推送 + +当用户要求提交代码并 push 时,**只输出具体的 shell 命令**,由用户自行执行。 + +## 规则 + +1. **禁止调用 `run_command` 执行 git 命令**(git add / commit / push),因为 git 命令容易卡住 +2. 只在回复中给出可直接复制粘贴的 shell 命令块 +3. 提交信息遵循 `git-commit` skill 中的中文提交规范 +4. 提交前先清除可能的锁文件 + +## 命令模板 + +```bash +# 清除锁文件 +rm -f /home/cloud/code/edge_collector/.git/index.lock + +# 提交并推送 +cd /home/cloud/code/edge_collector +git add -A && git -c commit.gpgsign=false commit -m "<类型>: <描述> + +- <改动详情1> +- <改动详情2>" +git push +``` diff --git a/.claude/skills/analyze-questions/SKILL.md b/.claude/skills/analyze-questions/SKILL.md new file mode 100644 index 0000000..d716bfc --- /dev/null +++ b/.claude/skills/analyze-questions/SKILL.md @@ -0,0 +1,31 @@ +--- +name: analyze-questions +description: 问题分析规范。用于当用户使用疑问句提问时,约束模型行为:优先分析问题并给出解答或排查思路,不要直接进行自动解决或修改代码。 +--- + +# 问题分析规范 + +当用户的提问是疑问句(例如:"为什么..."、"是什么..."、"怎么排查..."、"这个命令的作用是?"等)时,这通常意味着用户正在进行探索、学习、调试或排查,他们更需要理解现象和背后的原理,而不是立刻执行修改。 + +为了提供更好的交互体验,请严格遵守以下行为准则: + +## 1. 优先只读与收集信息 + +- 可以并且鼓励使用**只读工具**(如 `view_file`, `grep_search`, `list_dir`,以及通过 `run_command` 执行 `cat`、`ls`、查看日志或状态的命令)来获取上下文和现场信息。 +- 在收集到充分信息后,进行逻辑分析。 + +## 2. 深入分析与耐心解答 + +- 基于收集到的信息,直接回答用户的疑问,指出问题所在、原因、或者代码/命令的实际作用。 +- 保证解答的逻辑清晰,让用户能够理解你的推导过程。 + +## 3. 严禁自动修改或“自作主张” + +- **绝对不要**直接调用修改文件的工具(如 `write_to_file`, `multi_replace_file_content`, `replace_file_content`)。 +- **绝对不要**通过 `run_command` 自动执行具有副作用的命令(如安装依赖、修改配置、重启服务、删除文件等)。 +- 即使你已经找到了问题的解决方案,也不要“自作主张”地直接实施。 + +## 4. 提供建议并等待确认 + +- 在解答完疑问后,你可以提供解决该问题的候选方案或建议(例如:“如果您希望修复此问题,我建议修改文件 X 的 Y 行”或“您可以执行以下命令:...”)。 +- 最后,必须将控制权交还给用户,询问用户:“您希望我帮您进行这些修改吗?”或者“是否需要我帮您执行?”。只有在用户给予明确的指令(如“好的”、“修改吧”、“执行”)后,才能进行实际的操作。 diff --git a/.claude/skills/backend-conventions/SKILL.md b/.claude/skills/backend-conventions/SKILL.md new file mode 100644 index 0000000..64f1ade --- /dev/null +++ b/.claude/skills/backend-conventions/SKILL.md @@ -0,0 +1,84 @@ +--- +name: backend-conventions +description: 后端 C++ 规范。用于修改本仓库后端接口、Controller、Manager、配置文件与接口文档时,统一 JSON 处理、字段命名、响应结构与文档同步要求。 +--- + +# 后端规范 + +## JSON 规则 + +- 统一使用 `nlohmann/json` +- 在实现文件中统一写:`using json = nlohmann::json;` +- 禁止新增 `jsoncpp` 依赖 +- 解析请求体时必须处理非法 JSON 分支 + +## Drogon 响应构建 + +- 优先复用 `ResponseUtil`,避免每个接口自行拼装响应 +- 明确设置 `Content-Type: application/json` +- 可预期失败走业务错误响应,不直接向前端暴露底层异常细节 + +示例: + +```cpp +auto resp = HttpResponse::newHttpResponse(); +resp->setContentTypeCode(CT_APPLICATION_JSON); +resp->setStatusCode(k200OK); +resp->setBody(ResponseUtil::GenerateSuccessResponse(data).dump()); +callback(resp); +``` + +## 字段命名 + +- 请求体、响应体、配置文件统一 `snake_case` +- 禁止在 JSON 中混用 `camelCase` + +## 响应约定 + +- 响应结构和状态码语义以当前模块现状为准(不要凭空定义新格式) +- 错误码与错误文案保持稳定,避免前后端契约漂移 + +## 日志与错误信息 + +- 先复用同模块既有日志前缀和措辞 +- 对外错误信息默认中文(除非该接口已约定英文) +- 不要无故把已有中文日志改成英文 + +## 文档同步 + +- 改后端接口时,必须同步更新该模块 `docs/接口文档.md` +- 若前端有 API 封装,同提交同步更新封装层 +- 新增接口时补齐:路径、方法、参数、成功/失败示例 +- **每次新增或修改协议时,必须同步更新 `collector/docs/协议支持清单.md` 文档** +- **每次新增协议或修改协议细节时,必须在 `collector/docs/protocols/` 下新增或更新对应的协议实现文档,并且文档内强制要求写入底层依赖库来源及其具体安装/编译方式** + +## 协议开发规范 + +- **新增协议驱动时,严禁在应用层类中直接调用底层的原生 API(如原生 Socket API、原生串口操作函数等),除非有特殊需求需要和我确认。** +- **必须注入并使用通用的公共类进行通信,例如:** + - TCP 连接使用 `TcpTransport` + - UDP 连接使用 `UdpTransport` + - 串口连接使用 `SerialTransport` + - 这些类均应继承自核心抽象接口 `ITransport`。 + +## 协议配置规范 + +> 详细规则参见 `collector/docs/协议支持清单.md` 的"更新规范"章节。 + +### protocol_name + +- 全大写 + 下划线:`MODBUS_TCP`、`FINS_TCP` +- 同一协议不同连接方式**拆分为独立条目**,后缀:`_TCP`/`_RTU`/`_SERIAL`/`_OVERTCP` +- 驱动注册标识必须与 `protocol_name` 完全一致 + +### brand + +- 有品牌协议:使用品牌原名,不附加连接方式或描述 +- 通用协议(Modbus/OPC UA):使用标准协议名 +- 无品牌行业标准:使用**应用领域**(如 `"电力仪表"`, `"水气仪表"`),避免与 protocol_name 重复 +- ✅ `"西门子"`, `"电力仪表"` / ❌ `"哈斯串口"`, `"IEC104"`(与 protocol_name 冗余) + +### connection_type + +- 每个协议条目只允许一种 `connection_type`(`"ethernet"` 或 `"serial"`) +- 需要同时支持串口和以太网时,新增独立协议条目 diff --git a/.claude/skills/cloud-deploy-verify/SKILL.md b/.claude/skills/cloud-deploy-verify/SKILL.md new file mode 100644 index 0000000..4d45d92 --- /dev/null +++ b/.claude/skills/cloud-deploy-verify/SKILL.md @@ -0,0 +1,62 @@ +--- +name: cloud-deploy-verify +description: 云平台部署与验证流程。用于用户说“部署云平台”“用 deploy_cloud.sh 部署”“部署到云服务器”“云端部署并验证”“发布云平台前端/后端”等场景,默认使用 deploy_cloud.sh 部署到 ubuntu@119.45.4.75 并验证 cloud-server 与关键接口。 +--- + +# 云平台部署与验证 + +## 何时使用 + +- 部署云平台 +- 用 `deploy_cloud.sh` 部署 +- 部署到云服务器 +- 云端部署并验证 +- 发布云平台前端或后端 + +## 固定约定 + +- 部署脚本:`./deploy_cloud.sh` +- 默认目标:`ubuntu@119.45.4.75` +- 默认云平台地址:`http://119.45.4.75` +- systemd 服务:`cloud-server` +- 默认不要加 `--init`;只有用户明确要求初始化、清库、重置云端状态时才使用 `--init` + +## 执行流程 + +1. 在仓库根目录执行部署: + +```bash +./deploy_cloud.sh +``` + +2. 确认脚本完成并输出: + +```text +[OK] cloud-server is running +``` + +3. 验证远端服务状态: + +```bash +ssh ubuntu@119.45.4.75 'sudo systemctl is-active cloud-server' +``` + +4. 验证云平台登录与关键接口: + - 登录接口:`POST http://119.45.4.75/api/auth/login` + - OTA 包列表:`GET http://119.45.4.75/api/admin/edge-upgrades/packages` + - 如果本次改动涉及 OTA 包字段,确认响应包含预期字段,例如 `release_type`、`description`、`release_notes` + +5. 如果本次改动影响边缘侧 OTA 查询,再验证边缘侧代理透传: + - 先登录边缘侧,例如 87:`POST http://192.168.40.87/api/login` + - 再调用:`POST http://192.168.40.87/api/ota/cloud/packages` + - 确认云端字段能透传到边缘侧响应 + +## 注意 + +- `deploy_cloud.sh` 会构建云端前端和 `cloud_server`,同步 `runtime/cloud_server/`,迁移/校验 Mosquitto Dynamic Security,并重启 `cloud-server`。 +- 不要手写云端 rsync/scp/systemctl 流程,优先使用 `deploy_cloud.sh`。 +- 如果部署失败,先看脚本输出;服务启动失败再查: + +```bash +ssh ubuntu@119.45.4.75 'sudo journalctl -u cloud-server --since "5 min ago" --no-pager' +``` diff --git a/.claude/skills/cloud-deploy-verify/agents/openai.yaml b/.claude/skills/cloud-deploy-verify/agents/openai.yaml new file mode 100644 index 0000000..fc739c3 --- /dev/null +++ b/.claude/skills/cloud-deploy-verify/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: 云平台部署验证 +short_description: 使用 deploy_cloud.sh 部署云平台并验证关键接口 +default_prompt: Use this skill when the user asks to deploy the cloud platform, deploy to the cloud server, run deploy_cloud.sh, or verify a cloud frontend/backend release. From the repo root, run ./deploy_cloud.sh by default, targeting ubuntu@119.45.4.75. Do not pass --init unless explicitly requested. After deployment, verify cloud-server is running, test /api/auth/login and relevant cloud APIs, and if OTA package fields changed, also verify the 87 edge proxy /api/ota/cloud/packages returns those fields. diff --git a/.claude/skills/cloud-public-deploy/SKILL.md b/.claude/skills/cloud-public-deploy/SKILL.md new file mode 100644 index 0000000..3cbd341 --- /dev/null +++ b/.claude/skills/cloud-public-deploy/SKILL.md @@ -0,0 +1,120 @@ +--- +name: cloud-public-deploy +description: edge_collector 云平台公网部署流程规范。用于整理、审查或执行云平台公网部署方案时参考 deploy_cloud.sh,覆盖 package.sh --cloud-only、runtime/cloud_server 同步、远端配置保护、Mosquitto Dynamic Security、systemd/nginx 初始化、cloud-server 重启和公网接口验证。 +--- + +# 云平台公网部署 + +## 固定约定 + +- 部署脚本:`./deploy_cloud.sh` +- 默认目标:`ubuntu@119.45.4.75` +- 远端目录:`~/cloud_server` +- systemd 服务:`cloud-server` +- 公网入口:`http://119.45.4.75` +- 本地构建输出:`runtime/cloud_server/` + +## 使用边界 + +- 常规部署使用 `./deploy_cloud.sh`。 +- 只看流程或生成文档时可以参考本 skill,不直接执行。 +- 只有用户明确要求“初始化、清库、重置云端状态”时才允许加 `--init`。 +- 不要手写 rsync、scp、systemctl 流程替代 `deploy_cloud.sh`。 + +## deploy_cloud.sh 实际流程 + +```text +1. bash ./package.sh --cloud-only +2. 校验 runtime/cloud_server 和 cloud_server/config/server_config.json +3. 读取 MQTT dynsec、PostgreSQL、TDengine 配置 +4. 检查 SSH 连通性 +5. 备份远端 server_config.json 和 ai_config.json +6. rsync runtime/cloud_server/ 到 ~/cloud_server/ +7. 恢复/生成远端运行密钥,保留远端 AI 配置 +8. 迁移并校验 Mosquitto Dynamic Security +9. --init 模式下安装 systemd 服务和 nginx +10. 重启 cloud-server 并输出公网 URL +``` + +## 运行配置保护 + +部署脚本会保护: + +- `~/cloud_server/config/server_config.json` 中的 `jwt_secret`。 +- `custom_config.terminal.credential_key`。 +- `~/cloud_server/config/ai_config.json`。 + +审查或修改部署逻辑时,必须确认这些运行态配置不会被打包产物覆盖。 + +## MQTT Dynamic Security + +脚本会根据 `server_config.json` 配置: + +- 禁用旧的 Mosquitto 静态账号/ACL 配置。 +- 初始化或更新 `/var/lib/mosquitto/dynamic-security.json`。 +- 创建 gateway/cloud 角色和 cloud MQTT client。 +- 设置 `/data/#`、`/status/#`、`/ack/#`、`/cmd/#` 相关权限。 + +如果部署失败,先查 `mosquitto_ctrl`、`mosquitto_dynamic_security.so` 和 Mosquitto 服务状态。 + +## 初始化模式 + +`--init` 会执行高风险动作: + +- 停止 `cloud-server` 和 `mosquitto`。 +- 重置 PostgreSQL 数据库。 +- 重置 TDengine 数据库。 +- 清理 MQTT dynsec 状态。 +- 安装/覆盖 systemd service。 +- 配置 nginx 80 端口反代到 8081,443 自签名证书重定向到 HTTP。 + +未获用户明确确认时禁止使用 `--init`。 + +## 验证步骤 + +部署完成后至少验证: + +```bash +ssh ubuntu@119.45.4.75 'sudo systemctl is-active cloud-server' +ssh ubuntu@119.45.4.75 'sudo systemctl is-active mosquitto' +curl -s http://119.45.4.75/api/health +``` + +按改动范围补充: + +- 登录接口:`POST /api/auth/login` +- AI 配置/分析接口。 +- OTA 包列表接口。 +- MQTT 网关连接和设备在线状态。 +- 前端页面静态资源是否刷新。 + +## 故障排查 + +服务启动失败: + +```bash +ssh ubuntu@119.45.4.75 'sudo journalctl -u cloud-server --since "10 min ago" --no-pager' +``` + +nginx 异常: + +```bash +ssh ubuntu@119.45.4.75 'sudo nginx -t && sudo systemctl status nginx --no-pager' +``` + +MQTT dynsec 异常: + +```bash +ssh ubuntu@119.45.4.75 'sudo systemctl status mosquitto --no-pager' +``` + +## 文档输出 + +整理公网部署文档时必须写清: + +- 目标主机和远端目录。 +- 是否使用 `--init`。 +- 会保留哪些远端配置。 +- 会重启哪些服务。 +- 公网访问入口和验证接口。 +- 回滚方式和日志位置。 diff --git a/.claude/skills/cpp-coding-style/SKILL.md b/.claude/skills/cpp-coding-style/SKILL.md new file mode 100644 index 0000000..42a44ab --- /dev/null +++ b/.claude/skills/cpp-coding-style/SKILL.md @@ -0,0 +1,57 @@ +--- +name: cpp-coding-style +description: C/C++ 代码风格规范。用于本仓库 C/C++ 新增、修改、重构时,统一格式、命名、include 顺序和现代 C++ 用法。 +--- + +# C/C++ 风格规范 + +## 基础约束 + +- 使用 `C++17` +- 4 空格缩进 +- 大括号使用 K&R 风格 +- 单行长度尽量不超过 100 + +## 命名规则 + +- 类型(`class/struct/enum`):`PascalCase` +- 函数/方法:`PascalCase` +- 普通变量:`snake_case` +- 成员变量:`snake_case_` +- 常量:`kPascalCase` +- 宏:`ALL_CAPS` +- 命名空间:小写 + +## include 顺序 + +1. 对应头文件 +2. C 系统头 +3. C++ 标准库 +4. 第三方库 +5. 项目内头文件 + +组间空一行。 + +## 代码习惯 + +- 单行 `if/for/while` 也加大括号 +- 不使用 `using namespace std;` +- 优先 `constexpr`、`[[nodiscard]]`、RAII +- 优先 `static_cast`,避免 C 风格强转 +- `auto` 只在不降低可读性时使用 + +## 注释与日志 + +- 注释优先说明"为什么",不是"做了什么" +- 保留高价值中文注释,不随手删除 +- 强制包含 `#include "logger/Logger.hpp"`(或其相对路径) +- **统一使用流式日志宏**:`LOG_DEBUGS()`、`LOG_INFOS()`、`LOG_WARNS()`、`LOG_ERRORS()`,通过 `<<` 拼接内容。严禁使用 `std::cerr`/`std::cout`、第三方日志原生接口或混用其他宏。 + ```cpp + // 正确:流式 + LOG_ERRORS() << "SerialTransport: 无法打开串口 " << port_ << ": " << strerror(errno); + // 错误:传参格式 + LOG_ERRORS("SerialTransport: 无法打开串口 {}: {}", port_, strerror(errno)); + // 错误:原生输出 + std::cerr << "SerialTransport: 无法打开串口" << std::endl; + ``` +- 日志风格沿用同模块既有前缀和语气 diff --git a/.claude/skills/edge-82-release/SKILL.md b/.claude/skills/edge-82-release/SKILL.md new file mode 100644 index 0000000..ff5126a --- /dev/null +++ b/.claude/skills/edge-82-release/SKILL.md @@ -0,0 +1,48 @@ +--- +name: edge-82-release +description: 82主机发布流程。用于用户说“去82主机编译”“去82主机编译代码”“去82发布xxx版本”“发布并上传版本”时,默认到 192.168.40.82 的 /home/cat/code/edge_collector 执行 git pull 与打包;发布并上传时再把产物上传到云平台 admin 账号。 +--- + +# 82 主机发布流程 + +## 何时使用 + +- 去82主机编译代码 +- 去82主机编译 +- 去82发布 xxx 版本 +- 发布并上传版本 + +## 固定环境 + +- 主机:`cat@192.168.40.82` +- 代码根目录:`/home/cat/code/edge_collector` +- 构建目标:`arm64` +- 云平台:`http://119.45.4.75:8081` +- 云端账号:`admin` + +## 执行顺序 + +1. 去82主机编译代码 + - `cd /home/cat/code/edge_collector` + - `git pull` + - `./package.sh --edge` + +2. 去82发布 xxx 版本 + - `cd /home/cat/code/edge_collector` + - `git pull` + - `./package.sh --publish --version xxx` + +3. 发布并上传版本 + - 先按“去82发布 xxx 版本”执行 + - 再把 `publish/edge__arm64.tar.gz` 上传到云平台 + - 使用云平台默认 `admin` 账号登录 + +## 上传字段 + +- `file` +- `package_type=edge` +- `version=<版本号>` +- `target_arch=arm64` +- `release_type=stable` +- `visibility=platform` +- `enabled=true` diff --git a/.claude/skills/edge-82-release/agents/openai.yaml b/.claude/skills/edge-82-release/agents/openai.yaml new file mode 100644 index 0000000..370ca71 --- /dev/null +++ b/.claude/skills/edge-82-release/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: 82发布流程 +short_description: 82主机编译、发布和上传规则 +default_prompt: Use this skill when the user asks "去82主机编译", "去82主机编译代码", or asks to publish/publish-and-upload from host 82. For compile requests, go to /home/cat/code/edge_collector on 192.168.40.82, run git pull first, then run ./package.sh --edge. For publish requests, run ./package.sh --publish --version . Upload to the cloud admin account only when the user explicitly asks to publish and upload. diff --git a/.claude/skills/edge-bug-lessons/SKILL.md b/.claude/skills/edge-bug-lessons/SKILL.md new file mode 100644 index 0000000..a638618 --- /dev/null +++ b/.claude/skills/edge-bug-lessons/SKILL.md @@ -0,0 +1,91 @@ +--- +name: edge-bug-lessons +description: edge_collector Bug 经验库沉淀规范。用于用户要求 bug 教训、故障复盘、为什么流出、总结经验、沉淀规则时,把边缘侧、云平台、协议采集、部署同步、前端、AI、网络等问题整理为可检索的历史 lesson 和预防规则。 +--- + +# edge_collector Bug 经验库 + +## 目标 + +把一次故障从“修好了”沉淀为“以后能提前拦住”。重点记录根因链路、漏检点、验证方式和反哺动作。 + +## 触发场景 + +- “总结这次 bug” +- “为什么会流出” +- “写一个复盘” +- “沉淀经验” +- “以后怎么避免” +- 修复完成后需要补长期规则 + +## 默认落点 + +```text +docs/bugfix/BugLesson-YYYYMMDD-简述.md +docs/bugfix/BugLesson-index.md +``` + +如果已有更合适的专题目录,可放到: + +- `docs/鲁班猫*/` +- `collector/docs/protocols/` +- `docs/ops/` + +但索引仍建议保留在 `docs/bugfix/BugLesson-index.md`。 + +## Lesson 结构 + +```markdown +# 标题 + +**日期**: +**模块**: +**影响范围**: + +## 1. 问题现象 + +## 2. 根因链路 + +## 3. 流出路径 / 漏检点 + +## 4. 修复内容 + +## 5. 验证结果 + +## 6. 本可在哪一步拦住 + +## 7. 预防措施 + +## 8. 可复用规则 + +## 9. 反哺动作 + +## 10. 相关文件 +``` + +## 当前项目重点 + +优先沉淀以下类型: + +- 打包或同步覆盖运行态动态配置。 +- 97/94/82 等主机系统差异导致运行异常。 +- FANUC/西门子协议库、架构、链接方式问题。 +- 前端白屏、按钮无反馈、错误提示过泛。 +- AI 分析超时、空内容、内部配置泄露。 +- WiFi/4G/frpc/端口转发独立 agent 异常。 +- 云端设备在线状态、历史趋势、数据不连续误判。 + +## 写法要求 + +- 区分“已确认事实”和“推断”。 +- 根因必须落到文件、配置、命令、日志或环境差异。 +- 不写“加强测试”这类空话,要写可执行拦截点。 +- 反哺动作要明确更新哪个 skill、测试清单、文档或脚本检查项。 +- 涉及密钥、密码、Token 时必须脱敏。 + +## 索引格式 + +```markdown +| 日期 | 标题 | 模块 | 核心根因 | 漏检点 | 预防规则 | 文件 | +|------|------|------|----------|--------|----------|------| +``` diff --git a/.claude/skills/edge-bugfix/SKILL.md b/.claude/skills/edge-bugfix/SKILL.md new file mode 100644 index 0000000..f1db40f --- /dev/null +++ b/.claude/skills/edge-bugfix/SKILL.md @@ -0,0 +1,86 @@ +--- +name: edge-bugfix +description: edge_collector 缺陷排查与根因修复流程。用于用户报告边缘侧、云平台、协议采集、前端白屏、部署同步、远程主机 CPU/内存异常、脚本失败、接口失败等 Bug 或异常时,按读取证据、根因定位、最小修复、定向验证和报告沉淀推进。 +--- + +# edge_collector Bug 修复流程 + +## 适用范围 + +- 边缘服务:`collector`、`configurator`、`edge` systemd 服务。 +- 云平台:`cloud_server`、`frontend/cloud_app`、`deploy_cloud.sh`。 +- 前端:`frontend/config_app`、`frontend/cloud_app`。 +- 协议采集:FANUC、西门子、Modbus、OPC UA、传感器等。 +- 脚本/部署:`scripts/`、`package.sh`、`scripts/migrate_edge.sh`。 +- 远程主机:82/87/94/97、云服务器 `119.45.4.75`。 + +## 核心原则 + +1. 先只读取证据,后修改。 +2. 必须定位根因,禁止只修表面症状。 +3. 不回滚用户改动,不清空运行配置。 +4. 涉及远程同步默认使用既有项目脚本,不手写替代流程。 +5. 修改后必须给出定向验证命令和关键结果。 + +## 排查流程 + +### 1. 收集现场 + +按问题类型优先读取: + +- Git 状态:`git status --short` +- 相关日志:`logs/`、`journalctl -u edge`、`journalctl -u cloud-server` +- 配置:`runtime/edge/config/`、`collector/config/`、`configurator/config/` +- 前端:浏览器错误、接口响应、构建产物、路由 +- 远程主机:`uptime`、`free -h`、`df -h`、`systemctl status` + +远程数字主机遵循 `host-connection-defaults`;边缘同步遵循 `edge-sync-host`。 + +### 2. 定位根因 + +优先沿真实链路追踪: + +```text +用户现象 + -> 前端页面 / API + -> configurator 或 cloud_server + -> collector / agent / 脚本 + -> 配置文件 / SQLite / 网络 / systemd +``` + +典型链路: + +- 前端白屏:CSS -> DOM -> JS -> API -> 构建产物。 +- 云端接口失败:前端代理 -> cloud_server 路由 -> 数据库/外部服务。 +- 采集异常:设备配置 -> DriverRegistry -> 驱动日志 -> 协议依赖库。 +- 同步后异常:构建主机架构 -> 打包产物 -> runtime 配置排除 -> systemd 重启。 + +### 3. 修复策略 + +- 小范围修改,不做无关重构。 +- C++ 遵循 `cpp-coding-style`。 +- 后端接口/配置遵循 `backend-conventions`。 +- 前端遵循 `frontend-ui-conventions`、`frontend-debug`、`frontend-dialog`。 +- Shell 遵循 `shell-scripting`。 +- 第三方库遵循 `third-party-libs`。 + +### 4. 验证要求 + +按改动选择最小但可信的验证: + +- JSON 配置:`jq empty ` +- C++ collector:`cmake --build build --target collector -j2` +- configurator/cloud_server:对应 target 或项目测试脚本。 +- 前端:能运行 npm 的环境执行 `npm run build`。 +- 边缘打包:`./package.sh --edge-only` +- 远程部署:按用户明确要求再同步/重启。 + +### 5. 报告沉淀 + +复杂 Bug 或远程事故修复后,在 `docs/` 下写简短报告,建议位置: + +- 远程主机/设备类:`docs/鲁班猫*/` +- 协议类:`collector/docs/protocols/` +- 通用事故:`docs/` + +报告至少包含:现象、根因、修复、验证、后续预防。 diff --git a/.claude/skills/edge-business-rule-extractor/SKILL.md b/.claude/skills/edge-business-rule-extractor/SKILL.md new file mode 100644 index 0000000..976bef9 --- /dev/null +++ b/.claude/skills/edge-business-rule-extractor/SKILL.md @@ -0,0 +1,54 @@ +--- +name: edge-business-rule-extractor +description: edge_collector 业务与技术规则提取规范。用于从用户需求、讨论、故障复盘和实现方案中提取稳定规则,维护云边采集、上传策略、动态配置保护、权限、前端交互、部署同步和协议模板等项目规则。 +--- + +# edge_collector 规则提取 + +## 适用场景 + +- 用户反复强调某个约束。 +- 某个事故暴露出需要长期遵守的规则。 +- 方案中出现“必须、不能、只允许、默认、除非明确要求”等表述。 +- 需要把对话中的口头规范沉淀到文档或 skill。 + +## 规则类型 + +- `BR-COLLECT`:采集与上传规则。 +- `BR-CONFIG`:配置和动态文件保护规则。 +- `BR-DEPLOY`:打包、同步、部署规则。 +- `BR-UI`:前端交互和用户可见文案规则。 +- `BR-PERM`:权限和安全规则。 +- `BR-PROTOCOL`:协议模板和驱动规则。 +- `BR-AI`:AI 分析和模型配置规则。 + +## 当前项目典型规则 + +- 相同数据不上传,5 分钟强制上传;短时间点位不连续可能是正常现象。 +- 打包或同步不能携带目标主机运行态动态配置。 +- 同主机编译部署也要使用 `scripts/migrate_edge.sh`。 +- `install_all.sh` 只有用户明确要求时才执行。 +- 用户可见协议介绍不透露 helper、SDK、库路径等技术细节。 +- AI 分析报告不展示内部 AI 配置名、Provider 名称或模型细节。 + +## 输出格式 + +```markdown +| 编号 | 类型 | 规则 | 来源 | 影响范围 | 验证方式 | +|------|------|------|------|----------|----------| +| BR-DEPLOY-001 | 部署 | ... | 用户确认 | package/sync | ... | +``` + +## 执行流程 + +1. 从需求、对话或文档中提取候选规则。 +2. 去重,避免把同一规则写成多个版本。 +3. 判断规则是否长期有效,临时现场处理不沉淀为规则。 +4. 写明影响范围和验证方式。 +5. 如需落盘,优先更新 `docs/` 下已有规则/概览文档;没有则建议新增规则表。 + +## 注意 + +- 不把猜测写成规则。 +- 不把一次性临时命令写成规则。 +- 规则变更会影响部署或运行安全时,先让用户确认。 diff --git a/.claude/skills/edge-code-review/SKILL.md b/.claude/skills/edge-code-review/SKILL.md new file mode 100644 index 0000000..b6fcb7e --- /dev/null +++ b/.claude/skills/edge-code-review/SKILL.md @@ -0,0 +1,70 @@ +--- +name: edge-code-review +description: edge_collector 代码评审流程。用于用户要求 review、代码审查、提交前检查、质量审核时,按严重程度输出问题,覆盖 C++ 采集驱动、Drogon 接口、React 前端、脚本、打包部署、运行配置和测试缺口。 +--- + +# edge_collector 代码评审 + +## 输出规则 + +评审必须 findings first: + +1. 先列问题,按严重程度排序。 +2. 每条问题包含文件与行号。 +3. 没有问题时明确说明,并列出剩余风险或测试缺口。 +4. 摘要放在问题之后。 + +## 评审维度 + +### C++/采集端 + +- 是否破坏 `DriverRegistry` 注册名与协议配置一致性。 +- 是否错误链接第三方库或跨架构库。 +- 是否直接调用原生通信 API,绕过 `TcpTransport`/`UdpTransport`/`SerialTransport`。 +- 是否遵循 `PointData::UpdateValue` 类型约束。 +- 是否使用流式日志宏。 +- 是否存在线程、生命周期、子进程回收、fd 泄漏风险。 + +### 后端接口 + +- JSON 字段是否 `snake_case`。 +- 是否处理非法 JSON。 +- 是否复用 `ResponseUtil`。 +- 错误响应是否稳定且不暴露底层敏感细节。 +- 配置写入是否会覆盖运行态动态配置。 + +### 前端 + +- 是否复用现有组件。 +- 是否符合 CSS Modules 和暗色主题。 +- 弹窗是否使用统一对话框,不用原生 alert/confirm/prompt。 +- 交互失败是否给出清晰反馈。 +- 移动/窄屏是否溢出或遮挡。 + +### 脚本与部署 + +- 是否使用 `set -euo pipefail`。 +- 路径是否从脚本位置推导。 +- 是否误覆盖 `runtime/edge/config` 中动态配置。 +- 同步部署是否遵循 `scripts/migrate_edge.sh`。 +- 新增常驻服务是否独立,不耦合 edge 主服务。 + +### 测试与验证 + +- 是否有定向单元测试或脚本验证。 +- 协议改动是否更新协议文档。 +- 前端改动是否能构建或说明未构建原因。 +- 远程问题是否给出服务状态或接口验证。 + +## 高风险信号 + +命中以下内容需重点审查: + +- `collector/CMakeLists.txt` +- `package.sh`、`scripts/migrate_edge.sh` +- `collector/src/driver/` +- `configurator/config/*.json` +- `runtime/`、`data/`、动态配置文件处理 +- systemd 安装脚本 +- 远程同步/重启逻辑 + diff --git a/.claude/skills/edge-codex-automation/SKILL.md b/.claude/skills/edge-codex-automation/SKILL.md new file mode 100644 index 0000000..9cb402d --- /dev/null +++ b/.claude/skills/edge-codex-automation/SKILL.md @@ -0,0 +1,84 @@ +--- +name: edge-codex-automation +description: edge_collector Codex 自动化任务建设规范。用于新增、修改或评审自动化任务、定时检查、自动部署验证、远程主机巡检、日志汇总、报告生成等流程时,明确执行边界、调度来源、脚本位置、通知、手工验证和安全限制。 +--- + +# edge_collector Codex 自动化 + +## 适用场景 + +- 定时检查云平台或边缘主机状态。 +- 自动生成巡检报告。 +- 自动构建或验证,但不自动发布。 +- 自动拉取日志、磁盘、CPU、内存信息。 +- 自动检查 docs、skills、配置格式。 + +## 设计原则 + +- 自动化只能做边界清晰、可回滚、可验证的任务。 +- 涉及部署、重启、清库、删除、覆盖配置时必须有人确认。 +- 自动化脚本要独立,不能和 `edge` 主服务强耦合。 +- 运行日志必须可追溯。 +- 失败要有明确提示和下一步处理建议。 + +## 建设流程 + +1. 明确目标: + - 自动化要解决什么问题。 + - 成功标准和失败标准。 + - 运行在哪台主机、哪个目录。 + +2. 明确调度: + - 一次性、定时还是手动触发。 + - cron、systemd timer、CI 或其他调度器。 + - 时区和执行频率。 + +3. 明确权限: + - 是否需要 SSH。 + - 是否需要 sudo。 + - 是否会修改远程状态。 + - 是否访问密钥或配置文件。 + +4. 落地脚本: + - 脚本放到 `scripts/` 或 `.agents/` 约定目录。 + - Shell 遵循 `shell-scripting`。 + - Python 脚本保持独立、参数清晰、日志明确。 + +5. 手工验证一次: + - 先 `--dry-run` 或只读模式。 + - 再执行真实任务。 + - 检查退出码、日志、输出文件。 + +## 当前项目自动化边界 + +允许默认自动化: + +- 只读巡检。 +- 构建验证。 +- 文档/skill 校验。 +- 日志采集和摘要。 +- 接口健康检查。 + +必须确认后才执行: + +- `deploy_cloud.sh` +- `scripts/migrate_edge.sh` +- `install_all.sh` +- systemd restart/stop。 +- 数据库写入、清理、重置。 +- 删除文件、清理 `/tmp`、覆盖运行配置。 + +## 输出格式 + +```text +自动化任务: +- 名称: +- 目标: +- 执行脚本: +- 调度方式: +- 运行主机: +- 权限需求: +- 日志位置: +- 手工验证: +- 风险: +``` diff --git a/.claude/skills/edge-config-lifecycle/SKILL.md b/.claude/skills/edge-config-lifecycle/SKILL.md new file mode 100644 index 0000000..12c3567 --- /dev/null +++ b/.claude/skills/edge-config-lifecycle/SKILL.md @@ -0,0 +1,96 @@ +--- +name: edge-config-lifecycle +description: edge_collector 配置生命周期管理规范。用于新增、修改、打包、同步、动态生成或排除配置文件时,明确默认配置、运行态配置、用户保存配置、密钥配置、迁移保留、备份恢复和前端保存行为,防止覆盖现场配置。 +--- + +# edge_collector 配置生命周期 + +## 适用配置 + +- 边缘运行配置:`runtime/edge/config/` +- 云端运行配置:`~/cloud_server/config/` +- AI 配置:`ai_config.json` +- 端口转发:`port_forward.json` +- frpc/内网穿透配置。 +- 协议设备配置。 +- WiFi/4G 辅助配置。 +- 默认模板:`configurator/config/templates/` +- 用户可见协议描述:`configurator/config/protocols/` + +## 配置分类 + +### 默认配置 + +随代码发布,提供初始结构和默认值。 + +### 运行态配置 + +目标主机运行后由用户、前端或服务生成。打包和同步不能覆盖。 + +### 密钥配置 + +包含 key、secret、password、token。必须脱敏、禁止提交真实值。 + +### 模板配置 + +协议模板、默认点位、用户可选参数。可随版本更新,但要考虑兼容已有设备。 + +## 新增配置文件检查 + +新增配置时必须回答: + +- 默认文件放在哪里。 +- 运行态文件放在哪里。 +- 如果文件不存在,谁负责动态生成。 +- 打包是否包含。 +- 同步是否排除。 +- 前端保存是否会覆盖其他字段。 +- 是否包含密钥。 +- 是否需要备份和迁移。 + +## 打包与同步 + +修改以下脚本时必须检查配置影响: + +- `package.sh` +- `scripts/migrate_edge.sh` +- `deploy_cloud.sh` +- `scripts/install_all.sh` + +原则: + +- 默认配置可以进入包。 +- 运行态配置不能被 `--delete` 同步清掉。 +- 远端已有密钥配置必须保留。 +- 删除配置文件前必须确认是否会自动再生成。 + +## 前端保存 + +- 保存配置时只更新相关字段。 +- 不要用空对象覆盖整个配置文件。 +- 保存失败要显示具体原因。 +- 权限不足要按已有权限体系处理。 + +## 验证 + +至少验证: + +```bash +jq empty +``` + +同步/部署后验证: + +- 目标主机已有配置仍存在。 +- 新增默认配置可生成。 +- 服务重启后能读取配置。 +- 前端读取和保存正常。 + +## 风险信号 + +- `rsync --delete` +- `cp -r config` +- `cat > config.json` +- 前端保存整个 JSON。 +- 后端启动时无条件重写配置。 +- 示例配置中出现真实 key。 diff --git a/.claude/skills/edge-data-quality-analyzer/SKILL.md b/.claude/skills/edge-data-quality-analyzer/SKILL.md new file mode 100644 index 0000000..d83e6e7 --- /dev/null +++ b/.claude/skills/edge-data-quality-analyzer/SKILL.md @@ -0,0 +1,93 @@ +--- +name: edge-data-quality-analyzer +description: edge_collector 采集与上传数据质量分析规范。用于分析历史趋势、AI 分析输入、网关/设备/点位数据缺失、断点、不连续、重复值、时间戳异常、上传策略影响、离线缓存重传和云端展示差异,并输出可验证的数据质量结论。 +--- + +# edge_collector 数据质量分析 + +## 适用问题 + +- 云平台历史趋势看起来断续。 +- AI 分析使用点数明显少于原始点数。 +- 设备在线但云端显示离线。 +- 点位长时间不变化、重复上传或缺失。 +- 离线缓存重传后数据仍不完整。 +- 用户质疑采集频率、上传策略或降采样结果。 + +## 分析维度 + +1. 数据完整性:应有点数、实际点数、缺口时间段。 +2. 时间连续性:相邻时间间隔、断点、乱序、重复时间戳。 +3. 值质量:重复值、常量段、异常突变、空值、类型异常。 +4. 上传策略影响:相同数据不上传、5 分钟强制上传导致的短时不连续。 +5. 降采样影响:原始点数、展示点数、AI 分析点数、是否保留极值。 +6. 云边一致性:边缘本地数据、上传队列、云端历史数据是否一致。 + +## 排查流程 + +### 1. 确认对象 + +明确: + +- 网关名称和 ID。 +- 设备名称和 ID。 +- 点位名称和 ID。 +- 时间范围。 +- 页面或接口来源。 + +### 2. 查询链路 + +按真实链路分析: + +```text +设备采集 + -> collector 点位值 + -> 边缘本地缓存/上传队列 + -> 云端入库 + -> 历史趋势接口 + -> 图表降采样 / AI 分析输入 +``` + +### 3. 统计指标 + +输出至少包含: + +- 原始记录数。 +- 有效记录数。 +- 展示/分析使用记录数。 +- 最大采样间隔。 +- P50/P95 采样间隔。 +- 重复值比例。 +- 缺口时间段 Top N。 + +### 4. 解释结论 + +结论必须区分: + +- 正常策略导致:例如相同数据不上传、5 分钟强制上传。 +- 展示降采样导致:图表为了性能减少点数。 +- 采集异常导致:设备离线、驱动读失败、点位配置错误。 +- 上传异常导致:网络断开、离线缓存未重传、云端接口失败。 + +## AI 分析专项 + +当分析 AI 输入数据时: + +- 必须带上网关名称、设备名称、点位名称。 +- 必须说明原始点数和用于 AI 分析点数的区别。 +- 深度分析应提高采样点数、异常片段数量和上下文摘要,不只改变提示词。 +- 给 AI 的提示词要说明上传策略:相同数据不上传,5 分钟强制上传,因此短时间不连续可能是正常现象。 + +## 报告格式 + +```markdown +## 数据范围 + +## 关键统计 + +## 异常片段 + +## 原因判断 + +## 建议动作 +``` diff --git a/.claude/skills/edge-database-ops/SKILL.md b/.claude/skills/edge-database-ops/SKILL.md new file mode 100644 index 0000000..0828443 --- /dev/null +++ b/.claude/skills/edge-database-ops/SKILL.md @@ -0,0 +1,104 @@ +--- +name: edge-database-ops +description: edge_collector 数据库查询与安全操作规范。用于查询或排查云平台 PostgreSQL、TDengine、边缘 SQLite/本地数据、历史趋势、网关设备点位、用户权限和 AI 分析数据时,按只读优先、脱敏、备份、写操作确认和结果可追溯执行。 +--- + +# edge_collector 数据库操作 + +## 适用场景 + +- 查询云端网关、设备、点位、用户、权限数据。 +- 排查历史趋势、AI 分析输入、设备在线状态。 +- 验证离线缓存、上传结果、配置是否入库。 +- 对比边缘本地数据和云端数据。 +- 需要执行 SQL 修复或清理数据。 + +## 基本原则 + +- 默认只读。 +- 写操作必须用户明确确认。 +- 生产或云端写操作前必须说明影响范围和回滚方案。 +- 查询结果默认脱敏。 +- 不在回复中输出数据库密码、Token、Key。 + +## 先确认环境 + +执行前确认: + +- 目标:本机、边缘主机、云服务器。 +- 数据库类型:PostgreSQL、TDengine、SQLite 或文件型数据。 +- 数据库来源:配置文件、服务环境变量、用户提供。 +- 操作类型:查询、导出、修复、删除。 + +优先读取配置: + +- `cloud_server/config/server_config.json` +- `runtime/cloud_server/config/server_config.json` +- `runtime/edge/config/` +- 部署脚本和 systemd 环境。 + +## 查询流程 + +1. 先定位表和字段来源。 +2. 写出 SQL 或命令。 +3. 只读执行。 +4. 汇总关键结果,不粘贴大量原始数据。 +5. 对涉及用户、密钥、地址的数据脱敏。 + +## 写操作流程 + +写操作前必须给用户确认: + +```text +将执行: +- 数据库: +- 表: +- 条件: +- 影响行数预估: +- 回滚方式: +``` + +执行前建议备份受影响数据: + +```sql +SELECT * FROM
WHERE ; +``` + +必要时导出为临时文件,并说明路径。 + +## 常用只读检查 + +PostgreSQL: + +```sql +SELECT now(); +SELECT version(); +``` + +TDengine: + +```sql +SHOW DATABASES; +SHOW STABLES; +``` + +SQLite: + +```bash +sqlite3 ".tables" +sqlite3 "PRAGMA integrity_check;" +``` + +## 输出要求 + +- 说明数据来源。 +- 说明查询条件和时间范围。 +- 说明结论是事实还是推断。 +- 给出下一步建议。 + +## 禁止事项 + +- 未确认就执行 `UPDATE`、`DELETE`、`DROP`、`TRUNCATE`。 +- 把配置中的数据库密码打印到回复。 +- 用线上写操作验证猜测。 +- 将大量敏感原始数据贴到对话中。 diff --git a/.claude/skills/edge-deployment-writer/SKILL.md b/.claude/skills/edge-deployment-writer/SKILL.md new file mode 100644 index 0000000..3b1f860 --- /dev/null +++ b/.claude/skills/edge-deployment-writer/SKILL.md @@ -0,0 +1,68 @@ +--- +name: edge-deployment-writer +description: edge_collector 部署手册与上线方案编写规范。用于整理边缘侧、云平台、82/97/94/87 主机、runtime 同步、systemd 服务、回滚和验证步骤时,生成可执行部署文档。 +--- + +# edge_collector 部署文档 + +## 固定项目约定 + +- 云平台部署优先使用 `deploy_cloud.sh`。 +- 边缘打包使用 `package.sh`。 +- 边缘同步使用 `scripts/migrate_edge.sh`。 +- 用户明确要求执行 `install_all.sh` 时才执行;不要每次同步都运行。 +- 同主机编译部署也要使用 `scripts/migrate_edge.sh`。 + +## 部署文档结构 + +```text +目标与范围 +目标主机与账号 +前置条件 +构建步骤 +同步/部署步骤 +服务重启步骤 +验证步骤 +回滚方案 +风险与注意事项 +``` + +## 必须写清 + +- 源主机、目标主机、目标目录。 +- 是否会覆盖运行配置。 +- 是否需要重启 `edge`、`cloud-server` 或独立 agent。 +- 是否需要执行 `install_all.sh`。 +- 验证命令和预期输出。 + +## 常用验证 + +边缘: + +```bash +systemctl is-active edge +curl -s http://127.0.0.1/api/status +``` + +云端: + +```bash +systemctl is-active cloud-server +curl -s http://127.0.0.1:/api/health +``` + +脚本: + +```bash +bash -n scripts/.sh +``` + +## 回滚说明 + +文档必须说明: + +- 上一个 runtime/edge 或发布包位置。 +- 如何恢复二进制和 web 资源。 +- 哪些配置不能回滚覆盖。 +- 回滚后如何重启服务和验证。 + diff --git a/.claude/skills/edge-design-doc-writer/SKILL.md b/.claude/skills/edge-design-doc-writer/SKILL.md new file mode 100644 index 0000000..526019a --- /dev/null +++ b/.claude/skills/edge-design-doc-writer/SKILL.md @@ -0,0 +1,64 @@ +--- +name: edge-design-doc-writer +description: edge_collector 详细设计与方案文档编写规范。用于新增功能、协议适配、AI 功能、边缘 agent、云端功能、前端页面或部署机制前,输出适合本仓库 docs 结构的设计文档、接口草案、数据流、验证计划和实施拆分。 +--- + +# edge_collector 设计文档编写 + +## 适用文档 + +- 功能方案设计 +- 详细设计 +- 协议适配方案 +- 本地模型部署方案 +- 云边协同方案 +- 边缘 agent 方案 +- 前端页面方案 + +## 文档落点 + +- 通用方案:`docs/` +- 本地模型:`docs/本地模型/` +- 鲁班猫专题:`docs/鲁班猫*/` +- 协议实现:`collector/docs/protocols/` +- 采集架构:`collector/docs/` + +## 推荐内容 + +```text +背景与目标 +现状与问题 +设计原则 +总体架构 +目录与配置 +接口/API +数据流/状态流 +权限与安全 +实施步骤 +验证计划 +风险与对策 +``` + +## 本项目必须考虑 + +- 边缘侧和云端职责是否清晰。 +- 是否影响 `collector` 采集稳定性。 +- 是否需要新增独立 agent 或 systemd 服务。 +- 是否会覆盖运行时动态配置。 +- 是否需要 82/97/94/87 主机验证。 +- 是否需要 `package.sh` 或 `scripts/migrate_edge.sh` 改动。 +- 是否需要协议模板、用户可见介绍和技术文档分开。 + +## 图示 + +流程或状态变化可用 ASCII 图;复杂架构图使用 `edge-svg-diagram`。 + +## 方案验证 + +文档结尾必须写: + +- 单元测试或脚本验证。 +- 构建验证。 +- 远程部署验证(如需要)。 +- 回滚或降级策略。 + diff --git a/.claude/skills/edge-design-reviewer/SKILL.md b/.claude/skills/edge-design-reviewer/SKILL.md new file mode 100644 index 0000000..7cf3d99 --- /dev/null +++ b/.claude/skills/edge-design-reviewer/SKILL.md @@ -0,0 +1,91 @@ +--- +name: edge-design-reviewer +description: edge_collector 详细设计与方案评审规范。用于评审协议适配、云平台功能、边缘 agent、AI 分析、本地模型、前端页面、部署机制等设计文档,检查结构完整性、边界、接口、配置、数据流、前端可实现性、部署影响、测试和回滚。 +--- + +# edge_collector 设计评审 + +## 评审目标 + +确认设计文档足够指导实现、测试和部署,不留下关键歧义。 + +## 结论级别 + +- `[严重]`:会导致无法实现、运行风险或数据/配置损坏。 +- `[警告]`:可实现但存在质量、可维护性或验证缺口。 +- `[建议]`:改进项,不阻塞。 + +通过标准:无 `[严重]`,关键 `[警告]` 有明确处理计划。 + +## 评审维度 + +### 1. 文档结构 + +- 背景、目标、范围、不做什么是否明确。 +- 是否有现状分析和约束。 +- 是否有实施步骤和验证计划。 +- 是否写清假设和待确认项。 + +### 2. 云边职责 + +- 边缘侧、云平台、前端、独立 agent 职责是否清晰。 +- 是否把高风险或长耗时任务放到合适进程。 +- 新增常驻进程是否独立,不耦合 `edge` 主服务。 + +### 3. 接口与配置 + +- API 路径、方法、请求、响应、错误码是否完整。 +- JSON 字段是否符合当前后端约定。 +- 配置文件路径、默认值、动态生成规则是否明确。 +- 是否会覆盖运行态动态配置。 + +### 4. 数据流与状态流 + +- 采集、缓存、上传、云端入库、展示、AI 分析链路是否完整。 +- 状态机是否覆盖成功、失败、超时、重试、停止。 +- 离线、断网、重启、服务异常是否有处理。 + +### 5. 前端可实现性 + +- 页面布局、主要状态、按钮反馈、错误提示是否明确。 +- 用户可见文案是否隐藏内部技术细节。 +- 权限、空状态、loading、长内容滚动是否覆盖。 +- 复杂页面是否需要原型或图示。 + +### 6. 部署与运维 + +- 是否影响 `package.sh`、`deploy_cloud.sh`、`scripts/migrate_edge.sh`。 +- 是否需要 `install_all.sh`,是否明确执行条件。 +- 是否需要 systemd 服务、日志路径、重启策略。 +- 是否考虑 82/97/94/87 和云服务器差异。 + +### 7. 测试与回滚 + +- 是否有单元、构建、接口、前端、设备或远程验证。 +- 是否覆盖异常场景。 +- 是否有回滚或降级策略。 +- 是否能验证“不覆盖运行配置”。 + +## 输出格式 + +```markdown +## 评审结论 + +通过 / 不通过 / 有条件通过 + +## 问题列表 + +| 级别 | 位置 | 问题 | 影响 | 建议 | +|------|------|------|------|------| + +## 待确认项 + +## 建议补充验证 +``` + +## 注意 + +- 评审先列问题,再写总结。 +- 文件和行号尽量具体。 +- 不把个人偏好当成缺陷。 +- 如果设计引用官方能力或第三方 SDK,拿不准时要联网查证。 diff --git a/.claude/skills/edge-doc-coauthoring/SKILL.md b/.claude/skills/edge-doc-coauthoring/SKILL.md new file mode 100644 index 0000000..f45323d --- /dev/null +++ b/.claude/skills/edge-doc-coauthoring/SKILL.md @@ -0,0 +1,68 @@ +--- +name: edge-doc-coauthoring +description: edge_collector 文档协作规范。用于编写或更新方案设计、部署说明、协议文档、事故分析、本地模型方案、用户手册等 docs 文档时,帮助确定读者、落点、结构、验证依据和后续实施清单。 +--- + +# edge_collector 文档协作 + +## 文档落点 + +- 协议实现:`collector/docs/protocols/` +- 协议清单:`collector/docs/协议支持清单.md` +- 边缘/云端通用方案:`docs/` +- 鲁班猫设备问题:`docs/鲁班猫1/`、`docs/鲁班猫3/` +- 本地模型方案:`docs/本地模型/` +- 部署/同步/运维:`docs/` 或 `docs/ops/` + +## 写作流程 + +1. 明确读者:开发、运维、现场用户、管理后台用户。 +2. 明确目标:评估、实施、排障、交付说明、用户操作。 +3. 收集依据:代码路径、配置文件、脚本、远程验证、官方文档链接。 +4. 写清边界:第一版做什么、不做什么、风险和前置条件。 +5. 给出可执行步骤:命令、目录、配置示例、验证方法。 + +## 推荐结构 + +技术方案: + +```text +背景与目标 +当前现状 +方案设计 +目录/配置/API +实施步骤 +验证计划 +风险与对策 +参考资料 +``` + +事故分析: + +```text +问题现象 +影响范围 +现场证据 +根因分析 +修复方案 +验证结果 +预防措施 +``` + +用户说明: + +```text +功能用途 +使用步骤 +参数解释 +常见问题 +注意事项 +``` + +## 约束 + +- 给用户看的协议介绍不透露内部技术细节。 +- 技术方案可写实现细节,但要标注假设和验证状态。 +- 引用外部信息时提供链接。 +- 不把未经验证的能力写成已完成。 + diff --git a/.claude/skills/edge-framework-learner/SKILL.md b/.claude/skills/edge-framework-learner/SKILL.md new file mode 100644 index 0000000..3944483 --- /dev/null +++ b/.claude/skills/edge-framework-learner/SKILL.md @@ -0,0 +1,63 @@ +--- +name: edge-framework-learner +description: edge_collector 框架、SDK、协议库和工具链学习沉淀规范。用于需要学习并沉淀 ONNX Runtime、RKNN、FOCAS SDK、Snap7、Drogon、React/Vite、Playwright、交叉编译工具链、AI Provider SDK 等新技术,并生成适合本仓库使用的 skill 或技术笔记。 +--- + +# edge_collector 技术学习沉淀 + +## 适用场景 + +- 用户要求“学习某框架并生成 skill”。 +- 新接入第三方 SDK、协议库、AI Provider 或模型推理框架。 +- 当前知识可能过期,需要联网查官方文档。 +- 需要把一次调研变成后续可复用的项目规则。 + +## 信息来源 + +优先级: + +1. 官方文档、官方仓库、官方示例。 +2. 当前仓库已有实现和构建脚本。 +3. 设备或 SDK 随包文档。 +4. 社区资料,仅用于补充,并标注来源。 + +涉及外部技术版本、接口或模型能力时必须联网确认,避免凭记忆。 + +## 学习输出 + +```text +技术定位 +适用版本 +当前项目使用场景 +安装与依赖 +最小可用示例 +项目集成方式 +构建/部署注意事项 +常见错误 +验证命令 +是否需要新增 skill +``` + +## 生成 skill 时 + +- 名称使用小写短横线。 +- 放到 `.agents/skills//SKILL.md`。 +- frontmatter 只保留 `name` 和 `description`。 +- 内容必须面向 `edge_collector`,不要生成通用教程。 +- 复杂资料可放 `references/`,但优先保持 SKILL.md 简洁。 +- 用 `skill-creator` 的 `quick_validate.py` 校验。 + +## 本项目集成检查 + +新增技术必须检查: + +- 是否影响 `collector` 稳定性。 +- 是否需要新增第三方库目录和架构分层。 +- 是否需要修改 `package.sh` 或 `scripts/migrate_edge.sh`。 +- 是否会引入运行配置覆盖风险。 +- 是否需要 82/97/94/87 或云平台验证。 +- 是否需要文档区分用户说明和技术细节。 + +## 输出语气 + +给用户的是选型和落地建议,不堆砌官方概念;每条建议都要说明对当前工程的影响。 diff --git a/.claude/skills/edge-frontend-design/SKILL.md b/.claude/skills/edge-frontend-design/SKILL.md new file mode 100644 index 0000000..cc7861e --- /dev/null +++ b/.claude/skills/edge-frontend-design/SKILL.md @@ -0,0 +1,84 @@ +--- +name: edge-frontend-design +description: edge_collector 前端页面设计与 UI 落地规范。用于设计或优化边缘侧 frontend/config_app、云平台 frontend/cloud_app 页面、组件、布局、交互、按钮状态、弹窗、图表、AI 分析、WiFi、端口转发、内网穿透等用户界面时,结合当前 React/Vite/CSS Modules 暗色主题输出可落地设计。 +--- + +# edge_collector 前端设计 + +## 适用前端 + +- 边缘侧:`frontend/config_app` +- 云平台:`frontend/cloud_app` + +## 设计原则 + +- 先阅读相邻页面和 CSS Modules,沿用当前视觉语言。 +- 首屏直接呈现可用工具,不做营销式 landing page。 +- 工业/运维页面要安静、清晰、密集但不拥挤。 +- 不引入新的 UI 框架。 +- 不把内部技术细节展示给最终用户。 +- 卡片、按钮、启停、危险操作样式要与已有模块一致。 + +## 视觉基线 + +后续前端设计按以下口径走: + +- 暗色底。 +- 细边框。 +- 蓝紫作为主操作色。 +- 状态色克制使用,只用于表达成功、警告、错误、运行中等明确状态。 +- 避免营销页式大渐变。 +- 避免装饰感过强的科技视觉,如大面积霓虹、发光线框、玻璃拟态、纯装饰光效。 +- 页面应像工业网关/运维工具,而不是宣传页或展示大屏。 + +现有颜色基线: + +```text +背景:#0d0d14 / #14141e +面板:#1e1e2e +边框:#2a2a3a +主文字:#e0e0e0 +标题文字:#ffffff +主操作色:#6366f1 +``` + +## 工作流 + +1. 明确目标用户:现场用户、运维、管理员、开发。 +2. 梳理核心任务:用户进页面后最需要完成什么。 +3. 阅读现有页面,提取布局、按钮、表格、弹窗和状态样式。 +4. 先给信息架构,再给具体组件布局。 +5. 覆盖加载、空状态、错误、保存中、权限不足、操作成功。 +6. 实现时遵循 `frontend-ui-conventions`、`frontend-conventions`、`frontend-dialog`。 + +## 当前项目常见布局 + +- 高级功能:模块标签页 + 左右均分列表/配置区 + 底部操作区靠右。 +- 数据查看/AI 分析:图表区域与报告区域独立,报告内容向下延展,不向上挤占图表。 +- 配置页:表单和列表并排,避免单列垂直堆叠导致右侧空白。 +- 规则列表:单条启用/停用放操作列,不使用复选框表达启停。 + +## 交互要求 + +- 点击连接、扫描、保存、分析、启停等耗时操作,按钮必须出现 loading 或局部状态反馈。 +- 危险操作必须二次确认。 +- 失败提示要说明可执行下一步,不只显示接口失败。 +- 普通按钮、危险按钮、启停按钮风格要统一。 +- 长文本和长报告必须支持滚动查看,不能遮挡上方关键内容。 + +## 文案规则 + +- 用户可见文案使用业务语言。 +- 不展示 AI Provider 名称、内部模型名、helper、SDK 路径、接口路径、堆栈、SQL。 +- 参数说明写影响和建议值。 +- 空状态告诉用户下一步操作。 + +## 设计检查 + +- 是否有大片空白。 +- 文本是否溢出。 +- 窄屏是否可用。 +- 操作后是否有反馈。 +- 图表、表格、报告是否互相遮挡。 +- 权限不足是否有清晰状态。 +- 与相邻模块按钮和标签风格是否一致。 diff --git a/.claude/skills/edge-frontend-testing/SKILL.md b/.claude/skills/edge-frontend-testing/SKILL.md new file mode 100644 index 0000000..dd4a1c2 --- /dev/null +++ b/.claude/skills/edge-frontend-testing/SKILL.md @@ -0,0 +1,104 @@ +--- +name: edge-frontend-testing +description: edge_collector 前端测试与浏览器验证规范。用于边缘侧或云平台 React/Vite 页面白屏、布局错乱、按钮无反馈、弹窗异常、图表遮挡、接口失败、构建后验证时,使用 npm build、浏览器控制台、Network、Playwright 截图和交互脚本进行验证。 +--- + +# edge_collector 前端测试 + +## 适用范围 + +- `frontend/config_app` +- `frontend/cloud_app` +- 云平台公网页面 `http://119.45.4.75` +- 边缘网关页面,如 `http://192.168.40./` + +## 验证顺序 + +1. 构建验证。 +2. 页面加载验证。 +3. 控制台错误检查。 +4. Network 接口响应检查。 +5. 关键交互点击。 +6. 布局截图和窄屏检查。 +7. 状态反馈检查。 + +## 构建命令 + +按实际目录执行: + +```bash +npm run build +``` + +如果不能构建,最终说明原因,例如缺少依赖、Node 版本不对或远程主机不可用。 + +## Playwright 验证流程 + +使用浏览器验证时: + +```text +打开目标 URL + -> wait networkidle + -> 收集 console error + -> 截图 + -> 定位关键按钮/输入框 + -> 执行操作 + -> 检查 loading/toast/dialog/network + -> 再截图 +``` + +优先使用稳定选择器: + +- 可见文本。 +- button role/name。 +- 表单 label。 +- 现有 data 属性。 +- 必要时再用 CSS selector。 + +## 重点页面检查 + +- 高级功能:WiFi、内网穿透、端口转发、硬件控制。 +- 数据查看:历史趋势、AI 分析弹窗、AI 分析报告。 +- 离线缓存:参数默认值、保存反馈、状态展示。 +- OTA:包列表、升级确认、进度和失败提示。 +- 管理后台:AI 配置、权限、启用配置唯一性。 + +## 失败提示检查 + +前端不能只显示: + +```text +failed to request cloud config +AI 服务请求失败 +操作失败 +``` + +应尽量展示后端或 agent 给出的具体原因,并转成用户可理解文案: + +- 连接失败。 +- 请求超时。 +- 权限不足。 +- 配置缺失。 +- 服务未运行。 +- 返回格式异常。 + +## 截图要求 + +复杂 UI 改动至少检查: + +- 桌面宽度。 +- 窄屏或移动宽度。 +- 长内容状态。 +- 操作中状态。 +- 错误状态。 + +最终说明截图路径或验证 URL。 + +## 回归重点 + +- 页面不白屏。 +- 无严重 console error。 +- 按钮 loading 不导致布局抖动。 +- 报告和表格区域可滚动。 +- 文案不泄露内部实现。 +- 动态配置不会因为前端保存被清空。 diff --git a/.claude/skills/edge-local-dev-services/SKILL.md b/.claude/skills/edge-local-dev-services/SKILL.md new file mode 100644 index 0000000..e132130 --- /dev/null +++ b/.claude/skills/edge-local-dev-services/SKILL.md @@ -0,0 +1,94 @@ +--- +name: edge-local-dev-services +description: edge_collector 本地开发与联调服务管理规范。用于在本机或远程开发主机启动、检查、停止 edge/cloud 前后端开发服务和依赖服务,包含 collector、configurator、cloud_server、React/Vite 前端、PostgreSQL、TDengine、Mosquitto、端口占用和日志验证。 +--- + +# edge_collector 本地开发服务 + +## 适用场景 + +- 本机启动边缘侧或云平台开发环境。 +- 前端页面需要 dev server 联调。 +- 后端接口需要本地验证。 +- E2E 前需要确认依赖服务。 +- 端口冲突、服务没起来、接口连接失败。 + +## 先读配置 + +不要假设服务和端口,优先读取: + +- `docs/project-overview.md` +- `cloud_server/config/server_config.json` +- `configurator/config/` +- `frontend/*/package.json` +- `package.sh` +- `deploy_cloud.sh` +- `scripts/install_all.sh` +- systemd service 安装脚本 + +## 常见服务 + +- 边缘:`collector`、`configurator`、`edge` systemd 服务。 +- 云端:`cloud_server`、`cloud-server` systemd 服务。 +- 前端:`frontend/config_app`、`frontend/cloud_app`。 +- 依赖:PostgreSQL、TDengine、Mosquitto。 +- 独立 agent:frpc agent、port forward agent、4G/WiFi 相关脚本。 + +## 检查流程 + +1. 查看端口占用: + +```bash +ss -lntp +``` + +2. 查看服务状态: + +```bash +systemctl status edge --no-pager +systemctl status cloud-server --no-pager +``` + +3. 查看最近日志: + +```bash +journalctl -u edge --since "10 min ago" --no-pager +journalctl -u cloud-server --since "10 min ago" --no-pager +``` + +4. 验证接口: + +```bash +curl -s http://127.0.0.1/api/status +curl -s http://127.0.0.1:8081/api/health +``` + +## 前端开发 + +进入对应目录后: + +```bash +npm install +npm run dev +npm run build +``` + +如果 Node 环境在 97/ARM64 主机异常,优先参考 `scripts/set_env/install_nvm_npm.sh` 和本地 nvm 离线安装规则。 + +## 禁止事项 + +- 不要直接连接生产库做写操作。 +- 不要随意 kill 非本次启动的进程。 +- 不要删除用户已有容器、数据库或运行配置。 +- 不要把本地端口和临时密码写死进代码。 +- 不要每次同步后都执行 `install_all.sh`,除非用户明确要求。 + +## 输出 + +最终说明: + +- 启动或检查了哪些服务。 +- 使用了哪些端口。 +- 哪些接口验证通过。 +- 日志里是否有错误。 +- 如何停止本次启动的临时服务。 diff --git a/.claude/skills/edge-markdown-docs/SKILL.md b/.claude/skills/edge-markdown-docs/SKILL.md new file mode 100644 index 0000000..2fc0df1 --- /dev/null +++ b/.claude/skills/edge-markdown-docs/SKILL.md @@ -0,0 +1,91 @@ +--- +name: edge-markdown-docs +description: edge_collector Markdown 文档编写与处理规范。用于创建、更新、拆分、合并、校对 docs、collector/docs、协议文档、部署说明、测试报告、故障报告、本地模型方案等 Markdown 文档,并维护目录、链接、图片引用和读者边界。 +--- + +# edge_collector Markdown 文档处理 + +## 适用范围 + +- `docs/` +- `collector/docs/` +- `collector/docs/protocols/` +- `docs/本地模型/` +- `docs/鲁班猫*/` +- `.agents/skills/` + +## 工作流 + +1. 先确认读者:用户、运维、开发、客户交付。 +2. 选择落点:优先更新已有文档,避免重复文档。 +3. 读取相邻文档,保持标题层级和术语一致。 +4. 写完后检查链接、图片路径、代码块语言和表格可读性。 +5. 技术文档标注假设和验证状态,用户文档隐藏内部实现细节。 + +## 推荐结构 + +技术方案: + +```text +背景与目标 +现状与问题 +方案设计 +接口/配置/目录 +实施步骤 +验证计划 +风险与回滚 +``` + +操作说明: + +```text +功能用途 +使用前准备 +操作步骤 +参数说明 +常见问题 +注意事项 +``` + +故障报告: + +```text +问题现象 +影响范围 +证据 +根因 +修复 +验证 +预防措施 +``` + +## 格式规则 + +- 标题层级从 `#` 开始,不跳级。 +- 命令使用 `bash` 代码块。 +- JSON 使用 `json` 代码块。 +- 表格列不要过宽,必要时拆成列表。 +- 图片放到文档同级 `assets/` 或专题目录下,并用相对路径引用。 +- 引用外部资料时给链接。 + +## 当前项目特别规则 + +- 协议用户介绍不要透露 helper、SDK、库路径、内部进程。 +- 部署文档要写清是否会覆盖运行配置。 +- 云平台 AI 文档不要展示真实 key。 +- 远程主机文档不要写明密码。 +- 同主机编译部署仍要使用 `scripts/migrate_edge.sh`。 + +## 校验 + +至少执行: + +```bash +rg -n "TODO|待确认|change_me|password|secret" docs collector/docs .agents/skills +``` + +按文档类型补充: + +- JSON 示例:`jq empty` +- Shell 示例:`bash -n` +- SVG 引用:浏览器或图片查看器打开检查 diff --git a/.claude/skills/edge-mcp-tools/SKILL.md b/.claude/skills/edge-mcp-tools/SKILL.md new file mode 100644 index 0000000..9883ff2 --- /dev/null +++ b/.claude/skills/edge-mcp-tools/SKILL.md @@ -0,0 +1,182 @@ +--- +name: edge-mcp-tools +description: edge_collector 边缘侧本地模型与 MCP 工具接入规范。用于在鲁班猫/RK3566/RK3576/RK3588 等边缘设备部署本地模型后,设计或实现 MCP 工具服务,让模型安全调用网关状态、设备点位、历史数据、诊断、配置查询和运维只读能力。 +--- + +# edge_collector MCP 工具接入 + +## 目标 + +让边缘侧本地模型可以通过受控工具访问网关能力,而不是直接读取任意文件、执行任意命令或绕过现有服务。 + +典型链路: + +```text +本地模型 + -> MCP Client + -> edge MCP tools + -> configurator / collector / 本地数据库 / 只读诊断命令 +``` + +## 适用场景 + +- 在 RK3566/RK3576/RK3588 鲁班猫上部署 Qwen 等本地模型。 +- 给本地模型增加“查询设备状态”“分析点位趋势”“解释报警”“读取网关状态”等工具。 +- 把边缘侧诊断能力封装成 AI 可调用工具。 +- 设计 MCP 工具权限、输入输出和安全边界。 + +## 设计原则 + +- 默认只读。 +- 工具服务独立运行,不耦合 `edge` 主服务。 +- 本地模型不直接访问数据库文件、配置文件和 shell。 +- 所有工具必须有明确输入 schema、输出 schema 和错误语义。 +- 写配置、重启服务、删除数据等高风险动作第一版不开放。 +- 工具返回用户可理解信息,不泄露密钥、路径、Token、内部模型配置。 + +## 推荐第一版工具 + +优先做只读工具: + +- `edge_get_gateway_status`:读取网关状态、版本、运行时间。 +- `edge_list_devices`:列出设备名称、协议、在线状态。 +- `edge_list_points`:列出某设备点位名称、类型、单位。 +- `edge_read_latest_values`:读取指定设备/点位最新值。 +- `edge_query_history_summary`:查询历史数据摘要,不返回超大原始数据。 +- `edge_get_alarm_summary`:读取报警或异常摘要。 +- `edge_get_network_status`:读取网络、WiFi、4G、端口转发只读状态。 +- `edge_get_service_health`:读取 `edge`、独立 agent 状态。 + +暂不开放: + +- 修改协议配置。 +- 保存 AI Key。 +- 重启服务。 +- 删除缓存或历史数据。 +- 执行任意 shell。 +- 读取任意文件。 + +## 工具命名 + +- 使用 `edge_` 前缀。 +- 动词清晰:`get`、`list`、`query`、`analyze`。 +- 避免泛化工具名,例如 `run_command`、`read_file`。 + +## 输入输出 + +输入必须限制范围: + +```text +gateway_id +device_id 或 device_name +point_id 或 point_name +time_range +limit +``` + +输出建议结构: + +```json +{ + "ok": true, + "data": {}, + "warnings": [], + "source": "configurator", + "timestamp": "2026-06-16T00:00:00+08:00" +} +``` + +错误要可行动: + +```json +{ + "ok": false, + "error_code": "DEVICE_NOT_FOUND", + "message": "未找到指定设备,请确认设备名称或 ID", + "suggestion": "可先调用 edge_list_devices 查看可用设备" +} +``` + +## 与现有服务集成 + +优先通过现有 API 或受控本地接口访问: + +- `configurator` API。 +- `collector` 状态接口或已有数据接口。 +- 本地只读数据库查询。 +- systemd 只读状态命令。 + +不要绕过业务逻辑直接修改配置文件。 + +## 本地模型注意 + +参考已有本地模型文档: + +- `docs/本地模型/Qwen2.5-0.6B-Instruct在RK3566本地部署方案.md` +- `docs/本地模型/Qwen2.5-VL-3B-Instruct在RK3576鲁班猫3边缘图文模型部署方案.md` +- `docs/本地模型/Qwen2.5-14B-Instruct在16G_RK3588鲁班猫5部署方案.md` + +设计工具时必须考虑: + +- 模型上下文有限,工具返回要摘要化。 +- RK3566/RK3576 资源有限,工具查询要分页、限流。 +- 大历史数据先聚合摘要,再按需返回异常片段。 +- 离线运行时不要依赖云端 AI Provider。 + +## 安全边界 + +结合 `edge-security-secrets`: + +- 不返回 API Key、JWT、MQTT 密码、SSH 密码。 +- 不暴露真实配置文件完整内容。 +- 不开放任意命令执行。 +- 日志中记录工具名、参数摘要、耗时、结果状态,不记录敏感值。 +- 对外接口只监听本机或受控内网,默认不暴露公网。 + +## 部署方式 + +第一版建议使用 Python 独立 agent: + +- 遵循 `edge-python-agent`。 +- 使用 systemd 独立托管。 +- 配置文件动态生成但不覆盖已有配置。 +- 打包和同步遵循 `edge-config-lifecycle`。 + +服务名建议: + +```text +edge-mcp-tools +``` + +## 验证计划 + +至少验证: + +- 工具列表可发现。 +- 每个工具 schema 正确。 +- 正常查询返回结构化数据。 +- 设备不存在、点位不存在、时间范围过大时错误可理解。 +- 返回内容脱敏。 +- 大数据查询有 limit 或摘要。 +- 服务重启后配置保留。 +- 本地模型能完成一个端到端问题,例如“分析最近 1 小时某设备是否异常”。 + +## 文档输出 + +设计 MCP 工具时输出: + +```markdown +## 工具清单 + +## 权限边界 + +## 输入输出 schema + +## 数据来源 + +## 部署方式 + +## 安全与脱敏 + +## 验证计划 +``` diff --git a/.claude/skills/edge-observability/SKILL.md b/.claude/skills/edge-observability/SKILL.md new file mode 100644 index 0000000..095ef08 --- /dev/null +++ b/.claude/skills/edge-observability/SKILL.md @@ -0,0 +1,116 @@ +--- +name: edge-observability +description: edge_collector 运行观测与资源诊断规范。用于排查边缘主机或云服务器 CPU 高、内存占用、磁盘空间、/tmp 清理、进程数量、jq/python/agent 异常、服务日志、端口监听和系统负载时,按只读证据链输出分析结论。 +--- + +# edge_collector 运行观测 + +## 适用场景 + +- CPU 占用高。 +- 内存比其他主机高。 +- `/tmp` 或工程目录占用大。 +- `jq`、Python、agent 进程很多。 +- 服务频繁重启。 +- 网关在线状态异常。 +- 前端或云端接口偶发失败。 + +## 排查顺序 + +1. 系统概况。 +2. CPU 和进程。 +3. 内存。 +4. 磁盘。 +5. systemd 服务。 +6. 应用日志。 +7. 网络端口。 +8. 与对照主机比较。 + +## 常用命令 + +系统: + +```bash +uptime +free -h +df -h +uname -a +date +``` + +CPU/进程: + +```bash +ps -eo pid,ppid,user,stat,pcpu,pmem,rss,etime,cmd --sort=-pcpu | head -30 +ps -eo pid,ppid,user,stat,pcpu,pmem,rss,etime,cmd --sort=-rss | head -30 +``` + +进程树: + +```bash +pstree -ap +``` + +磁盘: + +```bash +du -h --max-depth=1 /home/cat 2>/dev/null | sort -h +du -h --max-depth=1 /tmp 2>/dev/null | sort -h +``` + +服务: + +```bash +systemctl status edge --no-pager +journalctl -u edge --since "30 min ago" --no-pager +systemctl list-units --type=service --state=running +``` + +端口: + +```bash +ss -lntp +``` + +## 分析规则 + +- 短时尖峰和持续高占用分开判断。 +- 先找父进程,再判断是脚本循环、服务重启还是用户命令。 +- 内存分析区分 RSS、缓存和可用内存。 +- 磁盘清理只给建议,删除必须等用户确认。 +- 与 119.45.4.75 或其他主机对比时,列出相同指标。 + +## 高风险操作 + +以下操作必须用户明确同意: + +- 删除文件或目录。 +- kill 进程。 +- 重启服务。 +- 清理日志。 +- apt 安装诊断工具。 + +## 报告格式 + +```text +结论: + +证据: +- CPU: +- 内存: +- 磁盘: +- 进程: +- 日志: + +判断: + +建议: +``` + +## 当前项目常见根因 + +- shell + `jq` 高频轮询导致短时 CPU 尖峰。 +- 未插 SIM/设备缺失导致 4G 脚本重复探测。 +- 前端构建产物或代码仓库占用较大。 +- `/tmp` 离线安装包、构建缓存未清理。 +- 独立 agent 异常退出后被 systemd 频繁拉起。 diff --git a/.claude/skills/edge-pdf-docs/SKILL.md b/.claude/skills/edge-pdf-docs/SKILL.md new file mode 100644 index 0000000..d13c49a --- /dev/null +++ b/.claude/skills/edge-pdf-docs/SKILL.md @@ -0,0 +1,76 @@ +--- +name: edge-pdf-docs +description: edge_collector PDF 文档读取、提取、转换和交付检查规范。用于处理客户 PDF、导出报告、部署手册、测试报告、扫描件、表格提取、PDF 转图片预览,以及从 DOCX/Markdown 生成 PDF 交付件。 +--- + +# edge_collector PDF 处理 + +## 适用场景 + +- 阅读客户 PDF 需求、手册、协议资料。 +- 从 PDF 提取文字或表格。 +- 把 Word/Markdown 报告转 PDF。 +- 将 PDF 页面转图片用于视觉检查。 +- 合并、拆分或旋转 PDF。 + +## 文本提取 + +优先使用: + +```bash +pdftotext -layout input.pdf output.txt +``` + +需要表格时使用 `pdfplumber`: + +```python +import pdfplumber + +with pdfplumber.open("input.pdf") as pdf: + for page in pdf.pages: + print(page.extract_text()) + print(page.extract_tables()) +``` + +扫描件需要 OCR 时,先说明 OCR 可能有识别误差,并保留人工复核步骤。 + +## PDF 转图片 + +用于检查版式、截图或报告附件: + +```bash +pdftoppm -png -r 150 input.pdf page +``` + +只转指定页: + +```bash +pdftoppm -png -r 150 -f 1 -l 3 input.pdf page +``` + +## 合并与拆分 + +优先使用 `qpdf`: + +```bash +qpdf --empty --pages a.pdf b.pdf -- merged.pdf +qpdf input.pdf --pages . 1-5 -- part.pdf +``` + +## 交付检查 + +生成 PDF 后检查: + +- 页面是否缺失。 +- 中文是否乱码。 +- 表格是否截断。 +- 图片是否模糊。 +- 页眉页脚和页码是否正确。 +- 是否包含未脱敏的密钥、账号、密码、内网地址。 + +## 当前项目注意 + +- 用户手册 PDF 不写内部技术细节。 +- 故障报告 PDF 要保留证据截图和验证命令摘要。 +- 部署报告 PDF 要写清目标主机但隐藏敏感凭据。 +- AI 分析报告 PDF 不展示内部 AI Provider 和模型配置。 diff --git a/.claude/skills/edge-presentation-docs/SKILL.md b/.claude/skills/edge-presentation-docs/SKILL.md new file mode 100644 index 0000000..bc12b62 --- /dev/null +++ b/.claude/skills/edge-presentation-docs/SKILL.md @@ -0,0 +1,70 @@ +--- +name: edge-presentation-docs +description: edge_collector PPT/汇报材料编写与处理规范。用于把方案设计、部署方案、测试结果、故障复盘、AI/本地模型方案、协议适配方案整理成汇报型 PPT 或演示大纲,并可读取、检查、转换已有 .pptx。 +--- + +# edge_collector 汇报材料处理 + +## 适用场景 + +- 方案汇报。 +- 项目进展汇报。 +- 故障复盘汇报。 +- 部署上线说明。 +- 本地模型或 AI 功能方案展示。 +- 协议适配方案展示。 + +## 默认结构 + +```text +1. 背景与目标 +2. 当前现状/问题 +3. 方案总览 +4. 核心设计或流程 +5. 实施计划 +6. 验证结果 +7. 风险与对策 +8. 下一步 +``` + +## 设计口径 + +- 面向工业网关和云边协同场景,风格稳重、清晰、克制。 +- 优先用流程图、架构图、对比表,而不是大段文字。 +- 一页只表达一个核心结论。 +- 保留必要证据:截图、日志摘要、测试结果、关键指标。 +- 不展示密钥、密码、真实 Token。 + +## 内容转换 + +从 Markdown 方案转 PPT 时: + +- 每个二级标题通常对应 1 页或 1 组页。 +- 长表格改成摘要表 + 附录。 +- 命令行只保留关键命令和结果,不放完整日志。 +- 复杂架构图优先使用 `edge-svg-diagram` 生成 SVG 后嵌入。 + +## 读取 PPTX + +提取文本: + +```bash +python3 -m markitdown input.pptx > output.md +``` + +没有 `markitdown` 时,先说明无法直接提取,改用 LibreOffice 或解包 XML。 + +检查结构: + +```bash +unzip -l input.pptx | rg "ppt/slides/slide|ppt/media|ppt/theme" +``` + +## 交付检查 + +- 标题是否能单独表达结论。 +- 字体和颜色是否统一。 +- 截图是否清晰。 +- 图表文字是否不截断。 +- 每页是否有明确层级。 +- 是否隐藏内部 AI 配置、密钥和调试信息。 diff --git a/.claude/skills/edge-project-overview/SKILL.md b/.claude/skills/edge-project-overview/SKILL.md new file mode 100644 index 0000000..c3cc18d --- /dev/null +++ b/.claude/skills/edge-project-overview/SKILL.md @@ -0,0 +1,72 @@ +--- +name: edge-project-overview +description: edge_collector 项目概览维护规范。用于读取、生成或更新本仓库项目总览,沉淀边缘侧、云平台、前端、协议采集、脚本部署、远程主机、运行目录、动态配置保护和常用验证命令,帮助新任务快速建立上下文。 +--- + +# edge_collector 项目概览 + +## 何时使用 + +- 用户要求“整理项目概览”“说明当前工程结构”。 +- 新增较大功能前需要建立上下文。 +- 部署、协议、前端、云端多模块同时涉及。 +- 文档或 skill 需要引用项目约定。 + +## 建议落点 + +默认维护: + +```text +docs/project-overview.md +``` + +如果已有同类文档,优先更新已有文档,不新增重复总览。 + +## 必须覆盖 + +```text +项目定位 +模块结构 +边缘侧服务 +云平台服务 +前端应用 +协议采集架构 +运行目录 runtime/edge +配置文件与动态配置保护 +构建与打包脚本 +部署与同步脚本 +常用远程主机 +常用验证命令 +风险与注意事项 +推荐阅读路径 +``` + +## 事实来源 + +生成或更新概览时优先读取: + +- `CMakeLists.txt`、`collector/CMakeLists.txt` +- `package.sh` +- `deploy_cloud.sh` +- `scripts/migrate_edge.sh` +- `scripts/install_all.sh` +- `frontend/*/package.json` +- `configurator/config/` +- `collector/docs/` +- `.agents/skills/` + +## 环境与主机 + +概览可记录常用主机,但不要写敏感密钥: + +- 82:常用 arm64 发布构建主机。 +- 97:arm64 编译/同步验证主机。 +- 94、87:边缘运行验证主机。 +- 云平台:`119.45.4.75`。 + +## 输出要求 + +- 明确“已确认事实”和“从文件推断”。 +- 不把历史临时问题写成永久事实。 +- 动态配置保护规则要写清楚,避免打包/同步覆盖运行配置。 +- 同主机编译部署也要使用 `scripts/migrate_edge.sh` 的规范需要写入。 diff --git a/.claude/skills/edge-project-plan-writer/SKILL.md b/.claude/skills/edge-project-plan-writer/SKILL.md new file mode 100644 index 0000000..bb71831 --- /dev/null +++ b/.claude/skills/edge-project-plan-writer/SKILL.md @@ -0,0 +1,80 @@ +--- +name: edge-project-plan-writer +description: edge_collector 项目计划与实施拆分编写规范。用于为协议适配、云平台功能、边缘 agent、AI 分析、本地模型、前端页面、部署机制、故障治理等工作编写项目计划、里程碑、任务拆分、风险清单和验证排期。 +--- + +# edge_collector 项目计划编写 + +## 适用场景 + +- 新协议适配计划。 +- 云平台功能迭代计划。 +- 边缘独立 agent 实施计划。 +- AI 分析或本地模型接入计划。 +- 前端复杂页面改造计划。 +- 部署/打包/同步机制优化计划。 +- 故障治理和稳定性专项计划。 + +## 推荐结构 + +```text +目标与范围 +现状与约束 +阶段划分 +里程碑 +任务拆分 +依赖关系 +验证计划 +部署计划 +风险与缓解 +交付物 +``` + +## 阶段模板 + +```text +阶段 1:调研与方案 +阶段 2:最小可用实现 +阶段 3:联调与异常场景 +阶段 4:部署验证 +阶段 5:文档与交付 +``` + +按任务实际裁剪,不要机械套用。 + +## 任务拆分要求 + +每个任务写清: + +- 目标。 +- 涉及目录。 +- 负责人或执行对象。 +- 前置依赖。 +- 验收标准。 +- 验证命令或验证页面。 + +## 当前项目必须考虑 + +- 是否影响 `collector` 稳定性。 +- 是否影响 `runtime/edge` 或 `runtime/cloud_server` 动态配置。 +- 是否需要 82/97/94/87 或云服务器验证。 +- 是否需要修改 `package.sh`、`deploy_cloud.sh`、`scripts/migrate_edge.sh`。 +- 是否需要新增 systemd 服务或独立 agent。 +- 是否需要用户文档和技术文档分开。 + +## 风险清单 + +常见风险: + +- 跨架构第三方库不可用。 +- 目标主机系统版本差异。 +- 前端构建环境不一致。 +- 配置同步覆盖运行态文件。 +- AI Provider 超时、费用或响应格式差异。 +- 真实设备不可用导致只能 mock 验证。 + +## 输出要求 + +- 计划要能直接转成执行清单。 +- 不确定项标为“待确认”,不要伪装成已完成。 +- 时间排期必须留出联调、回归和远程部署验证。 diff --git a/.claude/skills/edge-protocol-research/SKILL.md b/.claude/skills/edge-protocol-research/SKILL.md new file mode 100644 index 0000000..c98fa4e --- /dev/null +++ b/.claude/skills/edge-protocol-research/SKILL.md @@ -0,0 +1,81 @@ +--- +name: edge-protocol-research +description: edge_collector 协议调研与适配评估规范。用于调研 FANUC、西门子 CNC、PLC、传感器、第三方 SDK 或参考仓实现时,按当前协议模板、驱动代码、参考实现、官方资料和验证计划输出适配差异、点位补充和实现建议。 +--- + +# edge_collector 协议调研 + +## 适用场景 + +- 新增协议驱动。 +- 完善 FANUC/西门子 CNC 点位。 +- 参考外部仓采集程序。 +- 判断第三方 SDK 架构和库是否可用。 +- 协议模板是否合理、是否缺常用点位。 + +## 调研顺序 + +1. 读取当前协议模板和用户可见描述。 +2. 读取当前驱动实现和文档。 +3. 对比参考仓或历史实现。 +4. 拿不准的协议语义联网查官方资料或 SDK 文档。 +5. 输出差异、风险、实施建议和验证计划。 + +## 当前项目路径 + +优先查看: + +- `configurator/config/templates/` +- `configurator/config/protocols/` +- `collector/src/driver/` +- `collector/docs/protocols/` +- `third_party/` +- `collector/CMakeLists.txt` + +## 对比重点 + +- 连接参数是否够用。 +- 点位名称、类型、单位、默认采集周期是否合理。 +- 模板点位和驱动读取逻辑是否一致。 +- 是否保留现有 `PointData::UpdateValue` 行为。 +- 用户可见协议介绍是否隐藏内部技术细节。 +- 第三方库是否按架构分层放置。 +- ARM64/ARM32/x64 构建模式是否明确。 + +## 联网规则 + +遇到以下情况必须联网查证: + +- 协议函数含义不确定。 +- SDK 架构、库名、系统依赖不确定。 +- 西门子/FANUC 指标语义不确定。 +- 第三方资料可能过期。 + +优先官方文档、SDK 手册、厂商资料;社区资料只能作为补充。 + +## 输出格式 + +```markdown +## 当前现状 + +## 参考实现差异 + +## 点位/参数建议 + +## 驱动实现建议 + +## 构建与第三方库影响 + +## 用户文档影响 + +## 验证计划 + +## 风险与待确认 +``` + +## 禁止事项 + +- 不凭猜测写协议语义。 +- 不提交未知来源二进制库。 +- 不把参考仓问题照搬进当前工程。 +- 不在用户可见协议介绍中写 helper、SDK 路径、库文件细节。 diff --git a/.claude/skills/edge-prototype-design/SKILL.md b/.claude/skills/edge-prototype-design/SKILL.md new file mode 100644 index 0000000..5f81703 --- /dev/null +++ b/.claude/skills/edge-prototype-design/SKILL.md @@ -0,0 +1,50 @@ +--- +name: edge-prototype-design +description: edge_collector 高保真原型设计规范。用于设计边缘侧或云平台前端页面、复杂交互、管理后台页面、AI 分析、WiFi、端口转发、OTA、数据趋势等功能原型时,先分析现有 React/CSS Modules 风格,再输出适合当前项目落地的原型和实现建议。 +--- + +# edge_collector 原型设计 + +## 适用范围 + +- `frontend/config_app` 边缘侧页面。 +- `frontend/cloud_app` 云平台页面。 +- AI 分析、WiFi 管理、端口转发、内网穿透、OTA、离线缓存、数据趋势等复杂页面。 + +## 工作流 + +1. 阅读现有页面和 CSS Modules,提取当前视觉语言。 +2. 明确目标用户和核心任务。 +3. 先画信息架构和布局,不急着写代码。 +4. 给出关键状态:加载、空状态、失败、保存中、禁用、权限不足。 +5. 再进入实现,遵循 `frontend-ui-conventions`。 + +## 原型输出形式 + +按任务选择: + +- 文档内 ASCII 线框:适合接口/流程方案。 +- HTML 静态原型:适合复杂页面评估。 +- React 组件草案:适合直接落地到现有前端。 +- SVG 交互说明图:适合文档配图。 + +## 本项目 UI 约束 + +- 使用 React + Vite + CSS Modules。 +- 保持现有暗色主题。 +- 暗色底、细边框、蓝紫主操作色、状态色克制使用。 +- 避免营销页式大渐变和装饰感过强的科技视觉。 +- 原型应呈现工业网关/运维工具气质,优先清晰、稳定、可操作。 +- 不引入 Ant Design、Tailwind 或新的 UI 框架。 +- 按 `frontend-dialog` 使用统一弹窗。 +- 普通操作按钮、危险按钮、启停按钮样式要与现有模块一致。 +- 页面首屏应是可用工具,不做营销式 landing page。 + +## 设计检查 + +- 右侧是否有大片空白。 +- 文本是否溢出或遮挡。 +- 操作后是否有 loading/反馈。 +- 是否支持窄屏。 +- 危险操作是否二次确认。 +- 用户可见文案是否隐藏内部实现细节。 diff --git a/.claude/skills/edge-python-agent/SKILL.md b/.claude/skills/edge-python-agent/SKILL.md new file mode 100644 index 0000000..f6a1aa7 --- /dev/null +++ b/.claude/skills/edge-python-agent/SKILL.md @@ -0,0 +1,88 @@ +--- +name: edge-python-agent +description: edge_collector Python 独立 agent 开发规范。用于新增或修改 frpc agent、port forward agent、WiFi/4G 辅助进程、巡检脚本、数据分析脚本等 Python3 常驻或命令行工具时,统一配置、日志、systemd、资源占用、退出码和与 edge 主服务解耦要求。 +--- + +# edge_collector Python Agent + +## 适用范围 + +- `scripts/frp/start_*.py` +- `scripts/port_forward/start_*.py` +- WiFi/4G 辅助脚本。 +- 远程巡检和数据分析脚本。 +- 需要 systemd 托管的 Python 常驻进程。 + +## 命名与位置 + +- 常驻启动脚本使用 `start_` 前缀,保持现有风格。 +- 按功能放到独立目录,例如 `scripts/frp/`、`scripts/port_forward/`。 +- 不要把独立 agent 代码塞进 `collector` 或 `configurator`。 + +## 配置 + +- 配置文件放到运行目录的 `config/` 或功能子目录。 +- 支持配置缺失时生成默认文件,但不得覆盖已有配置。 +- 动态配置必须被打包和同步规则排除,避免目标主机运行配置被覆盖。 +- 密钥、Token、URL 不写死在代码中。 + +## 日志 + +- 使用 Python `logging`。 +- 日志包含时间、级别、模块、关键状态。 +- 不打印密钥、密码、Token。 +- 高频循环日志要限流,避免 CPU/磁盘压力。 +- 错误日志要保留具体原因,供前端展示更明确错误。 + +## 常驻进程要求 + +- 支持优雅退出 `SIGTERM`/`SIGINT`。 +- 主循环有固定 sleep 或事件等待,禁止无休眠空转。 +- 外部命令调用设置 timeout。 +- 子进程必须回收。 +- 网络请求必须设置连接和读取超时。 +- 异常后退避重试,不要短时间无限重启。 + +## systemd + +服务文件应明确: + +```text +WorkingDirectory +ExecStart +Restart=on-failure +RestartSec +User +Environment +``` + +新增服务应独立,不替代已有 `edge`、`frpc_agent` 或其他服务,除非用户明确要求迁移。 + +## CLI + +建议支持: + +```bash +--config +--log-level +--once +--dry-run +``` + +`--once` 适合调试和安装后验证。 + +## 验证 + +至少验证: + +```bash +python3 -m py_compile scripts//.py +python3 scripts//.py --help +``` + +常驻服务验证: + +```bash +systemctl status --no-pager +journalctl -u --since "5 min ago" --no-pager +``` diff --git a/.claude/skills/edge-release-notes/SKILL.md b/.claude/skills/edge-release-notes/SKILL.md new file mode 100644 index 0000000..444b019 --- /dev/null +++ b/.claude/skills/edge-release-notes/SKILL.md @@ -0,0 +1,90 @@ +--- +name: edge-release-notes +description: edge_collector 版本说明与变更日志编写规范。用于提交、push、边缘包发布、云平台部署、OTA 发布、阶段交付时,生成面向用户、运维和开发的 release notes,区分用户可见变化、部署影响、配置影响、验证结果和回滚说明。 +--- + +# edge_collector 版本说明 + +## 适用场景 + +- 提交前整理变更。 +- push 后总结。 +- 边缘版本发布。 +- 云平台部署。 +- OTA 包说明。 +- 阶段交付说明。 + +## 读者分层 + +- 用户可见:功能变化、操作入口、体验优化。 +- 运维可见:部署步骤、配置变化、服务重启、回滚。 +- 开发可见:代码结构、接口、测试、技术债。 + +不要把内部 helper、SDK 路径、密钥、模型配置写进用户可见说明。 + +## 推荐结构 + +```markdown +## 版本信息 + +- 版本: +- 日期: +- 范围: + +## 用户可见变化 + +## 运维与部署影响 + +## 配置变化 + +## 修复问题 + +## 验证结果 + +## 已知风险 + +## 回滚说明 +``` + +## 从 Git 生成摘要 + +可参考: + +```bash +git log --oneline -10 +git diff --stat HEAD~1..HEAD +git status --short +``` + +只总结和本次发布相关内容,不把无关工作区改动写进版本说明。 + +## 边缘发布说明 + +必须说明: + +- 目标架构。 +- 是否需要执行 `install_all.sh`。 +- 是否需要重启 `edge` 或独立 agent。 +- 是否影响运行态动态配置。 +- 是否通过目标主机验证。 + +## 云平台发布说明 + +必须说明: + +- 是否执行 `deploy_cloud.sh`。 +- 是否使用 `--init`。 +- 是否影响 `ai_config.json`、`server_config.json`。 +- 是否重启 `cloud-server`、Mosquitto、nginx。 +- 公网接口验证结果。 + +## OTA 包说明 + +面向用户时写: + +- 新增能力。 +- 修复问题。 +- 升级注意事项。 +- 回滚建议。 + +避免写内部提交号、代码路径和密钥。 diff --git a/.claude/skills/edge-release-prepare/SKILL.md b/.claude/skills/edge-release-prepare/SKILL.md new file mode 100644 index 0000000..f2d5b05 --- /dev/null +++ b/.claude/skills/edge-release-prepare/SKILL.md @@ -0,0 +1,67 @@ +--- +name: edge-release-prepare +description: 边缘侧版本发布确认流程。用于用户说“发布边缘侧版本”“准备发布边缘侧版本”“发布边缘侧版本 版本号 xxx”等场景:先去 82 主机拉取最新代码,整理版本信息和版本说明,等待用户确认后才允许执行 package.sh --publish。 +--- + +# 边缘侧版本发布确认 + +## 何时使用 + +- 发布边缘侧版本 +- 准备发布边缘侧版本 +- 发布边缘侧版本 版本号 xxx +- 先整理边缘侧发布说明 + +## 固定环境 + +- 82 主机:`cat@192.168.40.82` +- 82 代码根目录:`/home/cat/code/edge_collector` +- 构建目标:`arm64` +- 默认云平台:`http://119.45.4.75` +- 默认云平台账号:`admin` + +## 第一阶段:只准备,不发布 + +用户说“发布边缘侧版本”时,先执行: + +```bash +sshpass -p 'i7568737i' ssh -o StrictHostKeyChecking=no cat@192.168.40.82 \ + 'cd /home/cat/code/edge_collector && git pull && git status --short && git log -5 --oneline' +``` + +然后根据用户输入和最新代码状态整理发布草案,至少包含: + +- 目标版本号:用户已给则使用;未给则请用户确认版本号 +- 目标架构:`arm64` +- 发布类型:用户已给则使用;未给则建议 `release` 或请用户选择 +- 产物名称:`publish/edge__arm64.tar.gz` +- 版本信息:一句话概括本次发布 +- 版本说明:多行列点,来自用户说明、最近提交、已完成改动和验证结果 +- 后续动作预览:确认后将在 82 执行 `./package.sh --publish --version `,必要时上传云平台 + +## 确认边界 + +- 在用户明确确认前,禁止执行 `./package.sh --publish --version ...` +- 在用户明确确认前,禁止上传云平台 +- 用户确认后,再按 `edge-82-release` 的发布流程执行 + +## 建议输出格式 + +```text +发布草案: +- 版本号: +- 架构:arm64 +- 发布类型:release +- 产物:publish/edge__arm64.tar.gz + +版本信息: +<一句话说明> + +版本说明: +- <说明 1> +- <说明 2> + +确认后执行: +1. 82: ./package.sh --publish --version +2. 如需上传云平台,使用上述版本说明和发布类型 +``` diff --git a/.claude/skills/edge-release-prepare/agents/openai.yaml b/.claude/skills/edge-release-prepare/agents/openai.yaml new file mode 100644 index 0000000..46465f8 --- /dev/null +++ b/.claude/skills/edge-release-prepare/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: 边缘发布确认 +short_description: 去82拉最新代码,整理版本信息和说明,确认后才发布 +default_prompt: Use this skill when the user says "发布边缘侧版本" or asks to prepare an edge-side release. First SSH to cat@192.168.40.82, cd /home/cat/code/edge_collector, run git pull, inspect git status and recent commits, then produce a release draft with version, arm64 architecture, release type, artifact name, version summary, and release notes. Do not run ./package.sh --publish or upload to the cloud until the user explicitly confirms. diff --git a/.claude/skills/edge-remote-access/SKILL.md b/.claude/skills/edge-remote-access/SKILL.md new file mode 100644 index 0000000..3a6f9df --- /dev/null +++ b/.claude/skills/edge-remote-access/SKILL.md @@ -0,0 +1,110 @@ +--- +name: edge-remote-access +description: edge_collector 远程主机访问与操作规范。用于 SSH 到 82/87/94/97 等边缘主机或云服务器执行命令、采集日志、传文件、检查端口、做临时隧道和远程排障时,约束只读优先、命令安全、敏感信息脱敏和避免误操作。 +--- + +# edge_collector 远程访问 + +## 主机解析 + +数字主机连接规则由 `host-connection-defaults` 提供: + +```text +87 -> cat@192.168.40.87 +97 -> cat@192.168.40.97 +``` + +云服务器常用: + +```text +ubuntu@119.45.4.75 +``` + +如果用户明确给出账号、IP、密码或端口,以用户本次说明为准。 + +## 操作原则 + +- 只读排查优先。 +- 多条只读命令可以合并一次 SSH 执行。 +- 写操作、重启、删除、同步、清库必须有用户明确要求。 +- 不要在最终回复中暴露密码、Token、Key。 +- 不要手写替代项目已有部署脚本。 + +## 常用只读命令 + +```bash +hostname +uptime +date +uname -a +df -h +free -h +ss -lntp +systemctl status edge --no-pager +journalctl -u edge --since "10 min ago" --no-pager +``` + +云端: + +```bash +systemctl status cloud-server --no-pager +journalctl -u cloud-server --since "10 min ago" --no-pager +systemctl status mosquitto --no-pager +``` + +## 传文件 + +优先使用项目脚本: + +- 边缘同步:`scripts/migrate_edge.sh` +- 云平台部署:`deploy_cloud.sh` + +只有用户要求临时取日志、截图或单个文件时,才使用 `scp`/`rsync`。传输前说明源路径、目标路径和是否覆盖。 + +## sudo + +使用 sudo 前先确认是否必要。常见只读 sudo: + +```bash +sudo journalctl -u edge --since "10 min ago" --no-pager +sudo systemctl status edge --no-pager +``` + +避免执行: + +```bash +sudo rm -rf +sudo systemctl restart +sudo apt install +``` + +除非用户明确要求。 + +## 端口和网络 + +检查端口: + +```bash +ss -lntp +curl -s http://127.0.0.1/api/status +curl -s http://127.0.0.1:8081/api/health +``` + +排查同网段设备时: + +```bash +ip addr +ip route +ip neigh show +arp -an | grep +``` + +## 输出要求 + +最终说明: + +- 连接的主机。 +- 执行的关键只读检查。 +- 发现的异常证据。 +- 未执行的高风险动作。 +- 建议下一步。 diff --git a/.claude/skills/edge-requirement-interview/SKILL.md b/.claude/skills/edge-requirement-interview/SKILL.md new file mode 100644 index 0000000..78f5d0c --- /dev/null +++ b/.claude/skills/edge-requirement-interview/SKILL.md @@ -0,0 +1,60 @@ +--- +name: edge-requirement-interview +description: edge_collector 需求采访规范。用于较大功能、跨模块改造、协议适配、云端 AI、边缘 agent、前端复杂页面、部署机制调整或重要文档编写前,通过少量关键问题澄清目标、范围、成功标准、约束和交付物。 +--- + +# edge_collector 需求采访 + +## 触发场景 + +- 新增协议或重构协议采集。 +- 新增边缘独立 agent 或 systemd 服务。 +- 云平台新增 AI、数据分析、设备管理能力。 +- 前端新增复杂页面或复杂交互。 +- 修改打包、同步、部署、运行目录规则。 +- 编写重要方案、详细设计或交付文档。 + +## 原则 + +- 一次只问一个关键问题。 +- 优先问影响方案方向的问题。 +- 最多 8 个问题;需求很明确时可以少问或不问。 +- 用户已经给出明确实施指令时,不用采访拖延,直接执行并在关键假设处说明。 + +## 标准问题池 + +按需要选择: + +1. 核心目标是什么,完成后用户能做什么? +2. 涉及哪些模块,哪些明确不包含? +3. 成功标准是什么,如何验证? +4. 目标用户是谁,是现场用户、运维还是开发? +5. 是否需要兼容已有配置、协议模板或运行数据? +6. 是否涉及 82/97/94/87 或云服务器部署验证? +7. 是否允许新增独立进程、配置文件或 systemd 服务? +8. 文档需要写给谁看,放到哪个目录? + +## 输出摘要 + +采访结束或信息足够时,输出: + +```markdown +## 需求摘要 + +- 目标: +- 范围: +- 不包含: +- 成功标准: +- 关键约束: +- 交付物: +- 验证方式: +- 待确认: +``` + +## 本项目特别关注 + +- 不能覆盖运行时动态配置。 +- 用户可见协议介绍不暴露内部技术细节。 +- 边缘采集稳定性优先于 UI 或辅助功能。 +- 新增常驻进程应独立,不耦合 `edge` 主服务。 +- 同步部署遵循 `scripts/migrate_edge.sh`。 diff --git a/.claude/skills/edge-security-secrets/SKILL.md b/.claude/skills/edge-security-secrets/SKILL.md new file mode 100644 index 0000000..576b284 --- /dev/null +++ b/.claude/skills/edge-security-secrets/SKILL.md @@ -0,0 +1,96 @@ +--- +name: edge-security-secrets +description: edge_collector 密钥、权限和敏感配置处理规范。用于处理 AI Key、JWT secret、MQTT dynsec、SSH 密码、数据库密码、Token、配置同步、日志脱敏、提交检查和用户可见文档时,防止泄露、覆盖运行密钥或把敏感信息提交到仓库。 +--- + +# edge_collector 敏感配置安全 + +## 敏感信息范围 + +- AI Provider API Key。 +- `jwt_secret`。 +- terminal `credential_key`。 +- MQTT dynsec 管理员和客户端密码。 +- 数据库账号密码。 +- SSH 密码和私钥。 +- Token、Cookie、Session。 +- 内网穿透访问密钥。 +- 客户设备真实敏感地址。 + +## 基本原则 + +- 不在最终回复中打印完整密钥。 +- 不把密钥写死进代码。 +- 不提交真实配置。 +- 不用打包产物覆盖远端运行密钥。 +- 日志和前端错误提示要脱敏。 +- 用户文档隐藏内部模型、Provider、Key、URL 中的敏感部分。 + +## 配置文件 + +运行态配置优先保存在目标主机 `runtime` 或服务目录下: + +- `runtime/edge/config/` +- `~/cloud_server/config/server_config.json` +- `~/cloud_server/config/ai_config.json` + +打包和同步脚本必须保护动态配置。修改以下脚本时要特别检查: + +- `package.sh` +- `deploy_cloud.sh` +- `scripts/migrate_edge.sh` +- `scripts/install_all.sh` + +## 脱敏规则 + +展示时保留前后少量字符: + +```text +sk-abc...xyz +``` + +URL 中如包含 key、token、password 参数,必须隐藏参数值。 + +日志中禁止输出: + +```text +Authorization +api_key +password +secret +token +credential_key +``` + +## 提交前检查 + +提交前建议: + +```bash +git status --short +git diff --cached +rg -n "api[_-]?key|password|secret|token|credential_key|Authorization" . +``` + +发现真实密钥时: + +1. 不提交。 +2. 改为配置文件或环境变量。 +3. 如已暴露,提醒用户轮换密钥。 + +## 云平台 AI 配置 + +- 后端配置可保存 Provider、Base URL、模型名、思考模式等。 +- 前端和报告不展示内部 Provider 名称、模型细节和 Key。 +- AI 请求失败日志可以记录错误类型和状态码,但不要记录 Key。 + +## 远程操作 + +- SSH 命令中可使用既有默认连接规则,但最终回复不要打印密码。 +- 采集远程配置时,输出前先脱敏。 +- 复制配置文件前确认是否包含密钥。 + +## 用户可见文档 + +- 协议介绍、用户手册、AI 报告、导出报告不写内部技术细节和密钥。 +- 运维文档可以写配置路径和字段含义,但示例值必须使用占位符。 diff --git a/.claude/skills/edge-skill-builder/SKILL.md b/.claude/skills/edge-skill-builder/SKILL.md new file mode 100644 index 0000000..e554a7f --- /dev/null +++ b/.claude/skills/edge-skill-builder/SKILL.md @@ -0,0 +1,81 @@ +--- +name: edge-skill-builder +description: edge_collector 技能建设规范。用于从外部仓库、已有流程、项目经验、框架/SDK 学习结果中创建或改写 .agents/skills 下的 Codex skills,要求结合当前 C++/Drogon/React/Vite/云边部署/现场主机实际情况,避免原封不动照搬无关技术栈。 +--- + +# edge_collector 技能建设 + +## 目标 + +把项目中重复出现的流程和判断沉淀为 `.agents/skills//SKILL.md`,让后续任务能稳定复用。 + +## 适用来源 + +- 当前仓库脚本,如 `deploy_cloud.sh`、`package.sh`、`scripts/migrate_edge.sh`。 +- 已完成的故障排查和现场经验。 +- 外部仓库中的通用 skill。 +- 官方文档或 SDK 调研结果。 +- 用户明确确认的长期规则。 + +## 命名规则 + +- 使用小写短横线。 +- 本项目专用优先加 `edge-` 前缀。 +- 云平台专用可用 `cloud-` 前缀。 +- 名称要表达动作或场景,例如 `edge-frontend-testing`。 + +## frontmatter + +只写: + +```yaml +--- +name: +description: <做什么 + 什么时候使用 + 当前项目关键上下文> +--- +``` + +`description` 必须包含触发词,例如“部署云平台”“前端测试”“协议适配”“同步到97”。 + +## 改写原则 + +- 先读当前仓库真实文件,再写 skill。 +- 保留流程骨架,替换成当前工程技术栈。 +- 删除 Java、Spring、K3s、Ant Design、Umi、ClickHouse 等与当前工程不匹配的固定假设,除非当前文件真实使用。 +- 不写通用教程,只写能指导本仓库工作的规则。 +- 不把临时现场处理写成永久规则。 + +## 当前项目必须体现 + +- C++ collector 和 Drogon 后端。 +- React/Vite/CSS Modules 前端。 +- `runtime/edge` 与 `runtime/cloud_server`。 +- `package.sh`、`deploy_cloud.sh`、`scripts/migrate_edge.sh`。 +- 82/97/94/87 边缘主机和云服务器 `119.45.4.75`。 +- 运行态动态配置不能被打包或同步覆盖。 +- 用户可见文档不能透露内部技术细节。 + +## 校验 + +新增或修改 skill 后执行: + +```bash +python3 /home/cloud/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/ +``` + +同时检查: + +```bash +grep -R "Java\\|Spring\\|K3s\\|Ant Design\\|Umi\\|one_person" -n .agents/skills/ || true +``` + +如果出现这些词,要确认是项目真实需要,还是外部 skill 残留。 + +## 输出 + +最终向用户说明: + +- 新增或修改了哪些 skill。 +- 每个 skill 覆盖什么场景。 +- 是否通过校验。 +- 是否只改了 skill 文件,是否未提交。 diff --git a/.claude/skills/edge-spreadsheet-docs/SKILL.md b/.claude/skills/edge-spreadsheet-docs/SKILL.md new file mode 100644 index 0000000..98887a1 --- /dev/null +++ b/.claude/skills/edge-spreadsheet-docs/SKILL.md @@ -0,0 +1,83 @@ +--- +name: edge-spreadsheet-docs +description: edge_collector 表格、CSV、XLSX 文档处理规范。用于整理点位清单、协议模板、测试用例、数据质量统计、AI 分析数据摘要、设备清单、问题回溯表和导入导出表格,支持读取、生成、校验 CSV/XLSX。 +--- + +# edge_collector 表格文档处理 + +## 适用场景 + +- 点位清单和协议模板对照。 +- 系统测试用例表。 +- 数据质量统计表。 +- 设备/网关清单。 +- 故障问题回溯表。 +- AI 分析输入/输出摘要。 +- CSV/XLSX 导入导出检查。 + +## 工具选择 + +- 简单 CSV:优先用 Python `csv` 或 `pandas`。 +- XLSX 格式和样式:使用 `openpyxl`。 +- 需要公式:使用 Excel 公式,不在 Python 中硬编码计算结果。 +- 大文件分析:分块读取,避免一次性加载导致内存过高。 + +## 表格设计 + +每张表应明确: + +- 表名。 +- 数据来源。 +- 时间范围。 +- 字段含义。 +- 单位。 +- 是否脱敏。 +- 生成时间。 + +## 当前项目常用列 + +点位/设备: + +```text +网关名称, 网关ID, 设备名称, 设备ID, 点位名称, 点位ID, 协议, 数据类型, 单位, 说明 +``` + +测试用例: + +```text +编号, 模块, 场景, 前置条件, 操作步骤, 预期结果, 实际结果, 状态, 问题记录 +``` + +数据质量: + +```text +对象, 时间范围, 原始点数, 有效点数, 分析点数, 最大间隔, 缺口数量, 重复值比例, 结论 +``` + +## 校验 + +CSV: + +```bash +python3 - <<'PY' +import csv +with open("file.csv", newline="", encoding="utf-8-sig") as f: + rows = list(csv.reader(f)) +print(len(rows), rows[0] if rows else []) +PY +``` + +XLSX: + +```python +from openpyxl import load_workbook +wb = load_workbook("file.xlsx", data_only=False) +print(wb.sheetnames) +``` + +## 注意 + +- 中文 CSV 优先使用 `utf-8-sig`,方便 Excel 打开。 +- 导出给用户的表格不要出现内部字段名、接口路径或密钥。 +- 公式表必须检查 `#REF!`、`#DIV/0!`、`#VALUE!`、`#NAME?`。 +- 修改既有模板时保留原列顺序和样式,除非用户明确要求调整。 diff --git a/.claude/skills/edge-svg-diagram/SKILL.md b/.claude/skills/edge-svg-diagram/SKILL.md new file mode 100644 index 0000000..47e2c02 --- /dev/null +++ b/.claude/skills/edge-svg-diagram/SKILL.md @@ -0,0 +1,59 @@ +--- +name: edge-svg-diagram +description: edge_collector 可编辑 SVG 图示规范。用于用户要求画架构图、部署拓扑图、协议链路图、流程图、方案配图、报告示意图时,生成可提交到 docs 的静态 SVG,并结合当前边缘/云平台/鲁班猫/协议采集场景设计。 +--- + +# edge_collector SVG 图示 + +## 适用场景 + +- 云边架构图 +- 边缘 runtime 目录结构图 +- 协议采集链路图 +- FANUC helper / proxy 架构图 +- OTA 升级流程图 +- AI 本地模型部署图 +- WiFi/4G/frpc/端口转发 agent 关系图 + +## 输出位置 + +- 文档配图优先放在对应文档旁边的子目录,例如 `docs/assets/` 或专题目录下。 +- 文件名使用清晰中文或 `snake_case`,扩展名 `.svg`。 +- Markdown 中使用相对路径引用。 + +## 设计要求 + +- SVG 必须可编辑、可 diff。 +- 使用真实项目元素命名:`collector`、`configurator`、`cloud_server`、`runtime/edge`、`scripts/migrate_edge.sh`。 +- 不使用复杂渐变和难维护滤镜。 +- 字号、间距、线条保持清晰,适合 Markdown 预览。 +- 区域超过 3 个或节点超过 8 个时,先做布局骨架,再补细节。 + +## 风格规则 + +按用途选择风格,不要混用: + +- 文档/方案/报告配图:优先浅色、打印友好,白色或近白背景,深色文字,少量蓝/绿/橙用于区分云端、边缘、设备、风险。 +- 前端原型/交互说明图:应贴近当前前端暗色风格,参考 `frontend/config_app` 和 `frontend/cloud_app` 的视觉基线: + - 背景:`#0d0d14`、`#14141e` + - 边框:`#2a2a3a` + - 主文字:`#e0e0e0` + - 标题/高亮文字:`#ffffff` + - 强调色:`#6366f1` + - 状态色按现有页面语义选择,避免一整张图只有紫蓝色 + - 避免营销页式大渐变和装饰感过强的科技视觉 +- 用户故事中的业务场景图:优先清晰、业务化,不必强行模拟前端 UI;如果故事本身是前端页面或交互改造,再使用暗色项目风格。 + +## 推荐布局 + +- 云边拓扑:左边缘、右云端,中间网络/隧道。 +- 进程架构:上层 UI/API,中层服务,底层配置/数据库/设备。 +- 部署流程:从构建主机到 runtime 到目标主机。 + +## 验证 + +- 用浏览器或图片查看工具打开 SVG。 +- 确认文字不重叠、不截断。 +- 确认中文显示正常。 +- 确认风格与用途匹配:文档图可打印,前端原型图与项目暗色主题一致。 +- 文档引用路径有效。 diff --git a/.claude/skills/edge-sync-host/SKILL.md b/.claude/skills/edge-sync-host/SKILL.md new file mode 100644 index 0000000..ad9d699 --- /dev/null +++ b/.claude/skills/edge-sync-host/SKILL.md @@ -0,0 +1,73 @@ +--- +name: edge-sync-host +description: 边缘运行目录同步流程。用于用户说“同步到87主机”“同步到85主机”“把边缘包同步到某主机”等场景,默认执行 scripts/migrate_edge.sh,并把用户说出的数字主机作为 --dst_host;87 只是示例,其他数字主机同理。 +--- + +# 边缘运行目录同步 + +## 何时使用 + +- 同步到87主机 +- 同步到 85 主机 +- 把边缘包同步到某主机 +- 将 runtime/edge 部署到目标主机 + +## 固定规则 + +- 脚本:`scripts/migrate_edge.sh` +- 默认源:82 主机的 `/home/cat/code/edge_collector/runtime/edge` +- 默认目标目录:`/home/cat/edge` +- 数字主机解析:`87` -> `192.168.40.87` +- 默认用户:`cat` +- 默认密码:`i7568737i` +- 如果用户要求“在某台主机编译,再同步到同一台主机”,也必须使用 `scripts/migrate_edge.sh` 的同步方式;不要改成手写 `scp`、`rsync` 或本机 `cp`。 +- 同主机编译部署时,显式传入相同的源和目标主机,例如在 97 编译并同步到 97: + +```bash +bash scripts/migrate_edge.sh --src_host 97 --dst_host 97 +``` + +## 执行方式 + +用户说“同步到87主机”时,在本仓库根目录执行: + +```bash +bash scripts/migrate_edge.sh --dst_host 87 +``` + +同步完成后,必须在目标主机重启边缘服务并验证状态: + +```bash +sshpass -p 'i7568737i' ssh -o StrictHostKeyChecking=no cat@192.168.40.87 \ + 'echo i7568737i | sudo -S systemctl restart edge && systemctl is-active edge' +``` + +用户说其他数字主机时,把数字替换到 `--dst_host`: + +```bash +bash scripts/migrate_edge.sh --dst_host +``` + +随后也要把重启命令中的目标地址替换为 `192.168.40.`,执行 `sudo systemctl restart edge` 并确认 `systemctl is-active edge` 返回 `active`。 + +用户说“在 97 编译,同步到 97”这类同主机编译部署时,应先在对应主机完成构建: + +```bash +sshpass -p 'i7568737i' ssh -o StrictHostKeyChecking=no cat@192.168.40.97 \ + 'cd /home/cat/code/edge_collector && git pull && ./package.sh --edge-only' +``` + +然后仍然通过迁移脚本同步,源和目标主机保持一致: + +```bash +bash scripts/migrate_edge.sh --src_host 97 --dst_host 97 +``` + +最后重启同一台目标主机的 `edge` 服务并验证状态。 + +## 注意 + +- 87 只是示例,不是固定目标。 +- 不要手写 scp/rsync 流程,优先使用 `scripts/migrate_edge.sh`。 +- 同步成功后必须重启目标主机的 `edge` 服务;不要只同步文件就结束。 +- 如果用户明确指定源主机、目标用户、目标目录或密码,以用户本次明确值为准,并透传给脚本参数。 diff --git a/.claude/skills/edge-sync-host/agents/openai.yaml b/.claude/skills/edge-sync-host/agents/openai.yaml new file mode 100644 index 0000000..1595869 --- /dev/null +++ b/.claude/skills/edge-sync-host/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: 边缘同步主机 +short_description: 使用 migrate_edge.sh 同步 runtime/edge 并重启目标 edge 服务 +default_prompt: Use this skill when the user says "同步到87主机", "同步到85主机", or asks to sync the edge runtime package to a numbered host. Run bash scripts/migrate_edge.sh --dst_host from the repo root. Treat 87 only as an example; other numbered hosts map to 192.168.40.. After sync succeeds, SSH to cat@192.168.40. with password i7568737i, run sudo systemctl restart edge, and verify systemctl is-active edge returns active. Prefer the script over hand-written scp or rsync commands. diff --git a/.claude/skills/edge-system-test-writer/SKILL.md b/.claude/skills/edge-system-test-writer/SKILL.md new file mode 100644 index 0000000..f0af846 --- /dev/null +++ b/.claude/skills/edge-system-test-writer/SKILL.md @@ -0,0 +1,89 @@ +--- +name: edge-system-test-writer +description: edge_collector 系统测试文档编写规范。用于为协议采集、边缘前端、云平台 AI 分析、WiFi/4G、内网穿透、端口转发、OTA、离线缓存、部署同步等功能编写验收测试、系统测试、测试评审清单和问题回溯记录。 +--- + +# edge_collector 系统测试编写 + +## 适用范围 + +- 协议采集:FANUC、西门子、Modbus、OPC UA、传感器等。 +- 边缘功能:WiFi、4G、内网穿透、端口转发、离线缓存、OTA、高级功能页面。 +- 云平台:历史趋势、AI 分析、设备状态、配置管理。 +- 部署:`package.sh`、`deploy_cloud.sh`、`scripts/migrate_edge.sh`、systemd 服务。 + +## 文档落点 + +- 通用测试方案:`docs/` +- 协议测试:`collector/docs/protocols/` +- 鲁班猫/设备测试:`docs/鲁班猫*/` +- 本地模型测试:`docs/本地模型/` + +## 输出结构 + +```text +测试目标 +测试范围 +测试环境 +测试数据 +前置条件 +测试场景 +测试步骤与预期结果 +异常与恢复场景 +问题记录与回溯 +通过标准 +``` + +## 测试场景要求 + +每个功能至少覆盖: + +- 正常路径。 +- 参数非法或配置缺失。 +- 网络断开、服务重启、进程异常退出。 +- 同步/打包后动态配置是否被保留。 +- 前端操作反馈、失败提示、权限控制。 +- 远程目标主机差异,如 82/97/94/87 的架构和系统环境。 + +## 协议采集专项 + +测试点包括: + +- 驱动能否按协议模板加载。 +- 连接、读取、断线重连、设备离线恢复。 +- 点位值类型是否符合 `PointData::UpdateValue` 预期。 +- 用户可见协议介绍不暴露内部实现。 +- ARM64/ARM32 helper 或第三方库场景要覆盖构建和运行验证。 + +## 前端专项 + +测试点包括: + +- 页面不白屏。 +- 按钮有 loading、成功、失败反馈。 +- 弹窗使用项目统一对话框。 +- 窄屏和长内容不遮挡、不溢出。 +- 接口失败时展示可理解错误,不只显示通用失败。 + +## 验证命令 + +按实际改动选择: + +```bash +npm run build +cmake --build build --target collector -j2 +./package.sh --edge-only +bash -n scripts/.sh +jq empty +``` + +远程同步或重启必须等用户明确要求,并遵循对应部署 skill。 + +## 问题回溯 + +测试文档应保留问题回溯表: + +```markdown +| 问题 | 影响场景 | 根因位置 | 修复提交/文件 | 回归结果 | +|------|----------|----------|----------------|----------| +``` diff --git a/.claude/skills/edge-technical-zeroing-report/SKILL.md b/.claude/skills/edge-technical-zeroing-report/SKILL.md new file mode 100644 index 0000000..da0efc2 --- /dev/null +++ b/.claude/skills/edge-technical-zeroing-report/SKILL.md @@ -0,0 +1,80 @@ +--- +name: edge-technical-zeroing-report +description: edge_collector 技术归零与现场故障报告编写规范。用于边缘网关、协议采集、云平台、网络、4G/WiFi、内网穿透、端口转发、AI 分析、部署同步等故障需要形成正式根因报告、归零报告、事故复盘或客户交付说明时使用。 +--- + +# edge_collector 技术归零报告 + +## 目标 + +把现场故障从“现象描述”整理为证据闭环: + +```text +现象 + -> 影响范围 + -> 现场证据 + -> 排查路径 + -> 根因 + -> 修复 + -> 验证 + -> 预防措施 +``` + +## 适用故障 + +- 边缘服务异常、CPU/内存/磁盘异常。 +- 云端设备离线、历史数据缺失、AI 接口失败。 +- 协议采集失败、第三方库或跨架构运行问题。 +- 4G/WiFi、frpc、端口转发等独立 agent 异常。 +- 打包同步后运行异常、动态配置被覆盖。 + +## 报告结构 + +```text +问题概述 +影响范围 +现场环境 +现象与时间线 +证据清单 +排查过程 +根因分析 +修复措施 +验证结果 +预防措施 +结论 +``` + +## 证据要求 + +优先收集: + +- `git log`、`git status`、构建主机信息。 +- `journalctl`、应用日志、浏览器 console、接口响应。 +- `systemctl status`、进程、端口、CPU、内存、磁盘。 +- 配置文件差异,但注意隐藏密钥。 +- 远程主机系统版本和架构。 + +## 根因表达 + +结论必须具体到可操作层级: + +- 不写“网络问题”,要写是哪段链路、哪个接口、什么失败。 +- 不写“部署问题”,要写是哪个脚本、哪个文件、哪个动态配置规则。 +- 不写“兼容问题”,要写构建系统、库版本、架构或符号冲突证据。 + +## 归零判定 + +只有同时满足以下条件才写“已归零”: + +- 根因有证据支撑。 +- 修复已实施。 +- 回归验证通过。 +- 已说明预防同类问题的规则或检查项。 + +否则写“暂不具备归零条件”,并列出缺失证据。 + +## 文档落点 + +- 通用事故:`docs/` +- 鲁班猫设备:`docs/鲁班猫*/` +- 协议故障:`collector/docs/protocols/` diff --git a/.claude/skills/edge-user-manual-writer/SKILL.md b/.claude/skills/edge-user-manual-writer/SKILL.md new file mode 100644 index 0000000..f40c90d --- /dev/null +++ b/.claude/skills/edge-user-manual-writer/SKILL.md @@ -0,0 +1,56 @@ +--- +name: edge-user-manual-writer +description: edge_collector 用户手册与交付说明编写规范。用于为边缘侧前端、云平台、协议配置、AI 分析、WiFi、端口转发、内网穿透、OTA、离线缓存等用户可见功能编写操作说明、培训材料、交付文档和常见问题,避免暴露内部技术细节。 +--- + +# edge_collector 用户手册编写 + +## 读者 + +- 现场实施人员。 +- 运维人员。 +- 管理后台用户。 +- 客户侧使用人员。 + +## 文档落点 + +- 用户手册:`docs/` 或对应专题目录。 +- 协议用户说明:优先与协议文档分开,用户可见介绍不能写内部实现细节。 +- 鲁班猫设备操作:`docs/鲁班猫*/`。 + +## 推荐结构 + +```text +功能用途 +适用场景 +使用前准备 +操作步骤 +参数说明 +状态说明 +常见问题 +注意事项 +``` + +## 写作规则 + +- 面向用户目标写,不按代码模块写。 +- 只写用户能看到、能操作、能验证的内容。 +- 隐藏内部模型名、AI Provider 名称、helper、进程、库路径等技术细节,除非读者是运维人员且文档明确为运维手册。 +- 参数说明要写“影响和建议值”,不要只复述字段名。 +- 错误说明要写用户下一步可以怎么处理。 + +## 当前项目常见功能口径 + +- AI 分析:说明分析深度、提示词、数据不连续的业务原因,不显示内部 AI 配置。 +- WiFi 管理:说明扫描、刷新、加入隐藏网络、已保存网络连接、自动连接。 +- 内网穿透:说明映射启停、保存配置、云端配置失败提示。 +- 端口转发:说明规则启停、监听地址、目标地址、冲突端口。 +- 离线缓存:说明最大缓存、保留天数、重传批次、重传速率的影响。 + +## 检查清单 + +- 功能名称和界面文案一致。 +- 操作步骤能被现场用户照着完成。 +- 参数默认值和当前代码/配置一致。 +- 没有泄露内部接口、密钥、模型、库路径。 +- 有失败场景和恢复建议。 diff --git a/.claude/skills/edge-user-story-reviewer/SKILL.md b/.claude/skills/edge-user-story-reviewer/SKILL.md new file mode 100644 index 0000000..a2d0d9b --- /dev/null +++ b/.claude/skills/edge-user-story-reviewer/SKILL.md @@ -0,0 +1,108 @@ +--- +name: edge-user-story-reviewer +description: edge_collector 用户故事评审规范。用于审查协议适配、边缘功能、云平台功能、AI 分析、本地模型、前端页面、部署运维等用户故事是否清晰、可测、范围合适、验收标准完整,并识别拆分建议和风险。 +--- + +# edge_collector 用户故事评审 + +## 目标 + +确认用户故事能进入方案设计或实现阶段,避免范围不清、验收不可测、实现边界混乱。 + +## 评审结论 + +- 通过:可进入设计或实现。 +- 有条件通过:小问题已列出,可同步修正。 +- 不通过:存在严重范围、验收或安全风险。 + +## 检查维度 + +### 1. 价值清晰 + +- 是否写清角色、动作、价值。 +- 是否能说明“不做有什么影响”。 +- 是否避免只写“实现某接口/改某文件”。 + +### 2. 范围合适 + +- 一个故事是否只交付一个清晰能力。 +- 是否混入多个独立功能。 +- 是否写清不包含范围。 +- 是否能在一次迭代中完成验证。 + +### 3. 验收可测 + +- 每个验收场景是否有 Given/When/Then 或等价描述。 +- 是否覆盖正常路径、异常路径、边界条件。 +- 是否写明验证方式。 +- 是否能通过页面、接口、日志、构建、远程主机或设备验证。 + +### 4. 项目约束 + +- 是否会覆盖运行时动态配置。 +- 是否需要 `install_all.sh`,是否明确触发条件。 +- 是否涉及 82/97/94/87 或云服务器验证。 +- 是否需要新增 systemd 服务或独立 agent。 +- 是否影响 `collector` 稳定性。 + +### 5. 用户可见信息 + +- 是否泄露 helper、SDK、库路径、AI Provider、模型内部配置、密钥。 +- 用户文案是否面向现场用户或运维人员。 +- 错误提示是否可理解。 + +### 6. 拆分建议 + +遇到以下情况建议拆分: + +- 一个故事包含 4 个以上主要验收场景。 +- 同时改边缘、云端、前端、部署且无法独立验证。 +- 同时包含功能开发和大规模重构。 +- 同时包含用户功能和运维自动化。 +- 协议适配同时覆盖多个设备族或多个 SDK 运行方式。 + +### 7. 业务场景图 + +- 复杂流程、云边链路、协议采集链路、部署流程、AI 分析数据流、前端多区域交互是否提供 SVG。 +- SVG 是否放在用户故事文档旁边的 `assets/` 并被 Markdown 正文引用。 +- 图中是否只表达用户、业务对象、流程、状态和结果。 +- 是否泄露 helper、SDK、库路径、AI Key、内部模型配置、接口路径或调试信息。 +- 文档/方案型故事的图是否适合 Markdown 和打印预览。 +- 前端交互型故事的图是否贴近当前暗色前端风格。 + +## 输出格式 + +```markdown +## 评审结论 + +通过 / 有条件通过 / 不通过 + +## 问题列表 + +| 级别 | 位置 | 问题 | 影响 | 建议 | +|------|------|------|------|------| + +## 拆分建议 + +## 需要补充的验收标准 + +## 业务场景图检查 + +## 风险与待确认 +``` + +## 严重问题示例 + +- 没有验收标准。 +- 验收标准无法验证。 +- 没有写不包含范围,导致明显范围膨胀。 +- 涉及部署同步但未说明运行配置保护。 +- 涉及 AI Key、密码、Token 却没有安全边界。 +- 协议适配没有真实设备或 mock 验证方案。 +- 复杂用户故事缺少业务场景 SVG,导致流程和边界无法直观看清。 + +## 与其他 skill 协作 + +- 发现需求不清:转 `edge-requirement-interview`。 +- 发现规则未沉淀:转 `edge-business-rule-extractor`。 +- 发现故事过大:建议拆分后再进入 `edge-design-doc-writer`。 diff --git a/.claude/skills/edge-user-story-writer/SKILL.md b/.claude/skills/edge-user-story-writer/SKILL.md new file mode 100644 index 0000000..3b0a325 --- /dev/null +++ b/.claude/skills/edge-user-story-writer/SKILL.md @@ -0,0 +1,151 @@ +--- +name: edge-user-story-writer +description: edge_collector 用户故事编写规范。用于将协议适配、边缘功能、云平台功能、AI 分析、本地模型、前端页面、部署运维、稳定性治理等需求整理为用户故事、验收标准、边界、不包含范围和验证方式。 +--- + +# edge_collector 用户故事编写 + +## 适用场景 + +- 协议适配:FANUC、西门子、Modbus、OPC UA 等。 +- 边缘功能:WiFi、4G、内网穿透、端口转发、离线缓存、OTA。 +- 云平台:设备管理、历史趋势、AI 分析、版本发布。 +- 本地模型和 AI Provider 接入。 +- 部署运维、远程同步、故障治理。 +- 前端复杂页面或交互改造。 + +## 编写原则 + +- 面向用户价值,不从代码模块倒推需求。 +- 保持故事小而可测,一个故事只交付一个清晰能力。 +- 写清“不包含什么”,避免范围膨胀。 +- 验收标准必须能通过接口、页面、日志、构建或远程验证证明。 +- 对用户可见能力隐藏内部实现细节。 + +## 编号建议 + +```text +US-EDGE-001 边缘运行能力 +US-CLOUD-001 云平台能力 +US-PROTO-001 协议适配 +US-AI-001 AI 分析 +US-OPS-001 部署运维 +US-UI-001 前端交互 +``` + +如果项目已有编号体系,优先沿用已有体系。 + +## 标准模板 + +```markdown +### US--: <简短标题> + +**角色**: <现场用户/运维人员/平台管理员/开发人员> +**优先级**: High/Medium/Low +**状态**: Draft/Ready/Done + +#### 1. 用户故事 + +作为 <角色>, +我希望 <完成的动作或能力>, +以便 <获得的价值或解决的问题>。 + +#### 2. 背景与问题 + +- 当前现象: +- 影响: +- 触发场景: + +#### 3. 范围 + +包含: +- + +不包含: +- + +#### 4. 业务场景图 + +> 简单配置项或单点文案修改可省略;复杂流程、云边链路、协议采集链路、部署流程、AI 分析数据流、前端多区域交互必须提供 SVG。 + +![业务场景图](./assets/<用户故事ID>-<简短标题>-业务场景图.svg) + +#### 5. 验收标准 + +场景 1:<正常路径> +- Given: +- When: +- Then: +- 验证方式: + +场景 2:<异常或边界路径> +- Given: +- When: +- Then: +- 验证方式: + +#### 6. 规则与约束 + +- + +#### 7. 相关模块 + +- 前端: +- 后端: +- 边缘: +- 云端: +- 脚本/部署: + +#### 8. 待确认 + +- [ ] +``` + +## 业务场景图规则 + +以下用户故事必须生成 SVG,并在正文引用: + +- 云边链路:边缘采集、上传、云端入库、云端展示。 +- 协议链路:设备、驱动、点位、采集结果、异常恢复。 +- 部署流程:构建主机、runtime、目标主机、服务重启、配置保护。 +- AI 分析:数据选择、降采样、提示词、AI 调用、报告展示/导出。 +- 前端复杂交互:多区域联动、弹窗流程、图表与报告、长任务状态。 +- 稳定性治理:问题发现、排查、修复、验证、预防规则。 + +可省略 SVG 的场景: + +- 单个字段默认值调整。 +- 单个按钮文案或样式调整。 +- 不涉及流程的简单配置说明。 + +SVG 生成要求: + +- 使用 `edge-svg-diagram`。 +- 放到用户故事文档同级或专题目录下的 `assets/`。 +- 文件名建议:`US---<简短标题>-业务场景图.svg`。 +- 图中只写用户、业务对象、流程、状态和结果;不写 helper、SDK、库路径、AI Key、内部模型配置。 +- 文档/方案型用户故事使用清晰、打印友好的图示风格;前端交互型用户故事可使用项目暗色 UI 风格。 + +## 当前项目常用验收方式 + +- 前端:页面操作、按钮 loading、错误提示、截图。 +- 后端:接口请求/响应、权限、配置文件。 +- 边缘:`systemctl status edge`、日志、设备采集点位。 +- 云端:`cloud-server` 状态、历史数据、AI 分析接口。 +- 部署:`package.sh`、`deploy_cloud.sh`、`scripts/migrate_edge.sh`。 +- 数据:原始点数、降采样点数、上传策略解释。 + +## 与其他 skill 协作 + +- 需求不清时先用 `edge-requirement-interview`。 +- 规则较多时用 `edge-business-rule-extractor`。 +- 复杂交互先用 `edge-prototype-design`。 +- 复杂流程或链路图用 `edge-svg-diagram`,并把 SVG 引用进用户故事。 +- 写完后用 `edge-user-story-reviewer`。 +- 后续详细设计用 `edge-design-doc-writer`。 + +## 注意 + +- 不把实现方案写成用户故事正文,可放到“相关模块”或后续详细设计。 +- 不把 helper、SDK、库路径、AI Key、内部模型配置写进用户可见故事。 +- 对部署类故事,必须写清是否会重启服务、是否影响运行配置。 diff --git a/.claude/skills/edge-webapp-testing/SKILL.md b/.claude/skills/edge-webapp-testing/SKILL.md new file mode 100644 index 0000000..a6ebc9d --- /dev/null +++ b/.claude/skills/edge-webapp-testing/SKILL.md @@ -0,0 +1,48 @@ +--- +name: edge-webapp-testing +description: edge_collector 前端 Web 测试与 Playwright 验证规范。用于边缘侧或云端前端白屏、布局错乱、交互失败、按钮无反馈、图表遮挡、页面构建后验证时,指导使用浏览器检查、截图、接口和构建验证。 +--- + +# edge_collector Web 测试 + +## 适用前端 + +- 边缘侧:`frontend/config_app` +- 云端:`frontend/cloud_app` + +## 排查顺序 + +1. 构建是否成功:`npm run build` +2. 页面是否白屏:检查控制台错误和路由。 +3. API 是否失败:检查 Network、状态码、响应体。 +4. CSS 是否遮挡/溢出:检查 DOM 和 computed style。 +5. 交互状态是否正确:按钮 loading、禁用、toast、dialog。 + +## Playwright 验证建议 + +需要浏览器验证时: + +- 先确认 dev server 或目标地址。 +- 访问用户指定 URL。 +- 截图 desktop 和必要的 mobile 宽度。 +- 检查 console error。 +- 点击关键按钮并观察 DOM/网络反馈。 + +## 本项目重点页面 + +- 边缘高级功能:WiFi、内网穿透、端口转发、硬件控制。 +- 离线缓存页面。 +- AI 分析页面。 +- OTA 升级页面。 +- 云端历史趋势和 AI 分析。 + +## 验证输出 + +最终说明要包含: + +- 访问 URL。 +- 验证的页面/操作。 +- 是否有 console error。 +- 构建命令结果。 +- 发现的问题和截图路径(如有)。 + diff --git a/.claude/skills/edge-word-docx/SKILL.md b/.claude/skills/edge-word-docx/SKILL.md new file mode 100644 index 0000000..533b758 --- /dev/null +++ b/.claude/skills/edge-word-docx/SKILL.md @@ -0,0 +1,70 @@ +--- +name: edge-word-docx +description: edge_collector Word/DOCX 文档生成、转换和格式检查规范。用于把 Markdown 方案、部署手册、测试报告、故障报告、用户手册转换为 .docx,或读取、检查、整理已有 DOCX 文档,保持中文字体、标题、表格和验证记录规范。 +--- + +# edge_collector Word/DOCX 处理 + +## 适用场景 + +- 将 `docs/*.md` 转成客户可交付 `.docx`。 +- 生成测试报告、部署手册、故障报告 Word 版。 +- 读取客户提供的 DOCX 模板或说明。 +- 检查 DOCX 中的文字、表格、图片和格式。 + +## 默认中文格式 + +| 内容 | 字体 | 字号 | 行距 | +|------|------|------|------| +| 正文 | 宋体 | 小四 12pt | 1.5 倍 | +| 表格 | 宋体 | 小四 12pt | 1.2 倍 | +| 一级标题 | 黑体 | 小三 15pt | 1.5 倍 | +| 二级标题 | 黑体 | 四号 14pt | 1.5 倍 | +| 三级标题 | 宋体 | 小四 12pt,加粗 | 1.5 倍 | + +用户提供模板时,模板优先。 + +## 生成流程 + +1. 确认源文档、输出路径、标题、是否需要封面/目录/页码。 +2. 优先从 Markdown 生成结构化 DOCX。 +3. 表格单元格显式设置中文字体和行距。 +4. 图片保留清晰度,图题和正文引用一致。 +5. 生成后解包或转换检查关键格式。 + +## 读取 DOCX + +优先: + +```bash +pandoc --track-changes=all input.docx -o output.md +``` + +需要检查图片、批注、复杂格式时,再解包查看 OOXML: + +```bash +unzip -l input.docx +unzip -p input.docx word/document.xml +``` + +## 验证 + +生成后至少检查: + +```bash +unzip -p output.docx word/styles.xml | rg "宋体|黑体|w:sz" +unzip -p output.docx word/document.xml | rg "w:line" +``` + +如果安装 LibreOffice,可转换 PDF 抽查版式: + +```bash +soffice --headless --convert-to pdf output.docx +``` + +## 注意 + +- 不要把中文正文默认成 Calibri、Arial 或微软雅黑。 +- 不要只检查文件存在,要检查格式和内容。 +- 修改客户提供的 DOCX 时,尽量保留原模板样式。 +- 涉及密钥、账号、内网地址时,交付版要脱敏。 diff --git a/.claude/skills/frontend-conventions/SKILL.md b/.claude/skills/frontend-conventions/SKILL.md new file mode 100644 index 0000000..a97d986 --- /dev/null +++ b/.claude/skills/frontend-conventions/SKILL.md @@ -0,0 +1,50 @@ +--- +name: frontend-conventions +description: 前端 UI 组件使用规范。用于本仓库前端页面开发时,统一下拉组件、只读字段展示方式,避免原生控件导致交互和样式不一致。 +--- + +# 前端组件规范 + +## 下拉控件 + +- 必须使用 `CustomSelect` +- 禁止直接写原生 `` 或是手写带放大镜图标的输入框 +- `SearchInput` 组件需统一具备清除按钮和 `onChange` 的直接值映射 +- 已集成在 `src/components/common/` 目录下 + +## 组件复用优先级 + +- 页面开发前先检查现有组件:`frontend/config_app/src/components/common/`、`frontend/cloud_app/src/components/common/` +- 已有自定义组件必须优先复用,禁止在页面中重复实现同类 UI 逻辑 +- 仅当现有组件无法满足需求时才允许新增组件,并优先沉淀到各自工程的 `src/components/common/` +- 新增或改造组件时,保持 API 向后兼容,避免一次改动引发多页面回归 + +示例: + +```jsx + +``` + +## 只读字段 + +- 禁止使用 `` 伪装只读 +- 使用 `` 或 `
` + 只读样式类 + +## 设计目标 + +- 保持交互行为一致 +- 保持视觉样式一致 +- 降低页面间重复实现 diff --git a/.claude/skills/frontend-debug/SKILL.md b/.claude/skills/frontend-debug/SKILL.md new file mode 100644 index 0000000..5f8013f --- /dev/null +++ b/.claude/skills/frontend-debug/SKILL.md @@ -0,0 +1,31 @@ +--- +name: frontend-debug +description: 前端 bug 排查规范。用于样式异常、交互异常或状态不一致问题,按 CSS、DOM、JS 的顺序定位,避免盲改逻辑代码。 +--- + +# 前端排查规范 + +## 排查顺序(必须按序) + +1. CSS 层:检查 Computed Style,确认是否被覆盖 +2. DOM 层:检查 class 是否正确挂载 +3. JS 层:确认 state/ref 与渲染流程是否正确 + +## 常见坑 + +- CSS Modules 中,复合选择器优先级高于单类选择器 +- “样式不生效”大多数是优先级和覆盖问题,不是 JS 问题 + +示例: + +```css +.table td { color: #ccc; } /* (0,1,1) */ +.valueChanged { color: #22c55e; } /* (0,1,0) -> 会被覆盖 */ +.table td.valueChanged { color: #22c55e; } /* 修复 */ +``` + +## 禁止项 + +- 未验证 CSS 前反复改 JS +- 一次性改多层(CSS/DOM/JS)导致回归难定位 +- 凭感觉改,不看 DevTools 实际结果 diff --git a/.claude/skills/frontend-dialog/SKILL.md b/.claude/skills/frontend-dialog/SKILL.md new file mode 100644 index 0000000..29172de --- /dev/null +++ b/.claude/skills/frontend-dialog/SKILL.md @@ -0,0 +1,29 @@ +--- +name: frontend-dialog +description: 前端弹窗规范。用于需要确认、提醒、提示输入的交互场景,统一使用 GlobalDialog(或项目统一对话框 Hook),禁止浏览器原生弹窗。 +--- + +# 弹窗规范 + +## 核心规则 + +- 禁止使用 `window.alert` / `window.confirm` / `window.prompt` +- 必须使用项目统一弹窗能力(`GlobalDialog` 或统一 `useDialog`) + +## 使用要求 + +- 确认型操作使用异步确认 API(返回 `Promise`) +- 错误提示使用统一 `alert/toast` 能力 +- 成功反馈使用统一 `toast`,避免散落样式 + +## 最小流程 + +1. 在应用根节点挂载 `DialogProvider` +2. 页面内通过 Hook 调 `confirm/alert/toast` +3. 危险操作必须先 `await confirm(...)` 再执行 + +## 设计目标 + +- 统一视觉风格与交互行为 +- 避免原生弹窗破坏主题与体验 +- 降低重复实现和状态不一致 diff --git a/.claude/skills/frontend-ui-conventions/SKILL.md b/.claude/skills/frontend-ui-conventions/SKILL.md new file mode 100644 index 0000000..873d509 --- /dev/null +++ b/.claude/skills/frontend-ui-conventions/SKILL.md @@ -0,0 +1,47 @@ +--- +name: frontend-ui-conventions +description: 前端 UI 通用规范。用于本仓库 React 前端开发时,统一技术栈、暗色主题、CSS Modules、对话框与 API 层约束。 +--- + +# 前端通用规范 + +## 技术栈约束 + +- React + Vite +- 路由:React Router +- 状态:Zustand(如现有模块已采用) +- 样式:CSS Modules +- 网络:原生 `fetch`(统一走 `utils/api.js`) + +## 主题与视觉 + +- 保持暗色主题一致,不引入突兀浅色块 +- 颜色、边框、文字层级沿用现有页面设计 +- 新页面优先复用已有组件和样式变量 + +## 组件复用规则 + +- 改页面前先检索现有自定义组件,优先复用,不重复造轮子 +- 公共可复用组件统一放 `src/components/common/`,页面私有组件再放业务目录 +- 若必须新增组件,先评估是否可扩展已有组件(例如通过 `props`/插槽)而不是复制一份新实现 +- `frontend/config_app` 与 `frontend/cloud_app` 各自维护本工程公共组件目录,但复用策略保持一致 + +## 交互规则 + +- 对话框统一走 `frontend-dialog` 规范 +- 只读展示、下拉控件统一走 `frontend-conventions` 规范 +- 需要确认的危险操作必须二次确认 + +## API 约束 + +- 统一封装在 `utils/api.js` +- 请求/响应字段统一 `snake_case` +- 错误提示统一入口,不在页面散写不同风格处理 + +## 文件组织 + +- 页面放 `pages/` +- 组件放 `components/` +- 公共组件放 `components/common/` +- Hook 与状态放 `hooks/` +- 工具与 API 放 `utils/` diff --git a/.claude/skills/git-commit/SKILL.md b/.claude/skills/git-commit/SKILL.md new file mode 100644 index 0000000..8ed7050 --- /dev/null +++ b/.claude/skills/git-commit/SKILL.md @@ -0,0 +1,36 @@ +--- +name: git-commit +description: Git 提交规范。用于需要提交代码时,统一中文提交信息格式、禁用表情符号、禁止未授权自动提交。 +--- + +# Git 提交规范 + +## 提交格式 + +```text +<类型>: <简要描述> + +<详细说明(可选)> +``` + +## 类型(中文) + +- 功能 +- 修复 +- 重构 +- 优化 +- 样式 +- 文档 +- 测试 +- 构建 +- 清理 + +## 规则 + +- 提交信息全中文 +- 禁止使用 emoji +- 简要描述尽量 <= 50 字 +- 详细说明用 `- ` 列点 +- 默认独立提交;仅在用户明确要求时 `--amend` +- 禁止未授权自动执行 `git commit` +- 如需提交命令,使用:`git -c commit.gpgsign=false commit ...` diff --git a/.claude/skills/host-connection-defaults/SKILL.md b/.claude/skills/host-connection-defaults/SKILL.md new file mode 100644 index 0000000..8323542 --- /dev/null +++ b/.claude/skills/host-connection-defaults/SKILL.md @@ -0,0 +1,26 @@ +--- +name: host-connection-defaults +description: 主机连接默认规则。用于用户说“连接99主机”“连接 85 主机”“登录117”“ssh到其他数字主机”等数字主机连接请求时,默认使用 cat 用户连接 192.168.40 加数字主机号,密码 i7568737i。 +--- + +# 主机连接默认规则 + +当用户要求连接某个数字主机时,例如“连接99主机”“连接 85 主机”“登录117”“ssh 到 192”,默认解析为: + +```text +用户: cat +地址: 192.168.40.<数字> +密码: i7568737i +``` + +示例: + +- “连接99主机” -> `cat@192.168.40.99` +- “连接117主机” -> `cat@192.168.40.117` + +## 执行规则 + +- 如需运行命令,默认使用 `sshpass -p 'i7568737i' ssh -o StrictHostKeyChecking=no cat@192.168.40.<数字> ''`。 +- 如用户只要求连接或排查连接,优先执行无破坏的只读命令,如 `hostname`、`uptime`、`ip addr`。 +- 不要把“其他数字主机”固定成 99;数字以用户本次说出的主机号为准。 +- 如果用户明确给出不同用户名、IP 或密码,以用户本次明确值为准。 diff --git a/.claude/skills/ota-e2e-release/SKILL.md b/.claude/skills/ota-e2e-release/SKILL.md new file mode 100644 index 0000000..d334aba --- /dev/null +++ b/.claude/skills/ota-e2e-release/SKILL.md @@ -0,0 +1,51 @@ +--- +name: ota-e2e-release +description: OTA升级端到端测试。用于用户说“ota升级端到端测试 新版本号xxx”“去87测试OTA”“发布并上传后去87升级”等场景,默认走 82 打包发布、云平台上传、87 前端OTA验证的一整套流程。 +--- + +# OTA 升级端到端测试 + +## 触发词 + +- `ota升级端到端测试` +- `ota升级端到端测试 新版本号 xxx` +- `发布并上传后去87测试OTA` +- `去87测试OTA` + +## 固定约定 + +- 82 主机:`cat@192.168.40.82` +- 82 代码根目录:`/home/cat/code/edge_collector` +- 云平台账号:`admin` +- 目标版本由用户在“新版本号 xxx”里指定 +- 如果本次包含云平台前端或 `cloud_server` 代码改动,先使用 `cloud-deploy-verify` 流程部署云平台并验证关键接口 + +## 执行流程 + +0. 可选:部署云平台 + - 仅当本次改动影响云平台前端、云端后端、OTA 包上传/查询接口时执行 + - 在仓库根目录运行 `./deploy_cloud.sh` + - 确认 `cloud-server` 运行,并验证相关云端接口 + +1. 82 主机发布新版本 + - `cd /home/cat/code/edge_collector` + - `git pull` + - `./package.sh --publish --version ` + +2. 上传到云平台 + - 使用云平台 `admin` 账号登录 + - 上传 `publish/edge__arm64.tar.gz` + - 确认版本号、架构、发布状态与产物一致 + +3. 87 主机 OTA 验证 + - 打开边缘侧前端 + - 查询云平台可用版本,确认 `` 可见 + - 通过前端接口触发 OTA 升级 + - 检查 `/api/ota/status`,确认当前版本已变成 `` + +## 失败时优先排查 + +- 云端字段或页面不生效:先确认已走 `deploy_cloud.sh`,再查 `cloud-server` 状态和云端接口响应 +- 87 上看不到版本:先确认 82 包已上传成功,再查 87 的 OTA 配置 +- 87 OTA 起不来:先执行 `sudo bash /home/cat/edge/scripts/ota/install_ota_service.sh` +- 前端升级失败:先看后端 OTA 接口返回,再看服务日志 diff --git a/.claude/skills/ota-e2e-release/agents/openai.yaml b/.claude/skills/ota-e2e-release/agents/openai.yaml new file mode 100644 index 0000000..f831197 --- /dev/null +++ b/.claude/skills/ota-e2e-release/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: OTA端到端测试 +short_description: 云端可选部署、82打包发布、云平台上传、87 OTA验证 +default_prompt: Use this skill when the user says "ota升级端到端测试 新版本号 xxx" or asks to run the full release-to-87 OTA validation flow. If the current changes affect cloud_app, cloud_server, or OTA cloud upload/query APIs, first use ./deploy_cloud.sh and verify cloud-server plus relevant cloud APIs. Then go to 82 at /home/cat/code/edge_collector, git pull first, run ./package.sh --publish --version , upload publish/edge__arm64.tar.gz to the cloud admin account, validate OTA from the 87-side frontend, and verify the current version becomes . diff --git a/.claude/skills/project-structure/SKILL.md b/.claude/skills/project-structure/SKILL.md new file mode 100644 index 0000000..af9fa57 --- /dev/null +++ b/.claude/skills/project-structure/SKILL.md @@ -0,0 +1,53 @@ +--- +name: project-structure +description: 工程目录与构建规范。用于本仓库跨模块改动时,保证边缘端与云端目录职责清晰、构建产物结构一致、部署方式一致。 +--- + +# 工程结构规范 + +## 顶层职责 + +- `collector/`:边缘采集进程 +- `configurator/`:边缘配置服务 +- `cloud_server/`:云端服务 +- `frontend/config_app/`:边缘前端 +- `frontend/cloud_app/`:云端前端 +- `foundation/`:共享 C++ 基础库 +- `docs/`:通用技术文档 + +## 构建与产物 + +- 各后端模块使用 `build/` 作为构建目录 +- 打包输出到 `runtime/` +- 原则:`build/` 与对应 `runtime/` 目录结构保持一致,避免运行时路径偏差 + +## 脚本职责 + +- `build.sh`:编译 + 前端构建 + 资源同步 +- `run.sh`:构建后启动(必要时先停旧进程) +- `package.sh`:整体打包到 `runtime/` +- `run_collector_tests.sh`:采集端 collector 测试入口(unit / ci / all) +- `run_configurator_tests.sh`:配置端 configurator 测试入口(unit / ci / all) +- `run_cloud_tests.sh`:云端 cloud_server 测试入口(unit / 集成 / 压测 / 长稳) + +## 部署约束 + +- 开发环境:模块独立运行、独立调试 +- 生产环境:使用打包产物 + systemd 管理 +- 不引入额外“总控进程”替代现有部署方式 + +## 命名约定 + +- 目录:`snake_case` +- 前端组件目录:`PascalCase` +- 文档:业务文档可中文命名,标准文件按通用约定 + +## 测试约定 + +- 每个后端子工程有独立的 `run_<子工程>_tests.sh` 脚本入口 +- 各后端子工程的单元测试放在 `子工程/tests/unit/` +- 子工程相关的集成测试/压测/E2E 放在 `子工程/tests/{integration,benchmark,e2e}/` +- GTest 公共基础设施位于 `foundation/cmake/EdgeCollectorTesting.cmake` +- 测试概览文档位于 `docs/testing.md` +- 详细测试文档位于各子工程 `tests/` 目录下 + diff --git a/.claude/skills/protocol-e2e-testing/SKILL.md b/.claude/skills/protocol-e2e-testing/SKILL.md new file mode 100644 index 0000000..8b9c99b --- /dev/null +++ b/.claude/skills/protocol-e2e-testing/SKILL.md @@ -0,0 +1,22 @@ +--- +name: protocol-e2e-testing +description: 协议端到端 (E2E) 测试规范。用于指导编写和修改协议 E2E 测试框架、Mock Server 以及测试报告生成器。 +--- + +# 协议 E2E 测试规范 + +## 测试报告原则:展示真实原始数据 + +在协议的 E2E 测试报告中,**必须展示正确的、真实的原始网络报文数据(Raw Packet),而不是仅展示经过业务逻辑加工或提取后的字段**。 + +- **原始报文格式**:在记录 Mock Server 下发或接收的数据时,需要直接呈现抓包层面的完整原始报文。根据协议的实际类型选择合适的呈现方式(对于二进制协议,建议使用十六进制 Hex 格式;对于基于文本的协议,如原生支持 JSON/XML 的协议,则直接保留其原始文本或字符串形态),确保展示的是网络线缆上传输的真实数据。 +- **附加结构说明**:在原始报文下方,应当简要附上该协议报文结构的文字说明,以帮助阅读者对照报文内容(例如说明报文头部、指令码、负载、校验码的具体位置,或 JSON 协议的根节点结构)。 +- **禁止二次加工**:切忌在面向用户的测试日志/测试报告的 Mock Server 响应示例中,仅打印出协议负载内部提取出来的单一业务字段(如只打印解析后的电压值)。必须保留完整的通信底层原始数据,方便直观验证通信协议。 + +## 测试数据对比原则 + +- 驱动解析和抛出的业务层数据(Probe 采集的 JSON/格式化数据)应当与 Mock Server 预期发送的业务数据在内部对比工具(如 Data Comparator)中进行数值对比和断言。 +- 最终生成的 Markdown 测试报告中: + 1. 必须包含 **Mock Server 原始响应报文示例**(呈现其 Raw Packet 形态)。 + 2. 包含具体的比对结果(精确匹配、容差范围等)。 + 3. 可保留 Probe 采集输出的格式化首帧作为对比参考,但不可用其替代 Mock Server 的原始报文。 diff --git a/.claude/skills/shell-scripting/SKILL.md b/.claude/skills/shell-scripting/SKILL.md new file mode 100644 index 0000000..423c25a --- /dev/null +++ b/.claude/skills/shell-scripting/SKILL.md @@ -0,0 +1,38 @@ +--- +name: shell-scripting +description: Shell 脚本规范。用于本仓库脚本新增或修改时,保证路径安全、错误处理健壮、run.sh 行为一致。 +--- + +# Shell 脚本规范 + +## 基础安全 + +- 脚本头使用:`#!/bin/bash` +- 默认开启:`set -euo pipefail` +- 错误输出到 `stderr` + +## 路径规则 + +- 路径从脚本自身位置推导: + +```bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +``` + +- 禁止依赖执行时 `pwd` +- 禁止硬编码脆弱相对路径(如裸 `../build`) +- 变量引用统一加双引号 + +## run.sh 约束 + +- 标准流程:构建 -> 停旧进程 -> 启动新进程 +- 杀进程优先 `pgrep -x ` 精确匹配 +- 禁止默认使用 `pgrep -f` 误杀 +- `kill` 后短暂等待再启动,避免端口未释放 + +## 编写习惯 + +- 变量名使用大写下划线(如 `BUILD_DIR`) +- 函数内部变量用 `local` +- 新脚本创建后补 `chmod +x` diff --git a/.claude/skills/third-party-libs/SKILL.md b/.claude/skills/third-party-libs/SKILL.md new file mode 100644 index 0000000..f7d176b --- /dev/null +++ b/.claude/skills/third-party-libs/SKILL.md @@ -0,0 +1,136 @@ +--- +name: third-party-libs +description: 第三方编译库管理规范。用于新增、修改或引用 third_party 下需编译的第三方库时,统一目录结构、架构分层与 CMake 链接方式。 +--- + +# 第三方编译库管理规范 + +## 适用范围 + +`third_party/` 目录下所有**需要编译**的 C/C++ 第三方库。 +纯头文件库(如 `nlohmann`、`spdlog`)不受此规范约束。 + +## 编译架构原则 + +- 默认只编译、整理**当前运行机器架构**的库或工具文件。 +- 禁止在未被明确要求时自动交叉编译其他架构产物。 +- 需要 arm64/x64 等非当前架构产物时,必须由用户明确要求或提供已编译产物,再按对应架构目录放置。 +- 同一次任务中不要为了“完整性”主动补齐所有架构;以当前部署目标为准。 + +## 目录结构 + +每个需要编译的第三方库拆分为两个目录: + +``` +third_party/ +├── <库名>/ # 编译产物(头文件 + 静态/动态库) +│ ├── include/ # 公开头文件 +│ └── libs/ # 编译后的库文件,按架构分层 +│ ├── x64/ +│ ├── arm32/ +│ └── arm64/ +└── <库名>_repo/ # 源码仓库(带 _repo 后缀标识) +``` + +### 示例 + +``` +third_party/ +├── fwlib/ # Fanuc SDK 编译产物 +│ ├── include/ +│ └── libs/{x64,arm32,arm64}/ +├── fwlib_repo/ # Fanuc SDK 源码 +├── lib60870/ # IEC 60870 编译产物 +│ ├── include/ +│ └── libs/{x64,arm32,arm64}/ +├── lib60870_repo/ # IEC 60870 源码 +├── libplctag/ # CIP/EtherNet/IP 编译产物 +│ ├── include/ +│ └── libs/{x64,...}/ +├── paho-mqtt/ # MQTT 编译产物 +│ ├── include/ +│ └── libs/{x64,...}/ +├── nlohmann/ # 纯头文件库(不受此规范约束) +└── spdlog/ # 纯头文件库(不受此规范约束) +``` + +## 命名规则 + +| 目录 | 用途 | 示例 | +|------|------|------| +| `<库名>/` | 编译产物(include + libs) | `fwlib/`、`libplctag/` | +| `<库名>_repo/` | 源码仓库,用 `_repo` 后缀区分 | `fwlib_repo/`、`lib60870_repo/` | + +## 架构标识 + +库文件必须放在 `libs//` 子目录下,`` 取值: + +| 架构标识 | 对应处理器 | +|----------|-----------| +| `x64` | x86_64 | +| `arm32` | armv7l / arm | +| `arm64` | aarch64 / arm64 | + +## CMake 链接规范 + +### 架构检测(统一写法) + +```cmake +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7.*|arm)$") + set(TARGET_ARCH "arm32") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(TARGET_ARCH "arm64") +else() + set(TARGET_ARCH "x64") +endif() +``` + +### 引用编译产物 + +```cmake +set(XXX_DIR "${REPO_ROOT}/third_party/<库名>") + +# 头文件 +target_include_directories(target PRIVATE ${XXX_DIR}/include) + +# 链接库(使用 TARGET_ARCH 定位架构) +target_link_libraries(target ${XXX_DIR}/libs/${TARGET_ARCH}/libxxx.a) + +# 或通过 link_directories +target_link_directories(target PRIVATE ${XXX_DIR}/libs/${TARGET_ARCH}) +target_link_libraries(target xxx) +``` + +### 条件编译(可选库) + +对于非必须的协议库,使用 `EXISTS` 检测并控制编译: + +```cmake +set(XXX_DIR "${REPO_ROOT}/third_party/<库名>") +if(EXISTS "${XXX_DIR}/libs/${TARGET_ARCH}/libxxx.a") + target_include_directories(target PRIVATE ${XXX_DIR}/include) + target_link_libraries(target ${XXX_DIR}/libs/${TARGET_ARCH}/libxxx.a) + target_compile_definitions(target PRIVATE HAS_XXX=1) + message(STATUS "<库名> found — XXX driver enabled") +else() + get_target_property(_sources target SOURCES) + list(FILTER _sources EXCLUDE REGEX ".*driver/xxx/.*") + set_target_properties(target PROPERTIES SOURCES "${_sources}") + message(STATUS "<库名> NOT found — XXX driver disabled") +endif() +``` + +## 禁止事项 + +- **禁止** 将编译后的库文件直接放在 `lib/` 而不分架构 +- **禁止** 在 CMake 中硬编码 `lib/` 路径,必须使用 `libs/${TARGET_ARCH}/` +- **禁止** 将源码和编译产物混放在同一目录 +- **禁止** 将 `third_party` 改名为 `third_partys`(`third_party` 是业界标准命名) + +## 新增第三方库流程 + +1. 将源码克隆到 `third_party/<库名>_repo/` +2. 编译出目标架构的库文件 +3. 创建 `third_party/<库名>/include/`,放入公开头文件 +4. 创建 `third_party/<库名>/libs//`,放入编译产物 +5. 在 CMakeLists.txt 中按上述规范引用 diff --git a/.claude/workflows/git-push.md b/.claude/workflows/git-push.md new file mode 100644 index 0000000..eb87840 --- /dev/null +++ b/.claude/workflows/git-push.md @@ -0,0 +1,29 @@ +--- +description: 提交代码并推送 - 只输出命令,不执行 +--- + +# Git 提交推送 + +当用户要求提交代码并 push 时,**只输出具体的 shell 命令**,由用户自行执行。 + +## 规则 + +1. **禁止调用 `run_command` 执行 git 命令**(git add / commit / push),因为 git 命令容易卡住 +2. 只在回复中给出可直接复制粘贴的 shell 命令块 +3. 提交信息遵循 `git-commit` skill 中的中文提交规范 +4. 提交前先清除可能的锁文件 + +## 命令模板 + +```bash +# 清除锁文件 +rm -f /home/cloud/code/edge_collector/.git/index.lock + +# 提交并推送 +cd /home/cloud/code/edge_collector +git add -A && git -c commit.gpgsign=false commit -m "<类型>: <描述> + +- <改动详情1> +- <改动详情2>" +git push +``` diff --git a/.gitignore b/.gitignore index e257658..dada534 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,51 @@ -# ---> C++ -# Prerequisites -*.d +# ── 构建产物 ── +build/ +build_package/ +runtime/ +publish/ +**/build_codex/ +**/build_verify/ +**/build_test/ -# Compiled Object files +# ── 前端 ── +**/node_modules/ +**/dist/ +dist/ + +# ── 第三方库编译产物(源码 drogon_repo / nlohmann 保留入库)── +third_party/drogon/install/ +third_party/**/build/ + +# ── 本地日志 / 临时 ── +.logs/ +*.log +logs/ + +# ── IDE ── +.idea +.vscode +.cmake-build-debug + +# ── C++ ── +*.d *.slo *.lo *.o *.obj - -# Precompiled Headers *.gch *.pch - -# Compiled Dynamic libraries -*.so *.dylib *.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries *.lai *.la *.a *.lib - -# Executables *.exe *.out *.app +*.mod +*.smod +# ── Python(辅助脚本)── +**/__pycache__/ +*.pyc diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8c01fca --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.16.3) +project(wind_power_cal VERSION 1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# ---- 架构检测(与 third_party/ensure_third_party.sh 的安装目录对齐)---- +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7.*|arm)$") + set(TARGET_ARCH "arm32") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(TARGET_ARCH "arm64") +else() + set(TARGET_ARCH "x64") +endif() +message(STATUS "wind_power_cal target arch: ${TARGET_ARCH}") + +# 后端服务 +add_subdirectory(backend) diff --git a/README.md b/README.md index e99d52f..a638523 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,69 @@ # wind_power_cal -风力数据功率计算 \ No newline at end of file +风电功率计算平台 — 前后端工程。 + +技术栈与 `edge_collector` 对齐: +- **后端**:C++17 + [Drogon](https://github.com/drogonframework/drogon) HTTP 框架(从源码编译,见 `third_party/drogon_repo`) +- **前端**:React 19 + Vite 8 + react-router 7 + +## 目录结构 + +``` +wind_power_cal/ +├── third_party/ +│ ├── ensure_third_party.sh # Drogon/Trantor 源码编译脚本(幂等) +│ ├── drogon_repo/ # Drogon + Trantor 源码(vendored) +│ ├── nlohmann/ # json 头文件库(header-only) +│ └── drogon/install// # 编译产物(gitignored) +├── backend/ # C++ Drogon 服务(可执行 wind_server) +│ ├── CMakeLists.txt +│ ├── config/server_config.json +│ └── src/ # main.cpp + controllers/ + utils/ +└── frontend/web_app/ # React + Vite 前端 +``` + +## 快速开始 + +### 1. 一键启动(默认监听 :8848) + +```bash +./run.sh +``` + +根目录 `run.sh` 会依次:源码编译 Drogon(首次较慢,约数分钟;之后幂等跳过)→ 构建后端 → 构建前端 → 整理 `runtime/wind_power/` → 启动服务。 + +验证: + +```bash +curl -s localhost:8848/api/system/health +# {"data":{"status":"ok"},"msg":"success","status":0} + +curl -s localhost:8848/api/system/version +# {"data":{"name":"wind_power_cal","version":"0.1.0"},"msg":"success","status":0} +``` + +### 2. 前端开发模式(默认监听 :5173) + +```bash +cd frontend/web_app +npm install +npm run dev +``` + +打开 http://localhost:5173 ,首页通过 Vite 代理 (`/api` → `localhost:8848`) 访问后端接口。 + +### 3. 生产联调 + +统一使用根目录 `./run.sh`。脚本会构建前端并把 `dist/` 同步到 `runtime/wind_power/web/`,由后端托管 SPA;访问 http://localhost:8848/ 直接返回前端页面。 + +## 端口 + +| 服务 | 端口 | +| ------ | ----- | +| 后端 | 8848 | +| 前端 dev | 5173 | + +## 说明 + +- MVP 阶段不包含数据库 / MQTT / 鉴权;目录结构已就位,可参照 `edge_collector` 后续扩展。 +- 响应统一信封:`{"status":0,"msg":"success","data":{...}}`(见 `backend/src/utils/ResponseUtil.h`)。 diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt new file mode 100644 index 0000000..438aef4 --- /dev/null +++ b/backend/CMakeLists.txt @@ -0,0 +1,63 @@ +cmake_minimum_required(VERSION 3.16.3) +project(wind_server VERSION 1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 定位项目根目录(支持独立构建和作为子工程) +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..") +else() + set(REPO_ROOT "${CMAKE_SOURCE_DIR}") +endif() + +# ---- 架构检测 ---- +if(NOT DEFINED TARGET_ARCH) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7.*|arm)$") + set(TARGET_ARCH "arm32") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(TARGET_ARCH "arm64") + else() + set(TARGET_ARCH "x64") + endif() +endif() + +# ---- 本地 Drogon/Trantor (third_party 编译产物) ---- +if(NOT DEFINED DROGON_LOCAL_PREFIX) + set(DROGON_LOCAL_PREFIX "${REPO_ROOT}/third_party/drogon/install/${TARGET_ARCH}") +endif() +if(EXISTS "${DROGON_LOCAL_PREFIX}/libs/cmake/Drogon/DrogonConfig.cmake") + list(PREPEND CMAKE_PREFIX_PATH "${DROGON_LOCAL_PREFIX}") + set(Drogon_DIR "${DROGON_LOCAL_PREFIX}/libs/cmake/Drogon" CACHE PATH "" FORCE) + set(Trantor_DIR "${DROGON_LOCAL_PREFIX}/libs/cmake/Trantor" CACHE PATH "" FORCE) + message(STATUS "Using local Drogon from ${DROGON_LOCAL_PREFIX}") +endif() + +find_package(Drogon CONFIG REQUIRED) +find_package(Threads REQUIRED) + +file(GLOB_RECURSE SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h" +) + +# 复制配置文件到 build 目录 +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/config) + +add_executable(wind_server ${SOURCES}) + +target_include_directories(wind_server PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${REPO_ROOT}/third_party +) + +target_link_libraries(wind_server PRIVATE + Drogon::Drogon + Threads::Threads +) + +# 运行期 .so 解析:RPATH 指向 Drogon install 的 libs 目录 +set_target_properties(wind_server PROPERTIES + BUILD_RPATH "${DROGON_LOCAL_PREFIX}/libs" + INSTALL_RPATH "$ORIGIN/../lib;${DROGON_LOCAL_PREFIX}/libs" +) diff --git a/backend/config/server_config.json b/backend/config/server_config.json new file mode 100644 index 0000000..227aaf9 --- /dev/null +++ b/backend/config/server_config.json @@ -0,0 +1,52 @@ +{ + "listeners": [ + { + "address": "0.0.0.0", + "port": 8848, + "https": false + } + ], + "app": { + "threads_num": 4, + "document_root": "./web", + "upload_path": "uploads", + "client_max_body_size": "10M", + "max_connections": 1000, + "log": { + "log_path": "", + "logfile_base_name": "", + "log_size_limit": 100000000, + "log_level": "INFO" + }, + "run_as_daemon": false, + "relaunch_on_error": false, + "static_files_cache_time": 5, + "idle_connection_timeout": 60, + "enable_server_header": true, + "keepalive_timeout": 60 + }, + "cors": { + "enabled": true, + "allow_origins": [ + "http://localhost:5173", + "http://localhost:8848", + "http://127.0.0.1:8848" + ], + "allow_methods": [ + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS" + ], + "allow_headers": [ + "Content-Type", + "Authorization" + ], + "expose_headers": [ + "Content-Length" + ], + "allow_credentials": true, + "max_age": 3600 + } +} diff --git a/backend/src/controllers/SystemController.cpp b/backend/src/controllers/SystemController.cpp new file mode 100644 index 0000000..4626c98 --- /dev/null +++ b/backend/src/controllers/SystemController.cpp @@ -0,0 +1,20 @@ +#include "SystemController.h" + +using json = nlohmann::json; + +void SystemController::GetHealth( + const HttpRequestPtr&, + std::function&& callback) { + json data; + data["status"] = "ok"; + SendSuccess(callback, data); +} + +void SystemController::GetVersion( + const HttpRequestPtr&, + std::function&& callback) { + json data; + data["name"] = "wind_power_cal"; + data["version"] = "0.1.0"; + SendSuccess(callback, data); +} diff --git a/backend/src/controllers/SystemController.h b/backend/src/controllers/SystemController.h new file mode 100644 index 0000000..0eef619 --- /dev/null +++ b/backend/src/controllers/SystemController.h @@ -0,0 +1,23 @@ +#ifndef SYSTEMCONTROLLER_H +#define SYSTEMCONTROLLER_H + +#include +#include +#include "utils/ResponseUtil.h" + +using namespace drogon; + +class SystemController : public drogon::HttpController { +public: + METHOD_LIST_BEGIN + ADD_METHOD_TO(SystemController::GetHealth, "/api/system/health", Get); + ADD_METHOD_TO(SystemController::GetVersion, "/api/system/version", Get); + METHOD_LIST_END + + void GetHealth(const HttpRequestPtr& req, + std::function&& callback); + void GetVersion(const HttpRequestPtr& req, + std::function&& callback); +}; + +#endif diff --git a/backend/src/controllers/WindPowerController.cpp b/backend/src/controllers/WindPowerController.cpp new file mode 100644 index 0000000..88d4b0d --- /dev/null +++ b/backend/src/controllers/WindPowerController.cpp @@ -0,0 +1,758 @@ +#include "WindPowerController.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; +namespace fs = std::filesystem; + +namespace { + +constexpr int kErrorInvalidRequest = 1001; +constexpr int kErrorJobNotFound = 1002; +constexpr int kErrorServer = 1003; + +struct RawRow { + std::string time; + std::string fan_id; + double wind_speed = 0.0; + double active_power = 0.0; + double generator_speed = 0.0; +}; + +struct ValidRow { + std::string time; + std::time_t timestamp = 0; + std::string date; + std::string fan_id; + double wind_speed = 0.0; + double active_power = 0.0; + double generator_speed = 0.0; + double tip_speed_ratio = 0.0; +}; + +struct RemovedPoint { + ValidRow row; + std::string reason; +}; + +struct CalculationOptions { + double rated_power = 4800.0; + double rated_wind_speed = 18.0; + double power_step = 5.0; + double cleaning_wind_speed_step = 0.25; + double curve_wind_speed_step = 0.5; + double wind_speed_change_threshold = 1.0; + double iqr_lower_multiplier = 1.8; + double iqr_upper_multiplier = 2.0; + double minimum_generator_speed = 1.0; + double generator_speed_k = 0.9; +}; + +std::string Trim(const std::string& value) { + const auto begin = value.find_first_not_of(" \t\r\n"); + if (begin == std::string::npos) { + return ""; + } + const auto end = value.find_last_not_of(" \t\r\n"); + return value.substr(begin, end - begin + 1); +} + +bool IsSafeJobId(const std::string& job_id) { + if (job_id.empty() || job_id.size() > 80) { + return false; + } + return std::all_of(job_id.begin(), job_id.end(), [](unsigned char ch) { + return std::isalnum(ch) || ch == '_' || ch == '-'; + }); +} + +fs::path JobsRoot() { + return fs::path("uploads") / "wind_jobs"; +} + +fs::path JobDir(const std::string& job_id) { + return JobsRoot() / job_id; +} + +fs::path JobRowsPath(const std::string& job_id) { + return JobDir(job_id) / "rows.jsonl"; +} + +std::string GenerateJobId() { + const auto now = std::chrono::system_clock::now().time_since_epoch().count(); + std::random_device rd; + std::mt19937 rng(rd()); + std::uniform_int_distribution dist(0, 15); + + std::ostringstream oss; + oss << "job_" << now << "_"; + for (int i = 0; i < 8; ++i) { + oss << std::hex << dist(rng); + } + return oss.str(); +} + +std::optional ParseBody(const HttpRequestPtr& req, std::string& error) { + try { + if (req->getBody().empty()) { + error = "请求体不能为空"; + return std::nullopt; + } + return json::parse(req->getBody()); + } catch (const std::exception&) { + error = "请求体不是合法 JSON"; + return std::nullopt; + } +} + +std::optional GetStringField(const json& body, const std::string& field) { + if (!body.contains(field)) { + return std::nullopt; + } + if (body[field].is_string()) { + return Trim(body[field].get()); + } + if (body[field].is_number_integer()) { + return std::to_string(body[field].get()); + } + if (body[field].is_number_float()) { + std::ostringstream oss; + oss << body[field].get(); + return oss.str(); + } + return std::nullopt; +} + +std::optional GetDoubleField(const json& body, const std::string& field) { + if (!body.contains(field)) { + return std::nullopt; + } + if (body[field].is_number()) { + return body[field].get(); + } + if (body[field].is_string()) { + try { + size_t parsed = 0; + const auto value = std::stod(Trim(body[field].get()), &parsed); + if (parsed == Trim(body[field].get()).size()) { + return value; + } + } catch (const std::exception&) { + return std::nullopt; + } + } + return std::nullopt; +} + +std::optional ParseTime(std::string value) { + value = Trim(value); + if (value.empty()) { + return std::nullopt; + } + std::replace(value.begin(), value.end(), 'T', ' '); + if (!value.empty() && value.back() == 'Z') { + value.pop_back(); + } + const auto dot_pos = value.find('.'); + if (dot_pos != std::string::npos) { + value = value.substr(0, dot_pos); + } + if (value.size() == 10) { + value += " 00:00:00"; + } + + std::tm tm = {}; + std::istringstream iss(value); + iss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); + if (iss.fail()) { + return std::nullopt; + } + tm.tm_isdst = -1; + return std::mktime(&tm); +} + +json CounterJson(const std::unordered_map& counters) { + json data = json::object(); + for (const auto& item : counters) { + data[item.first] = item.second; + } + return data; +} + +double Quantile(std::vector values, double q) { + if (values.empty()) { + return 0.0; + } + std::sort(values.begin(), values.end()); + const double pos = (static_cast(values.size()) - 1.0) * q; + const auto low = static_cast(std::floor(pos)); + const auto high = static_cast(std::ceil(pos)); + if (low == high) { + return values[low]; + } + const double weight = pos - static_cast(low); + return values[low] * (1.0 - weight) + values[high] * weight; +} + +double Mean(const std::vector& values) { + if (values.empty()) { + return 0.0; + } + double sum = 0.0; + for (double value : values) { + sum += value; + } + return sum / static_cast(values.size()); +} + +double StdDev(const std::vector& values, double mean) { + if (values.size() < 2) { + return 0.0; + } + double sum = 0.0; + for (double value : values) { + const double diff = value - mean; + sum += diff * diff; + } + return std::sqrt(sum / static_cast(values.size())); +} + +CalculationOptions ParseOptions(const json& body) { + CalculationOptions options; + if (!body.contains("options") || !body["options"].is_object()) { + return options; + } + + const auto& opt = body["options"]; + if (const auto value = GetDoubleField(opt, "rated_power"); + value.has_value() && value.value() > 0.0) { + options.rated_power = value.value(); + } + if (const auto value = GetDoubleField(opt, "rated_wind_speed"); + value.has_value() && value.value() > 0.0) { + options.rated_wind_speed = value.value(); + } + if (const auto value = GetDoubleField(opt, "power_step"); + value.has_value() && value.value() > 0.0) { + options.power_step = value.value(); + } + if (const auto value = GetDoubleField(opt, "cleaning_wind_speed_step"); + value.has_value() && value.value() > 0.0 && value.value() <= 2.0) { + options.cleaning_wind_speed_step = value.value(); + } + if (const auto value = GetDoubleField(opt, "curve_wind_speed_step"); + value.has_value() && value.value() > 0.0 && value.value() <= 2.0) { + options.curve_wind_speed_step = value.value(); + } + if (const auto value = GetDoubleField(opt, "wind_speed_change_threshold"); + value.has_value() && value.value() >= 0.0) { + options.wind_speed_change_threshold = value.value(); + } + if (const auto value = GetDoubleField(opt, "iqr_lower_multiplier"); + value.has_value() && value.value() >= 0.0 && value.value() <= 10.0) { + options.iqr_lower_multiplier = value.value(); + } + if (const auto value = GetDoubleField(opt, "iqr_upper_multiplier"); + value.has_value() && value.value() >= 0.0 && value.value() <= 10.0) { + options.iqr_upper_multiplier = value.value(); + } + if (const auto value = GetDoubleField(opt, "minimum_generator_speed"); + value.has_value() && value.value() >= 0.0) { + options.minimum_generator_speed = value.value(); + } + if (const auto value = GetDoubleField(opt, "generator_speed_k"); + value.has_value() && value.value() >= 0.0) { + options.generator_speed_k = value.value(); + } + return options; +} + +void AddInvalid(std::unordered_map& counters, const std::string& reason) { + counters[reason] += 1; +} + +std::vector FilterLimitPower(const std::vector& rows, + const CalculationOptions& options, + int& removed_count, + std::vector& removed_points) { + removed_count = 0; + if (rows.empty()) { + return {}; + } + + double min_power = rows.front().active_power; + for (const auto& row : rows) { + min_power = std::min(min_power, row.active_power); + } + + std::set remove_indexes; + for (double interval = min_power; interval < options.rated_power; interval += options.power_step) { + std::unordered_map> grouped_by_date; + for (size_t i = 0; i < rows.size(); ++i) { + const auto& row = rows[i]; + if (row.active_power >= interval && + row.active_power < interval + options.power_step) { + grouped_by_date[row.date].push_back(i); + } + } + + for (const auto& group : grouped_by_date) { + if (group.second.empty()) { + continue; + } + double min_wind = rows[group.second.front()].wind_speed; + double max_wind = min_wind; + for (size_t index : group.second) { + min_wind = std::min(min_wind, rows[index].wind_speed); + max_wind = std::max(max_wind, rows[index].wind_speed); + } + if (max_wind - min_wind > options.wind_speed_change_threshold) { + remove_indexes.insert(group.second.begin(), group.second.end()); + } + } + } + + std::vector result; + result.reserve(rows.size()); + for (size_t i = 0; i < rows.size(); ++i) { + if (remove_indexes.count(i) == 0) { + result.push_back(rows[i]); + } else { + removed_points.push_back(RemovedPoint{rows[i], "limit_power"}); + } + } + removed_count = static_cast(remove_indexes.size()); + return result; +} + +template +std::vector FilterByWindBinIqr(const std::vector& rows, + const CalculationOptions& options, + ValueGetter value_getter, + int& removed_count, + std::vector& removed_points, + const std::string& reason) { + removed_count = 0; + if (rows.empty()) { + return {}; + } + + double min_wind = rows.front().wind_speed; + double max_wind = min_wind; + for (const auto& row : rows) { + min_wind = std::min(min_wind, row.wind_speed); + max_wind = std::max(max_wind, row.wind_speed); + } + + std::vector result; + result.reserve(rows.size()); + for (double interval = min_wind; interval < max_wind; + interval += options.cleaning_wind_speed_step) { + std::vector interval_rows; + std::vector values; + for (const auto& row : rows) { + if (row.wind_speed >= interval && + row.wind_speed < interval + options.cleaning_wind_speed_step) { + interval_rows.push_back(row); + values.push_back(value_getter(row)); + } + } + + if (interval_rows.empty()) { + continue; + } + + if (interval_rows.size() >= 4) { + const double q1 = Quantile(values, 0.25); + const double q3 = Quantile(values, 0.75); + const double iqr = q3 - q1; + const double lower = q1 - options.iqr_lower_multiplier * iqr; + const double upper = q3 + options.iqr_upper_multiplier * iqr; + + for (const auto& row : interval_rows) { + const double value = value_getter(row); + if (value >= lower && value <= upper) { + result.push_back(row); + } else { + ++removed_count; + removed_points.push_back(RemovedPoint{row, reason}); + } + } + } else { + result.insert(result.end(), interval_rows.begin(), interval_rows.end()); + } + } + + return result; +} + +std::string DatePart(const std::string& time_text) { + if (time_text.size() >= 10) { + return time_text.substr(0, 10); + } + return ""; +} + +} // namespace + +void WindPowerController::StartJob( + const HttpRequestPtr& req, + std::function&& callback) { + std::string error; + const auto body = ParseBody(req, error); + if (!body.has_value()) { + SendError(callback, kErrorInvalidRequest, error); + return; + } + + if (!body->contains("files") || !(*body)["files"].is_array() || + !body->contains("mapping") || !(*body)["mapping"].is_object()) { + SendError(callback, kErrorInvalidRequest, "缺少 files 或 mapping 参数"); + return; + } + + try { + fs::create_directories(JobsRoot()); + const auto job_id = GenerateJobId(); + fs::create_directories(JobDir(job_id)); + + std::ofstream meta(JobDir(job_id) / "metadata.json", std::ios::trunc); + meta << body->dump(2); + meta.close(); + + std::ofstream rows(JobRowsPath(job_id), std::ios::trunc); + rows.close(); + + json data; + data["job_id"] = job_id; + SendSuccess(callback, data); + } catch (const std::exception&) { + SendError(callback, kErrorServer, "创建计算任务失败"); + } +} + +void WindPowerController::UploadChunk( + const HttpRequestPtr& req, + std::function&& callback) { + std::string error; + const auto body = ParseBody(req, error); + if (!body.has_value()) { + SendError(callback, kErrorInvalidRequest, error); + return; + } + + const auto job_id = GetStringField(*body, "job_id"); + if (!job_id.has_value() || !IsSafeJobId(job_id.value()) || + !fs::exists(JobDir(job_id.value()))) { + SendError(callback, kErrorJobNotFound, "计算任务不存在"); + return; + } + if (!body->contains("rows") || !(*body)["rows"].is_array()) { + SendError(callback, kErrorInvalidRequest, "缺少 rows 参数"); + return; + } + + try { + std::ofstream out(JobRowsPath(job_id.value()), std::ios::app); + int accepted = 0; + for (const auto& row : (*body)["rows"]) { + if (!row.is_object()) { + continue; + } + out << row.dump() << '\n'; + ++accepted; + } + out.close(); + + json data; + data["accepted_rows"] = accepted; + SendSuccess(callback, data); + } catch (const std::exception&) { + SendError(callback, kErrorServer, "保存分片数据失败"); + } +} + +void WindPowerController::FinishJob( + const HttpRequestPtr& req, + std::function&& callback) { + std::string error; + const auto body = ParseBody(req, error); + if (!body.has_value()) { + SendError(callback, kErrorInvalidRequest, error); + return; + } + + const auto job_id = GetStringField(*body, "job_id"); + if (!job_id.has_value() || !IsSafeJobId(job_id.value()) || + !fs::exists(JobRowsPath(job_id.value()))) { + SendError(callback, kErrorJobNotFound, "计算任务不存在"); + return; + } + + const CalculationOptions options = ParseOptions(*body); + std::vector parsed_rows; + std::unordered_map invalid_reasons; + int raw_rows = 0; + + try { + std::ifstream in(JobRowsPath(job_id.value())); + std::string line; + while (std::getline(in, line)) { + if (Trim(line).empty()) { + continue; + } + ++raw_rows; + + json row; + try { + row = json::parse(line); + } catch (const std::exception&) { + AddInvalid(invalid_reasons, "invalid_json"); + continue; + } + + const auto time_text = GetStringField(row, "time"); + const auto fan_id = GetStringField(row, "fan_id"); + const auto wind_speed = GetDoubleField(row, "wind_speed"); + const auto active_power = GetDoubleField(row, "active_power"); + const auto generator_speed = GetDoubleField(row, "generator_speed"); + + if (!active_power.has_value() || !std::isfinite(active_power.value()) || + active_power.value() <= 0.0) { + AddInvalid(invalid_reasons, "invalid_active_power"); + continue; + } + if (!generator_speed.has_value() || !std::isfinite(generator_speed.value()) || + generator_speed.value() < + options.generator_speed_k * options.minimum_generator_speed) { + AddInvalid(invalid_reasons, "invalid_generator_speed"); + continue; + } + if (!time_text.has_value()) { + AddInvalid(invalid_reasons, "invalid_time"); + continue; + } + const auto timestamp = ParseTime(time_text.value()); + if (!timestamp.has_value()) { + AddInvalid(invalid_reasons, "invalid_time"); + continue; + } + if (!fan_id.has_value() || fan_id.value().empty()) { + AddInvalid(invalid_reasons, "empty_fan_id"); + continue; + } + if (!wind_speed.has_value() || !std::isfinite(wind_speed.value()) || + wind_speed.value() <= 0.0) { + AddInvalid(invalid_reasons, "invalid_wind_speed"); + continue; + } + + ValidRow valid_row; + valid_row.time = time_text.value(); + valid_row.timestamp = timestamp.value(); + valid_row.date = DatePart(time_text.value()); + valid_row.fan_id = fan_id.value(); + valid_row.wind_speed = wind_speed.value(); + valid_row.active_power = active_power.value(); + valid_row.generator_speed = generator_speed.value(); + parsed_rows.push_back(valid_row); + } + } catch (const std::exception&) { + SendError(callback, kErrorServer, "读取任务数据失败"); + return; + } + + std::sort(parsed_rows.begin(), parsed_rows.end(), [](const ValidRow& left, const ValidRow& right) { + if (left.fan_id != right.fan_id) { + return left.fan_id < right.fan_id; + } + return left.timestamp < right.timestamp; + }); + + std::vector deduped_rows; + std::set seen_keys; + int duplicate_rows = 0; + for (const auto& row : parsed_rows) { + const auto key = row.fan_id + "|" + std::to_string(row.timestamp); + if (!seen_keys.insert(key).second) { + ++duplicate_rows; + continue; + } + deduped_rows.push_back(row); + } + if (duplicate_rows > 0) { + AddInvalid(invalid_reasons, "duplicate_time"); + } + invalid_reasons["duplicate_time"] = duplicate_rows; + + std::unordered_map> rows_by_fan; + for (const auto& row : deduped_rows) { + rows_by_fan[row.fan_id].push_back(row); + } + + json fans = json::array(); + json curves = json::object(); + json bins = json::object(); + json scatter_points = json::object(); + json filtered_points = json::object(); + + std::vector fan_ids; + fan_ids.reserve(rows_by_fan.size()); + for (const auto& item : rows_by_fan) { + fan_ids.push_back(item.first); + } + std::sort(fan_ids.begin(), fan_ids.end()); + + int limit_power_count = 0; + int tip_speed_ratio_outlier_count = 0; + int speed_power_outlier_count = 0; + int cleaned_rows_count = 0; + for (const auto& fan_id : fan_ids) { + fans.push_back(fan_id); + auto fan_rows = rows_by_fan[fan_id]; + std::vector fan_removed_points; + + int removed = 0; + fan_rows = FilterLimitPower(fan_rows, options, removed, fan_removed_points); + limit_power_count += removed; + + for (auto& row : fan_rows) { + row.tip_speed_ratio = row.generator_speed * 3.14 * 162.0 * 78.0 * 30.0 / + row.wind_speed; + } + + fan_rows = FilterByWindBinIqr( + fan_rows, + options, + [](const ValidRow& row) { return row.tip_speed_ratio; }, + removed, + fan_removed_points, + "tip_speed_ratio_outlier"); + tip_speed_ratio_outlier_count += removed; + + fan_rows = FilterByWindBinIqr( + fan_rows, + options, + [](const ValidRow& row) { return row.active_power; }, + removed, + fan_removed_points, + "speed_power_outlier"); + speed_power_outlier_count += removed; + cleaned_rows_count += static_cast(fan_rows.size()); + + json fan_scatter = json::array(); + for (const auto& row : fan_rows) { + json point; + point["wind_speed"] = row.wind_speed; + point["active_power"] = row.active_power; + fan_scatter.push_back(point); + } + scatter_points[fan_id] = fan_scatter; + + json fan_filtered = json::array(); + for (const auto& removed_point : fan_removed_points) { + json point; + point["wind_speed"] = removed_point.row.wind_speed; + point["active_power"] = removed_point.row.active_power; + point["reason"] = removed_point.reason; + fan_filtered.push_back(point); + } + filtered_points[fan_id] = fan_filtered; + + json fan_curve = json::array(); + json fan_bins_json = json::array(); + const double curve_min = 1.0 - options.curve_wind_speed_step * 0.5; + const double curve_max = 25.0 + options.curve_wind_speed_step * 0.5; + for (double start = curve_min; start < curve_max; start += options.curve_wind_speed_step) { + const double end = start + options.curve_wind_speed_step; + std::vector values; + for (const auto& row : fan_rows) { + if (row.wind_speed > start && row.wind_speed <= end) { + values.push_back(row.active_power); + } + } + + if (!values.empty()) { + json point; + point["wind_speed_start"] = start; + point["wind_speed_end"] = end; + point["wind_speed"] = (start + end) / 2.0; + point["sample_count"] = values.size(); + point["average_power"] = Mean(values); + point["median_power"] = Quantile(values, 0.5); + point["stddev_power"] = StdDev(values, point["average_power"].get()); + point["p25_power"] = Quantile(values, 0.25); + point["p75_power"] = Quantile(values, 0.75); + point["confidence"] = "脚本分箱"; + fan_curve.push_back(point); + fan_bins_json.push_back(point); + } + } + + curves[fan_id] = fan_curve; + bins[fan_id] = fan_bins_json; + } + + invalid_reasons["limit_power"] = limit_power_count; + invalid_reasons["tip_speed_ratio_outlier"] = tip_speed_ratio_outlier_count; + invalid_reasons["speed_power_outlier"] = speed_power_outlier_count; + + const int invalid_total = raw_rows - cleaned_rows_count; + + json summary; + summary["raw_rows"] = raw_rows; + summary["valid_rows"] = std::max(0, cleaned_rows_count); + summary["invalid_rows"] = std::max(0, invalid_total); + summary["duplicate_rows"] = duplicate_rows; + summary["limit_power_rows"] = limit_power_count; + summary["tip_speed_ratio_outlier_rows"] = tip_speed_ratio_outlier_count; + summary["speed_power_outlier_rows"] = speed_power_outlier_count; + summary["invalid_reasons"] = CounterJson(invalid_reasons); + summary["fan_count"] = fans.size(); + + json data; + data["summary"] = summary; + data["fans"] = fans; + data["curves"] = curves; + data["bins"] = bins; + data["scatter_points"] = scatter_points; + data["filtered_points"] = filtered_points; + + try { + fs::remove_all(JobDir(job_id.value())); + } catch (const std::exception&) { + // 任务结果已经生成,临时文件清理失败不影响本次响应。 + } + + SendSuccess(callback, data); +} + +void WindPowerController::DeleteJob( + const HttpRequestPtr&, + std::function&& callback, + const std::string& job_id) { + if (!IsSafeJobId(job_id)) { + SendError(callback, kErrorInvalidRequest, "任务编号非法"); + return; + } + + try { + fs::remove_all(JobDir(job_id)); + SendSuccess(callback); + } catch (const std::exception&) { + SendError(callback, kErrorServer, "清理任务失败"); + } +} diff --git a/backend/src/controllers/WindPowerController.h b/backend/src/controllers/WindPowerController.h new file mode 100644 index 0000000..25e04f3 --- /dev/null +++ b/backend/src/controllers/WindPowerController.h @@ -0,0 +1,31 @@ +#ifndef WINDPOWERCONTROLLER_H +#define WINDPOWERCONTROLLER_H + +#include +#include + +#include "utils/ResponseUtil.h" + +using namespace drogon; + +class WindPowerController : public drogon::HttpController { +public: + METHOD_LIST_BEGIN + ADD_METHOD_TO(WindPowerController::StartJob, "/api/wind/jobs/start", Post); + ADD_METHOD_TO(WindPowerController::UploadChunk, "/api/wind/jobs/chunk", Post); + ADD_METHOD_TO(WindPowerController::FinishJob, "/api/wind/jobs/finish", Post); + ADD_METHOD_TO(WindPowerController::DeleteJob, "/api/wind/jobs/{1}", Delete); + METHOD_LIST_END + + void StartJob(const HttpRequestPtr& req, + std::function&& callback); + void UploadChunk(const HttpRequestPtr& req, + std::function&& callback); + void FinishJob(const HttpRequestPtr& req, + std::function&& callback); + void DeleteJob(const HttpRequestPtr& req, + std::function&& callback, + const std::string& job_id); +}; + +#endif diff --git a/backend/src/main.cpp b/backend/src/main.cpp new file mode 100644 index 0000000..d99dba4 --- /dev/null +++ b/backend/src/main.cpp @@ -0,0 +1,31 @@ +#include +#include + +#include + +#include "controllers/WindPowerController.h" + +using namespace drogon; + +int main() { + // 抑制 Drogon/trantor 内部日志噪声 + trantor::Logger::setLogLevel(trantor::Logger::kFatal); + + // 加载 Drogon 配置(监听端口 / CORS / 静态资源根目录) + LOG_INFO << "Loading server configuration..."; + app().loadConfigFile("config/server_config.json"); + app().registerController(std::make_shared()); + + // SPA 前端路由回退:未匹配路径统一返回 index.html,交由前端路由处理 + app().setCustom404Page( + HttpResponse::newFileResponse("./web/index.html", "", CT_TEXT_HTML), + false); + + LOG_INFO << "wind_server starting..."; + + // 阻塞运行,直到收到 SIGINT/SIGTERM + app().run(); + + LOG_INFO << "wind_server stopped"; + return 0; +} diff --git a/backend/src/utils/ResponseUtil.h b/backend/src/utils/ResponseUtil.h new file mode 100644 index 0000000..d2362cb --- /dev/null +++ b/backend/src/utils/ResponseUtil.h @@ -0,0 +1,90 @@ +#ifndef HYPEREDGEX_RESPONSEUTIL_H +#define HYPEREDGEX_RESPONSEUTIL_H + +#include +#include + +class ResponseUtil { +public: + // 生成成功响应,包含数据 + static nlohmann::json GenerateResponse(int status, const std::string& msg, const nlohmann::json& data) { + nlohmann::json response; + response["status"] = status; + response["msg"] = msg; + response["data"] = data; + return response; + } + + // 生成成功响应,无数据 + static nlohmann::json GenerateResponse(int status, const std::string& msg) { + nlohmann::json response; + response["status"] = status; + response["msg"] = msg; + response["data"] = nullptr; + return response; + } + + // 生成成功响应的便捷方法 + static nlohmann::json GenerateSuccessResponse(const nlohmann::json& data) { + return GenerateResponse(0, "success", data); + } + + // 生成成功响应的便捷方法(无数据) + static nlohmann::json GenerateSuccessResponse() { + return GenerateResponse(0, "success"); + } + + /** + * 生成成功响应(原始 JSON 字符串版本) + * 用于读配置文件原样返回前端的场景, + * 绕过 nlohmann::json 的 parse/dump 避免丢失 key 顺序。 + */ + static std::string GenerateSuccessResponseRaw(const std::string& rawJsonData) { + return "{\"status\":0,\"msg\":\"success\",\"data\":" + rawJsonData + "}"; + } + + // 生成错误响应的便捷方法 + static nlohmann::json GenerateErrorResponse(int status, const std::string& msg) { + return GenerateResponse(status, msg, nullptr); + } + + ResponseUtil() = delete; // 禁止实例化 +}; + +// Drogon HTTP 响应便捷辅助 +#if __has_include() + +#include +#include + +using DrogonCallback = std::function; + +inline void SendJson(DrogonCallback& cb, const nlohmann::json& body, + drogon::HttpStatusCode code = drogon::k200OK) { + auto resp = drogon::HttpResponse::newHttpResponse(); + resp->setStatusCode(code); + resp->setBody(body.dump()); + resp->setContentTypeCode(drogon::CT_APPLICATION_JSON); + cb(resp); +} + +inline void SendSuccess(DrogonCallback& cb, const nlohmann::json& data) { + SendJson(cb, ResponseUtil::GenerateSuccessResponse(data)); +} + +inline void SendSuccess(DrogonCallback& cb) { + SendJson(cb, ResponseUtil::GenerateSuccessResponse()); +} + +inline void SendError(DrogonCallback& cb, int code, const std::string& msg, + drogon::HttpStatusCode http = drogon::k200OK) { + SendJson(cb, ResponseUtil::GenerateErrorResponse(code, msg), http); +} + +inline void SendForbidden(DrogonCallback& cb, const std::string& msg = "无权限执行此操作") { + SendError(cb, 3, msg, drogon::k403Forbidden); +} + +#endif // __has_include drogon + +#endif //HYPEREDGEX_RESPONSEUTIL_H diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..c7d56a3 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# ===================================================== +# deploy.sh — 打包并部署 wind_power 到远端主机 +# +# 远端变更: +# 1. 本地执行 package.sh 生成 runtime/wind_power/ +# 2. rsync runtime/wind_power/ → ~/wind_power/ +# 3. 安装/更新 systemd 服务 wind_power.service +# 4. 重启 wind_power,并验证 8848 直连接口 +# +# 不安装、不配置 nginx;不修改 gitea。 +# +# 用法: +# ./deploy.sh +# ./deploy.sh ubuntu@1.2.3.4 +# ./deploy.sh --dry-run +# ===================================================== + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_TARGET="ubuntu@82.157.83.226" +SERVICE_NAME="wind_power" +LOCAL_APP_DIR="${ROOT_DIR}/runtime/wind_power" +DEPLOY_STAGE="${ROOT_DIR}/runtime/deploy" +SSH_PASSWORD="i7568737i~" + +TARGET="${DEFAULT_TARGET}" +DRY_RUN=false +for arg in "$@"; do + case "${arg}" in + --dry-run) DRY_RUN=true ;; + -*) echo "[deploy] 未知选项: ${arg}" >&2; exit 1 ;; + *) TARGET="${arg}" ;; + esac +done + +REMOTE_USER="${TARGET%%@*}" + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +CYAN='\033[0;36m' +NC='\033[0m' + +info(){ echo -e "${GREEN}[INFO]${NC} $*"; } +step(){ echo -e "${CYAN}[STEP]${NC} $*"; } +warn(){ echo -e "${YELLOW}[WARN]${NC} $*"; } +err(){ echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } + +command -v sshpass >/dev/null 2>&1 || err "缺少 sshpass:sudo apt install -y sshpass" + +SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15) + +ssh_cmd() { + sshpass -p "${SSH_PASSWORD}" ssh "${SSH_OPTS[@]}" "${TARGET}" "$@" +} + +step "1/5 本地打包..." +bash "${ROOT_DIR}/package.sh" --build-type Release +[ -f "${LOCAL_APP_DIR}/wind_server" ] || err "打包产物缺失: ${LOCAL_APP_DIR}/wind_server" + +step "2/5 准备远端配置..." +REMOTE_HOME="$(ssh_cmd 'echo $HOME')" +REMOTE_APP_PATH="${REMOTE_HOME}/wind_power" +info "远端应用目录: ${REMOTE_APP_PATH}" + +mkdir -p "${DEPLOY_STAGE}" +cat > "${DEPLOY_STAGE}/${SERVICE_NAME}.service" </dev/null || (sudo apt-get update -qq && sudo apt-get install -y -qq rsync >/dev/null)' +sshpass -p "${SSH_PASSWORD}" rsync -az --delete -e "ssh ${SSH_OPTS[*]}" \ + "${LOCAL_APP_DIR}/" "${TARGET}:${REMOTE_APP_PATH}/" +sshpass -p "${SSH_PASSWORD}" scp "${SSH_OPTS[@]}" \ + "${DEPLOY_STAGE}/${SERVICE_NAME}.service" \ + "${TARGET}:/tmp/${SERVICE_NAME}.service" +info "✅ 已上传" + +step "4/5 安装并重启 systemd 服务..." +ssh_cmd "SERVICE_NAME='${SERVICE_NAME}'" 'bash -s' <<'REMOTE' +set -euo pipefail +SERVICE_NAME="${SERVICE_NAME}" +mkdir -p "$HOME/wind_power/logs" +sudo cp "/tmp/${SERVICE_NAME}.service" "/etc/systemd/system/${SERVICE_NAME}.service" +sudo apt-get update -qq 2>/dev/null || true +sudo apt-get install -y -qq libjsoncpp25 libc-ares2 >/dev/null 2>&1 || true +sudo systemctl daemon-reload +sudo systemctl enable "${SERVICE_NAME}" >/dev/null 2>&1 || true +sudo systemctl restart "${SERVICE_NAME}" +echo "[remote] systemd ${SERVICE_NAME} 已安装并启动" +REMOTE + +step "5/5 校验 8848 直连服务..." +sleep 2 +ssh_cmd "SERVICE_NAME='${SERVICE_NAME}'" 'bash -s' <<'REMOTE' | sed 's/^/[remote] /' +set +e +echo "service: $(systemctl is-active ${SERVICE_NAME})" +echo "health: $(curl -s -m 5 localhost:8848/api/system/health)" +echo "version: $(curl -s -m 5 localhost:8848/api/system/version)" +echo "listen: $(ss -tlnp 2>/dev/null | grep ':8848' | head -1 | sed 's/.*users://')" +REMOTE + +echo "" +info "✅ 部署完成" +echo -e " wind_power: ${GREEN}http://${TARGET#*@}:8848/${NC}" +echo " 查看服务: ssh ${TARGET} 'systemctl status ${SERVICE_NAME}'" diff --git a/docs/接口文档.md b/docs/接口文档.md new file mode 100644 index 0000000..eefbf8a --- /dev/null +++ b/docs/接口文档.md @@ -0,0 +1,238 @@ +# 接口文档 + +## 通用响应 + +后端接口统一返回: + +```json +{ + "status": 0, + "msg": "success", + "data": {} +} +``` + +`status` 为 `0` 表示成功,非 `0` 表示业务失败。 + +## 系统接口 + +### GET /api/system/health + +返回服务健康状态。 + +成功响应: + +```json +{ + "status": 0, + "msg": "success", + "data": { + "status": "ok" + } +} +``` + +### GET /api/system/version + +返回服务版本信息。 + +成功响应: + +```json +{ + "status": 0, + "msg": "success", + "data": { + "name": "wind_power_cal", + "version": "0.1.0" + } +} +``` + +## 风功率计算接口 + +### POST /api/wind/jobs/start + +创建风功率计算任务。前端已完成 Excel 表头解析和字段映射,后端只保存任务元数据。 + +请求: + +```json +{ + "files": [ + { + "file_name": "01_风机历史数据.xls", + "row_count": 51906 + } + ], + "mapping": { + "time": "时间", + "fan_id": "风机编号", + "wind_speed": "风速(m/s)", + "active_power": "有功功率(kW)", + "generator_speed": "发电机转速(rpm)" + } +} +``` + +成功响应: + +```json +{ + "status": 0, + "msg": "success", + "data": { + "job_id": "job_123" + } +} +``` + +### POST /api/wind/jobs/chunk + +上传标准化后的数据分片。每行字段必须使用标准字段名。 + +请求: + +```json +{ + "job_id": "job_123", + "chunk_index": 0, + "rows": [ + { + "time": "2024-09-01 14:00:00", + "fan_id": "01#", + "wind_speed": 3.414, + "active_power": 114.3471, + "generator_speed": 1109.3143 + } + ] +} +``` + +成功响应: + +```json +{ + "status": 0, + "msg": "success", + "data": { + "accepted_rows": 1 + } +} +``` + +### POST /api/wind/jobs/finish + +完成任务并执行清洗、排序、分箱和功率曲线计算。 + +请求: + +```json +{ + "job_id": "job_123", + "options": { + "rated_power": 4800, + "rated_wind_speed": 18, + "power_step": 5, + "cleaning_wind_speed_step": 0.25, + "curve_wind_speed_step": 0.5, + "wind_speed_change_threshold": 1, + "iqr_lower_multiplier": 1.8, + "iqr_upper_multiplier": 2, + "minimum_generator_speed": 1, + "generator_speed_k": 0.9 + } +} +``` + +成功响应: + +```json +{ + "status": 0, + "msg": "success", + "data": { + "summary": { + "raw_rows": 51906, + "valid_rows": 38017, + "invalid_rows": 13889, + "duplicate_rows": 0, + "limit_power_rows": 0, + "tip_speed_ratio_outlier_rows": 0, + "speed_power_outlier_rows": 0, + "invalid_reasons": { + "invalid_active_power": 13889, + "limit_power": 0, + "tip_speed_ratio_outlier": 0, + "speed_power_outlier": 0 + }, + "fan_count": 1 + }, + "fans": ["01#"], + "curves": { + "01#": [ + { + "wind_speed_start": 3.0, + "wind_speed_end": 3.5, + "wind_speed": 3.25, + "sample_count": 120, + "average_power": 180.5, + "median_power": 176.2, + "stddev_power": 32.1, + "p25_power": 150.0, + "p75_power": 205.0, + "confidence": "脚本分箱" + } + ] + }, + "bins": { + "01#": [] + }, + "scatter_points": { + "01#": [ + { + "wind_speed": 3.414, + "active_power": 114.3471 + } + ] + }, + "filtered_points": { + "01#": [ + { + "wind_speed": 5.2, + "active_power": 430.0, + "reason": "speed_power_outlier" + } + ] + } + } +} +``` + +清洗规则: + +- `active_power <= 0` 的数据剔除。 +- `generator_speed < generator_speed_k * minimum_generator_speed` 的数据剔除。 +- 时间不可解析的数据剔除。 +- 风机编号为空的数据剔除。 +- `wind_speed <= 0` 的数据剔除。 +- 同一风机同一时间重复记录保留第一条。 +- 限功率识别按参考脚本执行:按功率分箱、按日期分组,组内风速跨度大于阈值时剔除。 +- 叶尖速比使用参考脚本公式 `generator_speed * 3.14 * 162 * 78 * 30 / wind_speed`,按风速分箱做 IQR 清洗。 +- 风速-功率关系按同一风速分箱和 IQR 参数清洗。 +- 最终曲线按 `curve_wind_speed_step` 左开右闭分箱,区间非空即输出平均功率点。 +- `scatter_points` 返回清洗后保留点,`filtered_points` 返回限功率和 IQR 阶段滤除的点。 + +### DELETE /api/wind/jobs/{job_id} + +清理未完成任务的临时文件。 + +成功响应: + +```json +{ + "status": 0, + "msg": "success", + "data": null +} +``` diff --git a/docs/风机历史数据无效值清洗与功率计算设计.md b/docs/风机历史数据无效值清洗与功率计算设计.md new file mode 100644 index 0000000..2ddea73 --- /dev/null +++ b/docs/风机历史数据无效值清洗与功率计算设计.md @@ -0,0 +1,353 @@ +# 风机历史数据无效值清洗与功率计算设计 + +## 背景与目标 + +当前 `docs/完整年数据/` 目录下保存了 16 台风机从 `2024-09-01 14:00:00` 到 `2025-09-01 14:00:00` 的历史数据。以 `01_2024-09-01_14-00-00-2025-09-01_14-00-00_风机历史数据.xls` 为例,文件为 10 分钟粒度历史数据,包含风速、发电机转速、桨叶角度和有功功率等字段。 + +本文档定义两类计算口径: + +- 无效值清洗:从历史数据中剔除停机、无风、无功率、异常转速、异常风速、疑似限功率和统计离群点。 +- 风机功率计算:直接使用实测 `有功功率(kW)` 作为时刻功率,并基于清洗后的数据计算风速-功率曲线和发电量。 + +## 数据输入 + +当前历史数据字段如下: + +| 字段 | 单位 | 用途 | +| --- | --- | --- | +| 时间 | - | 数据时间戳,当前主要为 10 分钟间隔 | +| 风机编号 | - | 区分风机,如 `01#` | +| 风机限功率时间(s) | s | 累计类字段,不能直接用 `> 0` 判断当前是否限功率 | +| 变流器发电机转速(rpm) | rpm | 辅助判断运行状态和转速异常 | +| 发电机转速(rpm) | rpm | 判断是否发电、计算叶尖速比时使用 | +| 风速(m/s) | m/s | 风速分箱和功率曲线计算 | +| 当前桨叶角度(°) | ° | 辅助识别停机、顺桨、限功率或异常状态 | +| 有功功率(kW) | kW | 实测输出功率,作为风机时刻功率 | + +## 设计原则 + +- 先清洗后计算,所有功率曲线、发电量、性能指标均基于清洗后的有效数据。 +- 实测功率优先,已有 `有功功率(kW)` 时不使用理论风功率公式反推时刻功率。 +- 清洗规则分层执行,基础无效值直接剔除,疑似限功率和统计离群点应可追溯标记。 +- 每个风速区间必须输出样本数,样本不足的区间不能作为正式功率曲线点。 +- 额定功率、额定风速、叶轮直径、齿轮箱速比等机型参数必须配置化,不能写死为旧脚本中的 `4800` 或 `6700`。 + +## 无效值清洗方案 + +### 1. 字段校验 + +读取每个 `.xls` 文件后,先检查必需字段: + +```text +时间 +风机编号 +发电机转速(rpm) +风速(m/s) +当前桨叶角度(°) +有功功率(kW) +``` + +如果缺少 `有功功率(kW)` 或 `风速(m/s)`,该文件不能用于功率曲线计算。如果缺少 `发电机转速(rpm)` 或 `当前桨叶角度(°)`,仍可做基础风速-功率统计,但不能执行完整运行状态清洗。 + +### 2. 时间清洗 + +时间字段需转换为标准时间戳。以下数据应剔除或单独标记: + +- 时间为空或无法解析。 +- 同一风机同一时间重复记录。 +- 时间倒序记录。 +- 与前一条记录间隔明显异常的数据段。 + +当前数据主要为 10 分钟粒度。若用于发电量积分,建议对异常间隔单独处理,不要默认每条都是 10 分钟。 + +### 3. 基础有效发电点过滤 + +用于计算风速-功率曲线时,建议保留满足以下条件的数据: + +```text +风速(m/s) > 0 +有功功率(kW) > 0 +发电机转速(rpm) > 1 +``` + +说明: + +- `有功功率(kW) = 0` 通常表示未发电、停机、待机或数据无效,不应参与功率曲线均值。 +- `发电机转速(rpm) <= 1` 通常表示机组未处于正常发电转动状态。 +- 风速为 0 或负数不符合功率曲线计算输入条件。 + +### 4. 物理边界过滤 + +按机型参数增加边界过滤: + +| 指标 | 建议规则 | 说明 | +| --- | --- | --- | +| 风速 | `0 < 风速 <= 35` | 超出范围优先视为传感器或导出异常 | +| 有功功率 | `0 < 有功功率 <= 额定功率 * 1.10` | 保留少量超额定裕度,过高视为异常 | +| 发电机转速 | `0 < 转速 <= 最大允许转速 * 1.05` | 最大允许转速从机型配置获取 | +| 桨叶角度 | `-10 <= 桨叶角度 <= 95` | 极端负角度或过大角度需结合状态判断 | + +当前 01 号文件最大有功功率约为 `3307 kW`,因此该机组额定功率应按约 `3300 kW` 级配置,不能使用旧脚本中的 `4800 kW` 或 `6700 kW`。 + +### 5. 疑似限功率识别 + +`风机限功率时间(s)` 是累计时间类字段,当前文件中全量数据均大于 0,不能用 `风机限功率时间(s) > 0` 判断当前记录是否限功率。 + +建议使用相邻记录增量辅助判断: + +```text +限功率时间增量 = 当前行 风机限功率时间(s) - 上一行 风机限功率时间(s) +``` + +疑似限功率点可按以下条件组合识别: + +- 限功率时间增量大于 0。 +- 风速较高,但有功功率长期低于同风速段正常中位功率。 +- 相邻多个 10 分钟点功率接近同一平台值,但风速变化明显。 +- 桨叶角度明显偏大,且功率未随风速增加。 + +建议先把疑似限功率数据标记为 `is_limited = true`,不要直接删除。计算正常功率曲线时排除;分析限功率损失时保留。 + +### 6. 叶尖速比异常过滤 + +如果需要使用叶尖速比辅助清洗,应采用标准公式,不建议沿用旧脚本中的硬编码乘法。 + +叶尖速比计算公式: + +```text +叶轮转速(rpm) = 发电机转速(rpm) / 齿轮箱速比 +叶尖速比 = 叶轮转速(rpm) * 2π / 60 * 叶轮半径(m) / 风速(m/s) +``` + +示例: + +```text +lambda = generator_rpm / gearbox_ratio * 2π / 60 * rotor_radius / wind_speed +``` + +叶轮半径、齿轮箱速比必须来自机型配置。清洗时可按风速分箱,对每个风速区间内的叶尖速比使用 IQR 或 MAD 剔除离群点。 + +### 7. 风速-功率离群点过滤 + +按风速分箱后,对每个风速区间内的 `有功功率(kW)` 做统计过滤。 + +推荐分箱宽度: + +```text +0.5 m/s +``` + +样本数规则: + +| 区间样本数 | 处理方式 | +| --- | --- | +| `< 10` | 不剔除,但标记为样本不足,不参与正式曲线 | +| `10 - 29` | 可做宽松离群过滤,结果标记为低置信度 | +| `>= 30` | 执行正式离群过滤,可用于功率曲线 | + +IQR 过滤规则: + +```text +Q1 = 区间功率 25% 分位数 +Q3 = 区间功率 75% 分位数 +IQR = Q3 - Q1 +下限 = Q1 - 1.5 * IQR +上限 = Q3 + 1.5 * IQR +保留 下限 <= 有功功率 <= 上限 的数据 +``` + +高风速额定平台区建议额外处理。额定风速以上,功率应接近额定功率;如果出现大量高风速低功率点,应优先判断为限功率、停机、故障或弃风,不应简单参与正常功率曲线均值。 + +## 风机功率计算方案 + +### 1. 时刻功率 + +历史数据中已有实测功率字段,时刻功率直接取: + +```text +风机时刻功率(kW) = 有功功率(kW) +``` + +不建议使用以下理论公式替代实测时刻功率: + +```text +P = 0.5 * ρ * A * Cp * v^3 +``` + +该公式适合估算风能理论功率或做性能分析,需要空气密度、叶轮扫掠面积和功率系数,不适合在已有实测有功功率时反算实际输出。 + +### 2. 风速-功率曲线 + +风速-功率曲线用于描述不同风速下机组的实测输出能力。计算步骤: + +```text +1. 使用清洗后的有效发电点。 +2. 按风速分箱,建议 0.5 m/s 一个区间。 +3. 每个区间统计有功功率。 +4. 输出平均功率、中位数功率、样本数、标准差和分位数。 +5. 样本不足或疑似限功率区间只标记,不作为正式曲线点。 +``` + +区间定义建议采用左开右闭: + +```text +(区间起点, 区间终点] +``` + +例如: + +```text +(3.75, 4.25] -> 风速中心 4.0 m/s +(4.25, 4.75] -> 风速中心 4.5 m/s +``` + +输出字段: + +| 字段 | 说明 | +| --- | --- | +| 风机编号 | 当前风机 | +| 风速区间起点 | 左边界 | +| 风速区间终点 | 右边界 | +| 风速中心 | 区间中点 | +| 样本数 | 清洗后有效点数量 | +| 平均功率(kW) | 区间功率均值 | +| 中位数功率(kW) | 区间功率中位数 | +| 功率标准差(kW) | 区间功率离散程度 | +| P25功率(kW) | 25% 分位数 | +| P75功率(kW) | 75% 分位数 | +| 置信度 | 正常、低样本、样本不足、疑似限功率 | + +### 3. 发电量计算 + +若需要计算年发电量,应对功率按时间积分。 + +若确认每条数据为 10 分钟间隔: + +```text +单条发电量(kWh) = 有功功率(kW) * 10 / 60 +总发电量(kWh) = 所有单条发电量求和 +``` + +更稳妥的通用算法是使用相邻时间戳差值: + +```text +时间间隔(h) = 下一条时间 - 当前时间 +单条发电量(kWh) = 当前有功功率(kW) * 时间间隔(h) +``` + +时间间隔异常时建议标记,不直接参与年发电量统计,或按业务规则补齐。 + +### 4. 理论风功率和性能指标 + +如需评估风能利用效率,可额外计算理论风功率: + +```text +扫掠面积 A = π * 叶轮半径^2 +理论风功率(kW) = 0.5 * 空气密度 * A * 风速^3 / 1000 +功率系数 Cp = 有功功率(kW) / 理论风功率(kW) +``` + +注意: + +- 空气密度应使用温度、气压、湿度或场站海拔修正。 +- Cp 只用于性能分析,不用于替代实测 `有功功率(kW)`。 +- Cp 超出合理范围时,应优先检查风速测量、空气密度、功率单位和数据清洗规则。 + +## 数据处理流程 + +```text +读取 XLS 文件 + -> 字段校验 + -> 时间解析与排序 + -> 基础有效发电点过滤 + -> 物理边界过滤 + -> 标记疑似限功率 + -> 可选:叶尖速比异常过滤 + -> 风速-功率分箱离群过滤 + -> 输出清洗后明细 + -> 输出风速-功率曲线表 + -> 输出功率曲线图 + -> 可选:计算发电量和性能指标 +``` + +## 参数配置建议 + +| 参数 | 建议默认值 | 说明 | +| --- | --- | --- | +| 风速分箱宽度 | `0.5 m/s` | 功率曲线统计常用粒度 | +| 最小正式样本数 | `30` | 低于该值标记低置信度 | +| 最小保留样本数 | `10` | 低于该值不参与正式曲线 | +| IQR 系数 | `1.5` | 常规离群点过滤 | +| 发电机最小转速 | `1 rpm` | 判断是否发电 | +| 功率上限 | `额定功率 * 1.10` | 防止异常高功率 | +| 风速上限 | `35 m/s` | 防止异常风速 | + +机型配置建议包含: + +```text +风机编号 +额定功率(kW) +额定风速(m/s) +叶轮直径(m) +齿轮箱速比 +最大允许发电机转速(rpm) +切入风速(m/s) +切出风速(m/s) +``` + +## 验证计划 + +### 脚本验证 + +- 读取 01 号风机文件,确认字段数量和字段名正确。 +- 统计原始行数、有效发电点数量、剔除数量和剔除原因。 +- 输出每个风速区间的样本数、平均功率和中位数功率。 +- 检查空文件、缺字段文件、全无有效数据文件不会报错。 + +### 结果验证 + +- 功率曲线应随风速整体上升,并在额定风速附近进入平台。 +- 01 号风机最大实测功率约 `3307 kW`,额定功率配置应与该量级一致。 +- 高风速低功率点应被标记为疑似限功率、停机或异常,而不是直接拉低正式曲线。 +- 样本数不足的风速区间应在结果中明确标记。 + +### 人工复核 + +- 抽查被剔除的数据,确认不是正常发电点被误删。 +- 抽查疑似限功率点,结合桨叶角度和限功率时间增量确认规则有效。 +- 对比厂家功率曲线或历史报告,确认实测曲线形态合理。 + +## 风险与对策 + +| 风险 | 影响 | 对策 | +| --- | --- | --- | +| 额定功率配置错误 | 功率上限过滤和平台判断失真 | 按每台机组实测最大功率和机型资料确认 | +| 把累计限功率时间误当状态量 | 大量正常数据被误删 | 使用相邻增量和功率形态综合判断 | +| 分箱样本数过少 | 曲线点随机波动大 | 输出样本数和置信度,低样本不参与正式曲线 | +| 叶尖速比公式错误 | 清洗结果偏差 | 使用标准公式,并配置叶轮半径和齿轮箱速比 | +| 时间间隔异常 | 发电量计算偏差 | 使用相邻时间差,并对异常间隔标记 | + +## 后续实施建议 + +建议将现有两个脚本整理为一个可配置处理流程: + +```text +clean_wind_history.py + - 读取单台或多台风机历史数据 + - 输出清洗后明细 + - 输出风速-功率曲线统计表 + - 输出功率曲线图 + - 输出清洗摘要报告 +``` + +同时保留每条数据的清洗标记字段,例如: + +```text +is_valid_generation +is_out_of_physical_range +is_suspected_limited +is_power_outlier +invalid_reason +``` + +这样后续既能生成正式曲线,也能追溯每条数据为什么被保留或剔除。 diff --git a/frontend/web_app/.gitignore b/frontend/web_app/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/web_app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/web_app/eslint.config.js b/frontend/web_app/eslint.config.js new file mode 100644 index 0000000..a5d66db --- /dev/null +++ b/frontend/web_app/eslint.config.js @@ -0,0 +1,39 @@ +import js from '@eslint/js' +import globals from 'globals' +import eslintPluginReactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + eslintPluginReactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: { + ...globals.browser, + ...globals.node, + }, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-hooks/immutability': 'off', + 'react-hooks/preserve-manual-memoization': 'off', + 'react-hooks/purity': 'off', + 'react-hooks/refs': 'off', + 'react-hooks/exhaustive-deps': 'off', + 'react-hooks/set-state-in-effect': 'off', + 'react-refresh/only-export-components': 'off', + }, + }, +]) diff --git a/frontend/web_app/index.html b/frontend/web_app/index.html new file mode 100644 index 0000000..0e3b562 --- /dev/null +++ b/frontend/web_app/index.html @@ -0,0 +1,13 @@ + + + + + + + 风电功率计算平台 + + +
+ + + diff --git a/frontend/web_app/package-lock.json b/frontend/web_app/package-lock.json new file mode 100644 index 0000000..1856194 --- /dev/null +++ b/frontend/web_app/package-lock.json @@ -0,0 +1,2763 @@ +{ + "name": "wind_power_web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wind_power_web", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.13.2", + "uplot": "^1.6.32", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "vite": "^8.0.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmmirror.com/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uplot": { + "version": "1.6.32", + "resolved": "https://registry.npmmirror.com/uplot/-/uplot-1.6.32.tgz", + "integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.1.4", + "resolved": "https://registry.npmmirror.com/vite/-/vite-8.1.4.tgz", + "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.16", + "rolldown": "~1.1.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/web_app/package.json b/frontend/web_app/package.json new file mode 100644 index 0000000..9fd33fe --- /dev/null +++ b/frontend/web_app/package.json @@ -0,0 +1,28 @@ +{ + "name": "wind_power_web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.13.2", + "uplot": "^1.6.32", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "vite": "^8.0.1" + } +} diff --git a/frontend/web_app/public/vite.svg b/frontend/web_app/public/vite.svg new file mode 100644 index 0000000..6473aa2 --- /dev/null +++ b/frontend/web_app/public/vite.svg @@ -0,0 +1 @@ + diff --git a/frontend/web_app/src/App.css b/frontend/web_app/src/App.css new file mode 100644 index 0000000..0e4d7d4 --- /dev/null +++ b/frontend/web_app/src/App.css @@ -0,0 +1,421 @@ +.home { + max-width: 1280px; + margin: 0 auto; + padding: 32px 24px 56px; +} + +.homeHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; + margin-bottom: 22px; +} + +.homeHeader h1 { + color: #fff; + font-size: 28px; + font-weight: 650; + line-height: 1.2; +} + +.homeHeader .subtitle { + margin-top: 8px; + color: #9ca3af; + font-size: 15px; +} + +.uploadButton, +.primaryButton, +.secondaryButton { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 38px; + padding: 0 16px; + border: 1px solid #6366f1; + border-radius: 8px; + background: #6366f1; + color: #fff; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, opacity 0.15s ease; +} + +.uploadButton:hover, +.primaryButton:hover { + background: #5558e8; + border-color: #5558e8; +} + +.secondaryButton { + min-height: 34px; + border-color: #343449; + background: #1a1a27; + color: #d1d5db; +} + +.secondaryButton:hover { + border-color: #6366f1; + background: #24243a; +} + +.uploadButton input { + display: none; +} + +.uploadButton.disabled, +.primaryButton:disabled, +.secondaryButton:disabled { + cursor: not-allowed; + opacity: 0.52; +} + +.workspace { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(360px, 0.78fr); + gap: 18px; + align-items: stretch; +} + +.panel { + margin-top: 18px; + background: #14141e; + border: 1px solid #2a2a3a; + border-radius: 8px; + padding: 18px; +} + +.workspace .panel { + margin-top: 0; +} + +.panelHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 14px; +} + +.panelHeader h2 { + color: #fff; + font-size: 17px; + font-weight: 620; +} + +.panelHeader span, +.muted { + color: #8f96a3; + font-size: 13px; +} + +.alert { + margin: 14px 0; + border-radius: 8px; + padding: 11px 14px; + font-size: 14px; +} + +.alert.error, +.error { + border: 1px solid rgba(248, 113, 113, 0.36); + background: rgba(127, 29, 29, 0.22); + color: #fecaca; +} + +.alert.info { + border: 1px solid rgba(99, 102, 241, 0.36); + background: rgba(49, 46, 129, 0.24); + color: #c7d2fe; +} + +.emptyState, +.emptyChart { + display: flex; + min-height: 96px; + align-items: center; + justify-content: center; + border: 1px dashed #343449; + border-radius: 8px; + color: #8f96a3; + font-size: 14px; + text-align: center; +} + +.fileList { + display: grid; + gap: 10px; + max-height: 260px; + overflow: auto; + padding-right: 2px; +} + +.fileItem { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + border: 1px solid #2a2a3a; + border-radius: 8px; + padding: 11px 12px; + background: #1a1a27; +} + +.fileItem strong { + min-width: 0; + overflow: hidden; + color: #e5e7eb; + font-size: 14px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.fileItem span { + flex: 0 0 auto; + color: #9ca3af; + font-size: 13px; +} + +.mappingGrid { + display: grid; + gap: 12px; +} + +.fieldControl { + display: grid; + grid-template-columns: 96px minmax(0, 1fr); + align-items: center; + gap: 10px; +} + +.fieldControl span { + color: #cbd5e1; + font-size: 14px; +} + +select { + min-width: 0; + width: 100%; + height: 36px; + border: 1px solid #343449; + border-radius: 7px; + background: #0f0f18; + color: #e5e7eb; + padding: 0 10px; + font-size: 14px; +} + +select:focus { + border-color: #6366f1; + outline: none; +} + +.actionRow { + display: flex; + justify-content: flex-end; + margin-top: 16px; +} + +.headerPreview { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.headerPreview span { + max-width: 220px; + overflow: hidden; + border: 1px solid #343449; + border-radius: 999px; + background: #1a1a27; + color: #d1d5db; + padding: 6px 10px; + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.resultSection { + display: grid; + gap: 18px; + margin-top: 18px; +} + +.summaryGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.metric { + border: 1px solid #2a2a3a; + border-radius: 8px; + background: #1a1a27; + padding: 14px; +} + +.metric span { + display: block; + color: #9ca3af; + font-size: 13px; +} + +.metric strong { + display: block; + margin-top: 6px; + color: #fff; + font-size: 24px; + font-variant-numeric: tabular-nums; +} + +.reasonList { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.reasonList span { + border-radius: 6px; + background: #202033; + color: #aeb7c4; + padding: 5px 8px; + font-size: 12px; +} + +.fanSelect { + width: 180px; +} + +.uplotWrap { + width: 100%; + min-height: 360px; +} + +.uplot { + width: 100% !important; + color: #cbd5e1; + font-family: inherit; +} + +.uplot canvas { + background: #10101a; + border-radius: 8px; +} + +.uplot .u-title, +.uplot .u-label, +.uplot .u-value { + color: #cbd5e1; +} + +.uplot .u-legend { + color: #cbd5e1; + font-size: 12px; +} + +.uplot .u-legend .u-series > * { + padding: 2px 6px; +} + +.chartTools { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; +} + +.chartTools span { + color: #9ca3af; + font-size: 13px; + white-space: nowrap; +} + +.tableWrap { + max-height: 420px; + overflow: auto; + border: 1px solid #2a2a3a; + border-radius: 8px; +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +th, +td { + border-bottom: 1px solid #242437; + padding: 10px 12px; + text-align: left; + white-space: nowrap; +} + +th { + position: sticky; + top: 0; + z-index: 1; + background: #1a1a27; + color: #cbd5e1; + font-weight: 600; +} + +td { + color: #d1d5db; + font-variant-numeric: tabular-nums; +} + +.confidence { + display: inline-flex; + border-radius: 999px; + padding: 3px 8px; + font-size: 12px; +} + +.confidence.ok { + background: rgba(34, 197, 94, 0.16); + color: #86efac; +} + +.confidence.low { + background: rgba(251, 191, 36, 0.16); + color: #fde68a; +} + +@media (max-width: 920px) { + .home { + padding: 24px 16px 42px; + } + + .homeHeader, + .workspace { + grid-template-columns: 1fr; + } + + .homeHeader { + display: grid; + } + + .summaryGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 560px) { + .fieldControl { + grid-template-columns: 1fr; + } + + .fileItem { + align-items: flex-start; + flex-direction: column; + } + + .summaryGrid { + grid-template-columns: 1fr; + } +} diff --git a/frontend/web_app/src/App.jsx b/frontend/web_app/src/App.jsx new file mode 100644 index 0000000..6313329 --- /dev/null +++ b/frontend/web_app/src/App.jsx @@ -0,0 +1,17 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import HomePage from './pages/HomePage'; +import './App.css'; + +function App() { + return ( + + + } /> + {/* SPA 回退:未匹配路由统一回到首页 */} + } /> + + + ); +} + +export default App; diff --git a/frontend/web_app/src/index.css b/frontend/web_app/src/index.css new file mode 100644 index 0000000..e480207 --- /dev/null +++ b/frontend/web_app/src/index.css @@ -0,0 +1,36 @@ +/* CSS Reset */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body, #root { + width: 100%; + height: 100%; +} + +body { + background: #0d0d14; + color: #e0e0e0; + font-family: 'Segoe UI', 'Inter', -apple-system, sans-serif; + font-size: 16px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +/* 自定义滚动条 */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: #0d0d14; +} +::-webkit-scrollbar-thumb { + background: #2a2a3a; + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: #3a3a4a; +} diff --git a/frontend/web_app/src/main.jsx b/frontend/web_app/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/frontend/web_app/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/web_app/src/pages/HomePage.jsx b/frontend/web_app/src/pages/HomePage.jsx new file mode 100644 index 0000000..efedf9b --- /dev/null +++ b/frontend/web_app/src/pages/HomePage.jsx @@ -0,0 +1,636 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import uPlot from 'uplot'; +import 'uplot/dist/uPlot.min.css'; +import * as XLSX from 'xlsx'; +import { + deleteWindJob, + finishWindJob, + startWindJob, + uploadWindChunk, +} from '../utils/api'; + +const REQUIRED_FIELDS = [ + { key: 'time', label: '时间' }, + { key: 'fan_id', label: '风机编号' }, + { key: 'wind_speed', label: '风速' }, + { key: 'active_power', label: '有功功率' }, + { key: 'generator_speed', label: '发电机转速' }, +]; + +const FIELD_HINTS = { + time: ['时间', 'time', 'timestamp', '日期'], + fan_id: ['风机编号', '风机', '机组编号', 'fan', 'turbine'], + wind_speed: ['风速', '平均风速', 'wind speed', 'windspeed'], + active_power: ['平均有功功率', '有功功率', 'active power', 'power'], + generator_speed: ['发电机转速', '平均发电机转速', 'generator speed', 'rpm'], +}; + +const FIELD_EXCLUDES = { + active_power: ['限功率', '限电', '时间', '累计'], +}; + +const CHUNK_SIZE = 4000; + +function normalizeHeader(value) { + return String(value ?? '') + .trim() + .toLowerCase() + .replace(/\s+/g, '') + .replace(/[()()_\-./]/g, ''); +} + +function inferMapping(headers) { + const normalized = headers.map((header) => ({ + header, + normalized: normalizeHeader(header), + })); + const mapping = {}; + + for (const field of REQUIRED_FIELDS) { + const hints = FIELD_HINTS[field.key].map(normalizeHeader); + const excludes = (FIELD_EXCLUDES[field.key] || []).map(normalizeHeader); + let best = null; + for (const item of normalized) { + if (!item.normalized || excludes.some((exclude) => item.normalized.includes(exclude))) { + continue; + } + let score = 0; + hints.forEach((hint, index) => { + const weight = hints.length - index; + if (item.normalized === hint) { + score = Math.max(score, 100 + weight); + } else if (item.normalized.includes(hint)) { + score = Math.max(score, 60 + weight); + } else if (hint.includes(item.normalized)) { + score = Math.max(score, 20 + weight); + } + }); + if (!best || score > best.score) { + best = { ...item, score }; + } + } + mapping[field.key] = best && best.score > 0 ? best.header : ''; + } + + return mapping; +} + +function excelSerialToDate(value) { + const days = Number(value); + if (!Number.isFinite(days) || days <= 0) { + return null; + } + const utcDays = Math.floor(days - 25569); + const utcValue = utcDays * 86400; + const dateInfo = new Date(utcValue * 1000); + const fractionalDay = days - Math.floor(days) + 0.0000001; + const totalSeconds = Math.floor(86400 * fractionalDay); + dateInfo.setSeconds(totalSeconds); + return dateInfo; +} + +function pad(value) { + return String(value).padStart(2, '0'); +} + +function formatDate(date) { + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` + + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; +} + +function normalizeTime(value) { + if (value instanceof Date && !Number.isNaN(value.getTime())) { + return formatDate(value); + } + if (typeof value === 'number') { + const date = excelSerialToDate(value); + return date ? formatDate(date) : ''; + } + + const text = String(value ?? '').trim(); + if (!text) return ''; + + const isoMatch = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/); + if (isoMatch) { + const [, year, month, day, hour, minute, second = '0'] = isoMatch; + return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`; + } + + const slashMatch = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/); + if (slashMatch) { + const [, month, day, rawYear, hour, minute, second = '0'] = slashMatch; + const year = rawYear.length === 2 ? `20${rawYear}` : rawYear; + return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`; + } + + return text; +} + +function normalizeNumber(value) { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : null; + } + const text = String(value ?? '').trim().replace(/,/g, ''); + if (!text) return null; + const number = Number(text); + return Number.isFinite(number) ? number : null; +} + +function chooseFirstDataSheet(workbook) { + for (const sheetName of workbook.SheetNames) { + const sheet = workbook.Sheets[sheetName]; + const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '', raw: true }); + const nonEmptyRows = rows.filter((row) => row.some((cell) => String(cell ?? '').trim() !== '')); + if (nonEmptyRows.length > 1) { + return { sheetName, rows: nonEmptyRows }; + } + } + return null; +} + +async function readExcelFile(file) { + const buffer = await file.arrayBuffer(); + const workbook = XLSX.read(buffer, { type: 'array', cellDates: true }); + const sheetData = chooseFirstDataSheet(workbook); + if (!sheetData) { + throw new Error(`${file.name} 没有可读取的数据工作表`); + } + + const headers = sheetData.rows[0].map((cell) => String(cell ?? '').trim()); + const dataRows = sheetData.rows.slice(1); + return { + id: `${file.name}_${file.size}_${file.lastModified}`, + file_name: file.name, + sheet_name: sheetData.sheetName, + headers, + rows: dataRows, + row_count: dataRows.length, + }; +} + +function getCell(row, headerIndex) { + if (headerIndex < 0) return ''; + return row[headerIndex]; +} + +function buildStandardRows(files, mapping) { + const firstHeaders = files[0]?.headers || []; + const indexes = Object.fromEntries( + REQUIRED_FIELDS.map((field) => [field.key, firstHeaders.indexOf(mapping[field.key])]), + ); + + const rows = []; + for (const file of files) { + const localIndexes = Object.fromEntries( + REQUIRED_FIELDS.map((field) => [field.key, file.headers.indexOf(mapping[field.key])]), + ); + for (const row of file.rows) { + rows.push({ + time: normalizeTime(getCell(row, localIndexes.time)), + fan_id: String(getCell(row, localIndexes.fan_id) ?? '').trim(), + wind_speed: normalizeNumber(getCell(row, localIndexes.wind_speed)), + active_power: normalizeNumber(getCell(row, localIndexes.active_power)), + generator_speed: normalizeNumber(getCell(row, localIndexes.generator_speed)), + }); + } + } + + return { rows, indexes }; +} + +function SummaryCards({ summary }) { + if (!summary) return null; + const cards = [ + ['原始行数', summary.raw_rows], + ['有效行数', summary.valid_rows], + ['剔除行数', summary.invalid_rows], + ['风机数量', summary.fan_count], + ]; + return ( +
+ {cards.map(([label, value]) => ( +
+ {label} + {Number(value || 0).toLocaleString()} +
+ ))} +
+ ); +} + +function PowerCurveChart({ points, scatterPoints, filteredPoints, showFiltered }) { + const chartRef = useRef(null); + const validPoints = useMemo( + () => (points || []).filter((point) => point.sample_count > 0), + [points], + ); + const validScatter = useMemo( + () => (scatterPoints || []).filter((point) => + Number.isFinite(point.wind_speed) && Number.isFinite(point.active_power)), + [scatterPoints], + ); + const validFiltered = useMemo( + () => (filteredPoints || []).filter((point) => + Number.isFinite(point.wind_speed) && Number.isFinite(point.active_power)), + [filteredPoints], + ); + + useEffect(() => { + if (!chartRef.current || !validPoints.length) return undefined; + + const sortedCurve = [...validPoints].sort((left, right) => left.wind_speed - right.wind_speed); + const curveX = sortedCurve.map((point) => point.wind_speed); + const curveY = sortedCurve.map((point) => point.average_power); + const maxScatterPower = validScatter.reduce( + (max, point) => Math.max(max, point.active_power), + 0, + ); + const maxFilteredPower = showFiltered + ? validFiltered.reduce((max, point) => Math.max(max, point.active_power), 0) + : 0; + const maxCurvePower = curveY.reduce((max, value) => Math.max(max, value), 0); + const maxPower = Math.max(maxCurvePower, maxScatterPower, maxFilteredPower, 1); + const width = Math.max(chartRef.current.clientWidth || 760, 320); + + chartRef.current.innerHTML = ''; + const chart = new uPlot({ + width, + height: 360, + cursor: { + drag: { x: true, y: true }, + }, + scales: { + x: { time: false }, + y: { range: [0, maxPower * 1.08] }, + }, + axes: [ + { + label: '风速 (m/s)', + stroke: '#9ca3af', + grid: { stroke: '#2a2a3a', width: 1 }, + }, + { + label: '功率 (kW)', + stroke: '#9ca3af', + grid: { stroke: '#2a2a3a', width: 1 }, + }, + ], + series: [ + {}, + { + label: '分箱平均功率', + stroke: '#60a5fa', + width: 2, + points: { show: true, size: 6, fill: '#93c5fd', stroke: '#0f172a' }, + }, + ], + hooks: { + draw: [ + (u) => { + const { ctx } = u; + ctx.save(); + ctx.fillStyle = 'rgba(251, 191, 36, 0.28)'; + for (const point of validScatter) { + const x = u.valToPos(point.wind_speed, 'x', true); + const y = u.valToPos(point.active_power, 'y', true); + if (Number.isFinite(x) && Number.isFinite(y)) { + ctx.beginPath(); + ctx.arc(x, y, 1.6, 0, Math.PI * 2); + ctx.fill(); + } + } + if (showFiltered) { + ctx.fillStyle = 'rgba(248, 113, 113, 0.42)'; + for (const point of validFiltered) { + const x = u.valToPos(point.wind_speed, 'x', true); + const y = u.valToPos(point.active_power, 'y', true); + if (Number.isFinite(x) && Number.isFinite(y)) { + ctx.beginPath(); + ctx.arc(x, y, 1.9, 0, Math.PI * 2); + ctx.fill(); + } + } + } + ctx.restore(); + }, + ], + }, + }, [curveX, curveY], chartRef.current); + + return () => chart.destroy(); + }, [validPoints, validScatter, validFiltered, showFiltered]); + + if (!validPoints.length) { + return
暂无可绘制的曲线数据
; + } + + return
; +} + +export default function HomePage() { + const [files, setFiles] = useState([]); + const [mapping, setMapping] = useState({}); + const [reading, setReading] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [progress, setProgress] = useState(''); + const [error, setError] = useState(''); + const [result, setResult] = useState(null); + const [selectedFan, setSelectedFan] = useState(''); + const [showFiltered, setShowFiltered] = useState(false); + + const headers = files[0]?.headers || []; + const missingFields = REQUIRED_FIELDS.filter((field) => !mapping[field.key]); + const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : []; + const selectedBins = selectedFan && result?.bins ? result.bins[selectedFan] || [] : []; + const selectedScatter = selectedFan && result?.scatter_points + ? result.scatter_points[selectedFan] || [] + : []; + const selectedFiltered = selectedFan && result?.filtered_points + ? result.filtered_points[selectedFan] || [] + : []; + + const totalRows = useMemo( + () => files.reduce((sum, file) => sum + file.row_count, 0), + [files], + ); + + async function handleFilesChange(event) { + const selectedFiles = Array.from(event.target.files || []); + if (!selectedFiles.length) return; + + setReading(true); + setError(''); + setResult(null); + setSelectedFan(''); + setShowFiltered(false); + try { + const parsedFiles = []; + for (const file of selectedFiles) { + parsedFiles.push(await readExcelFile(file)); + } + setFiles(parsedFiles); + setMapping(inferMapping(parsedFiles[0].headers)); + } catch (err) { + setError(err.message || '读取 Excel 失败'); + } finally { + setReading(false); + event.target.value = ''; + } + } + + async function handleSubmit() { + if (!files.length || missingFields.length) { + setError('请先上传文件并完成所有字段映射'); + return; + } + + let jobId = ''; + setSubmitting(true); + setError(''); + setResult(null); + setShowFiltered(false); + setProgress('正在准备标准化数据'); + + try { + const { rows } = buildStandardRows(files, mapping); + const start = await startWindJob({ + files: files.map((file) => ({ + file_name: file.file_name, + row_count: file.row_count, + })), + mapping, + }); + jobId = start.job_id; + + const chunkCount = Math.ceil(rows.length / CHUNK_SIZE); + for (let index = 0; index < chunkCount; index += 1) { + const chunkRows = rows.slice(index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE); + setProgress(`正在上传数据分片 ${index + 1}/${chunkCount}`); + await uploadWindChunk({ + job_id: jobId, + chunk_index: index, + rows: chunkRows, + }); + } + + setProgress('正在清洗数据并计算功率曲线'); + const calculation = await finishWindJob({ + job_id: jobId, + options: { + rated_power: 4800, + rated_wind_speed: 18, + power_step: 5, + cleaning_wind_speed_step: 0.25, + curve_wind_speed_step: 0.5, + wind_speed_change_threshold: 1, + iqr_lower_multiplier: 1.8, + iqr_upper_multiplier: 2, + minimum_generator_speed: 1, + generator_speed_k: 0.9, + }, + }); + setResult(calculation); + setSelectedFan(calculation.fans?.[0] || ''); + setProgress('计算完成'); + } catch (err) { + if (jobId) { + try { + await deleteWindJob(jobId); + } catch { + // 失败任务清理失败不影响用户看到主错误。 + } + } + setError(err.message || '计算失败'); + setProgress(''); + } finally { + setSubmitting(false); + } + } + + return ( +
+
+
+

风电功率计算平台

+

多 Excel 导入、字段映射、数据清洗与功率曲线计算

+
+ +
+ + {error &&
{error}
} + {progress &&
{progress}
} + +
+
+
+

1. 数据文件

+ {files.length ? `${files.length} 个文件 / ${totalRows.toLocaleString()} 行` : '等待上传'} +
+ {!files.length ? ( +
请选择一个或多个 Excel 文件,系统会读取首个非空工作表。
+ ) : ( +
+ {files.map((file) => ( +
+ {file.file_name} + {file.sheet_name} · {file.row_count.toLocaleString()} 行 +
+ ))} +
+ )} +
+ +
+
+

2. 字段映射

+ {missingFields.length ? `缺少 ${missingFields.length} 项` : '已完成'} +
+ {!headers.length ? ( +
上传文件后可指定列头对应关系。
+ ) : ( +
+ {REQUIRED_FIELDS.map((field) => ( + + ))} +
+ )} +
+ +
+
+
+ +
+
+

3. 列头预览

+ {headers.length ? `${headers.length} 列` : '暂无'} +
+ {headers.length ? ( +
+ {headers.map((header, index) => ( + {header || `未命名列 ${index + 1}`} + ))} +
+ ) : ( +
上传后展示首个文件的列头。
+ )} +
+ + {result && ( +
+
+
+

4. 清洗结果

+ 计算完成 +
+ +
+ {Object.entries(result.summary?.invalid_reasons || {}).map(([reason, count]) => ( + {reason}: {Number(count).toLocaleString()} + ))} +
+
+ +
+
+

功率曲线

+
+ + 曲线 {selectedPoints.length} 点 · 散点 {selectedScatter.length.toLocaleString()} 点 + · 滤除 {selectedFiltered.length.toLocaleString()} 点 + + + +
+
+ +
+ +
+
+

分箱明细

+ {selectedBins.length} 个区间 +
+
+
+ + + + + + + + + + + + {selectedBins.map((bin) => ( + + + + + + + + + ))} + +
风速区间平均功率中位数样本数标准差置信度
{bin.wind_speed_start.toFixed(2)} - {bin.wind_speed_end.toFixed(2)}{bin.average_power.toFixed(1)}{bin.median_power.toFixed(1)}{bin.sample_count}{bin.stddev_power.toFixed(1)}{bin.confidence}
+ + + + )} + + ); +} diff --git a/frontend/web_app/src/utils/api.js b/frontend/web_app/src/utils/api.js new file mode 100644 index 0000000..add9608 --- /dev/null +++ b/frontend/web_app/src/utils/api.js @@ -0,0 +1,102 @@ +/** + * @file api.js + * @description REST API 封装(原生 fetch + 统一响应信封 {status,msg,data}) + * 参考: edge_collector/frontend/cloud_app/src/utils/api.js(精简版,无鉴权) + */ + +const BASE_URL = '/api'; + +/** + * 通用请求封装 + * @param {string} url - 请求路径(相对 /api) + * @param {object} options - fetch 选项 + * @returns {Promise} 响应 data 字段 + */ +async function request(url, options = {}) { + const headers = { + 'Content-Type': 'application/json', + ...(options.headers || {}), + }; + + const resp = await fetch(`${BASE_URL}${url}`, { + cache: 'no-store', + ...options, + headers, + }); + const text = await resp.text(); + let json; + try { + json = JSON.parse(text); + } catch { + throw new Error(`服务器返回非法响应 (HTTP ${resp.status}),请检查后端服务状态`); + } + + // 业务失败 + if (json.status !== 0) { + throw new Error(json.msg || '请求失败'); + } + return json.data; +} + +/** + * 获取服务端版本信息 + * @returns {Promise<{name: string, version: string}>} + */ +export function getVersion() { + return request('/system/version'); +} + +/** + * 健康检查 + * @returns {Promise<{status: string}>} + */ +export function getHealth() { + return request('/system/health'); +} + +/** + * 创建风功率计算任务 + * @param {{files: Array<{file_name: string, row_count: number}>, mapping: object}} payload + * @returns {Promise<{job_id: string}>} + */ +export function startWindJob(payload) { + return request('/wind/jobs/start', { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +/** + * 上传标准化后的数据分片 + * @param {{job_id: string, chunk_index: number, rows: Array}} payload + * @returns {Promise<{accepted_rows: number}>} + */ +export function uploadWindChunk(payload) { + return request('/wind/jobs/chunk', { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +/** + * 完成任务并计算功率曲线 + * @param {{job_id: string, options: object}} payload + * @returns {Promise} + */ +export function finishWindJob(payload) { + return request('/wind/jobs/finish', { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +/** + * 清理未完成的计算任务 + * @param {string} jobId + * @returns {Promise} + */ +export function deleteWindJob(jobId) { + return request(`/wind/jobs/${encodeURIComponent(jobId)}`, { + method: 'DELETE', + }); +} diff --git a/frontend/web_app/vite.config.js b/frontend/web_app/vite.config.js new file mode 100644 index 0000000..74a8da7 --- /dev/null +++ b/frontend/web_app/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: '/', + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8848', + changeOrigin: true, + }, + }, + }, +}); diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..8ccaa7c --- /dev/null +++ b/package.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# ===================================================== +# package.sh — 编译并把 wind_power 产物整理到 runtime/wind_power/ +# +# 输出目录结构: +# runtime/wind_power/ +# ├── wind_server # 后端可执行 +# ├── config/ # server_config.json 等静态配置 +# ├── web/ # 前端静态资源(vite build 产物) +# ├── libs/ # 运行期依赖库(libdrogon/libtrantor .so) +# └── logs/ # 日志目录(运行时写入) +# +# 用法: +# ./package.sh # 默认 Release +# ./package.sh --build-type Debug +# ./package.sh --publish # 额外打包到 publish/wind_power-.tar.gz +# +# 参考: edge_collector/package.sh +# ===================================================== + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RUNTIME_DIR="${ROOT_DIR}/runtime/wind_power" +PUBLISH_DIR="${ROOT_DIR}/publish" +PKG_BUILD="${ROOT_DIR}/build_package" +BUILD_TYPE="Release" +PACKAGE_VERSION="0.1.0" +PUBLISH=false + +# ---- 参数解析 ---- +while [ $# -gt 0 ]; do + case "$1" in + --build-type) BUILD_TYPE="$2"; shift 2 ;; + --version) PACKAGE_VERSION="$2"; shift 2 ;; + --publish|publish) PUBLISH=true; shift ;; + *) echo "[package] 未知参数: $1"; exit 1 ;; + esac +done + +# ---- 颜色 ---- +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +step() { echo -e "${CYAN}[STEP]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +# 1. 确保 Drogon 已从源码编译(产物存在则秒过) +step "1/5 确保 Drogon ..." +THIRD_PARTY="${ROOT_DIR}/third_party" source "${ROOT_DIR}/third_party/ensure_third_party.sh" +ARCH="${TARGET_ARCH}" +info "target arch = ${ARCH}, drogon install = ${DROGON_INSTALL}" + +# 2. 构建后端 +step "2/5 构建后端 (${BUILD_TYPE}) ..." +cmake -S "${ROOT_DIR}/backend" -B "${PKG_BUILD}" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" +cmake --build "${PKG_BUILD}" -j"$(nproc)" + +# 3. 构建前端 +step "3/5 构建前端 ..." +( cd "${ROOT_DIR}/frontend/web_app" + [ -d node_modules ] || npm install --silent + npm run build ) + +# 4. 整理 runtime 目录 +step "4/5 整理 runtime/wind_power/ ..." +rm -rf "${RUNTIME_DIR}" +mkdir -p "${RUNTIME_DIR}"/{config,web,libs,logs} + +cp "${PKG_BUILD}/wind_server" "${RUNTIME_DIR}/" +cp -r "${ROOT_DIR}"/backend/config/* "${RUNTIME_DIR}/config/" +rsync -a --delete "${ROOT_DIR}/frontend/web_app/dist/" "${RUNTIME_DIR}/web/" + +# 运行期动态库(部署机无需安装 Drogon) +cp "${DROGON_INSTALL}"/libs/libdrogon.so* "${RUNTIME_DIR}/libs/" +cp "${DROGON_INSTALL}"/libs/libtrantor.so* "${RUNTIME_DIR}/libs/" +# 只保留实际文件,去掉会随版本变化的开发符号链接末级 +info "runtime libs: $(ls "${RUNTIME_DIR}/libs/")" + +# 5. 可选:打包为 tar.gz +if [ "${PUBLISH}" = true ]; then + step "5/5 打包 publish/wind_power-${PACKAGE_VERSION}.tar.gz ..." + mkdir -p "${PUBLISH_DIR}" + tar -czf "${PUBLISH_DIR}/wind_power-${PACKAGE_VERSION}.tar.gz" -C "${ROOT_DIR}/runtime" wind_power + info "✅ ${PUBLISH_DIR}/wind_power-${PACKAGE_VERSION}.tar.gz" +else + info "跳过打包(加 --publish 可生成 tar.gz)" +fi + +info "✅ 打包完成 → ${RUNTIME_DIR}" diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..c84d110 --- /dev/null +++ b/run.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# ===================================================== +# run.sh — 打包并前台运行 wind_power(后端托管前端 SPA) +# +# 流程:package.sh 打包 → 从 runtime/wind_power/ 启动 wind_server +# (后端同时提供 /api 与 SPA 静态资源,即“前后端一起跑”) +# +# 开发模式前端热更新: +# ./run.sh # 后端托管生产前端,监听 :8848 +# cd frontend/web_app && npm run dev # 前端 dev :5173,/api 代理到 :8848 +# +# 参考: edge_collector/run_cloud.sh +# ===================================================== + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="${ROOT_DIR}/runtime/wind_power" +BIN_NAME="wind_server" + +stop_existing_processes() { + local pids=() + mapfile -t pids < <(pgrep -x "${BIN_NAME}" 2>/dev/null || true) + if [ "${#pids[@]}" -eq 0 ]; then + return + fi + + echo "[run] 终止旧进程 ${BIN_NAME} PID: ${pids[*]}" + kill "${pids[@]}" 2>/dev/null || true + + for _ in {1..20}; do + mapfile -t pids < <(pgrep -x "${BIN_NAME}" 2>/dev/null || true) + if [ "${#pids[@]}" -eq 0 ]; then + echo "[run] 旧进程已退出" + return + fi + sleep 0.2 + done + + echo "[run] 旧进程未正常退出,强制终止 PID: ${pids[*]}" + kill -9 "${pids[@]}" 2>/dev/null || true + sleep 0.5 +} + +# 1. 终止旧进程 +stop_existing_processes + +# 2. 打包(传递额外参数,如 --build-type Debug) +echo "[run] 打包..." +bash "${ROOT_DIR}/package.sh" "$@" + +# 3. 启动 +cd "${APP_DIR}" +mkdir -p logs data +export LD_LIBRARY_PATH="${APP_DIR}/libs${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + +echo "[run] 启动 ${BIN_NAME} ..." +./${BIN_NAME} & +SERVER_PID=$! + +echo "" +echo "========================================" +echo " wind_power 已启动" +echo " PID: ${SERVER_PID}" +echo " 工作目录: ${APP_DIR}" +echo " 访问: http://localhost:8848 (Ctrl+C 停止)" +echo "========================================" + +# 4. 等待退出信号 +cleanup() { + echo "" + echo "[run] 正在停止..." + kill "${SERVER_PID}" 2>/dev/null || true + wait "${SERVER_PID}" 2>/dev/null || true + echo "[run] 已停止" +} +trap cleanup SIGINT SIGTERM + +wait "${SERVER_PID}" 2>/dev/null || true diff --git a/third_party/drogon_repo/.Doxyfile b/third_party/drogon_repo/.Doxyfile new file mode 100644 index 0000000..bd89cb2 --- /dev/null +++ b/third_party/drogon_repo/.Doxyfile @@ -0,0 +1,2539 @@ +# Doxyfile 1.8.16 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "Drogon" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all generated output in the proper direction. +# Possible values are: None, LTR, RTL and Context. +# The default value is: None. + +OUTPUT_TEXT_DIRECTION = None + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines (in the resulting output). You can put ^^ in the value part of an +# alias to insert a newline as if a physical newline was in the original file. +# When you need a literal { or } or , in the value part of an alias you have to +# escape them by means of a backslash (\), this can lead to conflicts with the +# commands \{ and \} for these it is advised to use the version @{ and @} or use +# a double escape (\\{ and \\}) + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, +# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files), VHDL, tcl. For instance to make doxygen treat +# .inc files as Fortran files (default is PHP), and .f files as C (default is +# Fortran), use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 5. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 5 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO, these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# (including Cygwin) ands Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. If +# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = lib/inc \ + orm_lib/inc + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: https://www.gnu.org/software/libiconv/) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f \ + *.for \ + *.tcl \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via Javascript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have Javascript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: https://developer.apple.com/xcode/), introduced with OSX +# 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the master .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. +# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/ + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /