云计算百科
云计算领域专业知识百科平台

华为云码道 CodeArts 实战:AI 智能体助力开发离线告警规则引擎(运维巡检小程序)

一键开通华为云码道 CodeArts 代码智能体:https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgithd

摘要:我用 CodeArts 代码智能体开发一个微信小程序「巡哨」,把运维巡检数据变成手机上可离线检索、可自定义告警规则的随身分析器——全部逻辑在端内完成,零服务端、零网络请求。本文重点记录端外采集工具链的完整落地:报告格式设计、默认脱敏机制、docker compose ps 输出解析器的三个坑,以及一次真实踩到的演示数据缺陷。

技术栈版本:码道 CodeArts 代码智能体(2026-09 挑战赛版本)| uni-app(Vue 3 + Vite)| TypeScript 5 | Node.js 22 | Vitest | 实测时间: 2026-09

在这里插入图片描述 在这里插入图片描述

一、项目背景:七服务的"人不在电脑前"困境

我维护着一套 docker-compose 生产环境:七个服务(nginx / backend / redis / mysql / api-gateway / order-service / worker),配了发布脚本与巡检流程。

这条链路在本机终端里很顺。但它有三个只在离开电脑时才暴露的缺口。

缺口一:发完版人不在电脑前,不知道服务到底起来了没有。 每次发布完只能等回到电脑前跑一遍 docker compose ps 才知道。中间这段空白只能靠运气。

缺口二:拿不到"本次发布让哪些指标变差了"的结论。 docker compose ps 只能给出当下这一刻的是与否,而真正有用的是差异——磁盘涨了几个点、某个容器内存比发布前高了多少、哪些探针延迟恶化了。人脑没法在两次扫屏输出之间做差分。

缺口三:告警阈值散落在记忆里,而且是复合语义。 "磁盘 85%、内存 1024MB、延迟 500ms"这些阈值每个服务都不一样。更麻烦的是运维真正想要的表达是复合的:CPU 超过 80% 且内存超过 1024MB、延迟超过 500ms 且连续三次采样都超。表单下拉框填数字这套交互,表达不了后者。

所以这次参赛,我决定做一个能把这三件事收拢到手机上的东西。

1.1 作品功能一览

模块功能状态
巡检采集 把巡检输出归一化为标准报告文件,默认脱敏 已实现
报告导入 四道安全校验 + 解析落盘 已实现
健康看板 七服务健康状态卡片,异常优先 已实现
告警规则引擎 自研 DSL,支持比较/逻辑/括号/for N 已实现
指标趋势 Canvas 时序曲线 已实现
差分诊断 对比两次巡检,输出变差/恢复清单 已实现

技术栈:uni-app(Vue 3 + Vite + TypeScript)编译到微信小程序端,Vitest 做单测,全程零服务端。

成品实拍按功能两两并排(左右各一屏): 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

1.2 与第 1 期作品的关系

第 1 期我做的是 ArticlePilot——一个带 Java 后端和 MySQL 的浏览器端内容分发管理系统。本期和它在三个层面做了切割:

维度第 1 期第 2 期
运行环境 浏览器 + 服务端 + 数据库 小程序,零服务端、零数据库、零网络请求
业务域 内容分发(写作链路) 运维巡检(发布链路)
核心技术 适配器模式 + 状态机 + RBAC 数据格式 + 自研 DSL + 差分存储

需要说明的是,两期前端都用 Vue 3 语法(本期通过 uni-app)。这在语法层面是重叠的,但代码零复用、功能零交叉——共享的只是我个人的工程习惯,以及"本地优先、端外工具链、导入安全校验"这套方法论。

二、设计前提:四条平台红线与一条业务红线

动手之前,我先把约束摆清楚。小程序架构的难点不在分层设计,而在平台硬约束下的取舍。

2.1 小程序四条平台红线

2026 年现行标准(微信官方文档):

