Skip to content

Repository files navigation

Life @ USTC Bot

在 QQ 里使用 Life@USTC 的聊天入口。连到 Life@USTC server,能力命名与 Web / CLI / MCP 对齐(见 interface hierarchy)。

面向谁

  • 想在私聊管理个人工作区,或在群聊查询公开校园信息的科大用户
  • 需要 NapCat / QQ 官方 Bot 接入校园工作区的部署者

用户能做什么

直接发送中文命令或别名,无需命令前缀:

域 示例
校园信息 学期 · 课程 · 教学班 · 老师 · 校车 · 天气 · 教室 · 第二课堂
工作区 日程 · 课表 · 下一节课 · 待办 · 作业 · 考试 · 订阅
社区 反馈 …(可路由给管理员)
账户 登录 / 登录状态 · 退出 · 账户 · 通知
使用帮助 帮助(含 帮助 课表 等专题)

别名如 td / hw / xc / kb / ddl 只是捷径。日程 是课表+作业+考试+待办的聚合;课表 只看上课安排,课表 2026秋 或 课表 26春 可生成带教学周范围的整学期课表。

校车可按路线和日期查询,例如 校车 周六 太湖路园区 东区、校车 周日 东区 太湖路园区、校车 工作日 东区 西区 或 校车 2026-09-06 东区 太湖路园区。周一-周五、周中、工作日 使用工作日时刻;周六和周日时刻不同,需分别查询。

第二课堂查询无需登录:第二课堂 查看活动列表,第二课堂 搜索 讲座 搜索名称,第二课堂 查看 <youngId> 查看报名时间、地点等详情;报名仍在校方第二课堂网站完成。明确的第二课堂命令也可在群聊使用。

课程、课堂、教学班和班级查询支持课程名、课程编号、教学班编号及 JW ID。例如 课堂 数学分析 课表、教学班 001548.06 考试、课程 查看 001548。私聊优先匹配本人当前学期的订阅;出现多个候选时列出教师、校区和 JW ID,由用户明确选择。群聊只搜索公开目录。纯数字默认表示 JW ID,纯数字课程编号可写为 code:1548。可用 学期 2026秋 指定学期;教学班课表默认查询本周,也可追加起止日期,如 课堂 数学分析 课表 2026-09-13 2026-09-19。

私聊发送 订阅 身份 <JW ID> <普通|助教|旁听> 可修改已有订阅的身份,例如 订阅 身份 12345 助教。教学班 JW ID 显示在订阅列表中;修改身份不会创建订阅,也不授予校方教学权限。

登录:设备码 OAuth;登录卡会同时显示完整授权网址和验证码,浏览器确认后自动恢复登录前未完成的请求。Token 自动刷新,失败则清凭证并提示重登。

通知(仅私聊):课表、作业、第二课堂和待办提醒默认开启,登录后开始推送;通知 开 / 通知 关 可切换全部提醒,也可分别开关。课前约 30 分钟、未完成作业/待办截止前 24 小时内推送图卡,第二课堂按订阅事件推送。无截止时间、已完成和已过期的待办不提醒。群聊不推个人通知。

提醒每 5 分钟轮询一次,各来源独立退避重试;去重记录持久化,重启不会重复发送,作业/待办更改截止时间后可再次提醒。凭据失效时保留开关并暂停,发送 登录 成功后恢复;通知 可查看暂停状态。

输出图卡:命令结果、帮助、错误、空结果、登录提示、确认与通知等宿主生成的内容统一发送 PNG 图卡,不发送文字 fallback。通知、反馈和登录等旁路输出先把图卡意图写入 durable outbox,再由投递边界渲染;渲染或平台暂时失败只重试投递,不重做业务操作。真实 LLM 回复可按模型来源保留文字,并可与宿主图卡组合。

AI 助手(可选):接 OpenAI 兼容模型 + server MCP。模型可以直接回答,也可以按完整命令手册调用 run_bot_command,或检索并使用 MCP;没有必须调用工具或先搜索再执行的要求。私聊支持服务端公布的全部 MCP 工具及资源、提示词,公开 MCP 查询无需登录,个人数据使用本人 OAuth 权限。普通写入直接执行,危险操作需要用户确认;结果不明的写入不会自动重试。命令和 MCP 统一返回结构化 JSON 给模型,宿主执行结果使用图卡,模型实际生成的 prose 可保留文字;两种输出来自同一次业务执行。群聊只开放公开 Bot 命令,个人能力与 MCP 在私聊使用。架构和上下文边界见 ARCHITECTURE.md。

私聊可发送转发消息、图片/表情包及文件供 AI 阅读。转发内容保留说话人与时间,文件使用现有 Kimi 配置提取,解析结果随对话保存;可读取平台提供的语音转写,暂不分析原始音视频,GIF 不能保证读取完整动画。正文、图片和执行回执尽量合并发送,危险操作仍单独排队确认。 对课表、校车、天气、教室位置等适合图示的内容,AI 优先使用已有图片命令并配简短说明;普通问答或用户要求纯文字时直接使用文字。模型调用 run_bot_command 后获得格式化 JSON:result 保留业务数据,images 返回可选图片的 ID;图片不会自动发送。模型在最终回复中用 ![](图片ID) 选择图片及其位置,可与文字组合。这个语法只引用已保存的图片,不执行命令或下载任意 URL。图片 ID 仅能在同一用户、同一会话中复用;旧图不代表最新数据。直接发送命令仍由 Bot 投递结果,并将图片 ID 写入模型上下文。校车查询成功时只发送图卡,不生成重复文字,也没有文字 fallback。

