# 用户名中的大写字母导致编辑器中可达提及检查失效

**URL:** <https://meta.discourse.org/t/uppercase-letters-in-username-break-reachable-mention-check-in-composer/401292>\
**Category:** Bug\
**Tags:** mentions\
**Created:** [2026年四月22日 19:47 UTC](https://meta.discourse.org/t/uppercase-letters-in-username-break-reachable-mention-check-in-composer/401292 "2026-04-22T19:47:26Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![thoka](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/thoka/32/115652_2.png) [@thoka](https://meta.discourse.org/u/thoka)\
**Post date:** [2026年四月22日 19:47 UTC](https://meta.discourse.org/t/uppercase-letters-in-username-break-reachable-mention-check-in-composer/401292/1 "2026-04-22T19:47:26Z")

</div>

在调查 [Private Topics Plugin - #109 by thoka](https://meta.discourse.org/t/private-topics-plugin/268646/109) 时，我发现：如果用户名包含大写字母，在受限分类中提及该用户时不会被报告。

如果我提及 `@SomeUser`，编辑器会请求  
`/composer/mentions.json?names[]=SomeUser&topic_id=10728`  
在返回结果中，用户名以小写形式返回，且未设置 `user_reasons`。

使用小写字母查询该用户名时，会返回 `"user_reasons": {"someuser":"category"}`。

如果在编辑器中使用小写字母输入用户名，则会向权限不足的用户显示警告。

如果使用编辑器提供的自动补全功能，输入的小写用户名会被替换为大写形式，因此不会被报告。

---

<div class="post-metadata">

**Author:** ![RGJ](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/rgj/32/523185_2.png) [@RGJ](https://meta.discourse.org/u/RGJ)\
**Post date:** [2026年四月22日 21:26 UTC](https://meta.discourse.org/t/uppercase-letters-in-username-break-reachable-mention-check-in-composer/401292/2 "2026-04-22T21:26:53Z")

</div>

找得好 @thoka！

问题出在这里

> <https://github.com/discourse/discourse/blob/main/app/controllers/composer_controller.rb#L29>

`users` 返回的是 `{"username_lower" => User 对象}`。

但如果 `name` 没有转换为小写，`users[name]` 就不存在。

修复方法：

```plaintext
if user = users[name.downcase]
...
elsif group = groups[name.downcase]
...

```

或者更好的做法：在方法开始时将所有名称统一转换为小写，因为该方法中存在不少问题。`groups` 已经很好地处理了 `where("lower(name) IN (?)", @names.map(&:downcase))`，但像 `visible_group_ids_for_allowed_check`、`topic_allowed_group_ids`、`mentionable_group_ids` 和 `members_visible_group_ids` 这样的函数都使用了 `where(name: @names)`，这同样会引入大小写敏感的问题。

---

<div class="post-metadata">

**Author:** ![zogstrip](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/zogstrip/32/512781_2.png) [@zogstrip](https://meta.discourse.org/u/zogstrip)\
**Post date:** [2026年四月23日 08:34 UTC](https://meta.discourse.org/t/uppercase-letters-in-username-break-reachable-mention-check-in-composer/401292/4 "2026-04-23T08:34:26Z")

</div>

正确的修复方案是

> <https://github.com/discourse/discourse/pull/37177>
>
> Leverages Rails' built-in \`normalizes\` feature to handle username
> normalization …consistently throughout the codebase. When you call
> \`User.where(username\_lower: value)\` or \`find\_by(username\_lower: value)\`,
> ActiveRecord now automatically normalizes the input value.
> 
> Key changes:
> 
> \- Adds \`normalizes :username\` (unicode normalize) and
> \`normalizes :username\_lower\` (unicode normalize + downcase) to User
> \- Adds \`normalizes :email\` (strip + downcase) to UserEmail
> \- Removes manual \`.downcase\` and \`.map(&:downcase)\` calls before AR queries
> \- Adds \`User#matches\_username?\` method for comparing usernames
> \- Simplifies \`filter\_by\_username\` and \`filter\_by\_username\_or\_email\` scopes
> to use \`ILIKE ANY(ARRAY\[?\])\` instead of branching on array vs single value
> \- Updates \`filter\_by\_username\` to normalize input for ILIKE patterns
> \- Updates \`find\_by\_username\` to rely on AR normalization
> \- Fixes a SQL injection vulnerability in search user ordering
> \- Uses before\_save callback for username\_lower assignment to ensure it
> runs even when validation is skipped (e.g., finish installation flow)
> \- Adds shoulda matcher tests for username normalizations
> 
> The \`normalize\_username\` class method is kept for cases where AR can't help:
> raw SQL queries, ILIKE patterns, and direct comparisons.
> 
> Ref - https://meta.discourse.org/t/393646

但这个改动太大了，我目前还不太放心合并 😬

因此，我将逐个修复各个“端点”，以便更易于审查并降低风险。

这是第一步：

> <https://github.com/discourse/discourse/pull/39482>
>
> Mentioning \`@SomeUser\` in a topic the mentioned user can't see (restricted categ…ory, PM they aren't invited to, topic they've muted) silently skipped the "cannot see this mention" warning popup whenever the typed name contained any uppercase letter. Same for mixed-case group names. Mention validation itself worked because of an existing client-side \`.toLowerCase()\` workaround, so the bug was easy to miss — mentions stayed \`\<a class="mention"\>\` but the user got no signal that the mentioned account wouldn't actually be notified.
> 
> Server side, \`ComposerController#mentions\` builds its \`users\` lookup keyed by \`username\_lower\` and \`groups\` keyed by the case-preserved DB \`name\`, then iterated \`@names.each { |n| users\[n\] || groups\[n\] }\` without normalizing — so any uppercase character missed both hashes and \`user\_reasons\`/\`group\_reasons\` came back empty. Four downstream group helpers (\`mentionable\_group\_ids\`, \`members\_visible\_group\_ids\`, \`topic\_allowed\_group\_ids\`, \`visible\_group\_ids\_for\_allowed\_check\`) used \`where(name: @names)\` which is case-sensitive in PostgreSQL, and the \`SiteSetting.here\_mention\` membership test compared raw strings.
> 
> Client side, \`link-mentions.js\` cached \`foundUsers\` / \`userReasons\` / \`foundGroups\` / \`groupReasons\` by the case as typed, and the prosemirror \`mention.js\` warning lookup did \`response.users.includes(name)\` plus \`response.user\_reasons\[name\]\` with the original case — both of which the server only ever returned in the casing it had on hand.
> 
> Normalize the controller's \`@names\` and \`@allowed\_names\` once at the top of the action, switch all four group helpers to \`LOWER(name) IN (?)\`, lower-case the response keys, and lower-case client caches and lookups end to end. Also extract the inline notified-member query into \`already\_notified\_member\_count\` and tighten the request specs to cover users, mentionable groups, group reasons in PMs, and \`allowed\_names\` (both user and group branches) with mixed-case input.
> 
> https://meta.discourse.org/t/401292

---

<div class="post-metadata">

**Author:** ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)\
**Post date:** [2026年五月13日 15:03 UTC](https://meta.discourse.org/t/uppercase-letters-in-username-break-reachable-mention-check-in-composer/401292/5 "2026-05-13T15:03:53Z")

</div>

自从基于从 645cb014c0 到 [102c93e2ea](https://github.com/discourse/discourse/commits/102c93e2eaaf857ea39570468e91d40956362e90) 的 188 次提交重新构建 Discourse 后，我注意到 Markdown 编辑器中出现了一个新的回归问题。

这个弹窗没有任何实际意义，但每次我尝试提及我的自定义代理 `@Forum_Research_Assis` 时，它都会出现。

我可以按照视频复现的方式稳定地复现此问题：

我认为 [这个相同的提交](https://github.com/discourse/discourse/commit/9a4cca2962a30ef0ab8e20784b271d045034d9fd) 缺少了当用户提及 AI 代理时进行绕过的逻辑。

如果您准备好了，我很乐意开始另一个拉取请求，不过我在我的唯一 GitHub 账号上已经有一个开放的拉取请求了。

---

<div class="post-metadata">

**Author:** ![zogstrip](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/zogstrip/32/512781_2.png) [@zogstrip](https://meta.discourse.org/u/zogstrip)\
**Post date:** [2026年五月13日 16:25 UTC](https://meta.discourse.org/t/uppercase-letters-in-username-break-reachable-mention-check-in-composer/401292/6 "2026-05-13T16:25:07Z")

</div>

看来我的“修复”暴露了 AI 代理的这个问题🤔

> <https://github.com/discourse/discourse/pull/39986>
>
> Mentioning an AI bot (e.g. \`@Forum\_Research\_Assis\`) in a topic the bot's User re…cord can't see — like a category restricted to a group the bot isn't part of — surfaced the "this user cannot see this mention" warning popup in the composer. The warning is misleading: AI bots reply via \`PostCreator.create!(... skip\_guardian: true)\` (\`playground.rb\`), so they respond regardless of whether their User can \`Guardian#can\_see?\` the topic.
> 
> The previous case-insensitive fix (9a4cca29) exposed this latent behavior. Pre-fix, mixed-case names hit a case-sensitive hash miss in \`ComposerController#mentions\` and silently returned an empty \`user\_reasons\`. Post-fix, the lookup hits correctly and the reachability check — which was always there — now fires for AI bot users that genuinely can't see the topic.
> 
> Adds a \`:composer\_mention\_user\_reason\` plugin modifier, applied after the standard reason is computed in \`user\_reason\`, so plugins can clear or transform it. The AI plugin registers against the modifier and returns \`nil\` for any user in \`DiscourseAi::AiBot::EntryPoint.all\_bot\_ids\` (covering both AI agent users and chat-bot-enabled LLM model users).
> 
> discobot and the system user are intentionally unaffected: discobot's \`PostCreator.create!\` does not skip the guardian, so the reachability warning remains accurate for it.
> 
> https://meta.discourse.org/t/401292

---

<div class="post-metadata">

**Author:** ![zogstrip](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/zogstrip/32/512781_2.png) [@zogstrip](https://meta.discourse.org/u/zogstrip)\
**Post date:** [2026年六月3日 16:06 UTC](https://meta.discourse.org/t/uppercase-letters-in-username-break-reachable-mention-check-in-composer/401292/7 "2026-06-03T16:06:52Z")

</div>


