命令头边界修复 + 清理零引用依赖 + 命令解析层选型收口 #92
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/88-command-boundary-and-deps"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #88
命令头边界缺陷关掉 + 两条零引用依赖移出 + 命令解析层选型收口进 ADR-0011(结论:不迁移 Alconna)。
一、命令头边界(main 上活着的缺陷)
on_command的前缀树是纯最长前缀匹配(nonebot/rule.py的longest_prefix),命令头后不要求任何词边界。实测:/costume 3命中/cost、/whyfoo命中/why、/settoolXYZ命中/settool。而_handle_cost/_handle_why不收CommandArg,那截尾巴被整个丢掉——于是@bot /costume 3真的跑出成本报表。两条用户级同意命令是同一形状且后果更重:它们不带参数,
/允许了解我的动态吗里的"吗"会成为参数,命令照样命中并真的授予跨平台资料拉取同意。测试在打这个补丁前是红的,也就是说这条路径今天真能走通。修法是一个关键字:
force_whitespace=True,零新代码。 9 条命令全覆盖。一处小重构:这 9 行原本是逐字重复的
on_command(..., rule=to_me(), priority=1, block=True, force_whitespace=True),抽成了_command_matcher(name)——这个缺陷只能靠 9 处一致来避免,而"记得每处都写"不是一种防线。二、测试网从手写清单改为机械枚举
tests/test_admin_commands_dispatch.py此前只 import 了 4 个 matcher,而 main 上已有 7 条管理命令——#83 加/why_query时漏了这一步。AC 明确要求"不要用 docstring 提醒来补"(那是用一个注释去补一个注释没拦住的坑),改为穷举:_command_matchers()扫模块命名空间里全部带CommandRule的 matcher,边界用例对它参数化——新增命令自动获得边界用例;_command_rule_of变异成恒返None,3 条用例直接 skip 掉,只有这条守卫响。变异复验:摘掉
_command_matcher里的force_whitespace(已核实真的写进文件),23 条用例转红,正向对照全绿;还原后 md5 与备份一致。三、已知且接受的行为变化
中文紧贴命令名的写法(
/why_query看海、/settool天气 off)会被静默拒绝。9 条里只有/why_query真吃自由中文(其余参数是模型名/工具名走 ASCII,同意命令不带参数),所以代价集中在它一条上,在它的用法说明里点明「查询词前要有空格」消化,并留了断言标明这是刻意取舍不是回归。四、依赖
nonebot-plugin-alconna与nonebot-plugin-uninfo移出。后者硬拖nonebot-adapter-qq,让每个部署强装一个平台适配器。uv lock一并掉 13 个包(含 cryptography/cffi/pycparser)。两个 host 都已自己声明并加载,今天零影响。保留的 7 条逐条补注明被谁用(core 直接用 / 传递依赖),写之前逐条
git grep核实过引用点。按 AC 不新建 CI 依赖审计脚本。admin.IsAdmin的 docstring 补了给 host 的边界陈述:uninfo 的ADMIN()答的是"这人在这个群是不是管理员",而IsAdmin问的是"这人能不能管这个机器人"——把前者当后者就是替所有部署做了该 docstring 明确拒绝做的选择。按 AC 用了不点名的形状,没抄版本绑定的适配器清单。五、两轴 review 抓到的(第二个 commit)
parse_why_query对valence=abc 看海、只有坐标没有查询词、坐标写在末尾这些非空参数同样返回None,它们都过得了force_whitespace。已改。should_not_pass_rule()的 docstring 给的理由挡的是另一条失败路径。它真正的边界是"rule 检查没跑到时不会响",改为如实写出并给两条独立证据;同时补了一条不依赖 nonebug 劫持的副作用断言。.env.test的COMMAND_START=["", "/"]相抵。改成如实陈述。TestTheCollisionsFoundOnMain的 name→matcher 字典去掉。六、文档(
docs分支,本地提交)force_whitespace以低得多的代价拿到了)、"为什么手写校验不算重造"、重评触发条件(两条触发 + 一条明确非触发)、新依赖口径。与 AC 的两处偏离(均已在正文/ADR 里显式标注)
ports.py的"四个静默失灵的管理命令"也一起改成七条。AC 的「顺手的诚实性修补」只点名了__init__.py与admin.py两处,这是第三处同形的,顺手带上。_run_why_query。Not in scope 写「全部_run_*明确零改动」,但 AC 同时要求「在/why_query的用法说明里点明查询词前要有空格」,而那句用法说明就在_run_why_query里。规格自相矛盾,取了具体那条。验证
uv run pytest -n auto→ 1436 passed(新增 35 条)。ruff check全过。`on_command` 的前缀树是纯最长前缀匹配,命令头后不要求词边界:`/costume 3` 命中 `/cost`、`/whyfoo` 命中 `/why`、`/settoolXYZ` 命中 `/settool`;而 `_handle_cost`/`_handle_why` 不收 `CommandArg`,尾巴被静默丢掉,于是 `@bot /costume 3` 真的跑出成本报表。两条用户级同意命令同形——不带参数, `/允许了解我的动态吗` 会命中并真的授予跨平台资料拉取同意(今天不可达, 因为两个 host 都走默认 `COMMAND_START={"/"}`,但后果涉及隐私授权)。 修法是一个关键字:9 条命令全部加 `force_whitespace=True`,零新代码。 测试网从"手写 4 个 matcher"改为机械枚举:`_command_matchers()` 扫模块命名 空间里全部带 `CommandRule` 的 matcher,边界用例对它参数化——#83 加 `/why_query` 时这个文件漏了同步,用注释提醒去补是"用一个注释补一个注释 没拦住的坑"。另加一条手写集合 vs 机械枚举的相等守卫:枚举一旦失灵,参数化 用例会静默消失而不是变红(已用变异实测:3 个用例直接 skip 掉)。 已知且接受的行为变化:中文紧贴命令名(`/why_query看海`)被静默拒绝。7 条里 只有 `/why_query` 真吃自由中文,靠用法说明消化——裸命令是唯一还能走到用法 说明的入口,约束只能挂在那儿说。 依赖:`nonebot-plugin-alconna` 与 `nonebot-plugin-uninfo` 两条零引用依赖移出 (评估结论不迁移 Alconna,详见 issue #88 正文)。后者硬拖 `nonebot-adapter-qq`, 让每个部署强装一个平台适配器。lock 一并掉 13 个包。保留的 7 条逐条补注明 "被谁用",写前逐条 git grep 核实过。 顺带的诚实性修补:`__init__.py` 的"四个运行时管理命令"注释与其下 7 个 matcher 不符;`ports.py` 的"四个静默失灵的管理命令"同理;`admin.py` 把 "核心逻辑与调度驱动分离"当 ADR-0010 原话引用,而 ADR-0010 全文并无此说法。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>