HoshinoBot Trigger Types: A Complete Guide to Prefix, Suffix, Keyword, and Regex

HoshinoBot provides four core trigger classes—PrefixTrigger, SuffixTrigger, KeywordTrigger, and RexTrigger—that match incoming messages based on leading text, trailing text, substring containment, and regular expression patterns respectively.

HoshinoBot, an open-source QQ bot framework maintained in the ice9coffee/hoshinobot repository, routes messages to service functions through a flexible trigger system defined in hoshino/trigger.py. Developers register handlers using decorators exposed by the Service class in hoshino/service.py, allowing precise matching of user commands and natural language input using four distinct matcher types.

The Four Core Trigger Classes

HoshinoBot implements four lightweight matcher classes that form the foundation of its message routing system. Each class specializes in a specific text-matching strategy and exposes a corresponding decorator method on the Service class.

PrefixTrigger (on_prefix)

The PrefixTrigger matches when a message segment begins with a specified string. According to the source code in hoshino/trigger.py (lines 26–65), this trigger stores all registered prefixes in a trie data structure for O(1) lookup efficiency. When a message arrives, the framework extracts the longest matching prefix and routes to the associated handler.

Register a prefix trigger using the @sv.on_prefix() decorator:

from hoshino import Service

sv = Service('example')

@sv.on_prefix('help', 'h')
async def show_help(bot, ev):
    await bot.send(ev, 'Available commands: ...')

The decorator implementation resides in hoshino/service.py (lines 200–213), which instantiates PrefixTrigger objects and adds them to the global trigger chain.

SuffixTrigger (on_suffix)

The SuffixTrigger matches when a message ends with a specific string. As implemented in hoshino/trigger.py (lines 67–106), this trigger reverses the input text and registered suffixes, then performs a trie lookup identical to the prefix mechanism—effectively creating a reversed trie for efficient suffix matching.

Use the @sv.on_suffix() decorator (defined in hoshino/service.py, lines 444–456) to register suffix handlers:

@sv.on_suffix('!', '?')
async def handle_excited(bot, ev):
    await bot.send(ev, 'That sounds exciting!')

KeywordTrigger (on_keyword)

The KeywordTrigger performs substring containment matching, activating when the specified keyword appears anywhere in the message text. Located in hoshino/trigger.py (lines 108–133), this trigger checks all registered keywords against either the raw message or a normalized version depending on the normalize parameter.

The @sv.on_keyword() decorator (lines 558–572 in hoshino/service.py) supports optional text normalization:

@sv.on_keyword('weather', '天气', normalize=True)
async def check_weather(bot, ev):
    await bot.send(ev, 'Current conditions: Sunny')

When normalize=True, the trigger uses hoshino.util.normalize_str to convert full-width characters to half-width, fold case to lowercase, and remove punctuation before matching.

RexTrigger (on_rex)

The RexTrigger provides full regular expression matching capabilities. Defined in hoshino/trigger.py (lines 132–150), this trigger iterates through registered compiled regex patterns and executes search() against the message text. Like keyword triggers, it supports the normalize parameter for case-insensitive and punctuation-insensitive matching.

Register regex triggers via @sv.on_rex() (lines 724–735 in hoshino/service.py):

@sv.on_rex(r'(\d{4})-(\d{2})-(\d{2})', normalize=True)
async def parse_date(bot, ev):
    match = ev['match']  # Access the MatchObject

    year, month, day = match.groups()
    await bot.send(ev, f'Date parsed: {year}/{month}/{day}')

When a regex matches, the MatchObject is injected into the event dictionary under the key 'match', accessible within the handler.

How the Trigger Chain Processes Messages

HoshinoBot evaluates triggers in a strict priority sequence defined at the bottom of hoshino/trigger.py (lines 64–76). When a message arrives, the framework constructs a trigger chain that executes in the following order:

  1. prefix – Longest matching prefix lookup via trie
  2. suffix – Longest matching suffix lookup via reversed trie
  3. _TextNormalizer – Extracts plain text and creates event.norm_text if normalization is required
  4. rex – Iterates through all registered regex patterns
  5. keyword – Checks all keywords against plain or normalized text

Only the first trigger that yields a matching ServiceFunc executes; subsequent triggers in the chain are skipped for that message. This short-circuit behavior ensures that prefix commands take precedence over keyword matches, while regex patterns are evaluated before keyword containment checks.

Implementing Triggers in Service Modules