AI 对话保留消息时间和说话人元数据,但普通回复不会自动附加时间前缀;日期、时区和当前时间只在问题相关时使用。

群聊:校车、校车 西区 高新区、课程 数学分析 等明确公开查询无需 @;普通聊天中仅仅出现“校车”等关键词不会触发,裸 ? 或未知 /xxx 也不会触发。课表、成绩、待办、订阅链接、账户和设置等个人能力始终只在私聊执行。回复 Presto 的公开查询时,可用“周日呢”这类短追问继承上一条路线。QQ 官方 Bot 通常只收到 @ 消息;NapCat 可以应用完整的无 @ 匹配规则。

接入方式

  • NapCat:OneBot 11 反向 WebSocket(或出站 WS);自动接受所有好友申请和邀请机器人入群的请求。其他用户申请加入已有群聊仍由群管理员处理。
  • QQ 官方 Bot:Webhook(推荐)和/或 Gateway

公开只读命令可按 TTL 缓存在本地 SQLite,部署版本参与缓存键,避免旧版本脏读。

给贡献者

环境变量、Compose 端口与反代路径见源码旁配置与 compose.yaml;开发检查用 go test ./...。 进程在 BOT_HEALTH_ADDR(默认 127.0.0.1:2282)提供 /live,只检查进程初始化和本地 SQLite,外部消息渠道断线不会触发容器重启。 编码约定以本仓库与 server 契约为准,不在此重复运维手册。

macOS 原生部署

生产 Bot 与渲染服务运行在 tkm-mac-mini,由系统级 launchd 自动启动。更新入口是 scripts/deploy-mac.sh。脚本默认通过 tiankaima@tkm-mac-mini 更新 /Users/tiankaima/Services/life-ustc-bot,只接受干净的已提交 版本:它用该提交创建临时源码归档,在本地临时副本中生成 Go vendor 目录,把归档传到远端, 再在 Darwin arm64 上使用 CGO 编译 Bot 和 Rust renderd。远端工具查找顺序是 ROOT/toolchain/go/bin(与 go.mod 对齐的 Go)、ROOT/toolchain/bin、/opt/homebrew/bin、/usr/local/bin 和 /usr/bin;Rust 构建使用 锁定的 Cargo.lock 和离线缓存,因此更新时需要预先准备好 Go vendor 所需的本地模块缓存以及 远端 Cargo registry 缓存。

远端的 config.json 是私有的 JSON 对象,键是 Bot 环境变量名。脚本只在远端用 /opt/homebrew/bin/python3 读取它并写入 Bot 的 launchd plist,不会 shell-source 或打印值。 BOT_DB_PATH、BOT_RENDER_ENDPOINT、BOT_BUILD_VERSION 以及本地健康地址由部署固定。部署使用 runtime-fonts/ 下的扁平字体目录(思源黑体 Regular/Bold TTC 和 Fira Code TTF),不要把凭据或 完整字体树放入源码归档。

部署前远端应已有 bin/、data/、logs/、build/、runtime-fonts/、config.json 和现有 SQLite 数据库;初次生产迁移由迁移工作另行完成。脚本会在 build/<deployment-id>/ 保留源码、 构建产物、旧二进制、数据库备份和日志。它先停 Bot,再备份并迁移 SQLite,随后原子替换二进制和 /Library/LaunchDaemons/dev.life-ustc.{bot,renderd}.plist;plist 为 root 所有、权限 600,两个 服务均设置 UserName 为部署用户(默认 tiankaima)、RunAtLoad、KeepAlive、工作目录和标准输出/错误日志。它 先启动并检查 dev.life-ustc.renderd,再启动并检查 Bot;失败时恢复二进制、数据库和 plist,并 重新加载部署前已加载的服务。部署锁和 launchd label 可避免同一 Bot 出现重复实例。

历史压缩使用独立的 conversation_compactions 表保存摘要和已覆盖的事件位置, 原始 conversation_events 不删除。部署脚本在停机备份后运行 migrate,将现有私聊记录中的显示名称回填到 users;此后私聊消息会更新用户的最近显示名称和观察时间,群名片不会覆盖全局名称。随后 校验完整数据库;正常启动只校验已有数据库,不隐式补表。普通消息复用固定摘要, 仅在上下文接近容量时再次压缩;摘要请求也计入 AI 用量。正常 LLM 任务没有整轮耗时、 工具次数或累计用量上限,运行期间通过心跳续租,摘要完成后继续处理原请求。

