Files
LangBot/plugins/misc/HumanTakeover/README.md

130 lines
7.9 KiB
Markdown

# HumanTakeover
[简体中文](readme/README_zh_Hans.md) | [日本語](readme/README_ja_JP.md)
A human-takeover & manual-reply plugin for LangBot. It lets a human operator take over any private or group conversation through a built-in Web console, block the AI from responding, and reply manually with text, images, or files.
## Features
- **Block AI on takeover**: When a conversation is taken over, the AI pipeline is stopped (`prevent_default`).
- **Web console (single Page component)**: Two-column layout with a collapsible info panel, following the LangBot design language and dark/light themes.
- **All conversations**: Lists every private chat and group chat; group messages distinguish individual senders.
- **Manual reply**: Send plain text, images (base64), and files (base64) from the console.
- **Per-conversation drafts**: The text box and a staged image/file belong to the conversation they were entered in; switching conversations saves and restores them, and a reply only carries attachments chosen for the conversation it is sent to.
- **10-minute auto-release**: If there is no human response within the configured timeout, the takeover is released automatically. A live countdown is shown during human silence.
- **Trigger words**: When a user message contains a configured trigger word, the conversation is flagged as unhandled (WeChat-style red dot on the avatar), an alert toast is shown, and (optionally) the conversation is auto-taken-over.
- **Profile cards**: Click a user/group avatar to view available info (ID, group, bot, adapter).
- **Persistence**: Conversations and messages are stored via LangBot `plugin_storage` (database-backed) and survive restarts.
- **Clear storage**: One-click button (with a custom confirm dialog) to wipe all stored data.
- **i18n**: Full Simplified Chinese / English interface.
## Configuration
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `takeover_timeout` | integer | 600 | Auto-release takeover if no human response within this many seconds. |
| `trigger_words` | array[string] | `[]` | Messages containing any of these words flag the session as unhandled and auto take over. |
| `auto_takeover_on_trigger` | boolean | true | Whether to automatically take over when a trigger word is matched. |
## Usage
1. Install and enable the plugin in LangBot, configure the fields above.
2. Open the **Human Takeover** page from the WebUI sidebar.
3. Select a conversation, click **Take Over** to block the AI, then reply manually.
4. Click the avatar to inspect user/group info; use **Clear Storage** to reset data.
## Storage and upgrades
Each session's metadata and history are now saved together under a separate
`ht_session_v2_<sha256>` key. Updating one conversation no longer rewrites every
conversation's history. Writes are serialized in the plugin process and storage
failures are returned to callers rather than reported as successful saves.
On first startup, the plugin reads the legacy `ht_sessions` / `ht_messages`
snapshots and copies **all** their records into the new format, without applying
retention during migration. A completion marker is written only after every
session has been saved. Interrupted migrations can be retried. The original keys
remain as an untouched migration-time backup; they are not updated afterward.
Subsequent startups read only the new records. Back up plugin storage before
upgrading: **downgrading does not include conversations changed after migration**.
The console's Clear storage action removes both formats and their history, and
discards any unsent draft.
A session record is limited to **8 MiB of UTF-8 JSON**, leaving room for SDK
base64 encoding within the 16 MiB transport frame. Exceeding this limit raises an
explicit error before sending the storage request; it does not trim message
content or discard previously saved history. New messages still use the existing
300-message retention policy. A failed save restores the last acknowledged cache;
this is not evidence that a remotely dispatched write was rejected.
A legacy session exceeding the limit stops initialization and leaves the original
snapshots intact; it requires export/recovery rather than automatic truncation.
A legacy aggregate already too large to read through the SDK also requires
out-of-band recovery. Failed initialization never falls back to empty storage.
This is single-plugin-process serialization, not a multi-writer transaction.
Clear storage deletes multiple keys and may partially complete on a storage
failure. Caller cancellation (including repeated cancellation) waits for the
shielded update, clear, or migration to settle before releasing serialization.
An acknowledged commit remains in the cache even if its caller was cancelled.
Any remote mutation error, including a timeout or lost connection, has an unknown
commit outcome: it marks storage uninitialized and blocks further writes, clear,
and ordinary reinitialization. Do not assume the write was rejected or retry it.
The barrier is re-checked after the installation lock is acquired, so a caller that
was already queued for that lock when the failure happened is blocked too and
cannot write over the unknown outcome. An operator must first establish that the
old host operations have finished (or stop the old host writer), reconcile
persisted data, then reload using `await plugin.reconcile()` or restart the plugin;
that explicit recovery path is the only one that lifts the barrier. A mere read,
an ordinary load, or plugin restart while old host requests can still commit is
**not** safe reconciliation. Partial clear/migration can then be retried.
If a manual reply was delivered but saving its
history failed, the console explicitly warns **not to resend** it.
## Session identity
A session key is `<bot_uuid>:<launcher_type>_<launcher_id>`: the trusted bot uuid
resolved from the event query is part of the key, so one installation that serves
several bots/adapters keeps separate history, takeover state and reply targets per
bot - two bots with the same group/user id no longer share one record. An event
whose bot identity cannot be resolved is not recorded at all, and a session record
rejects a different bot's or adapter's identity, so a foreign event cannot rewrite
the reply target of an existing session.
Rows written before session keys carried the bot identity keep their old key and
are left untouched: an unscoped key may already contain two bots' history, so it is
never migrated or merged into a bot-scoped session. They stay readable, are never
written by another bot's events (new messages always use a bot-scoped key), and
remain replyable only while their stored bot uuid is known. The console may list
such a row next to the bot-scoped session that continues the same conversation.
### Storage regression tests
With the shared-runtime SDK (`langbot-plugin`) installed, run from `HumanTakeover/`:
```sh
python -m pytest tests -q
```
Tests cover migration/readback, bounded per-session writes, concurrent updates,
failure-barrier handling, rollback/error reporting, bot-identity scoping,
retention and clearing. The stdio test uses the real SDK and an OS subprocess,
with a synthetic Host KV sink; it is not a full LangBot deployment or a live
database test, and it needs a host that can spawn that child process (it fails on
Windows with `WinError 10106`).
## Components
- **EventListener**: records messages, caches the adapter, matches trigger words, and blocks the AI while taken over.
- **Page (`console`)**: the Web management console (`index.html` + `console.py`).
## Shared runtime
`manifest.yaml` declares `sharedRuntime: shared-runtime-v1` +
`componentModel: stateless-v1`, so this plugin can be placed on a multi-tenant
shared worker: config is read per invocation, sessions/history/storage keys are
scoped to the installation binding and loaded on demand, and
`on_installation_revoked` drops a revoked installation's cache. See
[SHARED_RUNTIME.md](SHARED_RUNTIME.md) for the details and known limits. This
source is not certified.