How AionUi ActionExecutor Routes Messages to System, Platform, and Chat Handlers
AionUi's ActionExecutor acts as a centralized gateway that inspects every incoming unified message and routes it to the appropriate System, Platform, or Chat handler based on the message's action field, content type, and payload structure.
The ActionExecutor class in the AionUi repository (iOfficeAI/AionUi) is the core routing engine that bridges channel plugins (Telegram, Lark, DingTalk) with the application's business logic. By analyzing the structure of each IUnifiedIncomingMessage, the executor determines whether to invoke a registered action handler, stream an AI chat response, or execute platform-specific commands like device pairing.
The Routing Gateway: ActionExecutor Architecture
ActionExecutor maintains a registry-based routing system that categorizes handlers into three distinct groups:
| Handler Category | Responsibility | Source Module |
|---|---|---|
| System Actions | Session management, settings, help menus | src/channels/actions/SystemActions.ts |
| Chat Actions | AI message streaming, regeneration, copying, tool confirmation | src/channels/actions/ChatActions.ts |
| Platform Actions | Device pairing, authentication, platform-specific UI flows | src/channels/actions/PlatformActions.ts |
The routing decision occurs in handleIncomingMessage() (lines 27-48 of ActionExecutor.ts), which examines the message structure and delegates to either executeAction() for registered handlers or handleChatMessage() for AI streaming.
Action Registration and the actionRegistry
Before routing can occur, ActionExecutor populates its actionRegistry (a Map<string, IRegisteredAction>) during initialization via registerActions() (lines 86-96):
// ActionExecutor.registerActions()
private registerActions(): void {
// System actions (session, help, settings, …)
for (const action of systemActions) {
this.actionRegistry.set(action.name, action);
}
// Chat actions (send, regenerate, continue, copy, tool confirm)
for (const action of chatActions) {
this.actionRegistry.set(action.name, action);
}
// Platform actions (pairing flow)
for (const action of platformActions) {
this.actionRegistry.set(action.name, action);
}
}
Each action module exports an array of IRegisteredAction objects containing the action name, category, description, and handler function. For example, SystemActions.ts (lines 71-73) exports systemActions, while ChatActions.ts (lines 13-15) exports chatActions and PlatformActions.ts (lines 94-96) exports platformActions.
Message Routing Logic in handleIncomingMessage
The core routing algorithm inspects the IUnifiedIncomingMessage structure to determine the appropriate handler path:
private async handleIncomingMessage(message: IUnifiedIncomingMessage): Promise<void> {
const { platform, chatId, user, content, action } = message;
const plugin = this.getPluginForMessage(message);
// …pairing / authorization checks…
// Build the context that handlers will receive
const context: IActionContext = {
platform,
pluginId: `${platform}_default`,
userId: user.id,
chatId,
displayName: user.displayName,
originalMessage: message,
originalMessageId: message.id,
sendMessage: async (msg) => plugin.sendMessage(chatId, msg),
editMessage: async (msgId, msg) => plugin.editMessage(chatId, msgId, msg),
};
// ---------- ROUTING ----------
if (action) {
// Button‑press action (explicit name supplied)
await this.executeAction(context, action.name, action.params);
} else if (content.type === 'action') {
// Action encoded in the text payload
await this.executeAction(context, content.text, {});
} else if (content.type === 'text' && content.text) {
// Regular user message → AI chat flow
await this.handleChatMessage(context, content.text);
} else {
// Fallback for unsupported payloads
await context.sendMessage({ … });
}
}
The routing logic prioritizes explicit actions (button presses) over embedded actions, and routes plain text messages to the AI chat stream. Platform-specific commands like /start are handled via authorization checks before this routing block executes.
Executing Registered Actions via executeAction
When executeAction() is invoked (lines 60-71), it performs a registry lookup and invokes the corresponding handler:
private async executeAction(context: IActionContext, actionName: string, params?: Record<string, string>): Promise<void> {
const action = this.actionRegistry.get(actionName);
if (!action) {
await context.sendMessage({ …unknown‑action reply… });
return;
}
try {
const result = await action.handler(context, params);
if (result.message) {
await context.sendMessage(result.message);
}
} catch (error) {
await context.sendMessage({ …error reply… });
}
}
The handler is the function exported from one of the three action modules. For example, the session.new system action maps to handleSessionNew in SystemActions.ts, while chat.regenerate maps to handleRegenerate in ChatActions.ts.
Specialized Handler Categories
System Actions
System actions manage application state and configuration. Defined in src/channels/actions/SystemActions.ts, these handlers manage sessions, display help menus, and handle settings. They typically return immediate text responses or interactive keyboards rather than streaming content.
Chat Actions
Chat actions handle AI interaction flows. Located in src/channels/actions/ChatActions.ts, this category includes chat.send, chat.regenerate, chat.continue, chat.copy, and chat.tool_confirm. While these handlers return placeholder responses ("🔄 Regenerating…"), the actual AI streaming occurs in handleChatMessage() (lines 92-115), which invokes ChannelMessageService.sendMessage() to stream Gemini responses and convert them via convertTMessageToOutgoing().
Platform Actions
Platform actions manage platform-specific workflows like device pairing and authentication. Defined in src/channels/actions/PlatformActions.ts, these handlers return rich UI components tailored to each platform (Telegram, Lark, DingTalk). For example, handlePairingShow generates pairing codes with platform-specific markup:
export const handlePairingShow: ActionHandler = async (context) => {
const pairingService = getPairingService();
if (pairingService.isUserAuthorized(context.userId, context.platform)) {
return createSuccessResponse({ …authorized reply…, replyMarkup: getMainMenuMarkup(context.platform) });
}
const { code, expiresAt } = await pairingService.generatePairingCode(...);
return createSuccessResponse({
type: 'text',
text: `🔗 Device Pairing … <code>${code}</code>`,
parseMode: 'HTML',
replyMarkup: getPairingCodeMarkup(context.platform, code),
});
};
Helper functions like getMainMenuMarkup and getPairingCodeMarkup (lines 24-48 of the same file) select the appropriate UI components for each platform.
Summary
- ActionExecutor serves as the central gateway in
src/channels/gateway/ActionExecutor.ts, receiving all unified messages from channel plugins and routing them to the appropriate handler category. - Three handler categories—System, Chat, and Platform—are registered during initialization via
registerActions()(lines 86-96), which populates theactionRegistryMap with handlers fromSystemActions.ts,ChatActions.ts, andPlatformActions.ts. - Routing logic in
handleIncomingMessage()(lines 27-48) inspects the message structure: explicitactionfields route toexecuteAction(), embedded action content routes similarly, plain text routes tohandleChatMessage()for AI streaming, and platform commands are handled pre-routing. - Execution occurs in
executeAction()(lines 60-71), which looks up the handler by name in the registry and invokes it with the constructedIActionContext. - Chat handling special case: While chat actions like
chat.regenerateare registered handlers, the actual AI streaming happens inhandleChatMessage()(lines 92-115) viaChannelMessageService.sendMessage(), with action buttons attached after streaming completes.
Frequently Asked Questions
How does ActionExecutor decide between System, Platform, and Chat handlers?
ActionExecutor does not explicitly branch by category. Instead, it looks up the action name in the unified actionRegistry (populated during registerActions()). The category metadata (system, chat, platform) is stored on the IRegisteredAction object for organizational purposes, but the routing decision in executeAction() is purely name-based. The actual categorization matters during registration (which file exports the handler) and for UI organization, not for runtime routing logic.
What happens when a user sends plain text without pressing a button?
When handleIncomingMessage() detects content.type === 'text' and no explicit action field, it routes to handleChatMessage() (lines 92-115). This method constructs an IActionContext with streaming callbacks, then calls ChannelMessageService.sendMessage() to initiate the AI stream. As tokens arrive, they are converted to platform-specific formats via convertTMessageToOutgoing(). Once the stream completes, the executor attaches chat action buttons (regenerate, copy, continue) using getResponseActionsMarkup().
Can custom actions be added without modifying ActionExecutor.ts?
Yes. To add a custom action, you only need to modify the appropriate actions module (SystemActions.ts, ChatActions.ts, or PlatformActions.ts). Define your handler function conforming to the ActionHandler type, then add an entry to the exported array (e.g., chatActions). The ActionExecutor constructor automatically calls registerActions(), which iterates these arrays and populates the registry. No changes to ActionExecutor.ts are required unless you are creating an entirely new handler category.
How does the pairing flow work for new users?
Platform-specific pairing commands (like /start on Telegram) are handled as special cases before the main routing logic in handleIncomingMessage(). If the user is not authorized, the executor routes to PlatformActions handlers (e.g., handlePairingShow in PlatformActions.ts lines 79-92). This handler generates a pairing code via PairingService.generatePairingCode() and returns a platform-specific UI component (using getPairingCodeMarkup() lines 24-48) that renders appropriately for Lark, DingTalk, or Telegram. Once paired, subsequent messages route normally to System or Chat handlers.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →