diff --git a/docs.json b/docs.json index ec40b98fb..11f375f02 100644 --- a/docs.json +++ b/docs.json @@ -677,6 +677,7 @@ "group": "Component Development", "pages": [ "en/plugin/dev/components/add", + "en/plugin/dev/components/event-processor", "en/plugin/dev/components/event-listener", "en/plugin/dev/components/command", "en/plugin/dev/components/tool", @@ -980,6 +981,7 @@ "group": "组件开发", "pages": [ "zh/plugin/dev/components/add", + "zh/plugin/dev/components/event-processor", "zh/plugin/dev/components/event-listener", "zh/plugin/dev/components/command", "zh/plugin/dev/components/tool", @@ -1278,6 +1280,7 @@ "group": "コンポーネント開発", "pages": [ "ja/plugin/dev/components/add", + "ja/plugin/dev/components/event-processor", "ja/plugin/dev/components/event-listener", "ja/plugin/dev/components/command", "ja/plugin/dev/components/tool", diff --git a/en/plugin/dev/components/add.mdx b/en/plugin/dev/components/add.mdx index 4bc82626a..2e7665b69 100644 --- a/en/plugin/dev/components/add.mdx +++ b/en/plugin/dev/components/add.mdx @@ -4,6 +4,8 @@ title: "Adding Components" Plugins consist of one or more components that provide different functionalities to LangBot. Currently supported component types include: +- Plugin processor (EventProcessor): handles platform events in plugin code after an instance is created and bound. See the [tutorial](/en/plugin/dev/components/event-processor). +- Agent runner (AgentRunner): provides execution logic selected in an Agent configuration; it is distinct from a standalone plugin processor. - Event Listeners (EventListener): Listen to events during pipeline execution and modify context or pipelines. - Commands (Command): Triggered by user command messages starting with `!` (or other configured prefixes). - Tools (Tool): Called by LLMs during execution of LangBot's built-in Local Agent. @@ -70,6 +72,7 @@ To remove components, you can delete the corresponding information manually. For detailed usage of each component, please refer to: +- [EventProcessor](./event-processor) - [EventListener](./event-listener) - [Command](./command) - [Tool](./tool) diff --git a/en/plugin/dev/components/event-listener.mdx b/en/plugin/dev/components/event-listener.mdx index e36937c42..3050c078e 100644 --- a/en/plugin/dev/components/event-listener.mdx +++ b/en/plugin/dev/components/event-listener.mdx @@ -2,6 +2,10 @@ title: "Component: Event Listener" --- + +This component extends Pipeline processing steps. To handle Bot events such as incoming messages and new group members, see [Event Processor](/en/plugin/dev/components/event-processor). + + LangBot's pipeline generates events during runtime for plugins to hook into and process. Each plugin can only have one event listener (components/event_listener/default.yaml), but can register any number of events within it. ## Adding Event Listener Component diff --git a/en/plugin/dev/components/event-processor.mdx b/en/plugin/dev/components/event-processor.mdx new file mode 100644 index 000000000..1dec59c98 --- /dev/null +++ b/en/plugin/dev/components/event-processor.mdx @@ -0,0 +1,158 @@ +--- +title: "Component: Event Processor" +--- + +The EventProcessor component handles Bot events such as incoming messages and new group members. After installing the plugin, users create a **Plugin processor** in LangBot, select the component, and bind Bot events to it. + +For environment setup, see the [plugin development tutorial](/en/plugin/dev/tutor). + +## Adding an Event Processor Component + +A plugin can contain several event processors. Run `lbp comp EventProcessor` in the plugin directory. Enter `welcome` as the name and `Welcome new members` as the description. + +```bash +lbp comp EventProcessor +``` + +The generated `components/event_processor/welcome.yaml` defines the component, and `welcome.py` contains its implementation. + +## Manifest File: Event Processor + +Replace `welcome.yaml` with: + +```yaml +apiVersion: langbot/v1 +kind: EventProcessor +metadata: + name: welcome + label: + en_US: Welcome members + zh_Hans: 欢迎新成员 + ja_JP: 新しいメンバーを歓迎 + description: + en_US: Welcome new members and respond to /hello. + zh_Hans: 欢迎新成员,并响应 /hello 消息。 + ja_JP: 新しいメンバーを歓迎し、/hello に返信します。 +spec: + events: + - group.member_joined + - message.received + capabilities: + tool_calling: true + permissions: + tools: [detail, call] + config: + - name: greeting + type: string + label: + en_US: Greeting + zh_Hans: 欢迎语 + ja_JP: 歓迎メッセージ + required: true + default: Welcome aboard! + - name: reply_enabled + type: boolean + label: + en_US: Send replies + zh_Hans: 发送回复 + ja_JP: 返信する + default: true +execution: + python: + path: ./welcome.py + attr: Welcome +``` + +`spec.events` lists supported events, and `spec.config` defines settings users can edit on the processor detail page. This example handles new members and incoming messages, with a greeting and a reply switch. Sending replies requires the `tool_calling` and `permissions.tools` settings shown above. + +## Plugin Implementation + +Register handlers in the `Welcome` class's `initialize` method. Replace `welcome.py` with: + +```python +from langbot_plugin.api.definition.components.event_processor import ( + EventProcessor, + EventProcessorContext, +) +from langbot_plugin.api.entities.builtin.platform.events import ( + MemberJoinedEvent, + MessageReceivedEvent, +) +from langbot_plugin.api.entities.builtin.platform.message import Plain + + +class Welcome(EventProcessor): + async def initialize(self): + await super().initialize() + + @self.handler(MemberJoinedEvent) + async def on_join(ctx: EventProcessorContext): + name = ctx.event.member.nickname or str(ctx.event.member.id) + await ctx.log(f"Member joined: {name}") + if not ctx.config.get("reply_enabled", True): + await ctx.log("Replies are disabled for this processor") + return + greeting = ctx.config.get("greeting", "Welcome aboard!") + await ctx.reply(f"{name}, {greeting}") + await ctx.log("Welcome action completed") + + @self.handler(MessageReceivedEvent) + async def on_message(ctx: EventProcessorContext): + text = "".join( + item.text for item in ctx.event.message_chain if isinstance(item, Plain) + ).strip() + if text != "/hello": + await ctx.log("Message ignored: expected /hello") + return + if ctx.config.get("reply_enabled", True): + await ctx.reply(ctx.config.get("greeting", "Welcome aboard!")) + else: + await ctx.log("Replies are disabled for this processor") +``` + +`on_join` welcomes new members, and `on_message` replies to `/hello`. With **Send replies** turned off, they only write logs. + +`ctx.event` contains the event data, and `ctx.config` contains this processor's settings. Use `ctx.reply()` to reply and `ctx.log()` to write logs. Processing finishes when the handlers return. + +## Registering Events + +Register a handler with `@self.handler(EventClass)` and add the event identifier to `spec.events`. For example, `MemberJoinedEvent` corresponds to `group.member_joined`. + +| Event | Manifest identifier | SDK event class | +| --- | --- | --- | +| Message received | `message.received` | `MessageReceivedEvent` | +| Message edited | `message.edited` | `MessageEditedEvent` | +| Message deleted | `message.deleted` | `MessageDeletedEvent` | +| Message reaction | `message.reaction` | `MessageReactionEvent` | +| Feedback received | `feedback.received` | `FeedbackReceivedEvent` | +| Member joined | `group.member_joined` | `MemberJoinedEvent` | +| Member left | `group.member_left` | `MemberLeftEvent` | +| Member muted | `group.member_banned` | `MemberBannedEvent` | +| Group information updated | `group.info_updated` | `GroupInfoUpdatedEvent` | +| Friend request received | `friend.request_received` | `FriendRequestReceivedEvent` | +| Friend added | `friend.added` | `FriendAddedEvent` | +| Friend removed | `friend.removed` | `FriendRemovedEvent` | +| Bot invited to group | `bot.invited_to_group` | `BotInvitedToGroupEvent` | +| Bot removed from group | `bot.removed_from_group` | `BotRemovedFromGroupEvent` | +| Bot muted | `bot.muted` | `BotMutedEvent` | +| Bot unmuted | `bot.unmuted` | `BotUnmutedEvent` | +| Platform-specific event | `platform.specific` | `PlatformSpecificEvent` | + +See the SDK's [events.py](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/api/entities/builtin/platform/events.py) for available events and fields. Event support varies by platform. + +## Testing the Event Processor + +Configure the debug connection following the [plugin development tutorial](/en/plugin/dev/tutor), then run `lbp run` in the plugin directory. In LangBot: + +1. Create a **Plugin processor**, select **Welcome members** on its detail page, configure it, and save. +2. Select **Member joined** on the left, enter the member nickname, member ID, and group ID, then click **Run test**. +3. Inspect the reply and logs. You can also select **Message received** and enter `/hello` to test replies. + +Platform replies use Mock during debugging and send no real messages. Restart `lbp run` after editing component YAML. + +After installing the plugin, bind the Bot's **Member joined** and **Message received** events to this processor to handle actual events. + +## Next Steps + +- Explore more examples in [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo). +- To extend Pipeline steps such as preprocessing or completion of a model call, use [Event Listener](/en/plugin/dev/components/event-listener). diff --git a/en/plugin/dev/tutor.mdx b/en/plugin/dev/tutor.mdx index 21d3261da..7ab4b43fc 100644 --- a/en/plugin/dev/tutor.mdx +++ b/en/plugin/dev/tutor.mdx @@ -2,6 +2,10 @@ title: "Plugin Development Tutorial" --- + +To build a plugin for incoming messages, new group members, and other Bot events, complete the basic setup here and continue with the [Event Processor tutorial](/en/plugin/dev/components/event-processor). + + ## Architecture Foundation In version 4.0, we introduced a high-security, high-flexibility production-grade plugin system and provided developers with rich APIs and easy-to-use supporting tools. diff --git a/ja/plugin/dev/components/add.mdx b/ja/plugin/dev/components/add.mdx index 48a5902b7..65610aca9 100644 --- a/ja/plugin/dev/components/add.mdx +++ b/ja/plugin/dev/components/add.mdx @@ -4,6 +4,8 @@ title: "コンポーネントの追加" プラグインは、LangBotにさまざまな機能を提供する1つ以上のコンポーネントで構成されています。現在サポートされているコンポーネントタイプには次のものがあります: +- プラグインプロセッサー(EventProcessor):インスタンスを作成してイベントに紐付け、プラグインのコードで処理します。[チュートリアル](/ja/plugin/dev/components/event-processor)を参照してください。 +- Agent ランナー(AgentRunner):Agent の設定で選択する実行コンポーネントです。独立したプラグインプロセッサーとは異なります。 - イベントリスナー(EventListener): パイプライン実行中のイベントをリッスンし、コンテキストやパイプラインを変更します。 - コマンド(Command): `!`(または他の設定されたプレフィックス)で始まるユーザーコマンドメッセージによってトリガーされます。 - ツール(Tool): LangBotの組み込みLocal Agentの実行中にLLMによって呼び出されます。 @@ -70,6 +72,7 @@ components: 各コンポーネントの詳細な使用方法については、次を参照してください: +- [EventProcessor](./event-processor) - [EventListener](./event-listener) - [Command](./command) - [Tool](./tool) diff --git a/ja/plugin/dev/components/event-listener.mdx b/ja/plugin/dev/components/event-listener.mdx index e14f12e54..4c2840606 100644 --- a/ja/plugin/dev/components/event-listener.mdx +++ b/ja/plugin/dev/components/event-listener.mdx @@ -2,6 +2,10 @@ title: "コンポーネント: イベントリスナー" --- + +このコンポーネントはパイプラインの処理を拡張します。メッセージ受信やメンバー参加などの Bot イベントを処理する場合は、[イベントプロセッサー](/ja/plugin/dev/components/event-processor)を参照してください。 + + LangBotのパイプラインは実行時にイベントを生成し、プラグインがフックして処理できるようにします。各プラグインは1つのイベントリスナー(components/event_listener/default.yaml)のみを持つことができますが、その中で任意の数のイベントを登録できます。 ## イベントリスナーコンポーネントの追加 diff --git a/ja/plugin/dev/components/event-processor.mdx b/ja/plugin/dev/components/event-processor.mdx new file mode 100644 index 000000000..b6df1487d --- /dev/null +++ b/ja/plugin/dev/components/event-processor.mdx @@ -0,0 +1,158 @@ +--- +title: "コンポーネント: イベントプロセッサー" +--- + +イベントプロセッサー(EventProcessor)は、メッセージ受信やメンバー参加などの Bot イベントを処理するコンポーネントです。インストール後、ユーザーは LangBot で「プラグインプロセッサー」を作成してコンポーネントを選び、Bot のイベントを紐付けます。 + +環境の準備は[プラグイン開発チュートリアル](/ja/plugin/dev/tutor)を参照してください。 + +## イベントプロセッサーコンポーネントの追加 + +1 つのプラグインに複数のイベントプロセッサーを追加できます。プラグインのディレクトリで `lbp comp EventProcessor` を実行し、名前に `welcome`、説明に `Welcome new members` を入力します。 + +```bash +lbp comp EventProcessor +``` + +生成される `components/event_processor/welcome.yaml` はコンポーネントを定義するファイルで、`welcome.py` に処理を記述します。 + +## マニフェストファイル: イベントプロセッサー + +`welcome.yaml` を次の内容に変更します。 + +```yaml +apiVersion: langbot/v1 +kind: EventProcessor +metadata: + name: welcome + label: + en_US: Welcome members + zh_Hans: 欢迎新成员 + ja_JP: 新しいメンバーを歓迎 + description: + en_US: Welcome new members and respond to /hello. + zh_Hans: 欢迎新成员,并响应 /hello 消息。 + ja_JP: 新しいメンバーを歓迎し、/hello に返信します。 +spec: + events: + - group.member_joined + - message.received + capabilities: + tool_calling: true + permissions: + tools: [detail, call] + config: + - name: greeting + type: string + label: + en_US: Greeting + zh_Hans: 欢迎语 + ja_JP: 歓迎メッセージ + required: true + default: Welcome aboard! + - name: reply_enabled + type: boolean + label: + en_US: Send replies + zh_Hans: 发送回复 + ja_JP: 返信する + default: true +execution: + python: + path: ./welcome.py + attr: Welcome +``` + +`spec.events` は対応するイベント、`spec.config` は詳細画面でユーザーが変更できる設定です。この例ではメンバー参加とメッセージ受信に対応し、歓迎メッセージと返信の有効・無効を設定できます。返信には、上記の `tool_calling` と `permissions.tools` の指定が必要です。 + +## プラグインの実装 + +`Welcome` クラスの `initialize` メソッド内で処理関数を登録します。`welcome.py` を次の内容に変更してください。 + +```python +from langbot_plugin.api.definition.components.event_processor import ( + EventProcessor, + EventProcessorContext, +) +from langbot_plugin.api.entities.builtin.platform.events import ( + MemberJoinedEvent, + MessageReceivedEvent, +) +from langbot_plugin.api.entities.builtin.platform.message import Plain + + +class Welcome(EventProcessor): + async def initialize(self): + await super().initialize() + + @self.handler(MemberJoinedEvent) + async def on_join(ctx: EventProcessorContext): + name = ctx.event.member.nickname or str(ctx.event.member.id) + await ctx.log(f"Member joined: {name}") + if not ctx.config.get("reply_enabled", True): + await ctx.log("Replies are disabled for this processor") + return + greeting = ctx.config.get("greeting", "Welcome aboard!") + await ctx.reply(f"{name}, {greeting}") + await ctx.log("Welcome action completed") + + @self.handler(MessageReceivedEvent) + async def on_message(ctx: EventProcessorContext): + text = "".join( + item.text for item in ctx.event.message_chain if isinstance(item, Plain) + ).strip() + if text != "/hello": + await ctx.log("Message ignored: expected /hello") + return + if ctx.config.get("reply_enabled", True): + await ctx.reply(ctx.config.get("greeting", "Welcome aboard!")) + else: + await ctx.log("Replies are disabled for this processor") +``` + +`on_join` は参加したメンバーに挨拶し、`on_message` は `/hello` に返信します。「返信する」を無効にすると、ログだけを記録します。 + +`ctx.event` は現在のイベント、`ctx.config` はこのプロセッサーの設定です。返信には `ctx.reply()`、ログの記録には `ctx.log()` を使います。関数の実行が終わると、イベントの処理も終了します。 + +## イベントの登録 + +`@self.handler(イベントクラス)` で処理関数を登録し、対応する識別子を `spec.events` に追加します。例えば `MemberJoinedEvent` は `group.member_joined` に対応します。 + +| イベント | マニフェストの識別子 | SDK のイベントクラス | +| --- | --- | --- | +| メッセージ受信 | `message.received` | `MessageReceivedEvent` | +| メッセージ編集 | `message.edited` | `MessageEditedEvent` | +| メッセージ削除 | `message.deleted` | `MessageDeletedEvent` | +| メッセージへのリアクション | `message.reaction` | `MessageReactionEvent` | +| フィードバック受信 | `feedback.received` | `FeedbackReceivedEvent` | +| メンバー参加 | `group.member_joined` | `MemberJoinedEvent` | +| メンバー退出 | `group.member_left` | `MemberLeftEvent` | +| メンバーのミュート | `group.member_banned` | `MemberBannedEvent` | +| グループ情報更新 | `group.info_updated` | `GroupInfoUpdatedEvent` | +| 友達申請受信 | `friend.request_received` | `FriendRequestReceivedEvent` | +| 友達追加 | `friend.added` | `FriendAddedEvent` | +| 友達削除 | `friend.removed` | `FriendRemovedEvent` | +| Bot のグループ招待 | `bot.invited_to_group` | `BotInvitedToGroupEvent` | +| Bot のグループからの削除 | `bot.removed_from_group` | `BotRemovedFromGroupEvent` | +| Bot のミュート | `bot.muted` | `BotMutedEvent` | +| Bot のミュート解除 | `bot.unmuted` | `BotUnmutedEvent` | +| プラットフォーム固有のイベント | `platform.specific` | `PlatformSpecificEvent` | + +利用できるイベントとフィールドは、SDK の [events.py](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/api/entities/builtin/platform/events.py)を参照してください。対応イベントはプラットフォームによって異なります。 + +## イベントプロセッサーのテスト + +[プラグイン開発チュートリアル](/ja/plugin/dev/tutor)に従ってデバッグ接続を設定し、プラグインのディレクトリで `lbp run` を実行します。LangBot で次の操作を行ってください。 + +1. 「プラグインプロセッサー」を作成し、詳細画面で「新しいメンバーを歓迎」を選び、設定を保存します。 +2. 左側でメンバー参加を選択し、ニックネーム、メンバー ID、グループ ID を入力してテストを実行します。 +3. 返信とログを確認します。メッセージ受信を選んで `/hello` を入力すると、返信のテストもできます。 + +デバッグ中の返信は Mock で再現され、実際のメッセージは送信されません。コンポーネントの YAML を変更したら `lbp run` を再起動してください。 + +インストール後、Bot のメンバー参加とメッセージ受信をこのプロセッサーに紐付けると、実際のイベントを処理できます。 + +## 次のステップ + +- [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo)で他の実装例を確認できます。 +- パイプラインの前処理やモデル呼び出し完了時などに機能を追加する場合は、[イベントリスナー](/ja/plugin/dev/components/event-listener)を使用してください。 diff --git a/ja/plugin/dev/tutor.mdx b/ja/plugin/dev/tutor.mdx index 66b334a3a..a7911881c 100644 --- a/ja/plugin/dev/tutor.mdx +++ b/ja/plugin/dev/tutor.mdx @@ -2,6 +2,10 @@ title: "プラグイン開発チュートリアル" --- + +メッセージ受信やメンバー参加などを処理するプラグインを作る場合は、基本環境を準備した後、[イベントプロセッサーのチュートリアル](/ja/plugin/dev/components/event-processor)に進んでください。 + + ## アーキテクチャの基礎 バージョン4.0では、高セキュリティで柔軟性の高い本番環境グレードのプラグインシステムを導入し、開発者に豊富なAPIと使いやすいサポートツールを提供しています。 diff --git a/scripts/README-event-processor-validation.md b/scripts/README-event-processor-validation.md new file mode 100644 index 000000000..add5e9900 --- /dev/null +++ b/scripts/README-event-processor-validation.md @@ -0,0 +1,79 @@ +# EventProcessor tutorial verification + +The published tutorial is in `{zh,en,ja}/plugin/dev/components/event-processor.mdx`. +All three translations contain identical executable Python and component YAML. +Run the example checks with the SDK used by your LangBot installation. + +## Repeatable checks + +With the matching SDK installed in the active Python environment: + +```bash +python scripts/verify-event-processor-tutorial.py +``` + +This extracts the actual MDX examples, compares all translations, loads the +component, applies YAML defaults, and executes ten cases. They cover member names +and ID fallback, private/group commands, disabled replies, ordinary/empty/image +messages, and a failed delivery that must retain its logs without completing. +Its tool API is mocked; it does not claim platform delivery. + +With Node.js 24 or later: + +```bash +npm ci +npm test +npm run typecheck +npm run build +``` + +The build also checks static HTML, Markdown exports, navigation, canonical URLs, +alternate languages, and preservation of legacy documentation routes. + +The companion `langbot-plugin-demo/EventProcessorDemo` repository contains +`examples/event-matrix.json` and `scripts/event_matrix.py`. See its README for +authenticated Host checks covering all 17 event types and invalid payloads. +Use a dedicated observer instance with payload logging enabled and no Bot binding. + +## Verified development snapshot: 2026-09-09 + +Documentation work started from the updated `langbot-app/langbot-docs` main commit +`a86ecb4`, on branch `docs/4.11-event-processor`. The local directory still uses +the previous name `langbot-wiki`. + +The tested SDK was `dev/4.11.x` at +`f82b3ce935f9a33afee389fb39fe8dc29a45b615`, both as a freshly built wheel and via +a Git installation from the matching SDK branch in an isolated Python 3.12 environment. +The Host was `dev/4.11.x` at `ea3e32c` with local processor UI refinements. + +| Layer | Observed result | +| --- | --- | +| Documentation | 36 Node tests, 6 Python tests, TypeScript check and full build passed; all 30 post-build static checks passed | +| SDK CLI, packaging and event tests | 246 passed, including 92 new event-matrix cases and packaged CLI execution of generated EventProcessor/AgentRunner components | +| Host processor service and run ledger | 73 passed | +| Platform EBA conversion, routing and run-log authorization | 212 passed | +| EventProcessorDemo unit tests | 8 passed, including scenario subtests, lookup errors and instance isolation | +| Installed observer through HTTP → Host → Runtime | 48 passed: 42 valid cases and 6 rejected inputs; one completed run per valid input | +| Tutorial via developer Runtime and installed package | Both workflows passed; final installed checks cover 9 valid cases plus unsupported-event rejection | +| Python extracted from all three tutorial pages | 10 passed, also using the SDK installed from Git | +| Local OneBot transport → adapter → binding → installed processor | 5 passed: unbound, bound, disabled, higher priority and equal-priority order | +| Authenticated browser tutorial | Selected member joined, entered Alice/alice/tutorial-group, clicked once; one reply trace, one completed run, and automatic switch to Logs | + +The generated AgentRunner was also connected with `lbp run` and invoked through +the real Host, producing its expected echo. The tutorial EventProcessor was built +and uploaded as `tutorial-WelcomeEvents-0.1.0.lbpkg`; it continued to work after +the developer process stopped and the Host restarted. + +The localhost OneBot peer received exactly one `send_group_msg` in the bound and +equal-priority cases. Other cases sent none. It exercised normal routing, not the +debug Mock, but the remote platform was simulated locally: this is not evidence +of delivery through QQ or every other third-party service. Original QA Bot +bindings and the local extension limit were restored after testing. + +Python hot reload worked during development. Component YAML and manifest changes +required restarting `lbp run`; the tutorial explicitly documents that boundary. + +This matrix covers the registered event model and representative data, failure, +concurrency and routing branches. Arbitrary plugin business logic and every real +platform/account combination require their own integration checks. Local passes +are not a CI, publication or deployment result. diff --git a/scripts/verify-event-processor-tutorial.py b/scripts/verify-event-processor-tutorial.py new file mode 100644 index 000000000..43f88b209 --- /dev/null +++ b/scripts/verify-event-processor-tutorial.py @@ -0,0 +1,163 @@ +"""Execute the published tutorial with the matching 4.11 SDK installed. + +Run: python scripts/verify-event-processor-tutorial.py +The tool API is mocked; real Host/runtime checks are a separate validation layer. +""" + +import asyncio +import re +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_tutorial(): + examples = [] + for locale in ("zh", "en", "ja"): + path = ROOT / locale / "plugin/dev/components/event-processor.mdx" + source = path.read_text() + python = re.findall(r"^```python\n(.*?)^```", source, re.M | re.S) + manifests = [ + block + for block in re.findall(r"^```yaml\n(.*?)^```", source, re.M | re.S) + if "kind: EventProcessor" in block + ] + assert len(python) == len(manifests) == 1, path + examples.append((python[0], manifests[0])) + assert examples[0] == examples[1] == examples[2], "Locale examples differ" + code, manifest = examples[0] + namespace = {} + exec(compile(code, "event-processor.mdx", "exec"), namespace) + manifest = yaml.safe_load(manifest) + assert manifest["spec"]["events"] == ["group.member_joined", "message.received"] + assert manifest["spec"]["capabilities"]["tool_calling"] is True + assert manifest["spec"]["permissions"]["tools"] == ["detail", "call"] + defaults = {field["name"]: field["default"] for field in manifest["spec"]["config"]} + return namespace[manifest["execution"]["python"]["attr"]], defaults + + +async def main(): + component_class, defaults = load_tutorial() + component = component_class() + await component.initialize() + cases = [ + ( + "join", + { + "type": "group.member_joined", + "member": {"id": "alice", "nickname": "Alice"}, + }, + {}, + "Alice, Welcome aboard!", + ), + ( + "nickname-fallback", + {"type": "group.member_joined", "member": {"id": "alice"}}, + {}, + "alice, Welcome aboard!", + ), + ( + "join-disabled", + {"type": "group.member_joined"}, + {"reply_enabled": False}, + None, + ), + ( + "command-disabled", + { + "type": "message.received", + "message_chain": [{"type": "Plain", "text": "/hello"}], + }, + {"reply_enabled": False}, + None, + ), + ( + "ordinary-text", + { + "type": "message.received", + "message_chain": [{"type": "Plain", "text": "hello"}], + }, + {}, + None, + ), + ("empty-message", {"type": "message.received", "message_chain": []}, {}, None), + ( + "image-only", + { + "type": "message.received", + "message_chain": [ + {"type": "Image", "url": "https://example.com/image.png"} + ], + }, + {}, + None, + ), + ] + for chat_type in ("private", "group"): + cases.append( + ( + chat_type, + { + "type": "message.received", + "chat_type": chat_type, + "message_chain": [{"type": "Plain", "text": " /hello "}], + }, + {"greeting": "你好 🧩"}, + "你好 🧩", + ) + ) + + async def execute(name, data, config, reply, *, failure=False): + api = SimpleNamespace( + call_tool=AsyncMock( + side_effect=RuntimeError("Delivery failed") if failure else None, + return_value={"mock": True}, + ) + ) + component.get_run_api = Mock(return_value=api) + context = SimpleNamespace( + run_id=name, config={**defaults, **config}, event=SimpleNamespace(data=data) + ) + results = [] + try: + async for result in component.run(context): + results.append(result) + except RuntimeError as exc: + assert failure and str(exc) == "Delivery failed" + else: + assert not failure, "Delivery failure was swallowed" + assert sum(r.type == "run.completed" for r in results) == int(not failure) + assert all(r.run_id == name for r in results) + if reply is None: + api.call_tool.assert_not_called() + else: + api.call_tool.assert_awaited_once_with("event_reply", {"text": reply}) + started = [ + r.data["tool_call_id"] for r in results if r.type == "tool.call.started" + ] + completed = [ + r.data["tool_call_id"] + for r in results + if r.type == "tool.call.completed" + ] + assert len(started) == 1 and started == completed + if failure: + assert any(r.type == "processor.log" for r in results) + assert any(r.data.get("error") == "Delivery failed" for r in results) + print(f"PASS {name}") + + for case in cases: + await execute(*case) + await execute("delivery-failure", cases[0][1], {}, cases[0][3], failure=True) + print( + "All 10 tutorial cases passed; Chinese, English and Japanese code is identical." + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sitemap-alternates.xml b/sitemap-alternates.xml index 88cd9ce3d..44f351f32 100644 --- a/sitemap-alternates.xml +++ b/sitemap-alternates.xml @@ -2100,6 +2100,27 @@ + + https://langbot.app/docs/en/plugin/dev/components/event-processor + + + + + + + https://langbot.app/docs/zh/plugin/dev/components/event-processor + + + + + + + https://langbot.app/docs/ja/plugin/dev/components/event-processor + + + + + https://langbot.app/docs/en/plugin/dev/components/knowledge-engine diff --git a/tests/agent-static-build.test.mjs b/tests/agent-static-build.test.mjs index 0abc55a5a..9ee1c33f0 100644 --- a/tests/agent-static-build.test.mjs +++ b/tests/agent-static-build.test.mjs @@ -22,7 +22,7 @@ function links(node, result = []) { } test("every sitemap page has meaningful canonical Markdown and HTML discovery", async () => { - assert.equal(urls.length, 482); + assert.equal(urls.length, 485); for (const url of urls) { const route = relative(url); const text = await read(`${route}.md`); diff --git a/tests/fumapress-migration.test.mjs b/tests/fumapress-migration.test.mjs index 7738db42c..a99888ae5 100644 --- a/tests/fumapress-migration.test.mjs +++ b/tests/fumapress-migration.test.mjs @@ -170,8 +170,8 @@ test("MDX compatibility normalization removes internal .html route suffixes", () test("canonical MDX and nested OpenAPI sources are discovered", async () => { const docs = JSON.parse(await readFile(path.join(root, "docs.json"), "utf8")); const pages = await collectMdxDocuments(root); - assert.equal(pages.length, 305); - assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 97, zh: 111, ja: 97 }); + assert.equal(pages.length, 308); + assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 98, zh: 112, ja: 98 }); assert.deepEqual(collectOpenApiSources(docs), [ { locale: "en", source: "openapi/service-api-en.json", directory: "en/api-reference" }, { locale: "zh", source: "openapi/service-api-zh.json", directory: "zh/api-reference" }, @@ -216,7 +216,7 @@ test("prebuild is deterministic and preserves local assets and SEO files", async const first = await prepareFumapress({ root, outRoot: temp }); const second = await prepareFumapress({ root, outRoot: temp }); assert.deepEqual(first, second); - assert.equal(first.documents, 305); + assert.equal(first.documents, 308); assert.equal(first.fallbackDefaults, 0); assert.equal(first.localeOnlyDocuments, 14); assert.equal(first.redirects, 103); diff --git a/tests/fumapress-static-build.test.mjs b/tests/fumapress-static-build.test.mjs index b2254c36e..ee5bf234c 100644 --- a/tests/fumapress-static-build.test.mjs +++ b/tests/fumapress-static-build.test.mjs @@ -43,9 +43,9 @@ function staticHtmlForUrlPath(urlPath) { return path.join(publicRoot, ...parts, "index.html"); } -test("all 305 canonical localized documents have static HTML", async () => { +test("all canonical localized documents have static HTML", async () => { const documents = await collectMdxDocuments(root); - assert.equal(documents.length, 305); + assert.equal(documents.length, 308); const missing = []; for (const document of documents) { try { @@ -193,7 +193,7 @@ test("all locales emit the OpenAPI surface with endpoint semantics", async () => } }); -test("canonical sitemap routes match the legacy Mintlify directory structure", async () => { +test("canonical sitemap preserves legacy routes and includes current documents", async () => { const legacy = new Set((await readFile(path.join(root, "tests/fixtures/legacy-sitemap-routes.txt"), "utf8")) .trim().split("\n").map((route) => route.replace(/\/$/, ""))); // Mintlify accidentally indexed this repository maintenance note; preserve @@ -206,7 +206,11 @@ test("canonical sitemap routes match the legacy Mintlify directory structure", a const sitemap = await readFile(path.join(publicRoot, "sitemap.xml"), "utf8"); const actual = new Set([...sitemap.matchAll(/https:\/\/langbot\.app\/docs(\/[^<]+)<\/loc>/g)] .map((match) => decodeURIComponent(match[1]).replace(/\/$/, ""))); - assert.deepEqual([...actual].sort(), [...legacy].sort()); + const expected = new Set(legacy); + for (const document of await collectMdxDocuments(root)) { + expected.add(`/${document.replace(/\.mdx$/, "").replace(/\/index$/, "")}`); + } + assert.deepEqual([...actual].sort(), [...expected].sort()); }); const locales = ["en", "zh", "ja"]; diff --git a/tests/locale-static-build.test.mjs b/tests/locale-static-build.test.mjs index 96cb1900d..3cd370884 100644 --- a/tests/locale-static-build.test.mjs +++ b/tests/locale-static-build.test.mjs @@ -35,6 +35,6 @@ test("every localized HTML payload includes the preference client provider", asy } } for (const locale of ["en", "zh", "ja"]) await visit(path.join(root, locale)); - // 482 canonical content pages plus one framework 404 page per locale. - assert.equal(count, 485); + // 485 canonical content pages plus one framework 404 page per locale. + assert.equal(count, 488); }); diff --git a/zh/plugin/dev/components/add.mdx b/zh/plugin/dev/components/add.mdx index 3ad704041..e2a027f5d 100644 --- a/zh/plugin/dev/components/add.mdx +++ b/zh/plugin/dev/components/add.mdx @@ -4,6 +4,8 @@ title: "添加组件" 插件由一种或多种组件构成,为 LangBot 提供不同的功能。目前支持的组件类型有: +- 事件处理器(EventProcessor):处理机器人收到的消息、成员入群等事件。详见[开发教程](/zh/plugin/dev/components/event-processor)。 +- Agent 运行器(AgentRunner):为 Agent 提供模型调用、外部 Agent 服务连接等运行方式。 - 事件监听器(EventListener):监听流水线执行期间的事件,对上下文或流水线进行修改。 - 命令(Command):由用户通过`!`(或其他已设置的前缀)开头的命令消息触发。 - 工具(Tool):供 LangBot 内置的 Local Agent 在执行期间由 LLM 调用。 @@ -71,6 +73,7 @@ components: 各个组件的详细使用方式请参考: +- [EventProcessor](./event-processor) - [EventListener](./event-listener) - [Command](./command) - [Tool](./tool) diff --git a/zh/plugin/dev/components/event-listener.mdx b/zh/plugin/dev/components/event-listener.mdx index 4bd61cec2..b59963727 100644 --- a/zh/plugin/dev/components/event-listener.mdx +++ b/zh/plugin/dev/components/event-listener.mdx @@ -2,6 +2,10 @@ title: "组件:事件监听器" --- + +本组件用于扩展流水线的处理流程。如果您需要处理机器人收到的消息、成员入群等事件,请参阅[事件处理器](/zh/plugin/dev/components/event-processor)。 + + LangBot 的流水线在运行期间会产生一些事件,供插件 Hook 并处理。每个插件中只允许添加一个事件监听器(components/event_listener/default.yaml),但在其中可以注册任意数量的事件。 ## 添加事件监听器组件 diff --git a/zh/plugin/dev/components/event-processor.mdx b/zh/plugin/dev/components/event-processor.mdx new file mode 100644 index 000000000..9c8c51516 --- /dev/null +++ b/zh/plugin/dev/components/event-processor.mdx @@ -0,0 +1,158 @@ +--- +title: "组件:事件处理器" +--- + +事件处理器(EventProcessor)组件用于处理机器人收到的消息、成员入群等事件。用户安装插件后,可在 LangBot 中创建「插件处理器」并选择该组件,再绑定机器人的事件。 + +环境准备见[插件开发教程](/zh/plugin/dev/tutor)。 + +## 添加事件处理器组件 + +单个插件中可以添加多个事件处理器。请在插件目录执行命令 `lbp comp EventProcessor`,名称填写 `welcome`,描述填写 `Welcome new members`。 + +```bash +lbp comp EventProcessor +``` + +生成的 `components/event_processor/welcome.yaml` 定义组件信息,`welcome.py` 是处理程序。 + +## 清单文件:事件处理器 + +将 `welcome.yaml` 修改为: + +```yaml +apiVersion: langbot/v1 +kind: EventProcessor +metadata: + name: welcome + label: + en_US: Welcome members + zh_Hans: 欢迎新成员 + ja_JP: 新しいメンバーを歓迎 + description: + en_US: Welcome new members and respond to /hello. + zh_Hans: 欢迎新成员,并响应 /hello 消息。 + ja_JP: 新しいメンバーを歓迎し、/hello に返信します。 +spec: + events: + - group.member_joined + - message.received + capabilities: + tool_calling: true + permissions: + tools: [detail, call] + config: + - name: greeting + type: string + label: + en_US: Greeting + zh_Hans: 欢迎语 + ja_JP: 歓迎メッセージ + required: true + default: Welcome aboard! + - name: reply_enabled + type: boolean + label: + en_US: Send replies + zh_Hans: 发送回复 + ja_JP: 返信する + default: true +execution: + python: + path: ./welcome.py + attr: Welcome +``` + +`spec.events` 指定支持的事件,`spec.config` 定义用户可在处理器详情页修改的配置。本例支持成员入群和收到消息,并提供欢迎语、回复开关两个配置项。回复消息需要上面的 `tool_calling` 和 `permissions.tools` 配置。 + +## 插件处理 + +在 `Welcome` 类的 `initialize` 方法中注册事件处理函数。将 `welcome.py` 修改为: + +```python +from langbot_plugin.api.definition.components.event_processor import ( + EventProcessor, + EventProcessorContext, +) +from langbot_plugin.api.entities.builtin.platform.events import ( + MemberJoinedEvent, + MessageReceivedEvent, +) +from langbot_plugin.api.entities.builtin.platform.message import Plain + + +class Welcome(EventProcessor): + async def initialize(self): + await super().initialize() + + @self.handler(MemberJoinedEvent) + async def on_join(ctx: EventProcessorContext): + name = ctx.event.member.nickname or str(ctx.event.member.id) + await ctx.log(f"Member joined: {name}") + if not ctx.config.get("reply_enabled", True): + await ctx.log("Replies are disabled for this processor") + return + greeting = ctx.config.get("greeting", "Welcome aboard!") + await ctx.reply(f"{name}, {greeting}") + await ctx.log("Welcome action completed") + + @self.handler(MessageReceivedEvent) + async def on_message(ctx: EventProcessorContext): + text = "".join( + item.text for item in ctx.event.message_chain if isinstance(item, Plain) + ).strip() + if text != "/hello": + await ctx.log("Message ignored: expected /hello") + return + if ctx.config.get("reply_enabled", True): + await ctx.reply(ctx.config.get("greeting", "Welcome aboard!")) + else: + await ctx.log("Replies are disabled for this processor") +``` + +`on_join` 在成员入群时发送欢迎语,`on_message` 回复 `/hello`。关闭「发送回复」后,只记录日志。 + +处理函数中,`ctx.event` 保存当前事件的数据,`ctx.config` 是当前处理器的配置。调用 `ctx.reply()` 回复消息,`ctx.log()` 记录日志。函数执行完毕,本次处理即结束。 + +## 事件注册 + +通过 `@self.handler(事件类)` 注册处理函数,并将对应事件标识加入清单的 `spec.events`。例如,`MemberJoinedEvent` 对应 `group.member_joined`。 + +| 事件 | 清单中的标识 | SDK 事件类 | +| --- | --- | --- | +| 收到消息 | `message.received` | `MessageReceivedEvent` | +| 消息被编辑 | `message.edited` | `MessageEditedEvent` | +| 消息被删除 | `message.deleted` | `MessageDeletedEvent` | +| 消息表情回应 | `message.reaction` | `MessageReactionEvent` | +| 收到反馈 | `feedback.received` | `FeedbackReceivedEvent` | +| 成员加入群组 | `group.member_joined` | `MemberJoinedEvent` | +| 成员离开群组 | `group.member_left` | `MemberLeftEvent` | +| 成员被禁言 | `group.member_banned` | `MemberBannedEvent` | +| 群组信息更新 | `group.info_updated` | `GroupInfoUpdatedEvent` | +| 收到好友申请 | `friend.request_received` | `FriendRequestReceivedEvent` | +| 添加好友 | `friend.added` | `FriendAddedEvent` | +| 删除好友 | `friend.removed` | `FriendRemovedEvent` | +| 机器人被邀请入群 | `bot.invited_to_group` | `BotInvitedToGroupEvent` | +| 机器人被移出群组 | `bot.removed_from_group` | `BotRemovedFromGroupEvent` | +| 机器人被禁言 | `bot.muted` | `BotMutedEvent` | +| 机器人被解除禁言 | `bot.unmuted` | `BotUnmutedEvent` | +| 平台自定义事件 | `platform.specific` | `PlatformSpecificEvent` | + +可用事件及字段见 SDK 的 [events.py](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/api/entities/builtin/platform/events.py)。各平台支持的事件有所不同。 + +## 测试事件处理器 + +按[插件开发教程](/zh/plugin/dev/tutor)配置调试连接,在插件目录执行 `lbp run`。然后在 LangBot 中: + +1. 创建「插件处理器」,在详情页选择「欢迎新成员」组件,填写配置并保存。 +2. 在左侧选择「成员加入群组」,填写成员昵称、ID 和群组 ID,点击「运行测试」。 +3. 查看回复和日志。也可以选择「收到消息」,输入 `/hello` 测试回复。 + +调试中的平台回复使用 Mock,不发送真实消息。修改组件 YAML 后,请重启 `lbp run`。 + +安装插件后,在机器人中将「成员加入群组」和「收到消息」绑定到该处理器,即可处理实际事件。 + +## 接下来做什么 + +- 查看 [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo) 中的更多示例。 +- 如果要扩展流水线的预处理、模型调用完成等步骤,请使用[事件监听器](/zh/plugin/dev/components/event-listener)。 diff --git a/zh/plugin/dev/tutor.mdx b/zh/plugin/dev/tutor.mdx index 82d40245e..f5ffd0b95 100644 --- a/zh/plugin/dev/tutor.mdx +++ b/zh/plugin/dev/tutor.mdx @@ -2,6 +2,10 @@ title: "插件开发教程" --- + +要开发处理消息、成员入群等事件的插件,可在完成基础环境准备后,继续阅读[事件处理器教程](/zh/plugin/dev/components/event-processor)。 + + ## 架构基础 在 4.0 版本中,我们引入了一种高安全性、高灵活性的生产级插件系统,并为插件开发者提供了丰富的 API 和易用的配套工具。