约束项限制值对本项目的直接影响
主包体积 ≤ 2MB 禁止引入第三方规则引擎库、图表库;DSL 解析器必须自研
单个分包 ≤ 2MB 数据文件绝不能进代码包
总包体积 ≤ 20MB 同上
wx/storage 单 key 1MB / 总量 10MB 只放规则集与偏好,历史巡检序列放不下
本地用户文件 ≤ 200MB 巡检序列的唯一落点,必须做配额管理

这组数字直接决定了第三章的三条设计决策。

2.2 一条业务红线:运维数据不能上云

比平台约束更硬的是这条——巡检报告里含主机标识、服务名、端口。这类数据一旦传到公网接口或第三方 SaaS,就等于把内网拓扑画给了外人。

它直接否决了下面三个听起来更"现代"的方案:

#mermaid-svg-wLFzrcWFbvPcNIUB{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wLFzrcWFbvPcNIUB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wLFzrcWFbvPcNIUB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wLFzrcWFbvPcNIUB .error-icon{fill:#552222;}#mermaid-svg-wLFzrcWFbvPcNIUB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wLFzrcWFbvPcNIUB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wLFzrcWFbvPcNIUB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wLFzrcWFbvPcNIUB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wLFzrcWFbvPcNIUB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wLFzrcWFbvPcNIUB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wLFzrcWFbvPcNIUB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wLFzrcWFbvPcNIUB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wLFzrcWFbvPcNIUB .marker.cross{stroke:#333333;}#mermaid-svg-wLFzrcWFbvPcNIUB svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wLFzrcWFbvPcNIUB p{margin:0;}#mermaid-svg-wLFzrcWFbvPcNIUB .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-wLFzrcWFbvPcNIUB .cluster-label text{fill:#333;}#mermaid-svg-wLFzrcWFbvPcNIUB .cluster-label span{color:#333;}#mermaid-svg-wLFzrcWFbvPcNIUB .cluster-label span p{background-color:transparent;}#mermaid-svg-wLFzrcWFbvPcNIUB .label text,#mermaid-svg-wLFzrcWFbvPcNIUB span{fill:#333;color:#333;}#mermaid-svg-wLFzrcWFbvPcNIUB .node rect,#mermaid-svg-wLFzrcWFbvPcNIUB .node circle,#mermaid-svg-wLFzrcWFbvPcNIUB .node ellipse,#mermaid-svg-wLFzrcWFbvPcNIUB .node polygon,#mermaid-svg-wLFzrcWFbvPcNIUB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wLFzrcWFbvPcNIUB .rough-node .label text,#mermaid-svg-wLFzrcWFbvPcNIUB .node .label text,#mermaid-svg-wLFzrcWFbvPcNIUB .image-shape .label,#mermaid-svg-wLFzrcWFbvPcNIUB .icon-shape .label{text-anchor:middle;}#mermaid-svg-wLFzrcWFbvPcNIUB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wLFzrcWFbvPcNIUB .rough-node .label,#mermaid-svg-wLFzrcWFbvPcNIUB .node .label,#mermaid-svg-wLFzrcWFbvPcNIUB .image-shape .label,#mermaid-svg-wLFzrcWFbvPcNIUB .icon-shape .label{text-align:center;}#mermaid-svg-wLFzrcWFbvPcNIUB .node.clickable{cursor:pointer;}#mermaid-svg-wLFzrcWFbvPcNIUB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wLFzrcWFbvPcNIUB .arrowheadPath{fill:#333333;}#mermaid-svg-wLFzrcWFbvPcNIUB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wLFzrcWFbvPcNIUB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wLFzrcWFbvPcNIUB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wLFzrcWFbvPcNIUB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wLFzrcWFbvPcNIUB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wLFzrcWFbvPcNIUB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wLFzrcWFbvPcNIUB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wLFzrcWFbvPcNIUB .cluster text{fill:#333;}#mermaid-svg-wLFzrcWFbvPcNIUB .cluster span{color:#333;}#mermaid-svg-wLFzrcWFbvPcNIUB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wLFzrcWFbvPcNIUB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wLFzrcWFbvPcNIUB rect.text{fill:none;stroke-width:0;}#mermaid-svg-wLFzrcWFbvPcNIUB .icon-shape,#mermaid-svg-wLFzrcWFbvPcNIUB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wLFzrcWFbvPcNIUB .icon-shape p,#mermaid-svg-wLFzrcWFbvPcNIUB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wLFzrcWFbvPcNIUB .icon-shape .label rect,#mermaid-svg-wLFzrcWFbvPcNIUB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wLFzrcWFbvPcNIUB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wLFzrcWFbvPcNIUB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wLFzrcWFbvPcNIUB :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

方案D:端外采集到端内解析-采用

零服务端

采用

数据全程不出设备

方案C:接入监控 SaaS

依赖外部账号

否决

数据完全出网

方案B:云函数中转 + 云数据库

需常驻云资源

否决

运维数据出网到第三方平台

方案A:小程序直连服务器 API

需要公网接口 + 域名备案 + HTTPS 证书

否决

服务器暴露面扩大,还要下发 token

最终选的是第四条路:端外把巡检结果采集成文件,端内只做解析与分析。数据不落地到任何服务端。

三、架构设计:三条主线

#mermaid-svg-1Jrw0n7fKi09WnVq{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1Jrw0n7fKi09WnVq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1Jrw0n7fKi09WnVq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1Jrw0n7fKi09WnVq .error-icon{fill:#552222;}#mermaid-svg-1Jrw0n7fKi09WnVq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1Jrw0n7fKi09WnVq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1Jrw0n7fKi09WnVq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1Jrw0n7fKi09WnVq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1Jrw0n7fKi09WnVq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1Jrw0n7fKi09WnVq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1Jrw0n7fKi09WnVq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1Jrw0n7fKi09WnVq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1Jrw0n7fKi09WnVq .marker.cross{stroke:#333333;}#mermaid-svg-1Jrw0n7fKi09WnVq svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1Jrw0n7fKi09WnVq p{margin:0;}#mermaid-svg-1Jrw0n7fKi09WnVq .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-1Jrw0n7fKi09WnVq .cluster-label text{fill:#333;}#mermaid-svg-1Jrw0n7fKi09WnVq .cluster-label span{color:#333;}#mermaid-svg-1Jrw0n7fKi09WnVq .cluster-label span p{background-color:transparent;}#mermaid-svg-1Jrw0n7fKi09WnVq .label text,#mermaid-svg-1Jrw0n7fKi09WnVq span{fill:#333;color:#333;}#mermaid-svg-1Jrw0n7fKi09WnVq .node rect,#mermaid-svg-1Jrw0n7fKi09WnVq .node circle,#mermaid-svg-1Jrw0n7fKi09WnVq .node ellipse,#mermaid-svg-1Jrw0n7fKi09WnVq .node polygon,#mermaid-svg-1Jrw0n7fKi09WnVq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1Jrw0n7fKi09WnVq .rough-node .label text,#mermaid-svg-1Jrw0n7fKi09WnVq .node .label text,#mermaid-svg-1Jrw0n7fKi09WnVq .image-shape .label,#mermaid-svg-1Jrw0n7fKi09WnVq .icon-shape .label{text-anchor:middle;}#mermaid-svg-1Jrw0n7fKi09WnVq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1Jrw0n7fKi09WnVq .rough-node .label,#mermaid-svg-1Jrw0n7fKi09WnVq .node .label,#mermaid-svg-1Jrw0n7fKi09WnVq .image-shape .label,#mermaid-svg-1Jrw0n7fKi09WnVq .icon-shape .label{text-align:center;}#mermaid-svg-1Jrw0n7fKi09WnVq .node.clickable{cursor:pointer;}#mermaid-svg-1Jrw0n7fKi09WnVq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1Jrw0n7fKi09WnVq .arrowheadPath{fill:#333333;}#mermaid-svg-1Jrw0n7fKi09WnVq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1Jrw0n7fKi09WnVq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1Jrw0n7fKi09WnVq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1Jrw0n7fKi09WnVq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1Jrw0n7fKi09WnVq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1Jrw0n7fKi09WnVq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1Jrw0n7fKi09WnVq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1Jrw0n7fKi09WnVq .cluster text{fill:#333;}#mermaid-svg-1Jrw0n7fKi09WnVq .cluster span{color:#333;}#mermaid-svg-1Jrw0n7fKi09WnVq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1Jrw0n7fKi09WnVq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1Jrw0n7fKi09WnVq rect.text{fill:none;stroke-width:0;}#mermaid-svg-1Jrw0n7fKi09WnVq .icon-shape,#mermaid-svg-1Jrw0n7fKi09WnVq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1Jrw0n7fKi09WnVq .icon-shape p,#mermaid-svg-1Jrw0n7fKi09WnVq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1Jrw0n7fKi09WnVq .icon-shape .label rect,#mermaid-svg-1Jrw0n7fKi09WnVq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1Jrw0n7fKi09WnVq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1Jrw0n7fKi09WnVq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1Jrw0n7fKi09WnVq :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

