背景

在本地部署 AI 代理之后,一个很自然的需求是让它承担一些低风险、可重复的日常检查工作。例如:

  • 每天检查几台服务器是否正常运行;
  • 每天发送国际新闻摘要;
  • 每天发送常用货币之间的汇率信息;
  • 每天发送本地城市的一周天气预报;
  • 发现异常时通过邮件通知管理员。

这些任务看起来都可以用传统脚本完成,例如写一个 Python 脚本,再配合 cronsystemd timer 定时执行。但如果本地 AI 代理本身已经内置了 Heartbeat 机制,那么更合理的做法不是再造一套外部自动化系统,而是把任务边界写进代理的 Heartbeat 配置文件,让代理自己按授权范围执行。

本文记录一次围绕 Heartbeat 文件、邮件通知和发件人显示名的整理过程。


Heartbeat 文件的定位

最初容易产生一个误解:Heartbeat 文件是不是一份普通说明文档?

实际检查后可以确认,它更适合被理解为:

本地 AI 代理的周期任务授权清单。

它不是长期记忆文件,也不是系统状态总表,更不是脚本。它的核心作用是告诉代理:

  • 允许做哪些周期任务;
  • 多久检查一次;
  • 允许访问哪些对象;
  • 邮件发给谁;
  • 每个任务是否单独发邮件;
  • 只读边界在哪里;
  • 哪些动作明确禁止。

因此,Heartbeat 文件不应存放大量历史信息、主机清单、连接细节、密码、令牌、日志原文或临时排障内容。那些信息应该分别放在长期记忆文件、工具环境说明文件、每日记录文件或专门的配置文件中。

Heartbeat 文件只负责回答一个问题:

代理现在被授权定期做什么?


最初的需求

本次需求可以分成四类每日邮件:

  1. 服务器运行状态检查;
  2. 国际新闻 10 条简要摘要;
  3. 常用货币之间的汇率信息;
  4. 本地城市一周天气预报。

要求是:

  • 每个任务单独发一封邮件;
  • 邮件发到指定管理员邮箱;
  • 服务器检查只做只读状态确认;
  • 异常时只报告,不自动修复;
  • 不擅自重启服务、改配置、删日志、安装软件;
  • 不访问无关私人文件;
  • 不登录银行、社交账号、新闻网站会员账号等私人账户。

这样的需求非常适合写进 Heartbeat 文件。


不应该额外写一套完整脚本

一开始可以想到一种传统方案:

systemd timer
    ↓
Python 脚本
    ↓
SSH 检查服务器
    ↓
抓取新闻、汇率、天气
    ↓
调用本机邮件服务发信

这套方案当然可行,但它有一个问题:

如果脚本自己完成全部工作,那么即使没有 AI 代理,也能收到邮件。

这就偏离了 Heartbeat 文件的真正用途。此时 Heartbeat 文件只剩说明文档意义,而不是 AI 代理的周期任务入口。

更合理的架构应该是:

AI 代理内置 Heartbeat runner
    ↓
定期读取 Heartbeat 文件
    ↓
判断哪些任务被授权、哪些任务到期
    ↓
执行任务
    ↓
通过本机邮件系统发送报告

这种模式下,Heartbeat 文件不是脚本配置,而是代理行为的边界文件。


实际检查到的运行机制

后来通过只读检查确认:

  • 本地 AI 代理网关服务一直在运行;
  • Heartbeat runner 是代理进程内部功能;
  • 没有额外的外部 systemd timer
  • 没有额外的 cron 任务;
  • 代理状态中显示 Heartbeat 已启用;
  • 当前 Heartbeat 唤醒间隔是 30 分钟;
  • Heartbeat 执行时会读取 Heartbeat 文件,并将其内容纳入任务提示;
  • 邮件最终通过本机邮件服务提交,由本机 MTA 转发出去。

因此,之前“必须额外写脚本和 timer”的想法不适用于这个环境。

正确结论是:

只要本地 AI 代理网关服务持续运行,并且 Heartbeat 功能处于启用状态,维护 Heartbeat 文件通常就足够了。


Heartbeat 文件中的任务写法

Heartbeat 文件适合采用清晰的 Markdown 结构,而不是复杂配置格式。

推荐每个任务都包含这些字段:

Status
Approved
Cadence
Recipient
Purpose
Scope
Mode
Email behavior
Do not

例如,服务器检查任务可以写成:

Status: enabled
Cadence: daily
Recipient: 管理员邮箱

