"""数学选择题验证码:题目生成、选项按钮组装与按钮数据编解码。 按钮 data 格式为 ``v||<选项值>``:token 标识"当前这道题", 答错换题后旧消息上的按钮因 token 不匹配而失效,防止回放旧题。 """ from __future__ import annotations import random from dataclasses import dataclass from uuid import uuid4 from qqbotsdk.api import button, keyboard _BUTTON_PREFIX = "v" @dataclass(slots=True) class Captcha: """一道验证码;a/op/b 保留运算数便于测试与排查。""" question: str answer: int options: list[int] token: str a: int op: str b: int def body(self) -> str: """题面正文(不含欢迎/提示头),换题重发时复用。""" return f"### {self.a} {self.op} {self.b} = ?" def keyboard(self) -> dict: """选项按钮键盘,每行 2 个。""" rows = [ [button(str(v), encode_button_data(self.token, v)) for v in self.options[i : i + 2]] for i in range(0, len(self.options), 2) ] return keyboard(*rows) def encode_button_data(token: str, value: int) -> str: return f"{_BUTTON_PREFIX}|{token}|{value}" def parse_button_data(data: str) -> tuple[str, int] | None: """解析按钮 data 为 (token, 选项值);非本机器人按钮返回 None。""" parts = data.split("|") if len(parts) != 3 or parts[0] != _BUTTON_PREFIX: return None try: return parts[1], int(parts[2]) except ValueError: return None def welcome_markdown(cap: Captcha, timeout_minutes: int, subject: str = "新同学") -> str: """进群首题/测试指令消息(markdown + 按钮一起发);subject 为称呼前缀。""" return ( "**入群验证**\n" f"{subject}请在 **{timeout_minutes} 分钟内** 点击下方正确答案完成验证," "超时未通过将被移出本群。\n\n" + cap.body() ) def retry_markdown(cap: Captcha) -> str: """答错后换题重发的消息。""" return "答错了,换一题再试试:\n\n" + cap.body() def generate(option_count: int = 4) -> Captcha: """随机生成一道加减/乘法选择题;干扰项取答案邻域且互不重复。""" op = random.choice(("+", "-", "×")) match op: case "+": a, b = random.randint(1, 20), random.randint(1, 20) case "-": a = random.randint(5, 30) b = random.randint(1, a) # 保证差非负 case _: a, b = random.randint(2, 9), random.randint(2, 9) answer = a + b if op == "+" else a - b if op == "-" else a * b lo = max(0, answer - 12) distractors: set[int] = set() while len(distractors) < option_count - 1: cand = random.randint(lo, answer + 12) if cand != answer: distractors.add(cand) options = [answer, *distractors] random.shuffle(options) return Captcha( question=f"{a} {op} {b}", answer=answer, options=options, token=uuid4().hex[:12], a=a, op=op, b=b, )