命令头边界修复 + 清理零引用依赖 + 命令解析层选型收口 #92

Merged
Yushu merged 2 commits from feature/88-command-boundary-and-deps into main 2026-07-29 03:44:37 +00:00
Member

Closes #88

命令头边界缺陷关掉 + 两条零引用依赖移出 + 命令解析层选型收口进 ADR-0011(结论:不迁移 Alconna)。

一、命令头边界(main 上活着的缺陷)

on_command 的前缀树是纯最长前缀匹配(nonebot/rule.pylongest_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,边界用例对它参数化——新增命令自动获得边界用例;
  • 一条手写集合 vs 机械枚举的相等守卫:枚举一旦失灵(NoneBot 内部结构变更等),参数化用例会静默消失而不是变红。已实测:把 _command_rule_of 变异成恒返 None,3 条用例直接 skip 掉,只有这条守卫响。

变异复验:摘掉 _command_matcher 里的 force_whitespace(已核实真的写进文件),23 条用例转红,正向对照全绿;还原后 md5 与备份一致。

三、已知且接受的行为变化

中文紧贴命令名的写法(/why_query看海/settool天气 off)会被静默拒绝。9 条里只有 /why_query 真吃自由中文(其余参数是模型名/工具名走 ASCII,同意命令不带参数),所以代价集中在它一条上,在它的用法说明里点明「查询词前要有空格」消化,并留了断言标明这是刻意取舍不是回归

四、依赖

nonebot-plugin-alconnanonebot-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_queryvalence=abc 看海、只有坐标没有查询词、坐标写在末尾这些非空参数同样返回 None,它们都过得了 force_whitespace。已改。
  • should_not_pass_rule() 的 docstring 给的理由挡的是另一条失败路径。它真正的边界是"rule 检查没跑到时不会响",改为如实写出并给两条独立证据;同时补了一条不依赖 nonebug 劫持的副作用断言。
  • 同意命令那条"今天不可达"是对本仓之外两个仓当前配置的断言,且与本仓 .env.testCOMMAND_START=["", "/"] 相抵。改成如实陈述。
  • 三个只差一行的 dispatch 辅助合并;TestTheCollisionsFoundOnMain 的 name→matcher 字典去掉。

六、文档(docs 分支,本地提交)

  • ADR-0011 新增第 8 项选型「命令解析层」——该 ADR 缺这一项正是 alconna 反复冒头的根因。含:决策、拒绝 alconna 的三条实测理由、如实记下的反向证据(命令头边界这一维 alconna 确有真实收益,只是被 force_whitespace 以低得多的代价拿到了)、"为什么手写校验不算重造"、重评触发条件(两条触发 + 一条明确非触发)、新依赖口径。
  • ADR-0010 追加节收口 #80 留下的两个悬挂项;第三条是范围圈定不是悬挂项,按 AC 没把整节标成已收口。
  • CONTEXT.md 可测性条升格的是一对:纯函数直测 + 真实派发回归网。只升前半句是危险的——#80 那个 bug 恰恰是 29 条直调处理逻辑的单测放过去的。

与 AC 的两处偏离(均已在正文/ADR 里显式标注)

  1. ports.py 的"四个静默失灵的管理命令"也一起改成七条。AC 的「顺手的诚实性修补」只点名了 __init__.pyadmin.py 两处,这是第三处同形的,顺手带上。
  2. 动了 _run_why_query。Not in scope 写「全部 _run_* 明确零改动」,但 AC 同时要求「在 /why_query 的用法说明里点明查询词前要有空格」,而那句用法说明就在 _run_why_query 里。规格自相矛盾,取了具体那条。

验证

uv run pytest -n auto1436 passed(新增 35 条)。ruff check 全过。

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,边界用例对它**参数化**——新增命令**自动**获得边界用例; - 一条手写集合 vs 机械枚举的**相等守卫**:枚举一旦失灵(NoneBot 内部结构变更等),参数化用例会**静默消失**而不是变红。已实测:把 `_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=["", "/"]` 相抵。改成如实陈述。 - 三个只差一行的 dispatch 辅助合并;`TestTheCollisionsFoundOnMain` 的 name→matcher 字典去掉。 ## 六、文档(`docs` 分支,本地提交) - **ADR-0011 新增第 8 项选型「命令解析层」**——该 ADR 缺这一项正是 alconna 反复冒头的根因。含:决策、拒绝 alconna 的三条实测理由、**如实记下的反向证据**(命令头边界这一维 alconna 确有真实收益,只是被 `force_whitespace` 以低得多的代价拿到了)、"为什么手写校验不算重造"、重评触发条件(两条触发 + 一条明确非触发)、新依赖口径。 - **ADR-0010 追加节**收口 #80 留下的两个悬挂项;第三条是范围圈定不是悬挂项,按 AC 没把整节标成已收口。 - **CONTEXT.md 可测性条**升格的是**一对**:纯函数直测 **+ 真实派发回归网**。只升前半句是危险的——#80 那个 bug 恰恰是 29 条直调处理逻辑的单测放过去的。 ## 与 AC 的两处偏离(均已在正文/ADR 里显式标注) 1. **`ports.py` 的"四个静默失灵的管理命令"也一起改成七条**。AC 的「顺手的诚实性修补」只点名了 `__init__.py` 与 `admin.py` 两处,这是第三处同形的,顺手带上。 2. **动了 `_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>
规格轴抓到一条**事实错误的理由**(与本仓已有的两次同形错误一类):
`_run_why_query` 的注释写"裸命令是唯一还能走到这里的入口(参数为空时那道
要求不生效)"。假的——`parse_why_query` 对 `valence=abc 看海`、只有坐标没有
查询词、坐标写在文本末尾这些**非空**参数同样返回 `None`,它们命令名后都有
空格、过得了 `force_whitespace`,照样落到同一条用法说明。测试 docstring 复制
了同一句,一并改。

