@anura-bot/dispatcher
v1.0.1
Published
dispatch user message to command handlers.
Readme
@anura-bot/dispatcher
一个平台无关的聊天机器人命令分发器,提供类型安全的命令注册和调度能力。
特性
- 平台无关 - 适配任何聊天平台(Discord、Telegram、QQ 等)
- 类型安全 - 完整的 TypeScript 类型推断,编译时即可发现错误
- 参数验证 - 基于 Zod v4 的运行时参数校验和转换
- 作用域隔离 - 支持多环境命令管理(群聊、私聊等)
- 错误处理 - 统一的错误分类体系,便于错误处理和调试
- 元数据查询 - 轻松生成帮助文档和命令列表
- 链式调用 - 流畅的 API 设计,代码更优雅
安装
npm install @anura-bot/dispatcher zod核心概念
Scope(作用域)
Scope 用于区分不同的聊天环境。例如:
public- 群聊消息private- 私聊消息admin- 管理员环境
每个 scope 可以关联不同的上下文类型,同一命令在不同 scope 可以有不同的实现。
Context(上下文)
Context 是平台提供的消息上下文对象,包含发送者信息、回复方法等。Dispatcher 保证类型安全地传递给命令处理器。
Command(命令)
Command 是处理用户请求的核心单元,包括:
- 命令名称
- 参数定义(通过 Zod schema)
- 处理函数
- 可选的描述信息
Parameters(参数)
通过 Zod schema 定义参数类型,Dispatcher 自动完成:
- 运行时类型验证
- 类型转换
- 编译时类型推断
设计原则
职责边界
Dispatcher 专注于命令调度和参数验证,不负责参数解析。这是一个重要的设计决策:
Dispatcher 的职责
- ✅ 接收已解析的键值对参数(如
{ message: "hello" }) - ✅ 使用 Zod schema 验证参数类型
- ✅ 调度到对应的命令处理器
平台适配层的职责
- ✅ 解析用户原始输入(如
"/echo hello world") - ✅ 提取命令名称和参数
- ✅ 将顺序参数转换为键值对(如
"hello world"→{ message: "hello world" })
为什么这样设计?
解除耦合:不同平台有不同的参数格式:
- Discord:可能使用空格分隔
- Telegram:可能使用特殊语法
- Web 界面:可能直接提供表单数据
Dispatcher 保持平台无关,将解析逻辑留给适配层,实现更好的灵活性和可测试性。
快速开始
import { string } from "zod/v4";
import { dispatcher } from "@anura-bot/dispatcher";
// 定义消息上下文类型
type PublicMessage = {
sender: { id: string; name: string };
message: string;
respond(message: string): Promise<void>;
};
// 创建 dispatcher 实例
const app = dispatcher()
.scope("public")
.context<PublicMessage>()
.command(
"public",
"echo",
({ args: { message }, ctx: { respond } }) => respond(message),
[
{
name: "message",
display: "消息",
zod: string(),
},
],
"回显消息"
);
// 调用命令
await app.invoke(
"public",
"echo",
{
sender: { id: "user_123", name: "Alice" },
message: "/echo Hello",
async respond(msg) {
console.log(msg); // 输出: "Hello"
},
},
{ message: "Hello" }
);详细使用示例
多作用域支持
同一命令可注册到多个 scope:
type GroupMessage = {
groupId: string;
sender: User;
respond(msg: string): Promise<void>;
};
type PrivateMessage = {
sender: User;
respond(msg: string): Promise<void>;
};
const app = dispatcher()
.scope("group")
.context<GroupMessage>()
.scope("private")
.context<PrivateMessage>()
.command(
["group", "private"], // 注册到多个 scope
"help",
({ ctx }) => ctx.respond("这是帮助信息"),
[],
"显示帮助信息"
);多参数命令
import { string, number } from "zod/v4";
app.command(
"public",
"repeat",
({ args: { text, times }, ctx }) => {
const result = text.repeat(times);
return ctx.respond(result);
},
[
{
name: "text",
display: "文本",
description: "要重复的文本内容",
zod: string(),
},
{
name: "times",
display: "次数",
description: "重复次数",
zod: number().int().positive(),
},
],
"重复输出文本"
);可选参数
import { string, optional } from "zod/v4";
app.command(
"public",
"greet",
({ args: { name }, ctx }) => {
const greeting = name ? `Hello, ${name}!` : "Hello!";
return ctx.respond(greeting);
},
[
{
name: "name",
display: "名称",
description: "可选的用户名",
zod: optional(string()),
},
],
"打招呼"
);复杂参数类型
import { object, string, array } from "zod/v4";
app.command(
"admin",
"bulkBan",
({ args: { users, reason }, ctx }) => {
users.forEach((userId) => {
console.log(`Banning user ${userId} for: ${reason}`);
});
return ctx.respond(`Banned ${users.length} users`);
},
[
{
name: "users",
display: "用户列表",
zod: array(string()),
},
{
name: "reason",
display: "封禁理由",
zod: string(),
},
],
"批量封禁用户"
);查询命令元数据
用于生成帮助文档:
// 查询单个命令
const cmdInfo = app.query("public", "echo");
console.log(cmdInfo);
// {
// name: "echo",
// params: [{ name: "message", display: "消息", description: undefined }],
// description: "回显消息"
// }
// 查询所有命令
const allCommands = app.queryAll("public");
allCommands.forEach((cmd) => {
console.log(`/${cmd.name} - ${cmd.description}`);
});动态生成帮助命令
app.command(
["public", "private"],
"help",
({ scope, ctx }) => {
const commands = app.queryAll(scope);
const helpText = commands
.map((cmd) => {
const params = cmd.params.map((p) => `<${p.display || p.name}>`).join(" ");
return `/${cmd.name} ${params} - ${cmd.description || "无描述"}`;
})
.join("\n");
return ctx.respond(helpText);
},
[],
"显示帮助信息"
);API 文档
dispatcher()
创建一个新的 dispatcher 实例。
const app = dispatcher();.scope(name)
注册一个新的作用域。
参数:
name- 作用域名称
**返回:**包含 context() 方法的对象
app.scope("public");.context<T>()
为上一个作用域指定上下文类型。
类型参数:
T- 上下文类型
app.scope("public").context<PublicMessage>();.command(scopeNames, name, handler, params, description?)
注册一个命令。
参数:
scopeNames- 单个或多个作用域名称name- 命令名称handler- 命令处理函数params- 参数定义数组description- 可选的命令描述
Handler 函数参数:
{
command: string; // 命令名称
args: ParamsRecord; // 解析后的参数对象
scope: ScopeName; // 执行的作用域
ctx: Context; // 上下文对象
}参数定义格式:
{
name: string; // 参数名称
display?: string; // 显示名称(用于帮助文档)
description?: string; // 参数描述
zod: ZodType; // Zod schema
}.invoke(scope, name, ctx, args)
类型安全的命令调用(编译时检查命令和参数)。
参数:
scope- 作用域名称name- 命令名称ctx- 上下文对象args- 参数对象
await app.invoke("public", "echo", context, { message: "test" });.parse(scope, name, ctx, args)
无类型推断的命令调用(用于处理用户输入)。
await app.parse("public", "echo", context, userInput);.query(scope, command)
查询单个命令的元数据。
**返回:**命令元数据对象,或 undefined(未找到)
const info = app.query("public", "echo");.queryAll(scope)
查询作用域内所有命令的元数据。
**返回:**命令元数据数组
const commands = app.queryAll("public");.queryScopeNames()
获取所有注册的作用域名称。
**返回:**作用域名称数组
const scopes = app.queryScopeNames(); // ["public", "private"]错误处理
Dispatcher 提供三种错误类型:
CommandUnavailableError
命令不可用(未注册或作用域不匹配)。
import { CommandUnavailableError } from "@anura-bot/dispatcher";
try {
await app.invoke("public", "nonexistent", ctx, {});
} catch (e) {
if (e instanceof CommandUnavailableError) {
console.log("命令不存在");
}
}CommandArgumentUnmatchedError
命令参数不匹配(类型错误或缺失必需参数)。
import { CommandArgumentUnmatchedError } from "@anura-bot/dispatcher";
try {
await app.invoke("public", "repeat", ctx, { text: "hi", times: "abc" });
} catch (e) {
if (e instanceof CommandArgumentUnmatchedError) {
console.log("参数格式错误");
}
}CommandExecutionError
命令处理器执行过程中抛出错误。
import { CommandExecutionError } from "@anura-bot/dispatcher";
try {
await app.invoke("public", "buggy", ctx, {});
} catch (e) {
if (e instanceof CommandExecutionError) {
console.log("命令执行失败:", e.error);
}
}完整错误处理示例
async function handleUserCommand(scope, name, ctx, args) {
try {
await app.parse(scope, name, ctx, args);
} catch (e) {
if (e instanceof CommandUnavailableError) {
await ctx.respond("未知命令,输入 /help 查看可用命令");
} else if (e instanceof CommandArgumentUnmatchedError) {
await ctx.respond("参数格式错误,请检查输入");
} else if (e instanceof CommandExecutionError) {
await ctx.respond("命令执行失败,请稍后重试");
console.error("Handler error:", e.error);
} else {
throw e; // 未预期的错误
}
}
}实际应用场景
集成到 Discord Bot
import { Client, Message } from "discord.js";
import { dispatcher } from "@anura-bot/dispatcher";
import { string } from "zod/v4";
type DiscordContext = {
message: Message;
respond(text: string): Promise<void>;
};
const app = dispatcher()
.scope("discord")
.context<DiscordContext>()
.command(
"discord",
"ping",
({ ctx }) => ctx.respond("Pong!"),
[],
"测试机器人响应"
);
const client = new Client({ intents: ["Guilds", "GuildMessages"] });
client.on("messageCreate", async (message) => {
if (!message.content.startsWith("/")) return;
const [cmd, ...argParts] = message.content.slice(1).split(" ");
const args = parseArgs(argParts); // 自定义参数解析
await app.parse(
"discord",
cmd,
{
message,
respond: (text) => message.reply(text),
},
args
);
});集成到 Telegram Bot
import TelegramBot from "node-telegram-bot-api";
import { dispatcher } from "@anura-bot/dispatcher";
type TelegramContext = {
chatId: number;
userId: number;
respond(text: string): Promise<void>;
};
const app = dispatcher()
.scope("telegram")
.context<TelegramContext>()
.command(
"telegram",
"start",
({ ctx }) => ctx.respond("欢迎使用机器人!"),
[],
"开始使用"
);
const bot = new TelegramBot(TOKEN, { polling: true });
bot.onText(/\/(.+)/, async (msg, match) => {
const cmd = match[1].split(" ")[0];
const args = {}; // 解析参数
await app.parse(
"telegram",
cmd,
{
chatId: msg.chat.id,
userId: msg.from.id,
respond: (text) => bot.sendMessage(msg.chat.id, text),
},
args
);
});类型定义
BotRequest<Context, ParamsRecord, ScopeName>
传递给命令处理器的请求对象。
type BotRequest<Context, ParamsRecord, ScopeName> = {
command: string; // 命令名称
args: ParamsRecord; // 参数对象(已验证和转换)
scope: ScopeName; // 执行的作用域
ctx: Context; // 上下文对象
};Param
参数定义对象。
type Param = {
zod: ZodType; // Zod schema
name: string; // 参数名称
display?: string; // 显示名称
description?: string; // 参数描述
};许可证
MIT
贡献
欢迎提交 Issue 和 Pull Request!
