天气预报邮件助手(地点 / 收件人 / 天数均可变)
抓取任意地点未来 N 天天气(Open-Meteo),渲染成一封精美的 HTML 天气预报 + 出行提醒邮件,
并自动发送给指定收件人。地点、天数、收件人、邮件主题全部参数化。
When to use(触发条件)
用户要求「查某地天气并发邮件」「天气邮件」「给 xxx@xx.com 发天气预报」「未来 N 天天气预报邮件」
时使用。也适用于每日定时天气推送的自动化任务。
三条可变参数
| 维度 | 参数 | 说明 |
|---|---|---|
| 地点 | --place 沈河区 / --lat --lon |
决定抓取坐标,同时决定标题与主题 |
| 天数 | --days 3 |
1~16,默认 3 |
| 收件人 | --to a@b.com(可多个) |
发送阶段使用;--cc 抄送 |
邮件主题随地点与日期区间自动生成:
{地点显示名}未来{N}天天气预报(M月D日–M月D日)
例:沈河区 → 沈阳市沈河区未来3天天气预报(9月11日–9月13日);上海 → 上海市未来5天天气预报(9月11日–9月15日)。
(--title 可强制覆盖主题中的地点名。)
默认地址 / 默认收件人的解析顺序(不必每次都问用户)
地点与收件人共用同一套「三级回退 + 提示输入」逻辑(scripts/defaults.py):
| 级别 | 地点 | 收件邮箱 |
|---|---|---|
| 1 显式 | --place 沈河区 |
--to someone@example.com |
| 2 配置 | ~/.workbuddy/weather-mail-sender.json 的 default_place |
同文件的 default_recipients |
| 3 记忆体 | 只扫白名单:~/.workbuddy/MEMORY.md、~/.workbuddy/memory/、<工作区>/.workbuddy/memory/ 下的 .md/.txt |
同左 |
| 4 都没有 | 打印引导并 exit 4 | 打印引导并 exit 5 |
加 --no-memory 可完全关闭记忆体检索(只用命令行 + 配置)。
检索边界(权限最小化,v1.0.7 起):记忆体检索不会递归整棵 ~/.workbuddy/——
该目录下还有 skills/、plugins/、binaries/ 等与记忆无关的目录,全量递归会读到
所有已装技能的文档,既越权也容易误取他人文档里的邮箱。白名单之外还有四重约束:
目录黑名单(skills/plugins/binaries/node_modules/.git/cache/env/dist 等,
即使 --memory-dir 传了更宽的目录也不进入)、最大深度 3、单文件 2 MB、累计 32 MB。
记忆体证据权重(避免误判)
- 地点:历史产物 weather_data.json 的 place.input/display(权重 5)> 文本中的 --place X 痕迹(4)
> 已知地名词表命中(1)。不做「XX区/市」泛匹配——那会把「安全区」这类噪声当地名。
- 邮箱:同一行出现「收件人 / 邮箱 / 邮件 / 发给 / 发送至 / --to」等语境词=强证据;
否则需全文出现 ≥3 次。只取证据最强的一个,来源为 memory 时打印提示,可用 --to 覆盖。
代理行为约定:命令以 4(缺地点)或 5(缺收件人)退出时,向用户询问缺失项,拿到后重跑;
若用户希望以后不再问,重跑时加 --save-defaults 固化成本机默认值。
# 固化默认:以后直接 `run_all.py` 即可
python scripts/run_all.py --place 沈河区 --to you@example.com --save-defaults
# 相关参数
--config <file> # 指定配置文件(默认 ~/.workbuddy/weather-mail-sender.json)
--memory-dir <dir> # 追加参与检索的记忆体目录,可重复传入
--no-memory # 只用命令行 + 配置,不检索记忆体
--save-defaults # 把本次解析到的地点/收件人写入配置
脚本(scripts/,纯标准库,可移植)
fetch_weather.py:地点解析 + 天气抓取 →weather_data.jsongen_mail.py:渲染 HTML + 主题 →weather_mail.html/email_subject.txtsend_mail.py:CLI 兜底发送 + 回查「已发送」校验(--to缺省时同样走上面的解析顺序,缺失则 exit 5)run_all.py:一条龙(抓取 → 生成 → 可选发送)defaults.py:地点/收件人的默认值解析与记忆体检索(上一节)minify_mail.py:走连接器发送前必做的一步——把 HTML 压到 9 KB 以内并写出分块文件(见下节)
标准流程(SOP)
# 1) 一条龙:只生成(随后用连接器发送)
python scripts/run_all.py --place 沈河区 --days 3 --workdir out \
--brand "SHENHE DISTRICT · SHENYANG"
# 2) 一条龙:生成并直接发(无人值守 / 定时任务)
python scripts/run_all.py --place 上海 --days 3 --workdir out \
--to you@example.com --send
分步等价写法:
python scripts/fetch_weather.py --place 沈河区 --days 3 --out out/weather_data.json
python scripts/gen_mail.py --data out/weather_data.json --out out/weather_mail.html \
--subject-out out/email_subject.txt --brand "SHENHE DISTRICT · SHENYANG"
python scripts/send_mail.py --to you@example.com \
--subject-file out/email_subject.txt --body-file out/weather_mail.html
走连接器发送的标准三步(无人值守 / 定时任务必看)
# 3) 压缩:13 KB 的原文会发不出去,压到 9 KB 以内(同时产出分块文件)
python scripts/minify_mail.py --in out/weather_mail.html --out out/mail_min.html --wrap 1400
# -> 原文 13129 字符 -> 压缩后 9027 字符;chunks 文件共 7 块
随后 Read 出 out/mail_min.html.chunks.txt(Read 单行超过 2000 字符会截断,所以必须分块),
把各块按原序直接拼接回一整行(切片不增删字符),作为 body 调用
SendMessage(to, subject, body_format=HTML, skip_confirmation=true),最后 ListMessages(dir=sent) 回查。
发送通道(二选一,按可用性排序)
- 智能体邮箱连接器(对话内首选):先用 Read 读出
out/email_subject.txt,再用
minify_mail.py压缩正文并分块 Read 出全文(连接器没有--body-file之类的参数,
正文只能整体作为body字符串传入,且超过约 10 KB 会整包丢失,详见 Pitfalls),
再调用mcp__agent-mail__SendMessage(body_format=HTML)。
确认机制:未带skip_confirmation时会返回 42801 +confirmation_token;
带 token 重试时必须重新传一遍完整 body(token 有效期约 5 分钟),等于正文要发两遍。
所以用户已授权无人值守时,第一次调用就直接传skip_confirmation=true,
不要走「先试一次拿 token 再确认」的流程。
发完用ListMessages(dir=sent)回查是否进入「已发送」。 - agently-cli(自动化 / 连接器不可用时兜底):
scripts/send_mail.py内部调用
agently-cli message +send ... --confirmed,并自动回查发送状态。
需要npm install -g @tencent-qqmail/agently-cli;Windows 下建议显式设置
AGENTLY_WORKSPACE=workbuddy(否则 CLI 可能把凭据归到 codebuddy 工作区),
找不到 CLI 时可用环境变量AGENTLY_CLI指定绝对路径。
首次使用或报invalid_grant时必须先登录:CLI 的 OAuth 是交互式的,代理无法代跑,
要提示用户在自己的终端执行AGENTLY_WORKSPACE=workbuddy agently-cli auth login;
登录前 CLI 通道不可用,请改用通道 1。
发送后必须校验:连接器用 ListMessages(dir=sent);CLI 通道脚本会自动校验,
返回码 0=已发送并校验通过,2=发送失败,3=已入队但未校验到(稍后重试校验即可)。
校验不到就重发一次;仍失败则把具体错误报给用户,不要静默结束。
退出码一览:0 成功 | 2 发送失败 | 3 已入队未校验到 | 4 缺地点 | 5 缺收件人(4/5 需向用户询问)。
地点解析规则(重要)
顺序:显式 --lat/--lon → 内置回退表 → 本地缓存 → Open-Meteo 地理编码(限定 countryCode=CN)。
- 中文"区/县"级地名在 Open-Meteo 经常搜不到,或命中外省同名地(实测「沈河区」会误匹配到甘肃)。
scripts/fetch_weather.py 的 KNOWN_PLACES 已内置沈阳各区 + 主要城市,命中时自动补全为「沈阳市沈河区」。
- 新地点首次在线解析成功后写入用户目录 ~/.workbuddy/weather-mail-sender/places_cache.json
(不写技能安装目录,兼容只读安装);包内 references/places_cache.json 只是只读预置表。
用 --cache <path> 可改路径,--no-cache 完全不读写。
- 若仍解析失败,脚本会提示改用 --lat/--lon。新增常用地点可直接补进 KNOWN_PLACES。
- 解析后必须核对经纬度:Open-Meteo 地理编码会把「武功」误匹配到河南(33.33,113.57),
而真正的陕西咸阳市武功县是(34.26,108.21)。这类错误一旦写入 places_cache.json 会被缓存固化,
后续全部静默取错。发现坐标明显不对时:修正用户缓存里的经纬度与 display
(建议写全称「咸阳市武功县」),并同步补进 KNOWN_PLACES(内置表优先级高于缓存)。
已知坑(Pitfalls)
- 连接器正文有体积上限(约 10 KB),超限会「静默」丢参数:3 天模板原样输出 13 KB,
直接塞进SendMessage的body会让整段工具调用参数丢失,报错是莫名其妙的
check input schema required error field_name: to(看起来像没传收件人,其实是整包参数没了)——
极易误判成「连接器坏了」或「写法有问题」而反复重试。实测:13.1 KB ✗、10.8 KB ✗、9.0 KB ✓。
对策:发送前先跑scripts/minify_mail.py(折叠换行缩进 + 去掉 emoji 字体 span,13.1 KB → 9.0 KB,
渲染效果不变)。排障时快速自测——把body换成"TEST"发一次,能成功就说明是体积问题。 - Read 单行超过 2000 字符会截断:所以压缩后的单行 HTML 不能指望一次 Read 读全。
用minify_mail.py --wrap 1400产出.chunks.txt,按块读出后按原序无分隔符拼接还原。
不要自己手工去空白,容易把border:1px solid拼成border:1pxsolid这类坏样式。 - emoji 一律写原生字符,不要写数字实体:正文里的
&#128167;(💧)这类增补平面
(> U+FFFF)数字字符引用,会被 163 / QQ 邮箱等客户端的 HTML 解析器当成单个 UTF-16 码元,
解出非法字符后在收件端显示成「豆腐块」方框。头部大图标用原生 emoji 一直正常,
卡片详情区曾用小图标写成实体 → 收件后全变方框(1.0.9 已修)。
推论:任何 > U+FFFF 的字符(emoji、生僻字)都直接用原生字符,别做实体转义;
&deg;&nbsp;这类 BMP 实体没问题。改动图标后务必发一封实测看收件端效果。 - 天气描述有两份独立映射表:
fetch_weather.py(控制台输出)与gen_mail.py(卡片文案 + emoji)
各维护一份 WMO code → 中文描述的字典,改任何一条都必须两处同时改,否则控制台与邮件文案会不一致。 - URL 里的
%:fetch_weather.py的预报 URL 使用%格式化,新增字段时注意转义(%%)。 - 字面量别写
%%:gen_mail.py的提醒文案用%格式化,但 if/else 分支中不参与插值的那一支
应保持单%。曾把「降水概率均低于 10%%」写进无插值分支,导致邮件里真的显示10%%。
改文案时先确认该字符串后面有没有% (...)运算。 - 本机无
jq:本技能全部用 Python 标准库解析,不要改用curl | jq的写法。 - 时区固定
Asia/Shanghai(--timezone可覆盖);--days上限 16。 - 邮件客户端兼容:正文用 table 布局 + 内联样式,勿用 flex/grid/外部 CSS。
- 天气图标用 emoji,极少数客户端可能显示为方块,属可接受降级。
- CLI 的
--body-file必须是相对当前目录的路径(CLI help 明确要求):
send_mail.py已通过「cwd 切到正文目录 + 只传文件名」规避;若你手工拼 CLI 命令,
别直接传绝对路径。同时显式传--body-format(.html→html),不要依赖 CLI 自判。 - CLI 授权会过期:
refresh token is invalid or expired属于正常过期,不是脚本 Bug。
send_mail.py会识别并打印登录指引、返回码 2;此时转用智能体邮箱连接器即可。 - 连接器正文只能整体传入:
mcp__agent-mail__SendMessage不接受本地文件路径,
必须 Read 出 HTML 再作为body传;且带confirmation_token重试时需重传全文(见「发送通道」)。 - 日配额:连接器响应头
x_dailyquota_*会返回每日发信额度(默认 50 封/日),
重发前先看x_dailyquota_remaining,别盲目重试。
权限边界(对应 frontmatter 的三项 permission)
| 权限 | 实际用途 | 关闭方式 |
|---|---|---|
| 网络访问 | 仅 geocoding-api.open-meteo.com 与 api.open-meteo.com 两个公开只读端点 |
无需关闭(纯只读 GET) |
| 邮件发送 | 把生成的 HTML 发给使用者指定的收件人 | 不加 --send 即不发送 |
| 本地文件读取 | 只读白名单记忆文件(~/.workbuddy/MEMORY.md、~/.workbuddy/memory/、./.workbuddy/memory/ 下的 .md/.txt),用于推断默认地点与收件人 |
加 --no-memory 完全关闭 |
上限:目录深度 ≤3、最多 200 个文件、单文件 ≤2 MB、累计 ≤32 MB;内置目录黑名单
(skills/plugins/binaries/node_modules/.git/cache/env/dist…)永不进入,不做任何写回。
Compliance, privacy & portability
- 仅访问 Open-Meteo 公共 API(无需 key、只读);发信使用使用者自己已授权的智能体邮箱。
- 隐私:技能内不内置任何真实邮箱地址,也不随包附带任何配置文件——收件人只能来自
命令行--to、本机配置~/.workbuddy/weather-mail-sender.json或本机记忆体;
三者都没有时不发送(exit 5)。文档示例统一使用保留域you@example.com
(不会真实投递,send_mail.py检测到该域会打印 WARN)。 - 反滥用:仅向使用者本人显式指定的收件人发送,不做地址爬取、不做批量群发;
发送前须经使用者授权(连接器通道需用户确认或显式skip_confirmation)。 - 不存储密钥;
places_cache.json只存地点名与经纬度,且写在用户目录。 - 命令行安全:
send_mail.py一律shell=False并对主题/收件人做元字符净化与格式校验,
CLI 调用带超时;gen_mail.py对所有外部输入做 HTML 转义。 - 纯标准库 Python 3.8+,无
pip install依赖,可整体复制/打包安装。