端内:小程序

端外:tools/ops-collect

微信会话传输

docker compose ps 输出

解析器

按 schema 归一化

脱敏:主机转哈希别名

gzip → .opsreport

chooseMessageFile 导入

四道安全校验

报告解析 + 指标扁平化

core 纯逻辑层

DSL 规则引擎

差分诊断引擎

健康看板

诊断清单

USER_DATA_PATH 落盘

三条主线分别是数据格式、规则引擎、差分存储。数据格式的实现见第四章,后两条的实现思路在本章各占一节。

3.1 主线一:巡检报告格式与端外采集(已实现)

核心思路是把最重的计算移出小程序运行时:端外一次预处理,端内只做装载。详细实现见第四章。

3.2 主线二:自研告警规则 DSL

要表达 probe_nginx_latency_ms > 500 for 3 这类语义,有四条路:

方案表达力包体积代价可测性结论
表单式固定阈值 弱:"连续 N 次"表达不了 极小 一般 否决
手写 if-else 模板 差,加规则要改核心代码 否决
第三方规则引擎库 +数百 KB,挤压 2MB 主包 否决
自研轻量 DSL 够用:比较/逻辑/括号/for N 约 15KB 极好,纯函数 采用

它不是炫技的选择——2MB 主包预算里确实塞不下第三方引擎。

