一个基于 napcat-sdk 的 QQ 私聊客服催回复机器人。
它的目标很简单:客户发来私聊消息后,如果客服迟迟没有回复,机器人就把消息整理后推送到内部群里提醒;如果超时更久,再按里程碑继续催办;客服处理完成后,再自动结束监控并存档会话。
当前仓库已经实现了完整的消息监听、好友申请自动处理、群内快捷指令、夜间免打扰、状态持久化与会话归档。本文档基于当前代码现状整理,尽量保留原有使用视角。
- QQ 私聊作为客服入口
- 需要把未回复客户集中提醒到内部群
- 希望减少人工盯聊天窗口的成本
- 希望通过群内引用命令、贴表情、戳一戳来快速处理会话
- 希望保留客服响应耗时和会话记录,方便追踪
- 自动通过好友申请,并在内部群发送通过通知
- 好友通过后延迟发送欢迎消息,支持纯文本或结构化消息段
- 监听非白名单用户的私聊消息
- 对新消息做 1 分钟防抖,避免客户连续补充消息时频繁提醒
- 将待处理客户最近消息整理为合并转发并发送到内部群
- 提醒时可随机
@当前排班内可用成员;若当前无人值班,则直接发送提醒 - 按里程碑进行超时催办
- 支持夜间免打扰:夜间延后提醒,次日指定时间汇总发送
- 支持在通知群引用机器人消息后执行
.say/.more/.bye/.close/.list/.help - 支持通过配置的表情映射触发
say/more/bye/close/cancel/recall - 支持限时撤回:
.say发送成功后,点击通报消息上的撤回表情即可撤回刚发送给客户的私聊消息 - 支持 AI 回复建议:提醒合并转发的第一层附上根据客户最近对话生成的建议答复(OpenAI 兼容接口,可选图片输入,可在线开关)
- 支持
.say两段式回复:先输入.say,再发送下一条群消息作为要转发给客户的内容 - 支持客服直接私聊回复客户后自动结束会话
- 支持内部群戳一戳查看运行状态面板
- 支持状态持久化:程序重启后自动恢复待回复客户、监听消息和夜间延后通知
- 支持会话归档:对话周期内事件实时写入本地 SQLite 会话库,会话关闭后再按时间段拉取历史对账补漏,双保险保证记录完整
- 支持统计已完结会话的最短、最长、平均和中位回复耗时
- 支持临时静音开关:群内
.mute(无参数不限时)/.unmute,或本地 Shell 控制台开启;静音期间提醒暂存,解除后汇总发出;shellunmute与群内.unmute行为一致 - 支持本地终端 Shell 控制台:与 bot 同进程、同生命周期绑定;命令回执仅本地;日志插入时自动重绘 prompt,避免打断输入
- 终端支持会话处理:
say/bye/close(可用客户 QQ 或all=当前待回复队列);终端不提供more - 群内会话命令在引用消息之外,也支持用客户 QQ 或
all直接操作:.say <QQ|all> …/.bye <QQ|all>/.close <QQ|all>/.more <QQ|all>(带 Reply 时仍以引用为准;all=待回复队列)
- 客户发送私聊消息,机器人将其加入待回复队列。
- 如果客户在 1 分钟内继续发消息,计时会重置,并合并到同一轮待处理会话。
- 超过 1 分钟仍未回复时,机器人向内部群发送“新客户提醒”。
- 机器人会根据
milestones配置继续发送“超时催办”。 - 客服可以直接私聊回复客户,也可以在内部群引用消息执行命令,或使用配置好的表情快捷处理。
- 会话结束后,客户会从待回复队列中移除,回复耗时会计入统计;会话周期随即关闭,并按周期时间段拉取历史消息对账补漏。
- 若开启夜间模式,夜间时段的新客户提醒和里程碑提醒会暂存,并在次日指定时间汇总发送。
带引用时,除 .list 和 .help 外,其余会话命令都应当 引用(回复) 一条由机器人发送、且仍在监听窗口内的消息。通常引用的是合并转发消息,也可以是机器人发出的操作反馈消息。
也可不引用:对 .say / .bye / .close / .more 使用 .命令 <客户QQ|all> … 直接指定客户;带 Reply 时始终以引用解析目标。
| 指令 | 用途 | 示例 | 是否需要引用 |
|---|---|---|---|
.say <内容> |
向客户发送私聊消息 | .say 您好,请问有什么可以帮您? |
是 |
.say |
进入“等待发送内容”状态,触发者的下一条群消息会被转发给客户 | .say |
是 |
.say <QQ|all> <内容> |
不引用消息时,按客户 QQ 或 all(待回复队列)私聊发送 |
.say 12345678 您好 / .say all 请稍候 |
否(用 QQ/all) |
.say <QQ> |
不引用时进入等待输入,下一条群消息发给该 QQ(all 不支持等待输入) |
.say 12345678 |
否(用 QQ) |
.more |
获取客户最近 100 条历史消息,并以合并转发形式返回 | .more |
是 |
.more <QQ> |
不引用时,按客户 QQ 拉取历史合并转发;不支持 .more all;无参且队列仅 1 人时自动匹配 |
.more 12345678 |
否(用 QQ) |
.bye |
向客户发送结束语并关闭会话 | .bye |
是/否(无参且队列仅 1 人时自动匹配) |
.bye <QQ|all> |
不引用时,按客户 QQ 或队列全部发送结束语并关闭 | .bye all |
否(用 QQ/all) |
.close |
关闭会话,但不发送结束语 | .close |
是/否(无参自动匹配同上) |
.close <QQ|all> |
不引用时,按客户 QQ 或队列全部关闭会话 | .close all |
否(用 QQ/all) |
.list |
列出当前待回复客户及等待时长 | .list |
否 |
.status |
发送运行状态面板(与戳一戳机器人相同) | .status |
否 |
.mute [分钟] |
临时静音(不带参数不限时;.mute 30 为 30 分钟) |
.mute |
否 |
.unmute |
解除静音并汇总发出延后提醒(与终端 unmute 一致) |
.unmute |
否 |
.reload |
重载当前明文配置(兼容 .reload cfg) |
.reload |
否 |
.help |
显示帮助信息 | .help |
否 |
补充说明:
- 目标解析优先级: 带
Reply时始终走引用路径。无引用时:显式 QQ →all(.more不允许 all)→ 无参且待回复队列恰好 1 人则自动匹配该客户(人多/为空则提示指定 QQ)。 all= 当前待回复队列(不是全部好友);.say all必须附带内容。- 表情快捷操作: 多客户合并转发上,
say/bye/close作用域扩展为该消息内全部客户;more仅单客户,多客户时提示手动处理。 - 注意: 表情
more只支持单客户;多客户合并转发请用群命令.more <QQ>。 - 命令识别按前缀匹配,不是完整 token 精确匹配。 实现上运维类命令用
startswith判断(见src/group_msg.py的OPS_COMMAND_PREFIXES),因此.statusfoo会进.status分支并发送状态面板,.unmute-now会执行解除静音;.mute后跟非法参数(如.muted)会按参数解析失败提示,而不是忽略整条消息。请严格使用上表中的命令写法,只附带明确支持的参数(如.mute 30、.reload cfg),不要在命令后拼接后缀或自定义后缀。若后续改为分词精确匹配,以代码为准。 .say转发时会去掉群消息中的Reply段,其余消息段会原样发送给客户,因此文本、图片等消息段都可用。.say进入等待状态后,机器人会给提示消息贴上cancel表情;贴该表情即可取消本次发送。.say发送成功后,机器人会在通报消息上贴上recall撤回表情;在recall_window_seconds(默认 60 秒)内点击该表情即可撤回刚发送给客户的私聊消息,超时后点击会提示无法撤回。.list最多展示前 20 条待回复客户记录。- 如果引用的是已过期或无法识别的机器人消息,机器人会返回“操作已过期”或“无法识别的消息”。
机器人会自动给可操作的消息贴上预设表情,群成员可以直接点表情触发动作。表情与命令的对应关系由 emoji_mapping 控制。
默认示例:
| 动作 | 默认 QQ 表情 ID | 说明 |
|---|---|---|
say |
123 |
进入发送消息流程 |
more |
289 |
拉取最近历史消息 |
bye |
124 |
发送结束语并关闭会话 |
close |
75 |
直接关闭会话 |
cancel |
96 |
取消等待中的 .say |
recall |
89 |
限时撤回 .say 刚发送给客户的私聊消息 |
- 注意: 表情快捷操作中
more只支持 单客户 消息;say/bye/close在多客户合并转发上作用域为该消息内全部客户。 recall表情只在.say的成功通报消息上出现;在recall_window_seconds时间窗口内点击才会生效,撤回成功后机器人会在群内提示。
- 客户发消息后自动进入队列(白名单用户除外)。
- 若客户刚通过好友申请,在
processed_friend_requests_expire窗口期内发来的消息会被忽略,避免欢迎阶段误入队列。 - 客服直接私聊回复客户,会话自动结束。
- 客户在私聊窗口戳一戳机器人时,如果该客户当前在待回复队列中,机器人会发送结束语并结束会话。
在配置文件中可以设定夜间时段(例如 23:30 到 07:00)和次日汇总时间。夜间时段内:
- 新客户提醒不会立刻发送,而是暂存到内存中的延后通知队列
- 里程碑超时提醒也不会立刻发送,而是一起延后
- 到达
summary_time附近的 10 分钟窗口内,机器人会把夜间积累的客户汇总后统一发送 - 汇总消息仍会带合并转发内容,方便客服次日直接处理
注意:
- 夜间汇总按客户聚合,不区分“新客户提醒”还是“第几个里程碑提醒”。
- 夜间延后通知会写入状态文件,因此程序重启后仍可恢复。
用于会议、交接班等不想被提醒打扰、但又不想改夜间配置的场景。
- 开启(不限时,默认):内部群
.mute或终端mute;直到手动解除 - 开启(限时):
.mute 30/mute 30(单位分钟) - 解除并汇总:群内
.unmute或终端unmute(两者行为一致:解除静音 + 向通知群汇总发出延后提醒) - 命令回执:群命令回在群里;终端命令只打本地 stdout,不向群发送命令 ACK
- 限时静音到期:巡检任务自动解除,并汇总发出延后提醒
- 静音期间:新客户提醒与里程碑催办不会立刻推送到内部群,而是与夜间模式共用延后队列
- 解除或到期:若当前不在夜间时段,则将延后通知按客户聚合后汇总发出;若仍在夜间,则继续留给次日夜间汇总
- 状态:戳一戳机器人或
.status/ 控制台status可查看当前是否静音及剩余时长 - 持久化:
mute_until写入state_file(0未静音 /-1不限时 />0截止时间戳),重启后仍有效
bot 启动后会在同一进程读取终端 stdin。控制台与 bot 运行绑定:随 bot 启动,随 bot 优雅关停结束;控制台内 quit / exit / stop 会请求停止整个 bot(与 Ctrl+C 相同路径)。在 systemd 等无 stdin 环境下控制台会自动禁用,不影响 bot 主流程。
约束:终端命令的回执只写本地 stdout,不向任何 QQ 群发送命令 ACK。
unmute 除外——它与群内 .unmute 一致,会汇总把延后客户提醒发到通知群(业务消息,不是命令回执)。
日志与输入协调:运行中日志插入终端时,会清行打印日志并重绘 notifybot> 与已键入内容,避免命令被日志“打断”。
| 命令 | 用途 |
|---|---|
help |
显示帮助 |
status |
本地查看运行状态(含静音状态) |
list |
列出待回复客户 |
mute |
不限时静音(直到 unmute) |
mute <分钟> |
限时静音 |
unmute |
解除静音并汇总延后提醒(与群内 .unmute 一致) |
reload |
重载明文 config.yaml |
say <qq|all> <文本> |
私聊发送纯文本;all=当前待回复队列;成功后关待回复会话 |
bye <qq|all> |
发送结束语并关闭会话(all=队列全部) |
close <qq|all> |
关闭会话、不发结束语(all=队列全部) |
more |
终端不支持;请在通知群使用 .more(引用消息、.more <QQ>;无参且队列仅 1 人可自动匹配;无 more all) |
quit / exit / stop |
优雅停止 bot(保存状态并退出) |
终端说明:
- 命令 ACK 只写本地,不向 QQ 群发送命令回执;
unmute的汇总提醒、以及群内.more <QQ>的合并转发属于业务消息,不在「命令 ACK」范围内。 - 终端没有
more;历史合并转发只通过群内.more/.more <QQ>完成。 all仅指当前待回复队列,不是全部好友;群内say/bye/close支持 all,.more不支持 all;无参且队列仅 1 人时自动匹配。say/bye在客户端未运行时会本地拒绝;close不要求客户端在线。
停止 bot 也可使用终端 Ctrl+C(或向进程发送 SIGTERM)。触发后会执行优雅关停:
- 停止接收新事件,取消后台任务(巡检 / 控制台 / 通知 ack 冲刷)
- 关闭 NapCat WebSocket 连接
- 最后一次将运行状态写入
state_file - 进程退出
若信号处理器未能生效(极端情况),入口会兜底再保存一次状态。Shell 控制台的 quit / exit / stop 与 Ctrl+C 走同一优雅关停路径,不会只退出控制台而让 bot 继续跑。
项目运行时读取根目录下的明文 config.yaml。
由于仓库是公开的,真实配置不得直接提交:
- 仓库内提交:
config.example.yaml(结构模板)、config.yaml.enc(SOPS + age 密文)、.sops.yaml - 本机私有:明文
config.yaml(已 gitignore)、age 私钥*.agekey - 远端主机:解密后的明文
config.yaml,以及/etc/notifybot/age.key(权限 600)
# WebSocket 连接
ws_url: "ws://127.0.0.1:3001"
ws_token: "your_ws_token"
# 内部通知群号
internal_group_id: 123456789
# 白名单(这些 QQ 不会进入待回复队列)
whitelist:
- 100000
- 100001
# 超时里程碑(分钟)
milestones:
- 15
- 30
- 60
- 120
- 180
- 360
- 720
- 1440
- 2880
- 4320
# 最多监听多少条机器人发出的可操作消息
monitored_forward_limit: 15
# 会话结束语
closing_message: |-
本次会话暂时结束。感谢您的支持与信任,再见。(请勿回复)
# 新好友通过后发送的欢迎消息
welcome_message: |-
[自动回复]您好,感谢您添加好友。
如有问题可直接留言,我们会在工作时间尽快回复。
# 群内命令防抖(秒)
debounce_seconds:
say: 5
more: 5
bye: 5
close: 5
list: 5
# 好友申请去重窗口(秒)
processed_friend_requests_expire: 5
# 通过好友后,延迟多久再发送欢迎消息
friend_welcome_delay: 5
# 欢迎消息失败重试
friend_welcome_retries: 3
friend_welcome_retry_interval: 3
# 好友数阈值,达到后在群通知中附带警告
friend_count_limit: 3000
# 回复耗时统计最多保留多少条样本
reply_duration_maxlen: 1000
# 值班时间表:QQ -> 星期 -> 时间段列表
availability:
123456789:
monday:
- ["09:00", "12:00"]
- ["13:00", "18:00"]
tuesday: []
wednesday: []
thursday: []
friday: []
saturday: []
sunday: []
# 机器人消息监听有效期(秒)
max_listen_age: 86400
# 夜间免打扰配置
night_mode:
start: "23:30"
end: "07:00"
summary_time: "07:00"
# 状态持久化文件
state_file: "archives/state.json"
# 会话归档目录(SQLite 会话库 archive.db 也存放在此)
archive_dir: "archives"
# 会话库保留期(天),0 = 永久保留
archive_retention_days: 0
# 构造合并转发时,最多回看多久以内的历史消息(秒)
recent_message_max_age: 86400
# 表情映射:动作 -> QQ 表情 ID
emoji_mapping:
close: 75
more: 289
bye: 124
say: 123
cancel: 96
recall: 89
# .say 发送成功后允许通过表情撤回的时间窗口(秒)
recall_window_seconds: 60
# AI 回复建议(OpenAI 兼容接口:DeepSeek / 通义 / GLM / one-api 等均可)
# 注意:客户对话内容会发送给所配置的 LLM 服务;api_key 属敏感信息,走加密配置流程
ai_suggestion:
enabled: false
base_url: "https://api.deepseek.com/v1"
api_key: "sk-xxxx"
# 图片输入需视觉模型:DeepSeek 请用 deepseek-flash(deepseek-v4-pro 不支持 vision)
model: "deepseek-flash"
vision_enabled: true
image_detail: "low"
max_images_per_suggestion: 3
# system 提示词可自定义(多行),留空使用内置默认
system_prompt: |-
你是一家维修客服的助手。请根据以下客服与客户的最近对话,以客服身份草拟下一条发给客户的回复。
要求:只输出回复内容本身,不要任何解释、前缀或引号;语气友好专业、简洁;如果客户诉求还不明确,先回应已知信息并礼貌追问。
temperature: 0.7 # 采样温度,越高越发散
timeout_seconds: 12 # 单次生成超时,失败不影响提醒正常发送
max_context_messages: 20 # 送入模型的最近对话条数
max_suggestion_chars: 300 # 建议文本截断长度closing_message 和 welcome_message 除了支持字符串,也支持结构化消息段列表。当前代码内置支持的类型有:text、image、face、at、poke。
welcome_message:
- type: text
data:
text: "您好,感谢您添加好友。"
- type: image
data:
file: "https://example.com/welcome.png"| 配置项 | 说明 |
|---|---|
ws_url / ws_token |
NapCat WebSocket 地址与令牌 |
internal_group_id |
接收提醒、执行命令、查看状态的内部群号 |
whitelist |
不参与待回复监控的 QQ 号 |
milestones |
超时催办节点,单位为分钟 |
monitored_forward_limit |
同时保留多少条可引用的机器人消息映射 |
debounce_seconds |
各群命令的防抖窗口 |
processed_friend_requests_expire |
好友申请去重窗口;也用于刚通过好友时的消息忽略窗口 |
friend_welcome_delay |
通过好友后延迟多久再发送欢迎消息 |
friend_welcome_retries / friend_welcome_retry_interval |
欢迎消息失败重试策略 |
friend_count_limit |
好友数量告警阈值 |
reply_duration_maxlen |
回复耗时统计样本上限 |
availability |
当前可用成员时间表;用于提醒时随机 @ 一位值班成员 |
max_listen_age |
机器人消息保持可操作状态的最长时间 |
night_mode |
夜间免打扰起止时间与汇总时间 |
archive_dir |
会话归档目录(含 SQLite 会话库 archive.db) |
archive_retention_days |
会话库保留天数,0 表示永久保留 |
state_file |
运行状态持久化文件 |
recent_message_max_age |
构造提醒合并转发时回看的消息时间窗口 |
emoji_mapping |
表情 ID 到快捷动作的映射 |
ai_suggestion |
AI 回复建议(OpenAI 兼容接口配置);enabled: false 或缺省时功能关闭;vision_enabled 控制是否把对话图片 URL 送入视觉模型(DeepSeek 用 deepseek-flash);支持 .reload 在线开关 |
- Python
3.14+ - 可用的 NapCat / OneBot WebSocket 服务
- 一个用于接收提醒和执行命令的内部 QQ 群
推荐使用 uv:
uv sync如果你习惯使用 pip,也可以在满足当前 Python 版本要求的前提下安装项目依赖:
pip install -e .uv run python main.py或:
python main.py启动后程序会:
- 加载
config.yaml - 恢复
state_file中的运行状态 - 启动每 60 秒执行一次的巡检任务
- 连接 NapCat WebSocket,并在断开后自动重连
- 首次连通后向内部群发送“机器人已启动”通知
- 初始化当前好友数缓存
- 客户首次发消息后不会立刻提醒
- 只有当“距离最后一条客户消息已满 1 分钟”时,才会推送到内部群
- 如果客户在等待期间继续发消息,会重置计时并清空已上报的里程碑
- 开启
ai_suggestion后,提醒合并转发的第一层会附上根据该客户最近对话生成的 AI 建议回复,供客服参考后用.say发送;生成失败或超时会自动省略,不影响提醒本身 vision_enabled: true时,消息中的图片会先由 bot 下载并转为data:image/...;base64,...,再以 OpenAI 兼容image_url送入模型(优先客户侧,最多max_images_per_suggestion张);下载失败的图片会跳过,请求失败或无可用图时自动回退[图片]占位/纯文本重试- 日志会记录每次批量生成的
成功/总数(含 0/N)以及提醒合并转发第一层实际附带的建议条数,便于排查夜间汇总后无建议等问题
机器人会根据 milestones 的配置,在客户持续未获回复时逐级催办。每个客户每个里程碑只提醒一次;如果客户又发来新消息,该轮会话的里程碑记录会重置。
以下行为会让客户从待回复队列中移除:
- 客服直接私聊回复客户
- 在内部群执行
.say - 在内部群执行
.bye - 在内部群执行
.close - 在内部群点击配置好的
bye/close表情快捷操作 - 客户在私聊窗口戳一戳机器人,且该客户当前仍在待回复队列中
在内部群戳一戳机器人,或发送 .status,会返回状态面板,包括:
- 运行时长
- 当前待回复客户数
- 已完结会话数
- 最短、最长、平均、中位回复耗时
- 当前监听中的机器人消息数量
- 延后通知条数与临时静音状态
state_file 保存待回复客户(含其会话周期 ID)、监听消息映射、命令防抖时间、夜间延后通知、临时静音截止时间和最近一次夜间汇总日期,重启后自动恢复。
对话周期从客户进入待回复队列开始,到会话关闭结束。周期内发生的每一件事通过两层机制保证完整记录:
- 实时采集:周期内事件即时写入
archives/archive.db(SQLite,WAL 模式),崩溃/重启不丢、不依赖事后回拉 - 历史拉取对账:会话关闭后,按该周期的权威时间段
[started_at, ended_at]拉取双方历史消息,与已入库内容按message_id对账,只补实时采集遗漏的部分(如客服在其他设备回复且上报异常),并写入一条对账审计事件
记录的事件类型:
| 事件类型 | 含义 |
|---|---|
session_open / session_close |
周期开启/关闭(关闭含原因:say / bye / close / direct_reply / customer_poke 等) |
customer_message |
客户私聊消息(完整消息段) |
staff_reply |
客服回复(.say 发送或客服账号直接私聊,记录操作者/发送者) |
bot_send |
机器人主动发送给客户的系统消息(结束语、欢迎语等) |
bot_notice |
内部群提醒/催办(新客户提醒、里程碑催办、夜间汇总,含群消息 ID) |
command |
客服执行的会话操作命令(say / bye / close,含操作者 QQ) |
poke |
周期内的戳一戳 |
history_reconcile |
关闭后的历史拉取对账结果(窗口、拉取/补录条数、缺失清单) |
数据结构:sessions 表记录周期(客户、起止时间、耗时、关闭原因),session_events 表记录事件流(每条消息保留完整 OB11 消息段;图片等媒体保存 URL,注意 QQ 媒体链接会过期)。
检索示例:
-- 某客户的全部会话周期
SELECT * FROM sessions WHERE customer_uid = 123456 ORDER BY started_at;
-- 某个周期内的完整事件流
SELECT time, event_type, actor_uid, payload FROM session_events
WHERE session_id = 42 ORDER BY time;
-- 超过 1 小时才回复的会话
SELECT * FROM sessions WHERE duration > 3600 ORDER BY duration DESC;回复耗时统计在重启后自动从会话库恢复最近样本,不再清零。
旧版本按 archives/<qq>_<yyyymmdd>.jsonl 追加写的归档可用导入工具迁入会话库(幂等,可重复执行):
uv run python scripts/import_jsonl.py --dry-run # 预览
uv run python scripts/import_jsonl.py # 导入archive_retention_days(默认 0 = 永久保留)设置保留天数后,巡检任务每小时清理超期的已完结会话。
如果你使用 Claude Code、Codex 或其他编码代理来修改这个项目,建议先阅读 napcat-sdk 的专用说明:
这样通常能更快对齐:
napcat-sdk的核心对象和消息段类型NapCatClient的使用方式- 事件模型和
Reply/Text/NodeReference/NodeInline等常见段类型 user_id/group_id等字段的类型注意事项
- 多客户合并转发目前不能直接执行
.say/.more/.bye/.close,需要手动处理。 - 可操作消息的监听依赖
monitored_forward_limit和max_listen_age;超出窗口后再引用旧消息,机器人可能返回“操作已过期”。 .say的等待输入状态保存在内存中,程序重启后不会恢复。- 夜间汇总按客户聚合,不保留原始通知类型和里程碑层级。
- 状态文件和会话库会随使用时间持续增长;会话库可通过
archive_retention_days自动清理,状态文件需自行关注。 - 归档中的图片等媒体仅保存 URL,QQ 媒体链接会过期,无法永久还原原始文件。
.
├── main.py
├── config.yaml
├── pyproject.toml
├── uv.lock
├── README.md
├── scripts/
│ └── import_jsonl.py
├── archives/
└── src/
├── __init__.py
├── config.py
├── group_msg.py
├── history.py
├── main.py
├── message_sender.py
├── models.py
├── monitor.py
├── mute.py
├── new_user.py
├── private_msg.py
├── shell_console.py
├── state.py
├── storage.py
└── utils.py
各模块职责:
src/main.py:程序入口逻辑、事件分发、启动通知、重连、优雅关停(信号→取消任务→关连接→落盘状态)src/config.py:配置加载、全局状态初始化、NapCat 客户端实例src/private_msg.py:处理客户私聊消息、客服私聊回复、私聊戳一戳src/new_user.py:好友申请自动通过、欢迎消息发送、好友数提醒src/group_msg.py:群命令、群表情快捷操作、群戳一戳状态面板src/message_sender.py:提醒消息构造、合并转发、会话关闭、状态统计src/monitor.py:每 60 秒巡检一次,负责新客户提醒、里程碑催办、夜间汇总、静音到期与过期清理src/history.py:对话周期管理、实时事件采集、会话关闭后的历史拉取对账、启动恢复src/storage.py:SQLite 会话库表结构与读写(sessions / session_events)src/state.py:运行状态保存与恢复src/mute.py:临时静音开关与延后通知汇总src/shell_console.py:本地终端 Shell 控制台src/utils.py:时长格式化、值班成员判断、夜间时段判断src/models.py:TypedDict 类型定义scripts/import_jsonl.py:存量 JSONL 归档导入工具
- 复制示例并编辑明文:
cp config.example.yaml config.yaml # 编辑 config.yaml - 确保本机已有可用的 sops 与 age,且
.sops.yaml中的 age 公钥可用。 sops 推荐免污染安装,装入项目.tools/bin/:Windows PowerShellpowershell -ExecutionPolicy Bypass -File scripts\install-sops.ps1(无需 Git Bash);Git Bash / Linux / macOS 用bash scripts/install-sops。也可系统级安装(Windowswinget install Mozilla.SOPS,macOSbrew install sops)。 加密使用 整文件 binary 模式(因为availability使用数字 QQ 号作为键,SOPS 结构化 YAML 模式不支持非字符串键)。 - 加密并提交密文:
# Linux/macOS ./scripts/config-encrypt # Windows PowerShell ./scripts/config-encrypt.ps1 git add config.yaml.enc .sops.yaml git commit -m "chore: update encrypted config" git push origin main
- push 到
main后,GitHub Actions 会识别配置-only 变更:远端git pull→ 解密 → 原子替换明文config.yaml→ 不重启,等待 bot 热重载。
本地开发可把 age 私钥放在被忽略的
*.agekey(例如.tools/notifybot.agekey),远端生产私钥固定为/etc/notifybot/age.key。切勿提交私钥。
在部署主机放置 age 私钥:
sudo mkdir -p /etc/notifybot
sudo install -m 600 /path/to/age.key /etc/notifybot/age.key也可用环境变量 SOPS_AGE_KEY_FILE 覆盖默认路径。
部署主机的 sops 由 CI 在每次部署时通过 scripts/install-sops 自动安装到项目目录 .tools/bin/:不写系统路径、不需要 sudo、不污染主机环境,删掉项目目录即完全移除。版本固定在 workflow 的 SOPS_VERSION(当前 v3.13.3),下载后对照官方 checksums.txt 做 sha256 校验,幂等(已装同版本则跳过)。apply-encrypted-config / config-encrypt(bash 与 PowerShell 版)都会自动优先使用该副本,也可用 SOPS_BIN 显式指定。
若主机访问 GitHub Release 受限,可用 SOPS_RELEASE_BASE 覆盖下载源(指向相同目录结构的镜像)。
.reload:只重读当前明文config.yaml并回报成败,不git pull、不展示配置内容。- 兼容旧输入
.reload cfg/.update cfg,行为与.reload相同。 - 自动生效以 Actions 为准;群命令仅作文件已就位后的手动 reload。
同一工作流 .github/workflows/deploy-prod.yml 按变更分流:
| 变更内容 | 远端动作 |
|---|---|
仅 config.yaml.enc / 配置脚本 |
pull → 解密应用 → 等待热重载(不重启) |
src/、依赖、启动相关等 |
pull → 解密(如有)→ uv sync → systemctl restart notifybot |
| 两者都有 | 走完整重启路径 |
SSH_PRIVATE_KEY、REMOTE_USER、REMOTE_HOST- 可选
REMOTE_PORT(默认 22) - 建议配置 Cloudflare Access Service Token:
CLOUDFLARE_ACCESS_CLIENT_ID、CLOUDFLARE_ACCESS_CLIENT_SECRET
- clone 公开仓库只能看到密文与 example,没有 age 私钥无法还原生产配置。
- 历史中若曾提交过明文 token,请另行轮换;本流程负责后续不再明文入库。
- 支持多客户合并转发下的拆分处理
- 会话库的导出工具与群内检索命令(当前可用 SQL 直接查询
archives/archive.db) - 增加更细粒度的权限控制和群内角色区分
暂未声明。