docs(plugins): add EventProcessor component tutorial

This commit is contained in:
RockChinQ
2026-09-09 15:37:11 +08:00
parent 4f433ceee8
commit 2a8bd3ceb2
20 changed files with 787 additions and 10 deletions
@@ -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.
+163
View File
@@ -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())