#mermaid-svg-F7OZJhQlYHnhxjHG{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-F7OZJhQlYHnhxjHG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-F7OZJhQlYHnhxjHG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-F7OZJhQlYHnhxjHG .error-icon{fill:#552222;}#mermaid-svg-F7OZJhQlYHnhxjHG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-F7OZJhQlYHnhxjHG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-F7OZJhQlYHnhxjHG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-F7OZJhQlYHnhxjHG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-F7OZJhQlYHnhxjHG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-F7OZJhQlYHnhxjHG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-F7OZJhQlYHnhxjHG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-F7OZJhQlYHnhxjHG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-F7OZJhQlYHnhxjHG .marker.cross{stroke:#333333;}#mermaid-svg-F7OZJhQlYHnhxjHG svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-F7OZJhQlYHnhxjHG p{margin:0;}#mermaid-svg-F7OZJhQlYHnhxjHG .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-F7OZJhQlYHnhxjHG .cluster-label text{fill:#333;}#mermaid-svg-F7OZJhQlYHnhxjHG .cluster-label span{color:#333;}#mermaid-svg-F7OZJhQlYHnhxjHG .cluster-label span p{background-color:transparent;}#mermaid-svg-F7OZJhQlYHnhxjHG .label text,#mermaid-svg-F7OZJhQlYHnhxjHG span{fill:#333;color:#333;}#mermaid-svg-F7OZJhQlYHnhxjHG .node rect,#mermaid-svg-F7OZJhQlYHnhxjHG .node circle,#mermaid-svg-F7OZJhQlYHnhxjHG .node ellipse,#mermaid-svg-F7OZJhQlYHnhxjHG .node polygon,#mermaid-svg-F7OZJhQlYHnhxjHG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-F7OZJhQlYHnhxjHG .rough-node .label text,#mermaid-svg-F7OZJhQlYHnhxjHG .node .label text,#mermaid-svg-F7OZJhQlYHnhxjHG .image-shape .label,#mermaid-svg-F7OZJhQlYHnhxjHG .icon-shape .label{text-anchor:middle;}#mermaid-svg-F7OZJhQlYHnhxjHG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-F7OZJhQlYHnhxjHG .rough-node .label,#mermaid-svg-F7OZJhQlYHnhxjHG .node .label,#mermaid-svg-F7OZJhQlYHnhxjHG .image-shape .label,#mermaid-svg-F7OZJhQlYHnhxjHG .icon-shape .label{text-align:center;}#mermaid-svg-F7OZJhQlYHnhxjHG .node.clickable{cursor:pointer;}#mermaid-svg-F7OZJhQlYHnhxjHG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-F7OZJhQlYHnhxjHG .arrowheadPath{fill:#333333;}#mermaid-svg-F7OZJhQlYHnhxjHG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-F7OZJhQlYHnhxjHG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-F7OZJhQlYHnhxjHG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F7OZJhQlYHnhxjHG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-F7OZJhQlYHnhxjHG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F7OZJhQlYHnhxjHG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-F7OZJhQlYHnhxjHG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-F7OZJhQlYHnhxjHG .cluster text{fill:#333;}#mermaid-svg-F7OZJhQlYHnhxjHG .cluster span{color:#333;}#mermaid-svg-F7OZJhQlYHnhxjHG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-F7OZJhQlYHnhxjHG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-F7OZJhQlYHnhxjHG rect.text{fill:none;stroke-width:0;}#mermaid-svg-F7OZJhQlYHnhxjHG .icon-shape,#mermaid-svg-F7OZJhQlYHnhxjHG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F7OZJhQlYHnhxjHG .icon-shape p,#mermaid-svg-F7OZJhQlYHnhxjHG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-F7OZJhQlYHnhxjHG .icon-shape .label rect,#mermaid-svg-F7OZJhQlYHnhxjHG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F7OZJhQlYHnhxjHG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-F7OZJhQlYHnhxjHG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-F7OZJhQlYHnhxjHG :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

未注册标识符

规则文本probe_nginx_latency_ms > 500 for 3

tokenizeToken[]

parse 递归下降AstNode

resolve白名单校验指标

evaluateRuleOutcome

true / false / unknown

unknown 而非 false

这里有个容易被忽视的安全点:允许用户自定义规则,等于把半个求值器暴露给了输入。所以求值必须走沙箱——标识符只能取自预注册白名单、禁止函数调用与属性访问、禁止 eval 类动态求值。否则规则编辑器就是个现成的注入入口。

3.3 主线三:差分增量存储

真正有价值的信息不在单次快照,而在差异。设计上第二次巡检起只存差分字段,超过 150MB 按"最旧优先"淘汰。

四、落地实录:采集工具链

工具在 tools/ops-collect/,零第三方依赖,只用 Node 内置模块;小程序端所有页面的数据源质量,都由它决定。

4.1 报告格式设计