Purpose:
- 检查两台服务器的每日运行状态。

Scope:
- SSH 是否可连接
- 主机名
- 运行时间
- 系统负载
- 内存使用
- 根分区磁盘使用
- 失败的 systemd units

Mode:
- Read-only

Do not:
- 不自动重启服务
- 不重启主机
- 不修改系统配置
- 不修改防火墙、反向代理、隧道或路由器配置
- 不安装或更新软件包
- 不删除日志
- 不读取无关私人文件

新闻、汇率、天气任务也应采用类似结构。

关键不是写得复杂,而是边界清楚。


每日任务和 Heartbeat 唤醒频率不是一回事

这里有一个容易混淆的点:

  • Heartbeat runner 的唤醒频率;
  • 每个业务任务的执行频率。

例如:

Heartbeat runner: 每 30 分钟醒一次
新闻任务: 每天发一次
天气任务: 每天发一次
汇率任务: 每天发一次
服务器任务: 每天发一次

这并不矛盾。代理可以每 30 分钟醒一次,但只有在判断某个每日任务到期时才发邮件。

如果觉得 30 分钟太频繁,可以把代理的 Heartbeat 唤醒间隔改成 6 小时。但这不等于把每日任务改成每 6 小时执行一次。

正确改动目标是:

Heartbeat runner interval: 30m → 6h

而不是:

Task cadence: daily → 6h

否则可能导致新闻、天气、汇率一天发送多次。


邮件发送链路

本次邮件链路大致是:

AI 代理
    ↓
本机 sendmail/mail 接口
    ↓
本机 Postfix
    ↓
外部邮件服务器
    ↓
管理员邮箱

本机邮件服务已经可以正常发送邮件。日志中可以看到邮件从本机提交,再由 Postfix 转发到外部邮件服务器。

不过检查时发现一个细节:由于代理进程以管理员用户运行,本机 Postfix 日志中会出现类似:

uid=0 from=<root>

这说明本机提交邮件时的 envelope sender 默认来自当前系统用户。

这不是邮件客户端里看到的显示名,而是 Postfix 队列和投递过程中的发件地址信息。


envelope sender 与 From 头的区别

邮件里至少有两层“发件人”概念:

1. envelope sender

这是 SMTP 投递层面的发件地址。Postfix 日志中的 from=<...> 通常反映的是这一层。

它影响退信、投递链路、部分反垃圾判断等。

2. RFC 5322 From header

这是邮件内容头里的发件人,邮件客户端通常显示这一层。例如:

From: Display Name <sender@example.com>

想让收件箱显示成:

某节点 <某地址>

主要要控制的是 From: 头。

如果还希望投递层也一致,则发送邮件时还要指定 envelope sender,例如使用 sendmail -f


发件人显示名的验证

通过手动测试确认,只要显式提交:

Envelope sender: 某节点专用邮箱地址
From header: 某节点显示名 <某节点专用邮箱地址>

邮件客户端就可以正确显示为:

某节点 <某节点专用邮箱地址>

这说明本机 Postfix 和 sendmail 接口本身支持目标效果。

因此,最小改动不应该先去改 Postfix 的全局 header 规则,而应该先让 Heartbeat 邮件在生成时显式写入正确的 From 头。


推荐的 Heartbeat 邮件规则

在 Heartbeat 文件的全局邮件规则中,可以加入类似规则:

Default email rule:

- Send each approved task as a separate email.
- Do not combine unrelated heartbeat reports into one email.
- Use a clear subject line.
- Include timestamp, source/status, concise summary, and abnormal items when applicable.
- Set the RFC 5322 From header exactly to:
  某节点 <某节点专用邮箱地址>
- When submitting mail through sendmail, use envelope sender:
  某节点专用邮箱地址

这样,代理在生成邮件时就有明确要求:

  • 邮件内容头使用指定显示名;
  • 发送时使用指定 envelope sender;
  • 每个任务单独发信;
  • 邮件内容保持简洁。

如果代理遵守该规则,收件箱中就会显示期望的发件人格式。


Postfix 映射的作用

本机 Postfix 已经启用了发件地址映射。映射文件中已经将本机管理员用户和本地主机地址映射到专用发件地址。

这层配置可以作为保底。

但需要注意:

地址映射主要解决地址,不一定负责显示名。

也就是说,它可以把本地地址改写成专用邮箱地址,但不一定能把显示名改成指定节点名。

如果只是希望收件箱显示为:

