1实现原理 · 为什么它能做到
分成两层:一次性的「装好发信器」(存 webhook、绑定收件人分类、试发验证)+ 每次的「把话说清楚」(消息写作规范与模板)。发信本身是脚本 POST 到企业微信群机器人 webhook。
1. **One-time setup**: store the webhook URL, test connectivity, and create a reusable sender script. 2. **Send messages**: craft messages that distinguish state from delta, define every number, and avoid the misleading patterns that made earlier backup-sync notifications confusing.
腾讯系服务必须绕过本地代理,脚本在发起请求前主动清理六个代理环境变量——这是针对中国大陆网络环境的必要适配,不是可选优化。
def clear_proxy_env(): """Remove proxy env vars so Tencent endpoints are reached directly.""" for name in ( "http_proxy", "https_proxy", "all_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", ): os.environ.pop(name, None)
收件人身份被提升为强制配置项而非可推断项:config 必须有 recipient_scope(self/others)与 recipient_label,缺失即硬失败退出,不允许 agent 猜。
recipient_scope = config.get("recipient_scope") if recipient_scope not in VALID_RECIPIENT_SCOPES: print( f"Missing or invalid 'recipient_scope' in {CONFIG_PATH}; " "run scripts/set_recipient.py with self or others.", file=sys.stderr, ) sys.exit(1)
防替换链条:set_recipient.py 把发信脚本的绝对路径与 sha256 写进 config,发信前 send_wecom.py 重新计算并比对,任何一侧不匹配都拒绝发送。这是把「发信器自身被换掉」当成威胁模型来防。
sender_path = Path(sender_path_value).expanduser().resolve() running_sender = Path(__file__).resolve() if sender_path != running_sender or not sender_path.is_file(): print( f"Configured sender does not match this executable: {running_sender}", file=sys.stderr, ) sys.exit(1) if not isinstance(sender_sha256, str) or sha256_path(sender_path) != sender_sha256: print( "Configured sender digest is stale; rerun scripts/set_recipient.py.", file=sys.stderr, ) sys.exit(1)
自动发送(outbox 路径)走更严的一次性授权:必须由 runaway-guard 给出 payload 摘要,脚本核对 schema、scope、webhook 摘要、sender 指纹全部一致才发,且只尝试一次 HTTP。
"status": "pending_auto_self", "sender_skill": "notify-wecom", "recipient_scope": "self", "recipient_label": config["recipient_label"], "webhook_sha256": webhook_sha256,
配置写入是原子的:临时文件 + fsync + os.replace + 目录 fsync,且保留原文件权限位(默认 0600),避免半写坏配置或权限放宽。
mode = stat.S_IMODE(path.stat().st_mode) or 0o600 tmp = path.with_name(f".tinkle_{path.name}.{os.getpid()}.tmp") try: fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode) with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump(config, handle, ensure_ascii=False, indent=2) handle.write("\n") handle.flush() os.fsync(handle.fileno()) os.chmod(tmp, mode) os.replace(tmp, path)
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| api | 企业微信群机器人 webhook(腾讯官方) |
| cli | uv(以 --no-project 方式运行脚本,避免依赖项目环境) |
| cli | python3(脚本运行时;标准库 urllib 发请求) |
| cli | curl(文档提供的等价内联发送方式) |
| cli | env(清代理的 shell 内建) |
4风险提醒 风险提醒:橙色 · 评估后使用
- 本地持有 bearer 密钥 — config.json 的 webhook_url 等同于「可往目标群发任意消息」的密钥。SKILL.md 已要求 chmod 600 与置于 skill 包外,但密钥一旦泄漏(误提交仓库、备份同步、屏幕分享)即可被任意发送。建议按密钥管理:不进版本控制、定期在企微侧轮换。
- others(外部收件人)路径依赖人工确认门被执行 — 脚本能验证并拒绝越界的 outbox 自动发送,但『others 发送前必须展示 label 与正文并等人类确认』这一步是流程要求,由宿主 agent 执行;若 agent 忽略该步,脚本本身不会阻止一次 --message 直接发往 others 目标。
- 静默清理代理环境变量可能改变本机网络行为 — clear_proxy_env() 在进程内 pop 掉六个代理变量。在只能经代理访问外网的环境里,这次发送会走直连并失败;在更复杂的环境里也可能绕过预期的出口策略。这是针对腾讯服务的刻意设计,但使用者应知道它存在。
- 消息正文无净化,外部内容可直接出站 — 若把抓取的网页文本或工具输出原样拼成通知,会把不可信内容发进团队群(含潜在钓鱼链接或误导性数字)。规范要求「每个数字必须有定义」,但这是人的纪律而非代码约束。
- 仅支持纯文本,能力边界需明确 — SKILL.md 自述 'It does not handle rich media messages (markdown cards, news, images) — only plain text.',不要指望它发富文本卡片。
5第二遍独立确认
- [ok] 每条外部依赖的调用点是否真实存在 — webhook 端点(SKILL.md Quick Start 第 2 步 + 脚本 docstring + 内联 curl 三处一致)、uv --no-project(SKILL.md 命令块,脚本以 uv run 方式被调用)、python3/urllib(脚本 import 段与 urllib.request.Request/urlopen)、curl(SKILL.md 内联等价示例)、env -u / env|grep(SKILL.md 两处)。无推测项。
- [ok] 凭证/密钥处理是否有反例(例如把 webhook 打印出去、写入日志、发往第三方) — 反例检索失败:全目录 grep webhook 的每一处均不打印其值——SKILL.md 的 set_recipient 说明写 'without exposing or rewriting the webhook value';脚本只把它用于 urllib.request.Request(webhook_url, …),出错时的报错文案(load_config 的四条 sys.exit 提示)不含 URL 本身;唯一的 webhook 值落点是 config.json 与用户自行执行的 echo(SKILL.md 明示 chmod 600)。发信目标与密钥归属同一服务。第一遍结论成立。
- [ok] 「self 可自动发、others 需人工确认」是否只是文档口号(脚本是否真的挡) — 脚本层面真实生效:recipient_scope 不在 {self, others} 即 sys.exit(1);outbox 自动路径额外要求 payload.delivery 的 recipient_scope 必须为 'self'、recipient_label 与 webhook_sha256 必须与当前 config 完全一致,任一不符即拒绝('Outbox item is not bound to this explicit self target')。不是纯口号。
- [ok] 发信器指纹绑定能否被绕过(是否只在文档里说、代码里没验) — 代码里确实验:send_wecom.py 同时比对 sender_path != running_sender 与 sha256_path(sender_path) != sender_sha256,两处不符都 sys.exit(1)。相应地,config 本身被篡改时这两个字段可被一起改写——这是该机制的边界,已在 security.injection_surface 第 ③ 点写明,不算被遗漏。
- [ok] 写盘是否可能损坏配置或放宽权限 — set_recipient.py 用 O_EXCL 建临时文件、fsync 文件、os.chmod(tmp, mode) 后 os.replace,并对父目录 fsync;mode 取自原文件(stat.S_IMODE(path.stat().st_mode) or 0o600),不会放宽。tests 里 test_setter_preserves_webhook_and_mode 对 webhook 与权限有直接断言。结论成立。
- [ok] references 与 SKILL.md 的规范条数/内容是否一致 — SKILL.md 的 Message Best Practices 8 条(标题先行/状态vs增量/数字定义/地点用词/去噪音/精确不居高临下/验证指标/下一步)在 references/message_best_practices.md 中为 12 节(多出告警 vs 状态更新、告警额外规则、常见错误模板、发送前自检),属扩充而非冲突。
- [ok] 同一 pin 下本 skill 是否曾产出过旧侦查记录 — data/analysis 目录中不存在 setup-notifications-via-wecom.json 的旧记录(本次为首次产出)。同 repo 其他 slug 有旧记录的情况见各自文件。
- [ok] pin commit 与 skill.path 是否与任务书一致 — git rev-parse HEAD = d5c4678cb5d4fd6acc9c922690df035dbd33d247;目录位于仓库根,相对路径 setup-notifications-via-wecom;本目录最后提交 878f947(2026-09-07)。GitHub API 复核 MIT / stars 1392 / pushed 2026-09-15T09:30:04Z。
6结论
75c3e6008604ce81…d5c4678cb5