drsanwujiang / dicerobot
A TRPG dice robot based on Swoole.
Requires
- php: >=7.4
- ext-json: *
- ext-mbstring: *
- ext-swoole: ^4.6.0
- ext-zip: *
- cakephp/chronos: ^2.2
- imefisto/psr-swoole-native: ^1.0
- lcobucci/jwt: ^4.1
- monolog/monolog: ^2.3
- myclabs/php-enum: ^1.8
- nyholm/psr7: ^1.4
- php-di/php-di: ^6.3
- ramsey/uuid: ^4.2
- swlib/saber: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.5
- swoole/ide-helper: ^4.6.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
基于 QQ 开放平台的 TRPG 助手。
架构
dicerobot/
├── qq/ 平台适配:签名校验、access token、OpenAPI 客户端、webhook 入口
├── bot/ 运行时:事件归一化、回复配额、插件加载与调度
├── plugins/ 内置插件:dice / check / nickname / system
├── trpg/ 领域逻辑:掷骰引擎、检定规则(零 IO,不依赖任何外层包)
└── storage/ 持久化:SQLAlchemy 模型、仓储与迁移
依赖方向单向:plugins -> bot -> qq、bot -> trpg、bot -> storage。
qq 为叶子包,不依赖任何内部模块。webhook 以本地 Protocol 声明事件接收方,
由 app.py 负责装配,因此 bot 调用客户端发消息不会与 webhook 形成循环依赖。
trpg 同样不依赖任何其他层,该约束由 dicerobot/trpg/ruff.toml 在 CI 中强制。
开发
poetry install poetry run pytest poetry run ruff check . poetry run ruff format --check . # CI 会校验格式,提交前请先格式化 poetry run mypy # strict 模式,只检查 dicerobot 包
数据库迁移在应用启动时自动执行,也可手动运行:
poetry run alembic upgrade head # 升级到最新版本 poetry run alembic revision --autogenerate -m "说明" # 依据模型变更生成迁移
迁移只需 DATABASE_URL(缺省时用默认值),不依赖机器人凭据。
指令
| 指令 | 说明 |
|---|---|
.r [表达式] [理由] |
掷骰,如 .r 3d6+2 侦查、.r 4d6k3、.r (2d6+3)*2 |
.rh [表达式] [理由] |
暗骰,结果私聊发给掷骰者,群内只公布表达式 |
.set [面数] |
查看或设置默认骰 |
.ra <技能值> [理由] |
技能检定,如 .ra 60 侦查 |
.rab / .rap |
带奖励骰或惩罚骰的检定,如 .rab 60 侦查、.rab2 60(两个奖励骰) |
.rah / .rha |
暗检定,结果私聊发给发起者 |
.rahb / .rahp / .rhab / .rhap |
带奖励骰或惩罚骰的暗检定 |
.rule [规则] |
查看或设置检定规则(coc7、simple) |
.nn [昵称] |
设置昵称,.nn 清除 恢复默认 |
.bot on / .bot off |
启用或停用机器人,仅限群主与管理员 |
.plugin / .plugin off dice |
查看或启停插件,仅限群主与管理员 |
.help |
列出可用指令 |
.ping |
确认机器人在线 |
每条指令都有中文别名,如 .掷骰、.检定、.昵称;前缀也可用全角句号 。。
重复执行的次数写在指令之后或行尾都可以,.r 3#1d100、.r3#1d100 与 .r 1d100#3 等价。
N 为一到三位的正整数,其余写法(含 #0)视为正文的一部分;两处同时写出时无法判断以
谁为准,按未命中处理。
暗骰与暗检定经主动消息投递,用户若在客户端关闭了机器人的主动消息则收不到,此时会在 群内追加一句提示。
掷骰表达式
表达式采用 OneDice 标准的子集:
| 写法 | 含义 | 例 |
|---|---|---|
AdB |
A 个 B 面骰,A 与 B 都可缺省 | .r、.r 5d、.r 3d6 |
AdBkC |
取最高的 C 个相加 | .r 4d6k3 |
AdBqC |
取最低的 C 个相加 | .r 4d6q3 |
bN / pN |
带 N 个奖励骰或惩罚骰的 d100,N 缺省为 1 | .r b、.r p2 |
AdBbC / AdBpC |
A 个各带 C 个奖惩骰的 d100 | .r 2db3 |
| 混合运算 | + - * /、乘方 ^ 与括号 |
.r d50*3+(2-p2) |
奖惩骰把 d100 拆成十位骰与个位骰:追加若干个十位骰后,奖励骰取结果最小的一个,惩罚骰取
最大的一个,个位骰只掷一次。十位与个位都是 0 时结果为 100。奖惩骰因此固定为 D100,
.r 2d20b1 会报错,也不受 .set 所设默认骰的影响。
输出会展开被舍弃的十位骰,便于验算:
.r b3 → D100B3=25[奖励骰:6 0 1]=5
.rab 60 → D100B=63[奖励骰:1]=13/60,极难成功
.rab2 60 → D100B2=63[奖励骰:1 8]=13/60,极难成功
骰池(a)、双重十字(c)、命运骰(f)、多元组与循环不在支持范围内。
插件
每条指令都归属于某个插件。插件在模块层声明,指令以装饰器挂载:
plugin = Plugin(name="dice", display_name="掷骰", version="1.0.0", chat_settings=DiceChatSettings) @plugin.command("r", "roll", "掷骰", description="掷骰,如 .r 3d6+2 侦查", max_times=30) async def roll(context: CommandContext) -> None: settings = context.chat_settings(DiceChatSettings) ...
插件也可以响应非消息事件。这类事件自带 event_id,可作被动回复,因此不消耗主动
消息配额:
@plugin.event(EventType.GROUP_ADD_ROBOT, EventType.FRIEND_ADD) async def greet(context: EventContext) -> None: context.write("我是 DiceRobot,发送 .help 查看可用指令。")
同一事件可被多个插件响应,它们共用一个回复会话,输出合并为一条消息;单个插件出错 不影响其余插件,且不会向用户回复错误提示。
插件设置以 JSON 存储,读取时由声明的 pydantic 模型校验并补齐默认值,因此增删设置项
不需要迁移数据库。写回须显式调用 save_settings 或 save_chat_settings。
启停分三层依次判定:会话总开关(.bot)、插件全局开关、插件在本会话的开关
(.plugin)。标记 requires_enabled=False 的指令不受这三层影响,用于保证关闭之后
仍有恢复手段。
第三方插件是独立的分发包,通过 entry points 被发现:
[project.entry-points."dicerobot.plugins"] mycards = "mycards:plugin"
配置从环境变量或 .env 读取,参见 .env.example。
Windows 下若测试输出中的中文显示为乱码,设置 PYTHONUTF8=1 即可。
部署
平台侧的硬性约束
- 回调地址必须是 HTTPS,端口只能是 80、443、8080、8443 之一
- 需要在开放平台配置 IP 白名单,家用宽带的动态 IP 会导致请求被拒
步骤
-
在 QQ 开放平台 创建机器人,取得 AppID 与 AppSecret。
-
准备一台有公网 IP 的服务器和一个解析到它的域名。
-
复制配置并填入凭据:
cp .env.example .env
-
把
Caddyfile中的bot.example.com换成自己的域名。 -
启动:
docker compose up -d
Caddy 会自动申请证书;数据库与日志分别落在
./data与./logs。 -
在开放平台把回调地址填为
https://<域名>/qq/webhook。平台会立即下发一次校验 请求(op = 13),应用签名回传后地址才会生效。 -
在群里
@机器人 .ping,收到pong即联通。
自定义检定规则
检定规则是 data/rules/*.json,首次启动时自动写入内置的 coc7 与 simple,之后可直接编辑——
比如把极难成功从五分之一改成三分之一:
{ "name": "极难成功", "description": "骰值不大于技能值的三分之一", "condition": "roll <= skill // 3" }
condition 中只能使用 skill 与 roll 两个变量以及算术、比较与布尔运算,函数调用一类会被拒绝。
等级按顺序取首个匹配,故大成功、大失败这类特例要排在普通成功之前。改完重启生效;写错会在启动时
直接报错并指明是哪份文件的哪一级。新增规则只需放一份新文件,文件名与其中的 id 保持一致,之后
在群里用 .rule <id> 切换。
运维
数据库迁移在每次启动时自动执行,且可重复运行,容器重建无需额外操作。
docker compose logs -f dicerobot # 查看日志 docker compose pull && docker compose up -d # 更新
镜像以非 root 用户运行,健康检查探测 /health。