support bot, simplex-chat-nodejs: fix bot command parsing (#6964)

ciBotCommand's regex was unanchored, so a customer message like
"follow/read blog posts?" parsed as a /read command and the Grok
handler silently dropped the message. Anchor the regex with ^ so a
command requires `/` at the start of the (trimmed) message.

In the support bot, filter customer command parsing by the registered
keyword set: any unknown `/word` from a customer (e.g. /help) is now
routed as plain text instead of being silently dropped by Grok. This
also makes /grok when Grok is disabled behave consistently as text,
removing the previous ad-hoc workaround.
This commit is contained in:
sh
2026-05-12 09:52:50 +01:00
committed by GitHub
parent 78f987a448
commit 8841c73fb2
5 changed files with 109 additions and 12 deletions
+32 -7
View File
@@ -10,6 +10,22 @@ import {
} from "./messages.js"
import {profileMutex, log, logError, getGroupInfo} from "./util.js"
// Collects the keyword of every "command" entry in the bot's registered
// commands tree, descending into "menu" entries. Used to distinguish real
// commands from arbitrary text that happens to start with `/` (e.g. URLs,
// "/help" the user invented).
function commandKeywords(commands: T.ChatBotCommand[]): Set<string> {
const out = new Set<string>()
const visit = (cmds: T.ChatBotCommand[]): void => {
for (const c of cmds) {
if (c.type === "command") out.add(c.keyword)
else if (c.type === "menu") visit(c.commands)
}
}
visit(commands)
return out
}
// True for any non-terminal status — invited but not yet accepted, through
// connected. Used to decide whether a contact is already in the group so we
// don't trigger a re-invite (the SimpleX API resends the invitation for a
@@ -62,6 +78,11 @@ export class SupportBot {
// send to each group.
private syncedGroups = new Set<number>()
// Keywords from desiredCommands. A customer message is treated as a
// command only when its parsed keyword is in this set; anything else
// (URLs, "/help", arbitrary slashes) is routed as plain text.
private readonly customerKeywords: ReadonlySet<string>
constructor(
private chat: api.ChatApi,
private grokApi: GrokApiClient | null,
@@ -71,6 +92,12 @@ export class SupportBot {
private desiredCommands: T.ChatBotCommand[],
) {
this.cards = new CardManager(chat, config, mainUserId, config.cardFlushSeconds * 1000)
this.customerKeywords = commandKeywords(desiredCommands)
}
private customerCommand(chatItem: T.ChatItem): util.BotCommand | undefined {
const cmd = util.ciBotCommand(chatItem)
return cmd && this.customerKeywords.has(cmd.keyword) ? cmd : undefined
}
private get grokEnabled(): boolean {
@@ -357,7 +384,7 @@ export class SupportBot {
if (chatInfo.type !== "group") continue
if (chatItem.chatDir.type !== "groupRcv") continue
if (!util.ciContentText(chatItem)?.trim()) continue
if (util.ciBotCommand(chatItem)) continue
if (this.customerCommand(chatItem)) continue
const bc = chatInfo.groupInfo.businessChat
if (!bc) continue
if (chatItem.chatDir.groupMember.memberId !== bc.customerId) continue
@@ -444,9 +471,7 @@ export class SupportBot {
// 8. Customer message → derive state and dispatch
const state = await this.cards.deriveState(groupId)
const rawCmd = util.ciBotCommand(chatItem)
// When Grok is disabled, ignore /grok so it behaves like an unknown command
const cmd = rawCmd?.keyword === "grok" && !this.grokEnabled ? null : rawCmd
const cmd = this.customerCommand(chatItem)
const text = util.ciContentText(chatItem)?.trim() || null
switch (state) {
@@ -547,7 +572,7 @@ export class SupportBot {
if (!text) return // ignore non-text
// Ignore bot commands
if (util.ciBotCommand(chatItem)) return
if (this.customerCommand(chatItem)) return
// Only respond in business groups (survives restart without in-memory maps)
const bc = groupInfo.businessChat
@@ -569,7 +594,7 @@ export class SupportBot {
history.push({role: "assistant", content: histText})
} else if (histCi.chatDir.type === "groupRcv"
&& histCi.chatDir.groupMember.memberId === bc.customerId
&& !util.ciBotCommand(histCi)) {
&& !this.customerCommand(histCi)) {
history.push({role: "user", content: histText})
}
}
@@ -706,7 +731,7 @@ export class SupportBot {
if (ci.chatDir.type !== "groupRcv") continue
if (!grokBc || ci.chatDir.groupMember.memberId !== grokBc.customerId) continue
const t = util.ciContentText(ci)?.trim()
if (t && !util.ciBotCommand(ci)) customerMessages.push(t)
if (t && !this.customerCommand(ci)) customerMessages.push(t)
}
if (customerMessages.length === 0) {