Skip to content

About

企业微信推送课表Bot

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

企业微信推送课表Bot

项目名:Kebiao_Bot | 一台 792 MB 内存的 ARM 小主机,纯标准库,把课表变成每天准时出现的企业微信卡片。

不用 Docker、不用 pip、不用 Node、不装无头浏览器;常驻内存 9.2 MB,每天网络流量约 65 KB。


它做什么

时间 卡片 内容
每天 07:00 今日课表 日期、剩余门数、天气、每节课的时间 / 教室
每天 21:00 明日课表 同上,门数为明日总门数
每 5 分钟 下节课提醒 上课前 15 分钟推一次,带 N分后 HH:MM 开始
每 10 分钟 健康检查 5 项巡检,只有状态翻转才推

效果

卡片预览

上图由 tools/render_preview.py 用 Pillow 近似渲染,方便改文案后立刻检查折行和留白;真机效果以企业微信客户端为准。

架构

{ICS_URL}                ← 上游每天生成的 iCalendar(云端 CI 定时产出)
      │  每小时/每天下载一次
      ▼
/opt/kebiao/daily.py     下载 + 解析 VEVENT → 按日期筛选
      │
      ├──► card.py       今日 / 明日卡片
      ├──► next.py       课前 15 分钟提醒
      └──► weather.py    天气行(城市码或经纬度)
                │
                ▼
        /opt/notify/notify.py  → 企业微信群机器人(+ WxPusher 兜底)

关键设计:本机只做「消费端」。 抓课表需要账号密码,这部分交给云端 CI;小主机只下载公开的 ics 文件,因此机器上不存任何凭据,也解释了它为什么能安心跑在家庭网络里。

ics 里每个事件的字段:

BEGIN:VEVENT
DTSTAMP:20260101T000000        ← 生成时间(UTC),用来展示 Update:M.D
DTSTART:20260101T082500        ← 本地时间,无 TZID
DTEND:20260101T100000
SUMMARY:课程名
LOCATION:教室
DESCRIPTION:教师: xxx\n周次: 第1周\n节次: 第1-2节
END:VEVENT

目录结构