某节点 <某节点专用邮箱地址>

优先让邮件生成方写正确的 From: 头。

只有在代理无法按要求写入 From 头时,才考虑 Postfix 层面的 header_checks 等强制改写方案。


为什么不建议一开始就用 Postfix 强制改写

Postfix 的全局邮件头改写影响范围更大。

如果配置不慎,可能会影响:

  • 系统通知邮件;
  • 其他本地服务发出的邮件;
  • 管理员测试邮件;
  • 未来新增的邮件任务。

而 Heartbeat 邮件只是其中一类邮件。能在 Heartbeat 任务层解决,就不应一开始扩大到全局 MTA 层。

推荐顺序是:

1. 先让 Heartbeat 任务显式写 From 头
2. 验证收件箱显示是否正确
3. 如果不生效,再检查代理邮件发送实现
4. 最后才考虑 Postfix header 强制改写

关于 Heartbeat 频率

检查结果显示,代理当前 Heartbeat 唤醒频率偏高。对于每日新闻、天气、汇率、服务器状态报告来说,不需要非常频繁地唤醒。

如果任务目标只是每日邮件,6 小时一次已经足够:

Heartbeat runner: 6h
Task cadence: daily

这样一天最多检查几次是否到期,不会像 30 分钟一次那样频繁消耗资源。

改频率时应注意:

  • 不要修改 Heartbeat 文件中的每日任务为 6h;
  • 不要新增外部 timer;
  • 不要写额外脚本;
  • 优先使用代理自带配置命令;
  • 修改后需要用代理状态命令验证;
  • 如果需要重启代理服务,应确认配置已经保存。

推荐最终结构

整理后的系统结构应该是:

Heartbeat 文件
    ↓
定义已授权周期任务和邮件规则

AI 代理内置 Heartbeat runner
    ↓
每隔若干小时醒一次

本机邮件服务
    ↓
负责提交和转发邮件

管理员邮箱
    ↓
接收每日独立报告

不需要额外创建:

外部 cron
外部 systemd timer
完整 Python 报告脚本
重复邮件发送脚本

这样可以避免多套自动化系统同时运行,降低重复通知、状态不一致和维护复杂度。


最终经验

本次整理得到几个结论:

  1. Heartbeat 文件不是普通说明文档,而是代理周期任务的授权入口。
  2. 如果代理已经内置 Heartbeat runner,就不需要再额外写完整脚本。
  3. 每日任务的业务频率和 Heartbeat runner 的唤醒频率要分开理解。
  4. 邮件显示名优先通过邮件内容头 From: 控制。
  5. envelope sender 和收件箱显示名不是同一个概念。
  6. Postfix 地址映射可以作为保底,但不适合一开始就承担所有显示名改写。
  7. 对于只读检查类任务,Heartbeat 文件中必须明确禁止自动修复、重启、删日志、改配置等行为。
  8. 每个任务单独发邮件,比把所有内容合并成一封更便于过滤和归档。
  9. 若任务只是每日信息汇总,Heartbeat runner 没必要保持过高频率。
  10. 最重要的是让 AI 代理在清晰边界内工作,而不是让它无约束地“主动帮忙”。

示例:简化后的 Heartbeat 片段

Status: enabled

Default recipient:
- 管理员邮箱

Default cadence:
- Daily
- Timezone: 本地时区

Default email rule:
- Send each approved task as a separate email.
- Do not combine unrelated heartbeat reports into one email.
- Use a clear subject line.
- Include timestamp, source/status, concise summary, and abnormal items when applicable.
- Set the RFC 5322 From header exactly to:
  节点显示名 <节点专用邮箱地址>
- When submitting mail through sendmail, use envelope sender:
  节点专用邮箱地址

Approved periodic checks:
- server daily health check
- international news daily digest
- exchange rate daily summary
- local weekly weather forecast

Global boundaries:
- Read-only by default.
- Do not repair automatically.
- Do not restart services.
- Do not modify system files.
- Do not modify firewall, router, reverse proxy, tunnel, or gateway configuration.
- Do not install or update packages.
- Do not delete logs.
- Do not access unrelated private files.

这个结构足够清楚,也方便以后继续扩展。

真正重要的不是把 Heartbeat 文件写得复杂,而是让它始终保持三个特征:

短
清楚
有边界

这样,本地 AI 代理才能成为稳定的运维小助手,而不是新的不可控自动化风险源。

Leave a Reply

Your email address will not be published. Required fields are marked *