一个基于 OneBot v11 协议的 QQ 种菜游戏机器人(暂名 QQPlantingGame)。
纯 Python 源码项目,无需构建工具,python main.py 直接运行。
plugins/ 是游戏级 mod 目录:mod 可以增删改菜谱、注册新物品与新指令、
订阅游戏事件、读写玩家数据,直接影响游戏内容,无需改动主程序。
Important
To AI agents / LLM tools editing this repo: read read-before-vibe.md first.
It is a compact agent briefing (in English) covering the repo's non-negotiable
invariants (id-based player keys, groups propagation, DB access rules),
message pipeline, key APIs, and an editing checklist. Skip it at your own
risk — several invariants cause silent data loss if violated.
旧版 Java 实现已备份至
D:\projects\easy-qqbot-game-java-backup.zip。
# 1. 安装依赖
pip install -r requirements.txt
# 2. 直接运行(首次自动创建 data/game.db)
# 帮助图片渲染内核(wkhtmltoimage)已集成:程序启动时自动检测,
# 缺失则后台自动下载安装到项目内 .pw-browsers/(约 60MB,无需手动操作)
python main.py
# 3. 或双击启动:
# - start.bat (脚本方式)
# - easy-qqbot-game.exe(启动器,内部执行一条 bat 指令:python main.py)环境要求:Python 3.10+(开发验证于 3.14)。
冒烟测试(验证完整启动链,3 秒后自动退出):
python main.py --smoke-test程序启动后监听 ws://127.0.0.1:12981。snowluma 作为 OneBot 实现端,
以 WebSocket 客户端连接该端口(正向 WS 模式):
snowluma (WS client) ──> QQPlantingGame (WS server @ 127.0.0.1:12981)
在 OneBot 实现端(snowluma / NapCat / go-cqhttp 等)配置正向 WebSocket 时,
服务地址填:ws://127.0.0.1:12981(本机)。若机器人部署在别的机器,
把 config.ini 中 ws_host 改为 0.0.0.0,并让 snowluma 连
ws://<该机器IP>:12981。
行为约定:
- 机器人通过同一连接接收消息事件、调用
send_group_msg/send_private_msg回复 - 群聊回复:正确指令(被识别并执行,含业务成功/失败)回复会 @ 发送者
(
[CQ:at,qq=...]开头),防止多人同时玩时消息不对应;未识别指令与格式错误 不 @;私聊一对一不加 - 空指令体静默:单独发
?或@某人 ?(无指令名)不触发任何回复,不 @ 任何人 - 地址与端口可在
config.ini修改(onebot.ws_host/onebot.ws_port)
前缀可在
config.ini的[bot].command_prefixes中配置(如改为!)。
注册制:新玩家必须先
?注册 <id>注册账号(id 为 1-12 个字符、全服唯一), 之后所有指令都绑定到该 id;未注册玩家触发指令会收到提示。?帮助与?注册未注册时也可使用。
| 指令 | 说明 |
|---|---|
?注册 <id> |
注册账号:id 为 1-12 个字符(不含空格),全服唯一(云端查重 + 绑定 QQ);也可 ?注册 后直接回复 id |
?改id <新id> |
修改账号 id:消耗 1000 菜币,本地与云端同步改名(新 id 全服唯一) |
?获取id @某人 |
查询被 @ 的玩家的账号 id(需在群聊中 @ 目标) |
?找回账号 |
本地数据丢失后按当前 QQ 从云端找回账号(全量恢复:身份/财富/背包/作物/保险,云端为准) |
?帮助 |
查看全部指令与格式(渲染为图片,A 规格高清图;同类指令分组展示) |
?签到 |
每日签到领菜币,连续 7 天当天翻倍 |
?我的 |
查看个人农场信息(账号 id / QQ / 所属群聊,渲染为图片,B 规格) |
?仓库 |
查看菜币、种子、果实、物品、保险库等级与保险状态(渲染为图片,A 规格高清图) |
?商城(?打开商城) |
查看种子/物品价格与田块解锁价(渲染为图片,A 规格高清图) |
?购买 萝卜 |
购买种子(会追问数量,如回复 5 即买 5 个;也可 ?购买 萝卜 5 一步到位) |
?种植 萝卜 |
种到第一个空田格(每块田 4 格) |
?一键种植 萝卜 |
把指定菜种到所有空田格(种子不够会种下所有并提醒) |
?田块 |
查看作物状态:已成熟 / 剩余时:分:秒(渲染为图片,B 规格) |
?收获 |
收获全部已成熟作物 |
?出售 萝卜 |
出售该菜全部果实换菜币 |
?解锁田块 |
解锁下一块田(共 5 块,价格逐级递增) |
?使用 化肥 1 2 |
对第 1 块田第 2 格使用化肥(减 30% 剩余时间;每株作物只能施肥一次) |
?一键施肥(?全部施肥) |
给所有未成熟且未施过肥的作物施肥(每株只能一次,化肥不够会提醒) |
?购买保险 |
购买保险:一类·次数(1000 菜币/天,被偷上限降为 1 次)/ 二类·数量(1000 菜币/天,被偷只损失 10%);会追问选类型与天数 |
?保险库 |
查看保险库等级(每级 -10% 被偷概率,最高 4 级) |
?升级保险库 |
保险库升级(花费 10000×目标级数 菜币,云端同步) |
?偷钱 <目标id> |
偷取目标 50% 菜币(默认成功率 50%,目标保险库每级 -10%;每天最多偷 3 次,与云端核对次数) |
?本服财富榜(?财富榜) |
本服(全部玩家)菜币排行,前 N 名 + 我的排名(渲染为图片,A 规格高清图) |
?本群财富榜 |
本群玩家菜币排行(仅群内可见,前 N 名 + 我的排名,渲染为图片,A 规格高清图) |
?全球财富榜 |
服务器全球菜币排行(条数由服务器默认值决定,渲染为图片,A 规格高清图) |
金体系围绕金晶货币展开,全部为商城商品(?商城 查看价格,?购买 购买种子),
无需额外指令:
- 摇钱树(
?商城中种子价 5000 菜币):购买种子后?种植 摇钱树种在普通田块, 24 小时成熟,用通用?收获采收,产出 1 金晶(不产果实、不能出售)。 - 金土地(
?解锁金土地,第 1 块 5 金晶、第 2 块 10 金晶):金作物专属土地, 与普通田块开发相互独立。 - 金作物(如
?购买 金萝卜):每种普通菜自动生成「金<名>」版本(成熟时间一致, 种子价/售价 = 普通 ×3),只能种在金土地(?种植 金萝卜自动种到金土地,?金田块查看状态),成熟后?收获得金果实进仓库,?出售换成菜币。
旧版「?购买摇钱树 / ?收获金晶」独立指令已废弃:摇钱树现在是普通田块商品作物, 统一走 购买→种植→收获 通用流程;旧数据(独立字段种的树)启动时自动迁移为田块作物。
图片渲染规格:A 规格 = 帮助同款背景与高清参数(帮助/商城/仓库/三个财富榜); B 规格 = 压缩小图(我的/田块)。渲染失败自动回退纯文本; 背景图在项目内
resources/img/(相对路径,勿用绝对路径)。
所有指令执行后都会附带 2~3 条"接下来你可以"的示例指令(双引号括起)。
指令格式错误(如 ?购买 缺参数)会提示正确格式,但不 @ 玩家。
- 群白名单(
config.ini[bot].enabled_groups):逗号分隔的群号列表, 不在白名单的群消息会被完全忽略;留空 = 所有群可用。 - 多群支持:同一 QQ 玩家可同时属于多个群,
?我的会显示所属群列表; 群消息会自动把玩家记入该群(旧玩家首次在群里发指令后入榜)。 - 本群财富榜:只统计本群的玩家,其他群的大佬不会挤进你群的榜单。
?本服财富榜(本服):直接读本地 SQLite,按菜币降序显示前 N 名
(N = config.ini [bot].leaderboard_top_n,硬上限 15,配置超限启动时
警告并强制改回 15),并显示自己的排名。
?本群财富榜(本群):与前者相同,但只统计发送者所在群的玩家;
私聊中使用会提示需在群聊操作。
?全球财富榜(全球):
本地机器人 ── WebSocket 持久连接(实时推送全量数据变化,5 秒内)──▶ 全球服务器(49.235.134.245:9627)
本地机器人 ◀── 自检/离线更新/token/心跳/排行/其它服用户数据推送 ── 全球服务器
- 本地程序启动时先做 id/QQ 一致性校验(
/verify):云端对比本地与云端 id↔QQ 绑定是否一致;不一致则输出 ERROR 日志列出冲突账号,5 分钟后自动停机 (防止服务器 URL 更换后本地数据与云端冲突) - 云端是全量用户数据唯一权威,所有改动都同步到云端(不再 20 分钟定时上报):
本地任何数据变化(菜币/田块/签到/群/背包/作物/保险)5 秒内通过 WebSocket
全量推送到云端,服务器覆盖后向其它持有该用户副本的在线客户端推送
(离线客户端不推送,下次启动由
client_boot离线数据补齐) - 排行条数由服务器默认值(
TOP_N环境变量,默认 20)决定,本地不可改, 本地只显示云端返回的条数 - 服务器项目见
D:\projects\easy-qqbot-server(独立部署;HTTP API + WebSocket 通讯)
?偷钱 <目标id>:与云端核对后尝试偷取目标 50% 菜币 (目标有二类数量保险时只偷 10%)- 默认成功率 50%;目标保险库每级 -10% 成功率(4 级时仅 10%)
- 每人每天最多偷 3 次,每人每天最多被偷 3 次(云端记录并每日自动刷新)
- 无论成败,云端都会记录一次"被偷";金额上限由云端校验(防作弊)
?购买保险(会追问:选一类/二类 → 选天数,每天 1000 菜币):- 一类 · 次数保险:每天最多只可以被偷 1 次(云端每日刷新时将被偷次数置 2)
- 二类 · 数量保险:被偷时对方只能拿走你 10% 菜币(默认 50%)
?保险库/?升级保险库:初始 0 级,显示在?仓库中; 升级花费 10000×目标级数 菜币,每级降低 0.1 被偷概率,最高 4 级
- 启动自检(阻塞主进程):程序启动后、OneBot 服务开启前,先完成客户端
身份校验,任一步失败则拒绝启动(exit 1):
- 确认本地是否已有客户端身份(
data/client_token.json);没有则 交互式询问管理员输入客户端 id(1-12 字符,input())→ 向服务器注册, 服务器生成 16 位小写字母数字 token 绑定并持久化到本地 - 每次启动都向服务器发送 id + token(
client_boot协议)校验 - 校验通过后服务器把该客户端持有副本的全部用户全量最新数据下发给本地 (离线数据更新:覆盖客户端离线期间在别的客户端游玩产生的变更; 包含菜币/田块/签到/群/背包/作物/保险)
- 本地全量上报用户(
sync_users),服务器补建缺失用户并标记来源
- 确认本地是否已有客户端身份(
- 持久连接:自检通过后,客户端通过 WebSocket(
ws://服务器:9628)与 服务器保持长连接,每 30 秒发送心跳;断线自动重连(3 秒退避) - 用户来源标记:每个用户数据都带"来源客户端"(本地
players.origin_client+ 服务器users.origin_client);服务器记录该用户数据在哪些客户端有副本 - 双向更新:客户端 A 上报某用户变更 → 服务器更新权威数据 → 向所有
其它持有该用户副本的在线客户端推送
refresh_user(带完整数据) - 服务器权威结算实时推送:偷钱结算(
/steal_commit)、购买保险、 升级保险库由服务器直接改账 → 向持有该用户副本的所有在线客户端推送refresh_user(不依赖发起方上报,其他客户端数据即时一致) - 数据保护:某客户端收到推送但本地没有该用户 → 上报
user_missing→ 服务器询问是否恢复(restore_offer带完整数据)→ 客户端按config.ini[global].restore_auto自动应答:true(默认):恢复数据并保存false:放弃该用户,服务器删除其副本记录,本地删除该用户全部数据
- 服务器日志:只输出客户端连接/断开(
[时间戳] 客户端连接/断开: <id>), 心跳与数据更新不打日志 - webUI 管理面板(superClient 专用):服务器
config.json的superClient列表包含本客户端 id 时,本客户端启动时在本地 13059 端口开启编辑面板 (浏览器打开http://127.0.0.1:13059;superClient可配多个客户端):- 玩家列表 5 秒自动刷新(浅蓝/白/粉配色),点击「编辑」修改 菜币/保险库/田块/签到/群/保险到期日
- 保存直改服务器玩家数据库(只影响被编辑的玩家),服务器随即向所有
持有该玩家副本的在线客户端推送
refresh_user(含本客户端本地库), 离线客户端下次启动离线数据补齐 - 端口可在
config.ini[global].webui_port修改(默认 13059)
| 菜 | 种子价 | 成熟时间 | 果实售价 |
|---|---|---|---|
| 萝卜 | 10 | 10 分钟 | 30 |
| 白菜 | 40 | 30 分钟 | 120 |
| 土豆 | 120 | 60 分钟 | 360 |
| 番茄 | 350 | 2 小时 | 1000 |
| 茄子 | 900 | 4 小时 | 2500 |
| 西瓜 | 2000 | 8 小时 | 5000 |
| 南瓜 | 4200 | 12 小时 | 9000 |
| 金瓜 | 7000 | 24 小时 | 9800 |
约束:成熟时间 ≤ 1 天,种子价与售价 ≤ 10000(菜币)。
成熟判定不依赖定时器:程序记录种植指令的时间戳,玩家发送
?田块 / ?收获 时对比当前时间戳计算经过时间,报告成熟状态与
剩余时间(时:分:秒)。
- 注册:新玩家发送
?注册 <id>(如?注册 小菜农)注册账号; id 为 1-12 个字符(不含空格),中文/字母/数字均可 - 全服唯一 + 绑定 QQ:注册时先向云端服务器(
[global].global_server_url)查重并建号, 云端同时存储 id 与 QQ 号(同一 QQ 全服只能绑定一个 id);本地也写入玩家记录 - 追问:也可
?注册(不带 id),机器人会追问,下一条消息直接回复 id 即可 - 绑定 QQ:一个 QQ 号只能注册一个账号;注册后 QQ 仅用于识别消息来源, 所有游戏指令都绑定到你的 id(排行榜、?我的 均显示 id)
- 修改 id:
?改id <新id>消耗 1000 菜币,云端服务器扣费(权威)后 本地落库改名;云端迁移全部关联数据并向持有该用户副本的客户端推送改名同步 - 未注册拦截:未注册玩家触发任何游戏指令(?签到/?种植/?财富榜…)都会被
提示"请先发送 ?注册 注册账号";
?帮助与?注册未注册时也可用
- 注册成功即自动获得 5 个「萝卜」种子、1 块田(每块 4 格)
- 田块共 5 块,第 2~5 块需用菜币解锁:200 / 800 / 3000 / 9000
plugins/ 是游戏级 mod 目录,本声明规定 mod 的编写方式。
plugins/
├── my_mod/
│ └── __init__.py # 包形式(推荐,可拆多个 .py 文件)
└── another_mod.py # 单文件形式
每个 mod 模块必须定义模块级变量 PLUGIN(BotPlugin 实例)。
加载器靠 getattr(module, "PLUGIN") 发现插件,未定义则跳过并告警。
from core.plugin.api import BotPlugin
class MyMod(BotPlugin):
...
PLUGIN = MyMod() # ← 必须有这一行from core.plugin.api import BotPlugin
class MyMod(BotPlugin):
name = "mod 显示名" # 必需:str,日志与加载信息中显示
def on_load(self, context):
"""必需:插件加载入口。在这里注册指令 / 增删改菜谱 / 注册物品 / 订阅事件。"""
...
def on_unload(self):
"""可选:程序退出时释放资源。"""
...from core.plugin.api import CommandContext, CommandReply, GameCommand
class MyCommand(GameCommand):
name = "今日运势" # 必需:指令名(不含前缀 ?)
aliases = {"运势", "luck"} # 可选:别名集合
usage = "?今日运势" # 必需:帮助里展示的格式
description = "看看今天的运气" # 必需:指令简介
def execute(self, ctx: CommandContext) -> CommandReply:
# ctx.user_id 发送者 QQ(str,仅用于识别消息来源)
# ctx.player_id 玩家注册的业务 id(str;未注册/注册流程中为 None)
# ctx.group_id 群号(私聊为 None)
# ctx.args 指令名后的参数(已去首尾空格)
# ctx.context 全局 PluginContext(可访问 db / game / crops / events ...)
return CommandReply.of("🍀 今日运势:大吉!")回复约定:
- 普通回复:
CommandReply.of("文本") - 格式错误:
CommandReply.error("指令格式错误,正确格式:...")—— 不 @ 发送者 - 返回
None:不回复
| 入口 | 签名 / 作用 |
|---|---|
context.commands.register(cmd) |
注册 GameCommand(自动进入 ?帮助) |
context.crops.register(name, seed_cost, grow_minutes, sell_price) |
注册新菜(商城/购买/种植/出售即时生效) |
context.crops.modify(name, seed_cost=None, grow_minutes=None, sell_price=None) |
修改现有菜属性 |
context.crops.remove(name) |
删除一种菜(谨慎:删了商店就买不到) |
context.crops.find_by_name / find_by_id |
查菜谱 |
context.items.register(name, price, description="") |
注册新物品 |
context.events.subscribe(EventType, handler) |
订阅游戏事件(见下表) |
context.db |
读写玩家数据(find_by_id / add_quantity / all_crops ...) |
context.config |
读取全局配置 |
context.game |
调用游戏引擎(buy_seed / plant / harvest ...) |
| 事件类 | 字段 |
|---|---|
PlantEvent |
user_id / crop_name / plot_index(1基) / slot_index(1基) / planted_at_ms |
HarvestEvent |
user_id / crop_name / count |
SellEvent |
user_id / crop_name / count / coins |
BuyEvent |
user_id / crop_name / cost |
SignInEvent |
user_id / reward / streak |
UnlockPlotEvent |
user_id / plots |
from core.game.events import HarvestEvent, PlantEvent, SellEvent, BuyEvent, SignInEvent, UnlockPlotEvent
context.events.subscribe(HarvestEvent, self.on_harvest) # handler 接收一个事件对象事件处理函数抛异常不会影响游戏主流程(EventBus 捕获并记日志)。
新建 plugins/my_mod/__init__.py,重启即生效:
from core.db.database import Player
from core.game.events import HarvestEvent, SellEvent
from core.plugin.api import BotPlugin, CommandContext, CommandReply, GameCommand
class FortuneCommand(GameCommand):
name = "今日运势"
aliases = {"运势"}
usage = "?今日运势"
description = "看看今天的运气(示例 mod 指令)"
def execute(self, ctx: CommandContext) -> CommandReply:
return CommandReply.of("🍀 今日运势:大吉!")
class MyMod(BotPlugin):
name = "我的mod"
def on_load(self, context):
self._ctx = context
context.commands.register(FortuneCommand()) # 1) 新指令
context.crops.register("黄金萝卜", 5000, 20, 9999) # 2) 新菜
context.crops.modify("萝卜", sell_price=50) # 3) 改现有菜
context.items.register("魔法化肥", 1000, "示例物品") # 4) 新物品
context.events.subscribe(HarvestEvent, self.on_harvest) # 5) 订阅事件
def on_harvest(self, ev: HarvestEvent):
# 收获任意菜时额外 +10 菜币(玩家对象以 id 为业务键)
p = self._ctx.db.find_by_id(ev.user_id)
if p is not None:
self._ctx.db.update_player(Player(
p.id, p.qq_id, p.coins + 10, p.unlocked_plots,
p.last_sign_date, p.sign_streak, p.created_at, p.groups))
def on_sell(self, ev: SellEvent):
import logging
logging.getLogger("mod").info("[mod] %s 出售 %s x%d 得 %d 菜币",
ev.user_id, ev.crop_name, ev.count, ev.coins)
PLUGIN = MyMod()参考实现:plugins/dice(指令 ?掷骰子 + 新菜「黄金萝卜」+ 订阅收获事件送化肥)。
- mod 在启动时加载一次;修改 mod 后需重启生效(热加载在路线图中)
- mod 是全局的:注册的菜 / 物品 / 指令对所有玩家生效
Player是不可变 dataclass,改数据要用db.update_player(Player(...))整体替换db.transaction(fn)可用于多步写操作保证原子性- 用
context.game调用引擎方法会触发对应事件,注意避免事件循环(如事件里再调用 harvest)
?帮助 默认渲染为图片(wkhtmltoimage 无头渲染),不再刷屏:
- 启动自检:程序启动时强制检查浏览器渲染内核(开源 Qt WebKit 的 wkhtmltoimage),
缺失则阻塞下载安装到项目内
.pw-browsers/wkhtmltox/(约 60MB,带百分比进度日志), 完成后才继续加载;下载失败则记录 ERROR 并继续启动(帮助回退纯文本) - 布局:以
config.ini的[game].help_background为背景——- 背景图完整展示:边缘留白、顶部不模糊
- 顶部标题区显示「种菜帮助」(清晰,白字带阴影)
- 中间内容区为高斯模糊长方形面板(Pillow 预生成模糊背景切片, 视觉上就像原图该区域被模糊),指令列表以白色文字显示在模糊区上
- 实现:开源浏览器内核 **wkhtmltoimage(Qt WebKit)**无头渲染,非手绘
- 缓存:内容不变(mod 加载后固定)只渲染一次,启动时预渲染
- 回退:渲染内核缺失(下载失败)、或渲染失败时,自动回退为纯文本
- 发送:OneBot
[CQ:image,file=file:///...]绝对路径本地图片
部分指令回复渲染为图片(同一渲染内核,背景图在项目内 resources/img/,
全部相对路径):
- A 规格(帮助同款,高清):
?帮助、?商城、?仓库、?本服财富榜、?本群财富榜、?全球财富榜背景resources/img/plantingGameBackground-A.jpeg(help_background), 视口 760 / 2x 缩放 / JPEG 85 - B 规格(压缩小图):
?我的、?田块背景resources/img/plantingGameBackground-B.jpg(card_background), 视口 640 / 1x / JPEG 58(体积更小) - 两种规格布局相同:顶部标题不模糊 + 中间高斯模糊面板 + 白色文字
- 渲染失败一律回退纯文本(不影响指令可用性)
所有日志统一 UTC+8 时间戳(2026-08-02 10:35:14 +08:00,不受机器时区影响),
INFO 级默认输出:
- 指令消息:只记录正确触发指令的消息(
指令触发含原文 →指令完成含回复摘要); 普通聊天消息、白名单外消息不输出(DEBUG 级) - 数据库操作:SQL 日志(
DB 查询/DB 写入)为 DEBUG 级——正常启动 不显示,避免刷屏;需要排查数据库操作时把日志级别调到 DEBUG 即可看到完整 SQL - 启动自检:内核检测、下载进度百分比、安装结果
[2026-08-02 10:36:33 +08:00] INFO core.bot.engine - 指令触发: [群 123456] QQ789 执行「签到」(原文: ?签到)
[2026-08-02 10:36:33 +08:00] INFO core.bot.engine - 指令完成: QQ789 回复: 签到成功!获得 100 菜币...
[onebot]
ws_host = 127.0.0.1
ws_port = 12981
[bot]
command_prefixes = ?,?
enabled_groups =
leaderboard_top_n = 15
[game]
currency = 菜币
sign_base_reward = 100
sign_cycle_days = 7
sign_double_multiplier = 2
newbie_crop = 萝卜
newbie_seed_count = 5
newbie_initial_plots = 1
plot_unlock_costs = 200,800,3000,9000
fertilizer_price = 300
fertilizer_reduction = 0.30
db_path = data/game.db
plugins_dir = plugins
help_background = resources/img/plantingGameBackground-A.jpeg
card_background = resources/img/plantingGameBackground-B.jpg
[global]
global_enabled = true
global_server_url = http://49.235.134.245:9627
# 通讯重构:WebSocket 持久连接(启动自检/认证/心跳/实时推送/数据保护)
global_ws_url = ws://49.235.134.245:9628
# 客户端 id:可留空——首次启动交互式询问并自动注册,token 存 data/client_token.json
client_id =
# 数据恢复自动应答:true=要(默认)/ false=不要并删除本地数据
restore_auto = true
[bot].command_prefixes:指令前缀(逗号分隔,支持多前缀)[bot].enabled_groups:群白名单(逗号分隔的群号;留空 = 所有群可用)[bot].leaderboard_top_n:本服榜 / 本群榜条数(硬上限 15,超限启动警告并强制改回; 全球榜条数由服务器TOP_N默认值决定,本地不可改)
players:账号 id(主键,用户自设 1-12 字符)、QQ 号(UNIQUE,仅用于识别来源)、 菜币、已解锁田块数、上次签到日期、连签天数、创建时间、所属群列表(逗号分隔,多群)、 保险库等级(0-4)、来源客户端 id(数据保护用)inventory:玩家库存(seed 种子 / fruit 果实 / item 物品,按 id + 数量)crops:田块每格的作物(田块/格子下标、作物 id、种植时间戳、是否已施肥)insurance:本地保险到期日(一类次数 / 二类数量,展示用;权威在服务器)data/client_token.json:客户端 token(服务器注册后自动生成,勿手改)
云端全量备份:以上数据(除 token 外)全部在云端
server.db有权威副本—— 本地任何改动实时全量上报,云端覆盖后向其它在线客户端推送;客户端启动时用 云端权威数据做离线更新/丢失恢复(详见「通讯重构」一节)。
id 主键:业务对象是用户自设 id,一切表(players/inventory/crops)都用 id 关联。 旧版本数据库(qq_id 主键时代)不兼容,启动时会检测并提示"请删除 data/game.db 后重启(旧数据不兼容,需重新注册)"——升级到注册制需重建数据库。
# 本地机器人(157 个用例)
python -m unittest discover -s tests -v
# 全球财富榜服务器(52 个用例)
cd ../easy-qqbot-server && python -m unittest discover -s tests -v用例覆盖:菜谱约束与 mod 增删改、注册制(注册/查重/未注册拦截/注册追问/改 id)、 引擎全流程(签到/批量购买/一键种植/收获/出售/解锁/化肥/一键施肥)、 盗窃(偷钱成功率/保险库降概率/数量保险 10%/每日次数与刷新)、 保险(购买追问/本地扣款/服务器扣款)与保险库(升级/满级)、 本服与本群财富榜(含多群过滤、条数上限)、数据库建表与旧库迁移(vault_level/ origin_client/fertilized)、全球财富榜(含条数云端控制)、启动一致性校验(verify 冲突)、 购买/保险多轮追问、指令路由(前缀/别名/分组排序/格式错误不 @/建议指令/群白名单)、 云端 API 错误处理(HTTP 409 业务拒绝显示服务器真实原因,网络异常才报无法连接)、 真实 WebSocket 往返集成、服务器注册(id+QQ 绑定)/校验/改 id/上报/排行榜/盗窃/保险 API、 客户端注册(token)与 WebSocket 同步协议(client_boot 启动自检/离线更新/心跳/核对/ 实时推送/结算后推送/改 id 同步(user_id_changed)/数据恢复)、 全量同步(本地任意数据变化——菜币/田块/签到/群/背包/作物/保险——脏标记 5 秒内全量上报;服务器权威覆盖落库并推送其他在线客户端;旧 payload 兼容保留)。
| 方式 | 说明 |
|---|---|
python main.py |
直接运行(推荐开发时用) |
start.bat |
双击启动;含 chcp 65001 保证中文日志不乱码 |
easy-qqbot-game.exe |
双击启动;内部只执行一条 bat 指令 python main.py |
python main.py --smoke-test |
冒烟测试:启动 3 秒后自动退出 |
重新编译 exe(修改 Launcher.cs 后):
"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe" /nologo /out:easy-qqbot-game.exe /target:exe Launcher.cs| 现象 | 处理 |
|---|---|
启动报 ModuleNotFoundError: No module named 'websockets' |
执行 pip install -r requirements.txt |
start.bat 报 '~dp0' 不是内部或外部命令 |
批处理被改成 UTF-8/LF 了,用 PowerShell 重写为 CRLF + GBK 编码(见下) |
| 端口被占用 | 改 config.ini 的 ws_port,或先关掉占用 12981 的程序 |
| 中文日志乱码 | 确认通过 start.bat / exe 启动(内部已 chcp 65001);手动运行加 set PYTHONIOENCODING=utf-8 |
| snowluma 连不上 | 确认机器人已启动且日志出现 server listening on 127.0.0.1:12981;检查防火墙 |
重写 start.bat(若被错误保存为 UTF-8/LF,cmd 无法解析中文批处理):
$lines = @("@echo off","chcp 65001 >nul",'cd /d "%~dp0"',"python main.py %*",
"if errorlevel 1 ("," echo."," echo 启动失败:请确认已安装 Python 3.10+ 并执行 pip install -r requirements.txt"," pause",")")
[System.IO.File]::WriteAllLines("$PWD\start.bat", $lines, [System.Text.Encoding]::GetEncoding(936))easy-qqbot-game/
├── main.py # 入口:python main.py [--smoke-test]
├── read-before-vibe.md # ★ AI 编辑代理必读:结构/流水线/关键函数/编辑陷阱(英文)
├── config.py / config.ini # 配置
├── resources/img/ # ★ 第三方资源目录:图片渲染背景(A/B 两规格)
├── core/
│ ├── onebot/ # OneBot v11 正向 WebSocket(事件/API/服务端)
│ ├── plugin/ # mod API(BotPlugin/GameCommand/注册表/加载器)
│ ├── db/ # SQLite 数据层(players/inventory/crops/insurance + 事务)
│ ├── game/ # 游戏引擎(动态菜谱/物品/事件总线/GameService/盗窃/保险/保险库)
│ ├── cloud/ # ★ 云端同步客户端(WS 启动自检/离线更新/token/心跳/实时推送)
│ └── bot/ # 指令路由 + 内置指令(含财富榜/盗窃/保险)+ help_image 渲染
├── plugins/ # ★ 游戏 mod 目录(放进去即生效)
│ └── dice/ # 示例 mod:掷骰子 + 黄金萝卜
├── tests/ # unittest 测试(157 用例)
├── start.bat # 启动脚本(CRLF + GBK 编码)
├── install_help_render.bat # 帮助图片渲染内核安装脚本(wkhtmltoimage → .pw-browsers/wkhtmltox/)
├── easy-qqbot-game.exe # 启动器(内部执行 python main.py)
├── Launcher.cs # 启动器源码
└── requirements.txt # websockets(帮助图片渲染内核见 install_help_render.bat)
# 配套项目(独立部署):
D:\projects\easy-qqbot-server\ # 服务器:HTTP API(注册/上报/校验/改id/盗窃/保险/排行,0.0.0.0:9627)
# + WebSocket 通讯(启动自检/离线更新/token/心跳/实时推送/数据保护,0.0.0.0:9628)
- OneBot v11 正向 WS 接入(12981 端口)
- 种菜核心玩法(种植/收获/出售/田块/签到/化肥/一键种植/一键施肥)
- 游戏级 mod 系统(菜谱/物品/指令/事件)
- 财富榜(本服前 N + 本群前 N + 我的排名 + 全球榜)
- 全球财富榜(WebSocket 实时同步,服务器计算排行)
- 一键种植 / 购买数量追问
- 群白名单与多群支持(玩家可同时属于多个群)
- 注册制账号体系(id 全局唯一、云端查重+QQ 绑定、未注册拦截、改 id、获取id)
- 启动 id/QQ 一致性校验(云端冲突检测,冲突 5 分钟停机)
- 群聊回复策略:正确指令 @ 发送者、未识别/格式错误不 @(多人消息不对应问题)
- 帮助指令分组排序(同类指令相邻)
- 盗窃系统(每日 3 次上限、成功率、云端核对与每日刷新)
- 保险(一类次数 / 二类数量,追问购买)+ 保险库(4 级,降被偷概率)
- 通讯重构:阻塞启动自检(input 注册/token 校验/离线数据更新)+ 实时双向同步 + 数据保护
- 云端全量数据同步:背包/作物/田块/签到/群/保险全部上云(脏标记 5 秒内全量上报 + 权威覆盖推送 + 离线恢复)
- webUI 管理面板:服务器 config.json 配置 superClient → 客户端本地 13059 端口开编辑面板(5s 刷新,直改服务器库并同步所有客户端)
- 更多玩法(抽卡、答题、农场商店)
- mod 热加载(改 mod 无需重启)
- 玩家排行榜