{
"schemaVersion": "1.0",
"hostAlias": "h-c5f8b961",
"collectedAt": "2026-09-21T09:00:00+08:00",
"system": { "diskUsedPercent": 63.2, "load1": 0.45, "uptimeHours": 1420 },
"services": [
{ "name": "mysql", "status": "healthy", "cpuPercent": 12.4, "memMb": 512, "restartCount": 0 }
],
"probes": [{ "name": "http_health", "latencyMs": 128, "statusCode": 200 }]
}

三条格式纪律值得单独说:

数值缺失就是缺失,不用哨兵值。 不允许 -1、9999 这类表达"没采到"的写法。因为差分诊断会拿它当真实数据算百分比,一个 -1 能让整个诊断清单失真。

状态枚举收窄到四个。 healthy | unhealthy | exited | unknown。终端输出里常见的 "N/A"、"—-" 这类占位字符串一律拒绝——它们看着像"无数据",实际上是字符串类型,会让规则引擎的比较运算直接判错。

每个校验错误都带 JSON path。 services[2].cpuPercent 这样的定位,才能让用户知道到底哪一条数据坏了:

// src/core/report/validate.ts(节选)
const err = (code: string, message: string, path: string): ParseResult<never> => ({
ok: false,
error: { code, message, path },
})

// 有限数校验:拒绝 NaN / Infinity / null / 非数字类型
function isFiniteNumber(v: unknown): v is number {
return typeof v === 'number' && Number.isFinite(v)
}

if (!isFiniteNumber(s[key])) {
return err('INVALID_NUMBER', `${path}.${key} 必须是有限数值(拒绝 NaN/Infinity/null)`, `${path}.${key}`)
}

4.2 三种工作模式

# 1. 生成演示序列(零配置,给评审看)
node index.js –demo –out demo.opsreport –series-out demo-series.json

# 2. 解析 docker compose ps 的输出(状态映射:Up→healthy,Exit→exited)
docker compose ps | node index.js –from-file /dev/stdin –mode docker –out prod.opsreport

# 3. 校验并规范化已有 JSON
node index.js –from-file raw.json –mode json –out prod.opsreport

4.3 解析器踩的三个坑

采集端要对接的是 docker compose ps 这类给人看的终端输出,格式天然不适合机器解析。

坑一:docker compose ps 的列对齐空格数不固定。 它会按最长列宽对齐,列名之间的空格数随数据变化,按单个空格切分必然切碎:

// tools/ops-collect/index.js — readDockerMode(节选)
const cols = line.trim().split(/\\s{2,}|\\t+/).filter((c) => c.trim() !== '')
if (cols.length < 2) continue
let name = cols[0]
let statusText = cols[cols.length 1]
if (cols.length >= 3) {
name = cols[1]
statusText = cols.slice(2).join(' ')
}

坑二:容器名与服务名是两列,取错列整份报告就张冠李戴。 NAME SERVICE STATUS 三列布局下,容器名(infra-nginx-1)和服务名(nginx)都在前两列,报告里要的是服务名:

// 服务名列通常是第 2 列(NAME SERVICE STATUS);兼容只有 NAME + STATUS 两列的情况
if (cols.length >= 3) {
name = cols[1]
statusText = cols.slice(2).join(' ')
}

坑三:Up (unhealthy) 的括号标记才是权威状态。 状态列长得都像 “Up”,但 Up 20 minutes (unhealthy) 里藏着健康检查结论,必须让带标记的规则先于裸 Up 匹配——顺序错了全部误判为健康:

// tools/ops-collect/index.js(节选):unhealthy 必须先于 Up 判定
const DOCKER_STATUS_MAP = [
{ test: /unhealthy/i, status: 'unhealthy' },
{ test: /^\\s*Exit/i, status: 'exited' },
{ test: /^\\s*Restarting/i, status: 'unhealthy' },
{ test: /^\\s*Up/i, status: 'healthy' },
{ test: /^\\s*Created|^\\s*Paused/i, status: 'unknown' },
]

4.4 单位换算:MiB 不是 MB