标准轴抓到 9 处逐字重复的 `on_command(..., force_whitespace=True)`:这个缺陷
**只能靠 9 处一致来避免**,而"记得每处都写"不是防线。抽出 `_command_matcher()`
让新命令天生带齐,结构守卫保留下来兜住绕开它直接调 `on_command` 的写法。

其余修正:
- `_dispatch_expecting_*` 三个只差一行的辅助合并成一个带 `rule=` 的。
- `should_not_pass_rule()` 的 docstring 此前给的理由("事件都带 to_me")挡的是
  另一条失败路径。它真正的边界是"rule 检查没跑到时不会响",改为如实写出,并
  给出两条独立证据(加 force_whitespace 前这些断言全红;同批正向对照跑通)。
- `test_a_glued_suffix_does_not_trigger_the_command` 要了 `replies` 却不断言,
  补 `assert replies == []`——这条不依赖 nonebug 的劫持。
- 同意命令那条"今天不可达"的理由是对**本仓之外**两个仓当前配置的断言,且与本仓
  `.env.test` 的 `COMMAND_START=["", "/"]` 相抵。改成如实陈述,并说明留这道闸的
  真实理由是"成本为零、后果是隐私授权",不是"防一个不可能的情况"。
- `TestTheCollisionsFoundOnMain` 的 name→matcher 字典去掉,matcher 直接进
  parametrize + 显式 ids。
- "7 条命令里只有 /why_query 吃自由中文"更正为 9 条(同意命令不带参数)。

变异复验:摘掉 `_command_matcher` 里的 `force_whitespace`(已核实写入),23 条
用例转红、正向对照仍绿;还原后 md5 与备份一致。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Yushu merged commit 649673cb8d into main 2026-07-29 03:44:37 +00:00
Yushu deleted branch feature/88-command-boundary-and-deps 2026-07-29 03:44:38 +00:00
Sign in to join this conversation.
No description provided.