Kebiao_Bot/
├── .gitattributes    强制 LF(bash / systemd unit 吃不了 CRLF)
├── .gitignore
├── kebiao/
│   ├── settings.py    配置聚合(环境变量 / /etc/*.env / 默认值)
│   ├── daily.py       ics 下载 + 解析(被 card/next 复用)
│   ├── card.py        三种卡片生成 + 发送 + ASCII 预览
│   ├── next.py        课前提醒的挑选与去重
│   └── weather.py     天气行(qweather / open-meteo / sojson)
├── notify/
│   ├── notify.py      多通道发送(企业微信 + WxPusher)
│   ├── notify.sh      给 shell 用的包装
│   └── healthcheck.sh 5 项健康检查
├── deploy/
│   ├── *.service *.timer   systemd 单元(4 个定时任务)
│   ├── install.sh          一键部署(幂等)
│   ├── uninstall.sh        卸载(保留密钥文件)
│   ├── privacy-check.sh    发布前自检
│   ├── kebiao.env.example  非密钥配置模板
│   └── notify.env.example  密钥模板
├── samples/sample.ics 虚构课表(自检用)
├── tests/selftest.py  22 项不联网自检
├── tools/render_preview.py 画卡片预览图
└── docs/card-preview.png

生产布局:

/opt/kebiao/   settings.py daily.py card.py next.py weather.py
/opt/notify/   notify.py notify.sh healthcheck.sh
/etc/kebiao.env   非密钥配置(600)
/etc/notify.env   密钥(600)
/run/kebiao-cache.ics        课表缓存(tmpfs)
/run/kebiao-weather.json     天气缓存(tmpfs)
/run/kebiao-next.state       提醒去重(tmpfs)
/run/kebiao-card.state       卡片去重(tmpfs)
/run/notify-health.state     健康检查状态(tmpfs)

快速开始

# 1) 传到小主机(假设已配好 ssh)
scp -r Kebiao_Bot root@HOST:/root/

# 2) 部署(幂等,可反复跑)
ssh root@HOST 'bash /root/Kebiao_Bot/deploy/install.sh'

# 3) 填两个 env(脚本首次运行时会生成模板)
ssh root@HOST 'vi /etc/kebiao.env'    # KEBIAO_ICS_URL / KEBIAO_CARD_LINK
ssh root@HOST 'vi /etc/notify.env'    # WECOM_KEY

# 4) 干跑,确认排版后再等定时器
ssh root@HOST 'python3 /opt/kebiao/card.py --dry --tomorrow'
ssh root@HOST 'systemctl list-timers "kebiao-*" "notify-*" --no-pager'

手动触发一次真推送:python3 /opt/kebiao/card.py --force。

配置项

键 位置 说明
KEBIAO_ICS_URL kebiao.env 上游 ics 地址,占位符 {ICS_URL}
KEBIAO_CARD_LINK kebiao.env 整卡可点的落地页,必填,缺失会报 errcode 42045
KEBIAO_CITY_CODE kebiao.env 天气城市码,占位符 {CITY_CODE}
KEBIAO_LAT / KEBIAO_LON kebiao.env open-meteo 用的经纬度(免密钥)
KEBIAO_WEATHER_PROVIDER kebiao.env qweather,open-meteo,sojson 的顺序,留空自动判断
QWEATHER_HOST / QWEATHER_KEY kebiao.env 和风天气专属 API Host,占位符 {API_HOST}
KEBIAO_NEXT_MINUTES kebiao.env 提前提醒分钟数,默认 15
KEBIAO_WRAP_WIDTH kebiao.env value 列折行宽度,默认 14(客户端约 13–14 字可见)
HEALTH_URL 等 kebiao.env 健康检查:HEALTH_DISK_MAX / HEALTH_MEM_MIN / HEALTH_REPEAT_AFTER
CHANNEL notify.env wecom、wecom,wxpusher、stdout
WECOM_KEY notify.env 群机器人 Webhook 的 key,占位符 {WECOM_KEY}
WXP_SPT notify.env WxPusher appToken,占位符 {WXP_SPT}
KEBIAO_TIMER systemd 1 表示由 timer 触发,启用时间窗守卫

三张卡片

全部使用 template_card 的 text_notice 子类型。

字段 今日课表 明日课表 下节课提醒
source.desc NailongBot Update:{M.D} 同左 NailongBot
main_title.title 今日课表 明日课表 下节课提醒
main_title.desc 11月5日 周四 同左 15分后 08:25 开始
emphasis_content 剩余门数 / 门剩余 总门数 / 门课 不发送
sub_title_text 天气行 天气行 不发送
horizontal_content_list 每门课一条 每门课一条 只有一条
card_action 落地页(必填) 同左 同左

Update: 的日期取自 ics 里最大的 DTSTAMP,按东八区换算,只到日。

换行规则

卡片 value 列宽度有限,超过约 13–14 个汉字就会被客户端裁切;课名和教室必须完整显示,不缩写、不截断,所以由服务端主动折行:

  1. 优先断在 ——
  2. 其次断在 、():·,; 等标点(左括号留在下一行,避免孤儿行)
  3. 最后才硬切
def wrap_cn(text, width=18):
    if len(text) <= width:
        return text
    pos = text.find(u'——')
    if 2 < pos + 2 <= width:
        return text[:pos + 2] + '\n' + wrap_cn(text[pos + 2:], width)
    # 否则在标点处断开,最差情况硬切

wrap_cn 的默认宽度 18 是上限;卡片实际渲染用 KEBIAO_WRAP_WIDTH(默认 14),因为客户端 value 列比 18 窄。想验证效果直接 --dry,会打印带框线的 ASCII 预览。

定时任务

单元 触发 说明
kebiao-daily.timer OnCalendar=*-*-* 07:00:00,Persistent=true 今日课表
kebiao-tomorrow.timer OnCalendar=*-*-* 21:00:00,Persistent=true 明日课表
kebiao-next.timer OnBootSec=3min + OnUnitActiveSec=5min 课前提醒
notify-health.timer OnBootSec=2min + OnUnitActiveSec=10min 健康检查

约束:常驻服务绑 0.0.0.0、用 systemd 管理、systemctl start 一律加 --no-block、状态文件放 /run(tmpfs)减少 eMMC 写入。

踩坑清单

坑 现象 解法
Python 3.5 无 f-string SyntaxError: invalid syntax 全部改 % 或 .format()
date.fromisoformat AttributeError 改 datetime.strptime(x, '%Y-%m-%d').date()
天气接口 HTTPS 握手失败 SSLV3_ALERT_HANDSHAKE_FAILURE 改用 http://,并缓存到 /run
和风天气 403 Invalid Host 新版控制台要专属 API Host,从项目页复制
卡片缺 card_action errcode 42045 必填,哪怕不可见
卡片带 jump_list 底部多出「查看完整课表」入口 不需要就删掉整个字段
教室名被裁切 只显示一半 按 14–18 字主动折行,标点优先断
Persistent=true + 无 RTC 断电重启后凌晨补推,日期算成 2016 年 年份守卫 + 时间窗守卫
PowerShell 改 UTF-8 脚本 中文变乱码 用文件写入工具,别用 Set-Content 覆盖
内核 3.10 没有 MemAvailable 健康检查读不到可用内存 回落 MemFree + Buffers + Cached

两道守卫:

# 守卫一:时钟未校准则等待(本机无 RTC 电池)
while dt.date.today().year < 2025 and waited < 90:
    time.sleep(10)

# 守卫二:由 systemd 触发时只在时间窗内推送
if os.environ.get('KEBIAO_TIMER'):
    ok = (19 <= hour < 24) if tomorrow else (5 <= hour < 10)

卡片发送还有第三道守卫:/run/kebiao-card.state 记录「哪天推过哪张卡」,Persistent=true 补跑时直接跳过,--force 可重推。

隐私

本仓库只有占位符:{ICS_URL}、{CARD_LINK}、{CITY_CODE}、{API_HOST}、{WECOM_KEY}、{WXP_SPT}、{JW_HOST}。示例课表内容全部虚构,密钥只存在于本机 /etc/notify.env(600)。

发布前跑一次自检:

bash deploy/privacy-check.sh
git status          # 确认没有 secret.txt / 含真实数据的 *.json

脚本把敏感关键词拆开拼接,避免检查脚本自己命中自己;命中任何一条就返回非零。

另外三条硬规矩:密钥不进 git、不进日志(notify.py 的错误信息里只有 errcode,没有 URL)、不进截图。

资源占用

项 数值
常驻内存 9.2 MB(Python 单进程,跑完即退)
峰值(渲染时) ~30 MB
磁盘 ~30 KB 脚本
eMMC 写入 仅在状态变化时,几十字节级(状态都在 /run)
每日网络 约 60 KB(ics)+ 5 KB(天气)

开发与自检

python3 tests/selftest.py         # 22 项,不联网、不发送
python3 kebiao/card.py --dry --tomorrow --now 2026-11-04T21:00 --no-weather
python3 tools/render_preview.py   # 重新生成 docs/card-preview.png

兼容性:代码按 Python 3.5 写(无 f-string、无变量注解、无 date.fromisoformat),开发机上可以跑更高版本。改完代码建议这样校验语法:

python3 -c "import ast; ast.parse(open('kebiao/card.py').read(), feature_version=(3, 5))"

许可

MIT,见 LICENSE。

About

企业微信推送课表Bot

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages