feat(aiot): 告警冷却时间调整 + 截图本地保留 + 中文路径修复
- 离岗检测冷却时间: 300s → 600s(10分钟) - 入侵检测冷却时间: 120s → 300s(5分钟) - 入侵告警级别改为高(alarm_level=3) - COS 不可用时保留本地截图文件,不再上报后删除 - 修复 cv2.imwrite 中文路径失败,改用 imencode + write_bytes - 配置订阅在 LOCAL 模式下跳过 Redis 连接 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,9 @@ class AlarmUploadWorker:
|
||||
self._logger.error(f"Worker Redis 连接失败: {e}")
|
||||
return
|
||||
|
||||
# 启动时验证云端 API 可达性
|
||||
self._check_cloud_api()
|
||||
|
||||
self._stop_event.clear()
|
||||
self._thread = threading.Thread(
|
||||
target=self._worker_loop,
|
||||
@@ -92,6 +95,29 @@ class AlarmUploadWorker:
|
||||
self._thread.start()
|
||||
self._logger.info("AlarmUploadWorker 已启动")
|
||||
|
||||
def _check_cloud_api(self):
|
||||
"""启动时检查云端 API 是否可达(仅记录日志,不阻断启动)"""
|
||||
upload_cfg = self._settings.alarm_upload
|
||||
base_url = upload_cfg.cloud_api_url.rstrip("/")
|
||||
health_url = f"{base_url}/health"
|
||||
report_url = f"{base_url}/admin-api/aiot/alarm/edge/report"
|
||||
|
||||
self._logger.info(f"云端 API 地址: {base_url}")
|
||||
self._logger.info(f"告警上报端点: {report_url}")
|
||||
|
||||
try:
|
||||
resp = requests.get(health_url, timeout=5)
|
||||
if resp.status_code == 200:
|
||||
self._logger.info(f"云端健康检查通过: {health_url}")
|
||||
else:
|
||||
self._logger.warning(
|
||||
f"云端健康检查异常: {health_url}, status={resp.status_code}"
|
||||
)
|
||||
except requests.ConnectionError:
|
||||
self._logger.warning(f"云端不可达: {health_url},请确认服务已启动")
|
||||
except Exception as e:
|
||||
self._logger.warning(f"云端健康检查失败: {e}")
|
||||
|
||||
def stop(self):
|
||||
"""停止 worker"""
|
||||
if not self._thread or not self._thread.is_alive():
|
||||
@@ -156,18 +182,32 @@ class AlarmUploadWorker:
|
||||
snapshot_local_path = alarm_data.get("snapshot_local_path")
|
||||
object_key = None
|
||||
|
||||
if snapshot_local_path and os.path.exists(snapshot_local_path):
|
||||
object_key = self._upload_snapshot_to_cos(
|
||||
snapshot_local_path,
|
||||
alarm_id,
|
||||
alarm_data.get("device_id", "unknown"),
|
||||
)
|
||||
if object_key is None:
|
||||
# COS 上传失败,进入重试
|
||||
self._handle_retry(alarm_json, "COS 上传失败")
|
||||
return
|
||||
else:
|
||||
if snapshot_local_path:
|
||||
if snapshot_local_path:
|
||||
# 截图是异步保存的,等待文件写入完成(最多 3 秒)
|
||||
if not os.path.exists(snapshot_local_path):
|
||||
for _ in range(6):
|
||||
time.sleep(0.5)
|
||||
if os.path.exists(snapshot_local_path):
|
||||
break
|
||||
|
||||
if os.path.exists(snapshot_local_path):
|
||||
object_key = self._upload_snapshot_to_cos(
|
||||
snapshot_local_path,
|
||||
alarm_id,
|
||||
alarm_data.get("device_id", "unknown"),
|
||||
)
|
||||
if object_key is None:
|
||||
# COS 上传失败,进入重试
|
||||
self._handle_retry(alarm_json, "COS 上传失败")
|
||||
return
|
||||
elif object_key == "":
|
||||
# COS 未配置,使用本地截图路径作为回退
|
||||
captures_base = os.path.join("data", "captures")
|
||||
rel_path = os.path.relpath(snapshot_local_path, captures_base)
|
||||
rel_path = rel_path.replace("\\", "/")
|
||||
object_key = f"local:{rel_path}"
|
||||
self._logger.info(f"使用本地截图路径: {object_key}")
|
||||
else:
|
||||
self._logger.warning(f"截图文件不存在: {snapshot_local_path}")
|
||||
|
||||
# Step 2: HTTP 上报告警元数据
|
||||
@@ -190,8 +230,8 @@ class AlarmUploadWorker:
|
||||
self._stats["processed"] += 1
|
||||
self._logger.info(f"告警上报成功: {alarm_id}")
|
||||
|
||||
# 可选:删除本地截图
|
||||
if snapshot_local_path and os.path.exists(snapshot_local_path):
|
||||
# 仅在 COS 上传成功时删除本地截图;本地回退模式(local:)不删除
|
||||
if snapshot_local_path and os.path.exists(snapshot_local_path) and object_key and not object_key.startswith("local:"):
|
||||
try:
|
||||
os.remove(snapshot_local_path)
|
||||
self._logger.debug(f"已删除本地截图: {snapshot_local_path}")
|
||||
@@ -266,7 +306,8 @@ class AlarmUploadWorker:
|
||||
是否上报成功
|
||||
"""
|
||||
upload_cfg = self._settings.alarm_upload
|
||||
url = f"{upload_cfg.cloud_api_url}/admin-api/aiot/alarm/edge/report"
|
||||
base_url = upload_cfg.cloud_api_url.rstrip("/")
|
||||
url = f"{base_url}/admin-api/aiot/alarm/edge/report"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -274,10 +315,16 @@ class AlarmUploadWorker:
|
||||
if upload_cfg.edge_token:
|
||||
headers["Authorization"] = f"Bearer {upload_cfg.edge_token}"
|
||||
|
||||
# 过滤掉内部字段(以 _ 开头的控制字段不发送到云端)
|
||||
report_data = {k: v for k, v in alarm_data.items() if not k.startswith("_")}
|
||||
|
||||
self._logger.debug(f"HTTP 上报 URL: {url}")
|
||||
self._logger.debug(f"HTTP 上报数据: {report_data}")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
json=alarm_data,
|
||||
json=report_data,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
@@ -293,17 +340,26 @@ class AlarmUploadWorker:
|
||||
)
|
||||
return False
|
||||
else:
|
||||
self._logger.warning(f"HTTP 上报失败: status={response.status_code}")
|
||||
# 记录详细的错误信息便于排查
|
||||
resp_text = ""
|
||||
try:
|
||||
resp_text = response.text[:500]
|
||||
except Exception:
|
||||
pass
|
||||
self._logger.warning(
|
||||
f"HTTP 上报失败: url={url}, status={response.status_code}, "
|
||||
f"body={resp_text}"
|
||||
)
|
||||
return False
|
||||
|
||||
except requests.Timeout:
|
||||
self._logger.warning(f"HTTP 上报超时: {url}")
|
||||
return False
|
||||
except requests.ConnectionError as e:
|
||||
self._logger.warning(f"HTTP 上报连接失败: {e}")
|
||||
self._logger.warning(f"HTTP 上报连接失败: {url}, error={e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self._logger.error(f"HTTP 上报异常: {e}")
|
||||
self._logger.error(f"HTTP 上报异常: {url}, error={e}")
|
||||
return False
|
||||
|
||||
def _handle_retry(self, alarm_json: str, error: str):
|
||||
|
||||
Reference in New Issue
Block a user