docker stats 的 412MiB 是二进制单位(1048576 字节),直接当 MB 用会低估 4.86%。本期采集端尚未接入 docker stats,docker 模式下的 cpu/mem 字段以 0 占位(readDockerMode 里显式写死),但 schema 已经把纪律立好:所有数值字段必须是有限数,字符串 "412MiB" 这类值在校验层直接拒绝(INVALID_NUMBER)。

4.5 脱敏:默认就是安全的

这是我最看重的一处设计。运维数据一旦漏进公开仓库就无法撤回,与其指望"记得脱敏",不如让工具默认产出就是安全的:

prod-web-01.example.com → h-c5f8b961

用的是 sha256 前 8 位(aliasFor),同一原值永远得到同一别名。反查表落在 .hostmap(已被 gitignore 忽略),作者本人能查回原值,不影响真实使用。

需要保留原文时必须显式放行,并且会打印警告:

$ node index.js –from-file raw.json –mode json –host prod-web-01.example.com –keep-host –out prod.opsreport

[警告] 输出包含未脱敏的主机标识,请勿提交到公开仓库

4.6 演示序列:不是随机毛刺,是有剧本的数据

评审不会去配服务器、跑采集脚本。所以要有一份零配置可用的演示数据,而且必须"有故事"。

生成器是确定性公式、不用随机数(buildDemoSeries 里全是 base + slope * i)——同一份代码在任何机器上产出完全一致的序列,保证文档里写的数字和你跑出来的数字是同一份。内置四个场景:

场景数据表现对应规则
内存泄漏(发布后) backend 自 880MB 起每点 +27MB,第 7 点越过 1024MB svc_backend_mem_mb > 1024
磁盘水位上涨 79.0% 起每点 +0.45,第 15 点突破 85% disk_used_percent > 85
探针延迟恶化 360ms 起每点 +10ms,第 16 点超 500ms,末三点连续命中 probe_nginx_latency_ms > 500 for 3
瞬时抖动后自愈 redis 第 17 点 unhealthy,其余采样点 healthy svc_redis_status != healthy

实测运行结果(npm run collect:demo,即 node index.js –demo –out src/static/demo/demo-series.json –no-compress –series-out tools/ops-collect/demo-series.json):

[脱敏] 主机标识已哈希为 h-6a3cb155,对照表写入 src/static/demo/.hostmap(已被 .gitignore 忽略)
[输出] 演示序列 tools/ops-collect/demo-series.json(20 个采样点)
[输出] src/static/demo/demo-series.json
报告数 20 / 服务数 7
原始 JSON 17.4 KB → 未压缩 17.4 KB
hostAlias = h-6a3cb155

17.4KB 意味着演示数据可以直接随代码包分发,评审零配置就能看到完整功能,不用自己跑采集。

4.7 一次真实踩到的缺陷:for 3 命中不了

写完生成器后我加了个校验脚本 verify.js,专门回答一个问题:交付给评审的演示数据是不是真的能触发规则。

第一版生成器把 Nginx 延迟的增长斜率设成了 8ms/点,第一次运行 verify.js 就挂了:

svc_backend_mem_mb > 1024 | [HIT] | #7 | 14/20 (70.0%)
disk_used_percent > 85 | [HIT] | #15 | 6/20 (30.0%)
probe_nginx_latency_ms > 500 for 3 | [MISS] | – | 0/20 (0.0%)
svc_redis_status != healthy | [HIT] | #17 | 1/20 (5.0%)

[FAIL] 未命中 1/4: probe_nginx_latency_ms > 500 for 3

原因是斜率不够:第 17 个采样点是 360 + 17×8 = 496ms,差 4ms 没过线,末三个点不是连续命中。

把斜率改成 10ms/点后:

// tools/ops-collect/index.js — buildDemoSeries(节选)
// 斜率必须保证最后三个采样点都超过 500ms,否则 for 3 语义命中不了
// 取 10ms/点:第 16 点 360+16*10=520 > 500,第 17/18 点更高 → 连续三次命中
const nginxLatency = Math.round(360 + 10 * i)