The Service class in hoshino/service.py provides decorator methods that abstract trigger registration. Each decorator wraps the handler function in a ServiceFunc object (defined in lines 61–71) and registers it with the appropriate trigger class.

Basic Registration Syntax

All four trigger types follow a consistent decorator pattern:

from hoshino import Service

sv = Service('my_service')

# Prefix trigger

@sv.on_prefix('cmd')
async def handle_cmd(bot, ev):
    pass

# Suffix trigger

@sv.on_suffix('!')
async def handle_suffix(bot, ev):
    pass

# Keyword trigger

@sv.on_keyword('trigger_word')
async def handle_keyword(bot, ev):
    pass

# Regex trigger

@sv.on_rex(r'pattern')
async def handle_regex(bot, ev):
    pass

Combining Prefix and Suffix Triggers

Decorators are independent; applying multiple triggers to one function requires both conditions to be met separately (the framework checks each trigger individually):

@sv.on_prefix('calc')
@sv.on_suffix('=')
async def calculate(bot, ev):
    # Handles messages like "calc 2+2 ="

    text = ev.message.extract_plain_text()
    expr = text.strip().removeprefix('calc').removesuffix('=').strip()
    try:
        result = eval(expr)
        await bot.send(ev, f'Result: {result}')
    except:
        await bot.send(ev, 'Invalid expression')

Keyword Matching with Text Normalization

The normalize parameter enables fuzzy matching by preprocessing text through hoshino.util.normalize_str. This converts full-width alphanumeric characters to half-width, transforms Chinese punctuation to ASCII equivalents, and applies Unicode case folding:

@sv.on_keyword('hello', '你好', normalize=True)
async def greet(bot, ev):
    # Matches "Hello!", "HELLO", "hello", etc.

    await bot.send(ev, 'Hi there!')

Regex Pattern Matching with Capture Groups

Regex triggers provide the most flexibility, supporting pattern extraction via capture groups. The matched groups are accessible through ev['match'].groups():

@sv.on_rex(r'(\d+)\s*\+\s*(\d+)', normalize=True)
async def add_numbers(bot, ev):
    a, b = map(int, ev['match'].groups())
    await bot.send(ev, f'{a} + {b} = {a + b}')

Summary

  • PrefixTrigger matches the beginning of messages using a trie structure for efficient O(1) lookups; registered via @sv.on_prefix().
  • SuffixTrigger matches message endings by reversing text and using the same trie mechanism; registered via @sv.on_suffix().
  • KeywordTrigger performs substring containment checks with optional Unicode normalization; registered via @sv.on_keyword().
  • RexTrigger executes regular expression searches with full capture group support; registered via @sv.on_rex().
  • The trigger chain executes in fixed priority: prefix → suffix → text normalizer → regex → keyword, with only the first match firing.
  • All trigger decorators are implemented in hoshino/service.py while the matching logic resides in hoshino/trigger.py.

Frequently Asked Questions

What is the execution priority of HoshinoBot trigger types?

HoshinoBot processes triggers in a fixed chain order defined in hoshino/trigger.py: prefix triggers execute first, followed by suffix triggers, then the text normalizer (if needed), then regex triggers, and finally keyword triggers. Only the first trigger that finds a match executes its handler; subsequent triggers in the chain are ignored for that message.

How does text normalization work with keyword and regex triggers?

When normalize=True is passed to @sv.on_keyword() or @sv.on_rex(), the trigger preprocesses the message using hoshino.util.normalize_str. This function converts full-width characters to half-width, replaces Chinese punctuation with ASCII equivalents, applies Unicode case folding (lowercasing), and removes extra whitespace. The normalized text is stored in event.norm_text while the original remains in event.message.

Can I apply multiple triggers to a single handler function?

Yes. You can stack decorators such as @sv.on_prefix('cmd') and @sv.on_suffix('!') on the same async function. Each decorator registers the function independently with its respective trigger class. The order of decorators does not affect registration, though the handler will be invoked if either trigger condition is met (not both simultaneously).

Where are the trigger classes and decorators defined in the source code?

The four trigger classes (PrefixTrigger, SuffixTrigger, KeywordTrigger, RexTrigger) are defined in hoshino/trigger.py (lines 26–150). The decorator methods (on_prefix, on_suffix, on_keyword, on_rex) that register handlers with these triggers are implemented in hoshino/service.py (lines 200–735). The global trigger chain that orchestrates execution order is constructed at the bottom of hoshino/trigger.py (lines 64–76).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →