{"_id":"@anura-bot/dispatcher","_rev":"2-01bd40b28cb51a505f2a474b0ecd6cbb","name":"@anura-bot/dispatcher","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@anura-bot/dispatcher","version":"1.0.0","_id":"@anura-bot/dispatcher@1.0.0","maintainers":[{"name":"tunafish2k","email":"lyt080529@163.com"}],"dist":{"shasum":"a5fd9956a9b08685d6612cf51b72248bf0c0473d","tarball":"https://registry.npmjs.org/@anura-bot/dispatcher/-/dispatcher-1.0.0.tgz","fileCount":9,"integrity":"sha512-C3bT54aR0pX7q+5FzlVRVQTBs+VgmhxgJNtyiX70rQLrik0grA2rTGhNMXm4CVHytb6T6hvsnobZV4+Ssqilfg==","signatures":[{"sig":"MEQCIFYGkE5E/gg7orVtHtFnEgPfmKuugV6bBGddH4/km4MkAiA+pvwYxNWCZmLXAXHfRBBGkn7W8bkOZ6HLZ1MFPReoHA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":50614},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"8086cedfb680ceae8e925b0d795353f09d0323d6","scripts":{"build":"tsc"},"_npmUser":{"name":"tunafish2k","email":"lyt080529@163.com"},"_npmVersion":"11.6.0","description":"dispatch user message to command handlers.","directories":{},"_nodeVersion":"24.9.0","dependencies":{"zod":"^4.1.13"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.9.3","@types/node":"^25.0.3"},"_npmOperationalInternal":{"tmp":"tmp/dispatcher_1.0.0_1766412381616_0.33665121103763496","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@anura-bot/dispatcher","description":"dispatch user message to command handlers.","version":"1.0.1","type":"commonjs","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc"},"dependencies":{"zod":"^4.1.13"},"devDependencies":{"@types/node":"^25.0.3","typescript":"^5.9.3"},"_id":"@anura-bot/dispatcher@1.0.1","gitHead":"796d2bf33806343e0a58729b050899d05fe12224","_nodeVersion":"24.9.0","_npmVersion":"11.6.0","dist":{"integrity":"sha512-8eYb2GhI02fE0ev9t7KOOTtqo/ZmxPtvGIkFzUAPA7OTfw8vbBDfNs60u5YS/GjLjNeP08fZWn4znFKgWEbx3g==","shasum":"1a0eb5cf69bd9850dd4580aa4fe77ea743afa17d","tarball":"https://registry.npmjs.org/@anura-bot/dispatcher/-/dispatcher-1.0.1.tgz","fileCount":14,"unpackedSize":36776,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDjnIixE5MZnAKjThivq4JRD74jLUbFH4p2lGIfFPhwJAIhALWCOqdmCXPIrn69FYcTTOEbsnUdXahEOdk3razTYGPY"}]},"_npmUser":{"name":"tunafish2k","email":"lyt080529@163.com"},"directories":{},"maintainers":[{"name":"tunafish2k","email":"lyt080529@163.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dispatcher_1.0.1_1766413498961_0.8683763193586163"},"_hasShrinkwrap":false}},"time":{"created":"2025-12-22T14:06:21.393Z","modified":"2025-12-22T14:24:59.332Z","1.0.0":"2025-12-22T14:06:21.776Z","1.0.1":"2025-12-22T14:24:59.126Z"},"description":"dispatch user message to command handlers.","maintainers":[{"name":"tunafish2k","email":"lyt080529@163.com"}],"readme":"# @anura-bot/dispatcher\n\n一个平台无关的聊天机器人命令分发器，提供类型安全的命令注册和调度能力。\n\n## 特性\n\n- **平台无关** - 适配任何聊天平台（Discord、Telegram、QQ 等）\n- **类型安全** - 完整的 TypeScript 类型推断，编译时即可发现错误\n- **参数验证** - 基于 Zod v4 的运行时参数校验和转换\n- **作用域隔离** - 支持多环境命令管理（群聊、私聊等）\n- **错误处理** - 统一的错误分类体系，便于错误处理和调试\n- **元数据查询** - 轻松生成帮助文档和命令列表\n- **链式调用** - 流畅的 API 设计，代码更优雅\n\n## 安装\n\n```bash\nnpm install @anura-bot/dispatcher zod\n```\n\n## 核心概念\n\n### Scope（作用域）\n\nScope 用于区分不同的聊天环境。例如：\n- `public` - 群聊消息\n- `private` - 私聊消息\n- `admin` - 管理员环境\n\n每个 scope 可以关联不同的上下文类型，同一命令在不同 scope 可以有不同的实现。\n\n### Context（上下文）\n\nContext 是平台提供的消息上下文对象，包含发送者信息、回复方法等。Dispatcher 保证类型安全地传递给命令处理器。\n\n### Command（命令）\n\nCommand 是处理用户请求的核心单元，包括：\n- 命令名称\n- 参数定义（通过 Zod schema）\n- 处理函数\n- 可选的描述信息\n\n### Parameters（参数）\n\n通过 Zod schema 定义参数类型，Dispatcher 自动完成：\n- 运行时类型验证\n- 类型转换\n- 编译时类型推断\n\n## 设计原则\n\n### 职责边界\n\n**Dispatcher 专注于命令调度和参数验证**，不负责参数解析。这是一个重要的设计决策：\n\n#### Dispatcher 的职责\n- ✅ 接收**已解析的键值对参数**（如 `{ message: \"hello\" }`）\n- ✅ 使用 Zod schema 验证参数类型\n- ✅ 调度到对应的命令处理器\n\n#### 平台适配层的职责\n- ✅ 解析用户原始输入（如 `\"/echo hello world\"`）\n- ✅ 提取命令名称和参数\n- ✅ 将**顺序参数转换为键值对**（如 `\"hello world\"` → `{ message: \"hello world\" }`）\n\n#### 为什么这样设计？\n\n**解除耦合**：不同平台有不同的参数格式：\n- Discord：可能使用空格分隔\n- Telegram：可能使用特殊语法\n- Web 界面：可能直接提供表单数据\n\nDispatcher 保持平台无关，将解析逻辑留给适配层，实现更好的灵活性和可测试性。\n\n## 快速开始\n\n```typescript\nimport { string } from \"zod/v4\";\nimport { dispatcher } from \"@anura-bot/dispatcher\";\n\n// 定义消息上下文类型\ntype PublicMessage = {\n  sender: { id: string; name: string };\n  message: string;\n  respond(message: string): Promise<void>;\n};\n\n// 创建 dispatcher 实例\nconst app = dispatcher()\n  .scope(\"public\")\n  .context<PublicMessage>()\n  .command(\n    \"public\",\n    \"echo\",\n    ({ args: { message }, ctx: { respond } }) => respond(message),\n    [\n      {\n        name: \"message\",\n        display: \"消息\",\n        zod: string(),\n      },\n    ],\n    \"回显消息\"\n  );\n\n// 调用命令\nawait app.invoke(\n  \"public\",\n  \"echo\",\n  {\n    sender: { id: \"user_123\", name: \"Alice\" },\n    message: \"/echo Hello\",\n    async respond(msg) {\n      console.log(msg); // 输出: \"Hello\"\n    },\n  },\n  { message: \"Hello\" }\n);\n```\n\n## 详细使用示例\n\n### 多作用域支持\n\n同一命令可注册到多个 scope：\n\n```typescript\ntype GroupMessage = {\n  groupId: string;\n  sender: User;\n  respond(msg: string): Promise<void>;\n};\n\ntype PrivateMessage = {\n  sender: User;\n  respond(msg: string): Promise<void>;\n};\n\nconst app = dispatcher()\n  .scope(\"group\")\n  .context<GroupMessage>()\n  .scope(\"private\")\n  .context<PrivateMessage>()\n  .command(\n    [\"group\", \"private\"], // 注册到多个 scope\n    \"help\",\n    ({ ctx }) => ctx.respond(\"这是帮助信息\"),\n    [],\n    \"显示帮助信息\"\n  );\n```\n\n### 多参数命令\n\n```typescript\nimport { string, number } from \"zod/v4\";\n\napp.command(\n  \"public\",\n  \"repeat\",\n  ({ args: { text, times }, ctx }) => {\n    const result = text.repeat(times);\n    return ctx.respond(result);\n  },\n  [\n    {\n      name: \"text\",\n      display: \"文本\",\n      description: \"要重复的文本内容\",\n      zod: string(),\n    },\n    {\n      name: \"times\",\n      display: \"次数\",\n      description: \"重复次数\",\n      zod: number().int().positive(),\n    },\n  ],\n  \"重复输出文本\"\n);\n```\n\n### 可选参数\n\n```typescript\nimport { string, optional } from \"zod/v4\";\n\napp.command(\n  \"public\",\n  \"greet\",\n  ({ args: { name }, ctx }) => {\n    const greeting = name ? `Hello, ${name}!` : \"Hello!\";\n    return ctx.respond(greeting);\n  },\n  [\n    {\n      name: \"name\",\n      display: \"名称\",\n      description: \"可选的用户名\",\n      zod: optional(string()),\n    },\n  ],\n  \"打招呼\"\n);\n```\n\n### 复杂参数类型\n\n```typescript\nimport { object, string, array } from \"zod/v4\";\n\napp.command(\n  \"admin\",\n  \"bulkBan\",\n  ({ args: { users, reason }, ctx }) => {\n    users.forEach((userId) => {\n      console.log(`Banning user ${userId} for: ${reason}`);\n    });\n    return ctx.respond(`Banned ${users.length} users`);\n  },\n  [\n    {\n      name: \"users\",\n      display: \"用户列表\",\n      zod: array(string()),\n    },\n    {\n      name: \"reason\",\n      display: \"封禁理由\",\n      zod: string(),\n    },\n  ],\n  \"批量封禁用户\"\n);\n```\n\n### 查询命令元数据\n\n用于生成帮助文档：\n\n```typescript\n// 查询单个命令\nconst cmdInfo = app.query(\"public\", \"echo\");\nconsole.log(cmdInfo);\n// {\n//   name: \"echo\",\n//   params: [{ name: \"message\", display: \"消息\", description: undefined }],\n//   description: \"回显消息\"\n// }\n\n// 查询所有命令\nconst allCommands = app.queryAll(\"public\");\nallCommands.forEach((cmd) => {\n  console.log(`/${cmd.name} - ${cmd.description}`);\n});\n```\n\n### 动态生成帮助命令\n\n```typescript\napp.command(\n  [\"public\", \"private\"],\n  \"help\",\n  ({ scope, ctx }) => {\n    const commands = app.queryAll(scope);\n    const helpText = commands\n      .map((cmd) => {\n        const params = cmd.params.map((p) => `<${p.display || p.name}>`).join(\" \");\n        return `/${cmd.name} ${params} - ${cmd.description || \"无描述\"}`;\n      })\n      .join(\"\\n\");\n    return ctx.respond(helpText);\n  },\n  [],\n  \"显示帮助信息\"\n);\n```\n\n## API 文档\n\n### `dispatcher()`\n\n创建一个新的 dispatcher 实例。\n\n```typescript\nconst app = dispatcher();\n```\n\n### `.scope(name)`\n\n注册一个新的作用域。\n\n**参数：**\n- `name` - 作用域名称\n\n**返回：**包含 `context()` 方法的对象\n\n```typescript\napp.scope(\"public\");\n```\n\n### `.context<T>()`\n\n为上一个作用域指定上下文类型。\n\n**类型参数：**\n- `T` - 上下文类型\n\n```typescript\napp.scope(\"public\").context<PublicMessage>();\n```\n\n### `.command(scopeNames, name, handler, params, description?)`\n\n注册一个命令。\n\n**参数：**\n- `scopeNames` - 单个或多个作用域名称\n- `name` - 命令名称\n- `handler` - 命令处理函数\n- `params` - 参数定义数组\n- `description` - 可选的命令描述\n\n**Handler 函数参数：**\n```typescript\n{\n  command: string;    // 命令名称\n  args: ParamsRecord; // 解析后的参数对象\n  scope: ScopeName;   // 执行的作用域\n  ctx: Context;       // 上下文对象\n}\n```\n\n**参数定义格式：**\n```typescript\n{\n  name: string;           // 参数名称\n  display?: string;       // 显示名称（用于帮助文档）\n  description?: string;   // 参数描述\n  zod: ZodType;          // Zod schema\n}\n```\n\n### `.invoke(scope, name, ctx, args)`\n\n类型安全的命令调用（编译时检查命令和参数）。\n\n**参数：**\n- `scope` - 作用域名称\n- `name` - 命令名称\n- `ctx` - 上下文对象\n- `args` - 参数对象\n\n```typescript\nawait app.invoke(\"public\", \"echo\", context, { message: \"test\" });\n```\n\n### `.parse(scope, name, ctx, args)`\n\n无类型推断的命令调用（用于处理用户输入）。\n\n```typescript\nawait app.parse(\"public\", \"echo\", context, userInput);\n```\n\n### `.query(scope, command)`\n\n查询单个命令的元数据。\n\n**返回：**命令元数据对象，或 `undefined`（未找到）\n\n```typescript\nconst info = app.query(\"public\", \"echo\");\n```\n\n### `.queryAll(scope)`\n\n查询作用域内所有命令的元数据。\n\n**返回：**命令元数据数组\n\n```typescript\nconst commands = app.queryAll(\"public\");\n```\n\n### `.queryScopeNames()`\n\n获取所有注册的作用域名称。\n\n**返回：**作用域名称数组\n\n```typescript\nconst scopes = app.queryScopeNames(); // [\"public\", \"private\"]\n```\n\n## 错误处理\n\nDispatcher 提供三种错误类型：\n\n### `CommandUnavailableError`\n\n命令不可用（未注册或作用域不匹配）。\n\n```typescript\nimport { CommandUnavailableError } from \"@anura-bot/dispatcher\";\n\ntry {\n  await app.invoke(\"public\", \"nonexistent\", ctx, {});\n} catch (e) {\n  if (e instanceof CommandUnavailableError) {\n    console.log(\"命令不存在\");\n  }\n}\n```\n\n### `CommandArgumentUnmatchedError`\n\n命令参数不匹配（类型错误或缺失必需参数）。\n\n```typescript\nimport { CommandArgumentUnmatchedError } from \"@anura-bot/dispatcher\";\n\ntry {\n  await app.invoke(\"public\", \"repeat\", ctx, { text: \"hi\", times: \"abc\" });\n} catch (e) {\n  if (e instanceof CommandArgumentUnmatchedError) {\n    console.log(\"参数格式错误\");\n  }\n}\n```\n\n### `CommandExecutionError`\n\n命令处理器执行过程中抛出错误。\n\n```typescript\nimport { CommandExecutionError } from \"@anura-bot/dispatcher\";\n\ntry {\n  await app.invoke(\"public\", \"buggy\", ctx, {});\n} catch (e) {\n  if (e instanceof CommandExecutionError) {\n    console.log(\"命令执行失败:\", e.error);\n  }\n}\n```\n\n### 完整错误处理示例\n\n```typescript\nasync function handleUserCommand(scope, name, ctx, args) {\n  try {\n    await app.parse(scope, name, ctx, args);\n  } catch (e) {\n    if (e instanceof CommandUnavailableError) {\n      await ctx.respond(\"未知命令，输入 /help 查看可用命令\");\n    } else if (e instanceof CommandArgumentUnmatchedError) {\n      await ctx.respond(\"参数格式错误，请检查输入\");\n    } else if (e instanceof CommandExecutionError) {\n      await ctx.respond(\"命令执行失败，请稍后重试\");\n      console.error(\"Handler error:\", e.error);\n    } else {\n      throw e; // 未预期的错误\n    }\n  }\n}\n```\n\n## 实际应用场景\n\n### 集成到 Discord Bot\n\n```typescript\nimport { Client, Message } from \"discord.js\";\nimport { dispatcher } from \"@anura-bot/dispatcher\";\nimport { string } from \"zod/v4\";\n\ntype DiscordContext = {\n  message: Message;\n  respond(text: string): Promise<void>;\n};\n\nconst app = dispatcher()\n  .scope(\"discord\")\n  .context<DiscordContext>()\n  .command(\n    \"discord\",\n    \"ping\",\n    ({ ctx }) => ctx.respond(\"Pong!\"),\n    [],\n    \"测试机器人响应\"\n  );\n\nconst client = new Client({ intents: [\"Guilds\", \"GuildMessages\"] });\n\nclient.on(\"messageCreate\", async (message) => {\n  if (!message.content.startsWith(\"/\")) return;\n\n  const [cmd, ...argParts] = message.content.slice(1).split(\" \");\n  const args = parseArgs(argParts); // 自定义参数解析\n\n  await app.parse(\n    \"discord\",\n    cmd,\n    {\n      message,\n      respond: (text) => message.reply(text),\n    },\n    args\n  );\n});\n```\n\n### 集成到 Telegram Bot\n\n```typescript\nimport TelegramBot from \"node-telegram-bot-api\";\nimport { dispatcher } from \"@anura-bot/dispatcher\";\n\ntype TelegramContext = {\n  chatId: number;\n  userId: number;\n  respond(text: string): Promise<void>;\n};\n\nconst app = dispatcher()\n  .scope(\"telegram\")\n  .context<TelegramContext>()\n  .command(\n    \"telegram\",\n    \"start\",\n    ({ ctx }) => ctx.respond(\"欢迎使用机器人！\"),\n    [],\n    \"开始使用\"\n  );\n\nconst bot = new TelegramBot(TOKEN, { polling: true });\n\nbot.onText(/\\/(.+)/, async (msg, match) => {\n  const cmd = match[1].split(\" \")[0];\n  const args = {}; // 解析参数\n\n  await app.parse(\n    \"telegram\",\n    cmd,\n    {\n      chatId: msg.chat.id,\n      userId: msg.from.id,\n      respond: (text) => bot.sendMessage(msg.chat.id, text),\n    },\n    args\n  );\n});\n```\n\n## 类型定义\n\n### `BotRequest<Context, ParamsRecord, ScopeName>`\n\n传递给命令处理器的请求对象。\n\n```typescript\ntype BotRequest<Context, ParamsRecord, ScopeName> = {\n  command: string;       // 命令名称\n  args: ParamsRecord;    // 参数对象（已验证和转换）\n  scope: ScopeName;      // 执行的作用域\n  ctx: Context;          // 上下文对象\n};\n```\n\n### `Param`\n\n参数定义对象。\n\n```typescript\ntype Param = {\n  zod: ZodType;               // Zod schema\n  name: string;               // 参数名称\n  display?: string;           // 显示名称\n  description?: string;       // 参数描述\n};\n```\n\n## 许可证\n\nMIT\n\n## 贡献\n\n欢迎提交 Issue 和 Pull Request！\n","readmeFilename":"README.md"}