修复后 npm run collect:verify 四条规则全部命中:

svc_backend_mem_mb > 1024 | [HIT] | #7 | 14/20 (70.0%)
disk_used_percent > 85 | [HIT] | #15 | 6/20 (30.0%)
probe_nginx_latency_ms > 500 for 3 | [HIT] | #18 | 3/20 (15.0%)
svc_redis_status != healthy | [HIT] | #17 | 1/20 (5.0%)

[PASS] 4/4 条规则全部命中

这个坑的价值在于:如果没写这个校验脚本,"内置演示数据可体验"就只是一句宣传语,等评审打开一看什么都没触发,前面所有设计都白做。

在这里插入图片描述

五、用 CodeArts 开发:实测感受

到目前为止,我给 CodeArts 投喂的都是中小型、边界清晰的任务——采集工具链逐个模块投喂,每个块都带验收标准。

有个做法已经验证有效:让 AI 先输出实现计划,我确认后再动手。工具链这种"输入输出都很明确"的任务,AI 一次通过率很高;真正需要我介入修正的,是话术没覆盖到的语义细节(比如 Up (unhealthy) 的括号标记才是权威状态这条,是我验收时才发现的)。

下面两处投喂现场比较有代表性。工程初始化属于"输入输出都明确"的任务,一次通过:一段话术交代「克隆仓库 → 套 uni-preset-vue(vite-ts)模板 → 装依赖 → 构建验证」,Agent 自主调用 AtomGit CLI 走完全链路,5 分 11 秒后交付的是 npm run build:mp-weixin 构建通过的工程:

在这里插入图片描述

而工程骨架这类带架构取舍的任务,走的是"先出计划、确认后动手":Agent 先给出目录结构(src/core 纯逻辑层禁止 import uni.* / vue)、分包配置与主题系统三段方案,我逐条确认后才放行实现:

在这里插入图片描述

六、开发节奏与质量纪律

开发按「端外工具链 → 工程骨架 → 导入链路 → 规则引擎 → 差分诊断 → 质量收口」的顺序推进,每一步都先让 Agent 出实现计划、确认后再动手。

其中一条贯穿全程的硬纪律:src/core/** 必须是零平台依赖的纯 TypeScript(不 import 任何 uni.* / vue 模块)。

这条纪律同时解决两个问题——控住 2MB 主包红线,以及让核心逻辑能脱离小程序宿主环境做高覆盖率单测。

七、开源地址与运行方式

项目仓库:https://atomgit.com/dickeryang/patrolops-mp.git

本地运行:

# 端外工具链(仅需 Node >= 18)
npm run collect:demo # 生成内置演示序列(src/static/demo/demo-series.json)
npm run collect:verify # 校验演示数据能触发全部 4 条规则

# 小程序端
npm install
npm run dev:mp-weixin
# 微信开发者工具 → 导入 dist/dev/mp-weixin(勾选「保存时自动编译小程序」)

视频演示地址

视频演示地址

八、小结

回到开头那三个缺口,本篇完成的是第一步:把给人看的终端输出,变成结构化、安全、可传播的数据文件。

三件我认为做对了的事:

  • 格式纪律优先于功能。数值不用哨兵值、状态收窄枚举、错误带 JSON path——这三条让后续的差分诊断和规则求值不必为脏数据兜底。
  • 默认脱敏而不是提醒脱敏。–keep-host 才放行原文的设计,比在 README 里写"记得脱敏"可靠得多。
  • 给演示数据写校验脚本。它当场抓出了 for 3 命中不了的缺陷,这个 bug 靠肉眼读代码是发现不了的。
  • 觉得有用就点个赞,有什么想法欢迎评论区交流。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 华为云码道 CodeArts 实战:AI 智能体助力开发离线告警规则引擎(运维巡检小程序)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!