NapCat 与 Bot 同机运行,OneBot 反向 WebSocket 连接 ws://127.0.0.1:2280/ws, NAPCAT_REVERSE_ADDR 使用 127.0.0.1:2280。图片在 Mac 下载或渲染后,通过 OneBot Base64 或官方 QQ 分片上传发送,不再启动公共图片 HTTP 服务。

Bot 的 HTTPS_PROXY、HTTP_PROXY 和 NO_PROXY 放在远端 config.json 中;生产出口应使用 Mac 本地代理。当前 http://127.0.0.1:17890 是 Clash Verge 的专用 mixed listener, 实际出站由 Clash 的 merge profile 和当前规则决定,不能根据监听端口认定为 DIRECT; 排查连接故障时应核对目标域名对应的代理日志、策略组和上游节点。模型调用中的 EOF 可能来自代理连接超时,不能据此认定服务端 RLS 或 MCP 失败。该监听不依赖 cn 的 SSH 隧道。 切换出口前,必须在 QQ 开放平台的接口 IP 白名单中加入 Mac 的公网出口 IP,否则 QQ API 返回 11298。 官方 QQ 的公网入站反代通过 Tailscale 转发到 Mac 的 Webhook 监听地址; 是否启动 Gateway 和 Webhook,分别由 BOT_ENABLE_QQ_BOT_GATEWAY、BOT_ENABLE_QQ_BOT_WEBHOOK 控制。

./scripts/deploy-mac.sh

只检查当前提交和目标信息而不执行 SSH、构建或远端改动时,设置 DEPLOY_DRY_RUN=1。若使用不同 主机或路径,可覆盖 REMOTE_HOST、REMOTE_USER、REMOTE_ROOT;健康检查等待时间可用 DEPLOY_HEALTH_TIMEOUT(1–3600 秒)覆盖。

图卡由 Typst renderd 服务渲染,延续迁移前的简洁排版:近白画布、细横线、 18pt 标题、13pt 正文、9pt 页脚,数字与代码使用 Fira Code,中文及课程名称使用思源黑体 / Noto CJK。 校车查询只筛选线路,选中的线路保留全部站点和时刻表,并突出查询站点;未公布的时刻显示「—」。 校车表格每列宽 300pt,多条线路按实际渲染高度紧凑分配到两列,两列独立排列,避免短表旁的大块空白。文字图卡正文宽 480pt;表格行高及段落换行交由 Typst 自动处理,不插入额外断行字符。 下一班信息放在标题右侧;待办表格填满正文宽度,帮助菜单的命令列与说明列按 1:2 分配宽度。 课表保留完整节次和日期列,今天使用淡青色,课程使用原有浅色;长课程名自然换行。 天气画布宽 630pt,两个地点左右并列,湿度与风速在各列内纵向排列,保留温度曲线、降水概率和每日温度范围条。高度随内容变化,默认以 3× 输出 PNG。

启动 renderd 后可重新生成旧版原图和 Typst 对照页(旧版代码仅在临时目录内执行):

./scripts/render-reference.sh
go run ./cmd/render-examples -endpoint http://127.0.0.1:9123/render -out examples

打开 examples/index.html 查看七组固定数据、固定时间的对照图;点击图片查看完整 PNG。 仅生成 Typst 图卡时可省略第一步。该命令及 CI 校验图片尺寸范围,渲染或文件写入失败时返回非零退出码。

make build 会先校验 api/openapi.provenance,再从仓库内固定的 api/openapi.json 重新生成客户端,因此构建不依赖网络且可复现。更新契约时需提供 server 的完整提交 SHA,例如:

make sync-openapi generate \
  OPENAPI_SOURCE=../server/public/openapi.generated.json \
  OPENAPI_SERVER_SHA=<40-character-server-commit>

教室地图查询

在 Bot 中直接发送教室编号(例如 5201、3A204、GT-B110),即可自动收到对应的位置图片;群聊中无需 @ Bot。支持小写、全角编号,也可以发送 教室 3A204 或 3A204 在哪里?。 有单间标注时会返回高亮楼层图;只有楼层概览时返回概览图。查询成功时只发送图片,位置、楼层和地图状态保留在给 LLM 的 JSON 中。 群聊允许这类公开查询,课表和日程不会自动附加地图。

私聊中的自然语言查询也可以通过 MCP 的 catalog_rooms_map 工具完成。工具先返回 同一份教室地图数据,Bot 只投递地图图片,不附加位置文字或地图链接。没有可用地图时报告查询失败;图片投递失败按原任务重试,不退回文字。

本地完整 E2E

仓库旁存在 ../server checkout 时,可以启动隔离的 PostgreSQL、真实本地 Life@USTC Worker、Bot 进程和 NapCat 协议测试端,完整验证设备登录、原请求自动恢复、 iCalendar 私有链接投递及 .ics feed:

make dev-e2e

脚本使用独立的 Compose project、数据库和临时目录,不读取 Bot 的 .env,也不会连接 QQ 或生产服务。其他目录或端口可通过 LIFE_USTC_SERVER_DIR、 DEV_E2E_SERVER_PORT、DEV_E2E_POSTGRES_PORT 和 DEV_E2E_INSPECTOR_PORT 覆盖。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages