|
| 1 | +--- |
| 2 | +title: '@fraqjs/plugin-conversation' |
| 3 | +--- |
| 4 | + |
| 5 | +<NpmBadge packageName="@fraqjs/plugin-conversation" /> |
| 6 | + |
| 7 | +`@fraqjs/plugin-conversation` 提供了对多轮对话的支持,帮助开发者编写交互式机器人功能。 |
| 8 | + |
| 9 | +## 安装与配置 |
| 10 | + |
| 11 | +将插件添加至 `dependencies`,然后在创建 `Context` 时引入并配置插件: |
| 12 | + |
| 13 | +```typescript |
| 14 | +import { ConversationPlugin } from '@fraqjs/plugin-conversation'; |
| 15 | + |
| 16 | +ctx.install(ConversationPlugin, { |
| 17 | + // 在这里传入 ConversationService 的配置选项 |
| 18 | +}); |
| 19 | +``` |
| 20 | + |
| 21 | +`ConversationPlugin` 有如下配置项: |
| 22 | + |
| 23 | +- `defaultTimeout`:默认的对话超时时间,单位是毫秒。默认值为 30000(30 秒)。如果用户在这个时间内没有回复,当前对话会自动结束。 |
| 24 | +- `onCollision`:当用户触发一个新的会话时,如何处理旧的会话。可选值有: |
| 25 | + - `reject-incoming`:默认值,拒绝建立新的会话,抛出一个 `ConversationRejectionError`。 |
| 26 | + - `abort-existing`:结束旧的会话,建立新的会话,并且在旧的会话处抛出一个 `ConversationAbortionError`。 |
| 27 | + |
| 28 | +如果你是插件开发者,请将本插件添加到项目的 `peerDependencies` 中,并在自己的插件中声明依赖: |
| 29 | + |
| 30 | +```typescript |
| 31 | +import { ConversationService } from '@fraqjs/plugin-conversation'; |
| 32 | + |
| 33 | +definePlugin({ |
| 34 | + name: 'my-plugin', |
| 35 | + inject: { |
| 36 | + conversation: ConversationService, |
| 37 | + }, |
| 38 | + apply(ctx) { |
| 39 | + // 使用 ctx.conversation 来访问 ConversationService |
| 40 | + }, |
| 41 | +}); |
| 42 | +``` |
| 43 | + |
| 44 | +## 开发交互功能 |
| 45 | + |
| 46 | +考虑下面的对话场景: |
| 47 | + |
| 48 | +``` |
| 49 | +用户: 天气 |
| 50 | +机器人: 你想查询哪个城市的天气呢? |
| 51 | +用户: 北京 |
| 52 | +机器人: 北京今天晴,最高气温... |
| 53 | +``` |
| 54 | + |
| 55 | +传统的 `ctx.router.command` 遵循单纯的命令-响应模式,不适合处理多轮对话。但使用 `ConversationService`,你可以轻松实现这个场景: |
| 56 | + |
| 57 | +```typescript |
| 58 | +ctx.router.command('天气', {}, async (session) => { |
| 59 | + await session.reply(msg`你想查询哪个城市的天气呢?`); |
| 60 | + const city = await ctx.conversation.open<string>( |
| 61 | + session, |
| 62 | + ({ router, done }) => { |
| 63 | + router.rawPattern({ city: param.str() }, async (_, { city }) => { |
| 64 | + done(city); |
| 65 | + }); |
| 66 | + }, |
| 67 | + ); // city: string | null |
| 68 | + if (city === null) { |
| 69 | + return; |
| 70 | + } |
| 71 | + const weatherInfo = await getWeatherForCity(city); |
| 72 | + await session.reply(msg`${weatherInfo}`); |
| 73 | +}); |
| 74 | +``` |
| 75 | + |
| 76 | +在这个例子中,插件接收到 “天气” 这个输入时,会先回复一个问题,然后通过 `ctx.conversation.open` 启动一个新的会话。`open<R>` 函数传入的回调函数接受一个 `ConversationContext<R>` 对象,包含以下属性: |
| 77 | + |
| 78 | +- `session`:当前会话的 `Session` 对象,与第一个参数传入的 `session` 是同一个实例。 |
| 79 | +- `router`:一个新的 `Router` 实例,用于在会话中定义输入模式和对应的处理函数。在这个 `Router` 中定义的路由只会在当前会话中生效,不会影响全局的 `ctx.router`。此外,传入这个 `router` 的回调函数的 `session` 中,`raw` 字段会被替换为触发当前路由的原始输入内容,其余字段与你传入 `open` 的 `session` 相同。 |
| 80 | +- `done(result: R)`:一个函数,用于结束会话并返回结果。当你调用 `done` 时,`ctx.conversation.open` 返回的 Promise 会被 resolved,值就是你传入 `done` 的参数。 |
| 81 | +- `abort(reason?: string)`:一个函数,用于主动终止会话,并且可以传入一个字符串作为终止原因。当你调用 `abort` 时,`ctx.conversation.open` 返回的 Promise 会被 rejected,错误类型是 `ConversationAbortionError`,错误消息包含你传入的终止原因。 |
| 82 | + |
| 83 | +我们在代码中使用了其中的 `router` 来定义了一个 `rawPattern` 路由,这个路由会匹配用户的任意输入,并且将输入内容作为字符串参数 `city` 传入回调函数。当用户输入符合这个模式时,我们调用 `done(city)` 来结束会话,并将用户输入的城市名称作为结果返回。 |
| 84 | + |
| 85 | +## 处理特殊情况 |
| 86 | + |
| 87 | +从代码中可以看出,`open` 方法有可能返回 `null`,这表示会话超出了配置的等待时间限制,你需要显式地检查这种情况来处理超时。代码中的处理是直接 `return`,即不再执行后续逻辑;但你也可以发送一个提示消息,或使用 `while` 循环来重新发起会话等。 |
| 88 | + |
| 89 | +`open` 方法还可能抛出两种错误: |
| 90 | + |
| 91 | +- `ConversationRejectionError`:当发生会话冲突且 `onCollision` 配置为 `reject-incoming` 时抛出,表示新的会话请求被拒绝。 |
| 92 | +- `ConversationAbortionError`: |
| 93 | + - 当发生会话冲突且 `onCollision` 配置为 `abort-existing` 时,旧的会话会被结束,并在旧的会话处抛出这个错误。 |
| 94 | + - 当调用 `abort` 方法时抛出,表示当前会话被主动终止,并且包含终止原因。 |
| 95 | + |
| 96 | +你可以通过 `try...catch` 来捕获这些错误,并进行相应的处理。 |
0 commit comments