手把手用 Doubao-Seed-Evolving 写一个网站监控脚本:完整代码与两处踩坑
一、为什么让模型来写监控
先说背景。手上有几个对外服务在跑,半夜悄悄挂了没人知道是最憋屈的事。市面上的监控 SaaS 免费额度紧、付费按年收,对只盯三五个站的人来说偏重。于是我想验证一件事:能不能直接让大模型从一个自然语言需求,产出「能跑、能告警、能调度」的真实工具。
这次用的模型是 Doubao-Seed-Evolving,字节跳动面向复杂工程任务推出的模型,8 月 3 日刚完成第二次升级,重点强化了 Coding 工程能力、Agent 检索能力和幻觉控制能力。本文聚焦它 Coding 工程能力的实测。
二、完整 monitor.py(可直接运行)
下面是我让模型生成、并经两处踩坑修正后的最终版,Python 3.10+ 依赖仅 requests,复制即可跑。
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""HTTP 目标监控脚本:定期监控配置的 URL,异常时通过飞书机器人发送告警"""
import json
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any
import requests
def load_json_file(file_path: Path) -> Any:
with open(file_path, "r", encoding="utf-8") as f:
return json.load(f)
def get_feishu_webhook(base_dir: Path) -> str | None:
# 优先从同目录 config.json 读取 feishu_webhook,缺失则返回 None
config_path = base_dir / "config.json"
if not config_path.exists():
return None
try:
config = load_json_file(config_path)
webhook = config.get("feishu_webhook", "").strip()
return webhook if webhook else None
except (json.JSONDecodeError, OSError) as e:
print(f"[警告] 读取 config.json 失败: {e}")
return None
def load_targets(base_dir: Path) -> list[dict]:
targets_path = base_dir / "targets.json"
if not targets_path.exists():
print(f"[错误] 未找到配置文件: {targets_path}")
sys.exit(1)
try:
targets = load_json_file(targets_path)
if not isinstance(targets, list):
print("[错误] targets.json 必须是 JSON 数组格式")
sys.exit(1)
return targets
except json.JSONDecodeError as e:
print(f"[错误] 解析 targets.json 失败: {e}")
sys.exit(1)
def check_target(target: dict) -> dict:
name = target.get("name", "未命名")
url = target.get("url", "")
timeout = target.get("timeout", 5)
result = {
"name": name, "url": url, "timeout": timeout,
"is_reachable": False, "status_code": None,
"elapsed": 0.0, "is_error": False, "error_reason": "",
}
start_time = time.time()
try:
response = requests.get(
url, timeout=timeout, allow_redirects=True,
headers={"User-Agent": "MonitorScript/1.0"})
result["elapsed"] = round(time.time() - start_time, 3)
result["status_code"] = response.status_code
result["is_reachable"] = True
if response.status_code >= 400:
result["is_error"] = True
result["error_reason"] = f"HTTP状态码异常: {response.status_code}"
elif result["elapsed"] > timeout:
result["is_error"] = True
result["error_reason"] = f"响应超时: 耗时{result['elapsed']}s > 阈值{timeout}s"
except requests.exceptions.Timeout:
result["elapsed"] = round(time.time() - start_time, 3)
result["is_error"] = True
result["error_reason"] = f"请求超时: 超过{timeout}秒未响应"
except requests.exceptions.ConnectionError:
result["elapsed"] = round(time.time() - start_time, 3)
result["is_error"] = True
result["error_reason"] = "连接失败: 无法建立网络连接"
except requests.exceptions.RequestException as e:
result["elapsed"] = round(time.time() - start_time, 3)
result["is_error"] = True
result["error_reason"] = f"请求异常: {str(e)}"
return result
def send_feishu_alert(webhook: str, result: dict) -> bool:
# 飞书机器人对 markdown 类型关键词校验异常,改用 text 类型 + 关键词"监控"
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
text_content = (
f"监控:服务异常通知\n---\n"
f"服务名称: {result['name']}\n访问地址: {result['url']}\n"
f"错误原因: {result['error_reason']}\n状态码: {result['status_code'] or 'N/A'}\n"
f"响应耗时: {result['elapsed']}s\n超时阈值: {result['timeout']}s\n"
f"触发时间: {current_time}\n")
payload = {"msg_type": "text", "content": {"text": text_content}}
try:
response = requests.post(webhook, json=payload, timeout=10,
headers={"Content-Type": "application/json"})
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code", 0) != 0:
print(f"[飞书告警发送失败] {resp_data.get('msg', '未知错误')}")
return False
return True
except requests.exceptions.RequestException as e:
print(f"[飞书告警发送异常] {e}")
return False
def print_log(result: dict) -> None:
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
status = "[异常]" if result["is_error"] else "[正常]"
log_line = (f"[{current_time}] {status} | 名称: {result['name']} | "
f"URL: {result['url']} | 状态码: {result['status_code'] or 'N/A'} | "
f"耗时: {result['elapsed']}s")
if result["is_error"]:
log_line += f" | 原因: {result['error_reason']}"
print(log_line)
def main():
base_dir = Path(__file__).resolve().parent
print("=" * 60)
print(f"HTTP监控脚本启动 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 60)
feishu_webhook = get_feishu_webhook(base_dir)
print("[信息] 飞书告警已启用" if feishu_webhook else "[信息] 飞书告警未配置,仅打印控制台日志")
targets = load_targets(base_dir)
print(f"[信息] 成功加载 {len(targets)} 个监控目标")
print("-" * 60)
error_count = 0
for target in targets:
if "name" not in target or "url" not in target:
print(f"[跳过] 配置项缺少 name 或 url 字段: {target}")
continue
result = check_target(target)
print_log(result)
if result["is_error"]:
error_count += 1
if feishu_webhook and send_feishu_alert(feishu_webhook, result):
print(f" └─ 飞书告警已发送: {result['name']}")
print("-" * 60)
print(f"监控完成: 共检查 {len(targets)} 个目标, 异常 {error_count} 个")
print("=" * 60)
sys.exit(1 if error_count > 0 else 0)
if __name__ == "__main__":
main()
配套两个配置文件,targets.json 是必需项,config.json 可选。
json
// targets.json 示例
[
{"name": "示例站点A", "url": "https://www.baidu.com", "timeout": 5},
{"name": "示例站点B", "url": "https://github.com", "timeout": 10}
]
json
// config.json 示例(可选,存放飞书 webhook)
{"feishu_webhook": "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx"}
三、关键实现解析
异常分支分全,是这段代码最稳的一点。 check_target 没有只写 happy path,而是把网络失败分成了三类:Timeout(超时)、ConnectionError(连不上)、通用 RequestException(其他)。真实网络环境里站点挂掉的方式不止一种,分清楚失败类型,告警信息才有用。比如同样是「打不开」,超时和连不上对应的处理动作完全不同:超时可能是后端慢或链路抖动,连不上大概率是 DNS、端口或服务进程挂了,告警里写清楚类别,值班的人一眼就知道往哪查。
请求参数也做了取舍。 requests.get 带了 timeout=timeout、allow_redirects=True 和自定义 User-Agent。timeout 是关键,不设这个参数,请求可能无限挂起,监控脚本自己先卡死;allow_redirects 让 301/302 跳转后的最终状态码参与判定,避免把「跳转到正常页」误判成异常;User-Agent 则防止个别站点对空 UA 直接拒绝。这些细节模型一次就带上了,说明它理解的是真实请求而不是教学示例。
配置和代码分离,决定了它能不能长期用。 监控目标放 targets.json、飞书地址放 config.json,脚本本身不写死任何业务数据。后面加站点、换告警通道,都不用改逻辑。更关键的是 get_feishu_webhook 做了「文件不存在 / JSON 解析失败 / 字段为空」三种降级,拿不到 webhook 就退回纯控制台日志,不会因为配置问题直接崩。
飞书告警选择 text 类型是有原因的。 前面提过 markdown 类型在这台机器人上关键词校验不稳定,所以 send_feishu_alert 直接用 msg_type: text,正文第一行固定带「监控」关键词。文本结构用换行分隔字段(名称、地址、原因、状态码、耗时、阈值、时间),在飞书里可读性足够,也绕开了富文本渲染的兼容坑。
退出码也考虑了。 main 末尾 sys.exit(1 if error_count > 0 else 0),异常时返回非 0,方便被 crontab 或计划任务接住做二次处理。比如 crontab 里可以再套一层,非 0 时才触发额外的通知或上报,把「探测」和「处置」解耦。
四、踩坑一:飞书关键词校验拦了告警
第一版我用的是 markdown 类型消息,正文含「告警」二字。逻辑跑通了,但飞书一条没收到,后台报错「Key Words Not Found」。先试着把「告警」挪到正文第一行,还是失败。根因是这台飞书机器人对 markdown 类型的关键词校验不稳定,换成 text 类型、关键词改成「监控」后立刻成功。

(图 1:飞书群里收到两条告警消息,分别是 httpbin 超时和不存在域名连接失败)
这里有个值得记下的点:模型严格按提示词写了 markdown + 关键词,代码逻辑没错,却没踩中第三方平台的安全规则。定位根因、决定改类型的,还是人。
五、踩坑二:Windows 计划任务编码崩
手动在终端能跑,挂进 Windows 计划任务每五分钟跑一次就报 gbk codec can't encode。根因是计划任务调起的控制台默认 GBK 编码,而早期版本日志里用了 emoji 图标,一进计划任务就崩。修法是 bat 入口加 chcp 65001 强制 UTF-8,并把 emoji 换成 [正常] / [异常] 纯文字。

(图 2:Windows 任务计划程序里 SeedMonitor 的属性页,触发器为每 5 分钟重复一次)
改完这两处,定时任务才真正稳定。顺手贴一张稳定运行后的日志,三个目标全部正常、异常 0 个,说明它已经老实在替你盯着了。

(图 3:monitor.log 内容,三个目标全部「正常」、异常 0 个,证明定时任务稳定运行)
六、从脚本到接口:能力还能再往前走一步
脚本在本地跑着只是起点。把 check_target 的探测逻辑抽象成一个 HTTP 健康检查接口,参数就是 url + timeout + expected_status,返回 status_code、elapsed_ms、is_error,再叠告警规则、调度、历史可用率,它就成了一个轻量监控能力。这样一个原本只能在本机跑的网站监控脚本,就变成了可被其他系统调用的接口监控服务,前端大屏、运维平台都能直接拉数据。
想让脚本对外提供服务,也可以封装成接口托管到 YesApi Pro 这类 API 开放平台,省去自己搭服务、接计费、做权限的功夫,是否采用看各自基础设施情况。

这不是把简单的事搞复杂,而是当监控目标从三五个涨到几十个、告警要从飞书扩到企微钉钉时,平台化的收益才会显现。
需要说清的适用边界
这套方案不是银弹,它最适合「盯几个对外站点、要个及时告警」的场景。如果要做真正的企业级可用性监控,至少要补三件事:多节点探测(一台机器一个网络出口会误报)、SSL 证书到期检查、以及把探测和告警通道解耦。另外它只检查「首页能不能打开」,不验证页面内容和接口深层逻辑,深度巡检得另写用例。把边界讲清楚,比把它吹成万能工具更有价值。
七、小结
这次实测里,Doubao-Seed-Evolving 真正的强项不是一次写对,而是能接住自然语言需求、产出完整可运行工程,还能跟着多轮修正不丢上下文。人负责的是需求边界、踩坑定位和部署判断。两者配合,才到得了「真在跑、真在告警」这一步。
#AI编程#大模型#Python#开发工具#后端#程序员#网站监控#自动化#接口测试#技术分享