npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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!