feat(gateway): add LINE Messaging API platform plugin (#23197)

* feat(gateway): add LINE Messaging API platform plugin

Adds LINE as a bundled platform plugin under `plugins/platforms/line/`,
synthesized from the strongest pieces of seven open community PRs. The
adapter requires zero core edits — `Platform("line")` is auto-discovered
via the bundled-plugin scan in `gateway/config.py`, and all hooks
(setup, env-enablement, cron delivery, standalone send) are wired
through `register_platform()` kwargs the way IRC and Teams do it.

Highlights merged into one plugin:

- **Reply token preferred, Push fallback.** Try the free reply token
  first (single-use, ~60s TTL); fall back to metered Push when the
  token is absent, expired, or rejected. (PR #21023)
- **Slow-LLM Template Buttons postback.** When the LLM is still running
  past `LINE_SLOW_RESPONSE_THRESHOLD` (default 45s), the adapter burns
  the original reply token to send a "Get answer" button bubble. The
  user taps it to fetch the cached answer via a fresh reply token —
  also free. State machine: PENDING → READY → DELIVERED, ERROR for
  cancelled runs (orphan resolves to `LINE_INTERRUPTED_TEXT` after
  /stop). Set threshold to 0 to disable. (PR #18153)
- **Three-allowlist gating** — separate user / group / room allowlists
  with `LINE_ALLOW_ALL_USERS=true` dev-only escape hatch. (PR #18153)
- **Markdown URL preservation.** Strip bold/italic/code-fence/heading
  markers (LINE renders them literally) but keep `[label](url)` →
  `label (url)` so URLs stay tappable. (PR #18153)
- **System-message bypass** for ` Interrupting`, ` Queued`, etc. —
  busy-acks reach the user as visible bubbles instead of being
  swallowed into the postback cache. (PR #18153)
- **Media via public HTTPS URLs.** LINE doesn't accept binary uploads;
  images/audio/video must be HTTPS-reachable. The adapter serves
  registered tempfiles under `/line/media/<token>/<filename>` from the
  same aiohttp app. Allowed-roots traversal guard covers
  `tempfile.gettempdir()`, `/tmp` (→ `/private/tmp` on macOS), and
  `HERMES_HOME`. `LINE_PUBLIC_URL` overrides URL construction for
  setups behind tunnels/proxies. (PR #8398)
- **5-message-per-call batching.** LINE rejects >5 messages per
  Reply/Push; smart-chunker caps text at 4500 chars per bubble.
- **Inbound dedup** via `webhookEventId` LRU. (PR #21023)
- **Self-message filter** via `/v2/bot/info` userId lookup. (PR #21023)
- **Loading-animation indicator** wired to LINE's `chat/loading/start`
  endpoint, DM-only (LINE rejects it for groups/rooms). (PR #21023)
- **Out-of-process cron delivery** via `_standalone_send`, so
  `deliver: line` cron jobs work even when cron runs detached from
  the gateway.
- **Webhook hardening** — 1 MiB body cap, constant-time HMAC-SHA256
  signature verification, dedup, scoped lock so two profiles can't
  bind the same channel.

Validation
----------

- `scripts/run_tests.sh tests/gateway/test_line_plugin.py` →
  73 passed in 1.05s
- `scripts/run_tests.sh tests/gateway/test_line_plugin.py
  tests/gateway/test_irc_adapter.py
  tests/gateway/test_plugin_platform_interface.py
  tests/gateway/test_platform_registry.py
  tests/gateway/test_config.py` → 193 passed, 7 skipped
- E2E import + register + signature roundtrip + `Platform("line")`
  bundled-plugin discovery verified against current `origin/main`.

Closes the seven open LINE PRs (#18153, #16832, #6676, #21023, #14942,
#14988, #8398) by superseding them with a single plugin-form
implementation that takes the best idea from each.

Co-authored-by: pwlee <32443648+leepoweii@users.noreply.github.com>
Co-authored-by: Jetha Chan <jetha@google.com>
Co-authored-by: Cattia <openclaw@liyangchen.me>
Co-authored-by: perng <charles@perng.com>
Co-authored-by: Soichiro Yoshimura <soichiro0111.dev@gmail.com>
Co-authored-by: David Zhou <77736378+David-0x221Eight@users.noreply.github.com>
Co-authored-by: Yu-ga <74749461+yuga-hashimoto@users.noreply.github.com>

* docs(platforms): document platform-specific slow-LLM UX pattern

Add a 'Platform-Specific Slow-LLM UX' section to the platform-adapter
developer guide covering the _keep_typing override pattern that LINE
uses for its Template Buttons postback flow.

Three subsections:
- Pattern: subclass _keep_typing to layer mid-flight UX (with code)
- Pattern: subclass send to route through a cache instead of sending
- When this pattern is appropriate (vs. always-Push fallback)

Plus a short pointer in gateway/platforms/ADDING_A_PLATFORM.md so
tree-readers find the prose walkthrough on the docsite.

Filed because the LINE plugin (PR #23197) was the first bundled
adapter to need this pattern — every prior plugin (irc, teams,
google_chat) handles slow responses with the default typing-loop and
a regular send_text. Documenting now while the rationale is fresh.

---------

Co-authored-by: pwlee <32443648+leepoweii@users.noreply.github.com>
Co-authored-by: Jetha Chan <jetha@google.com>
Co-authored-by: Cattia <openclaw@liyangchen.me>
Co-authored-by: perng <charles@perng.com>
Co-authored-by: Soichiro Yoshimura <soichiro0111.dev@gmail.com>
Co-authored-by: David Zhou <77736378+David-0x221Eight@users.noreply.github.com>
Co-authored-by: Yu-ga <74749461+yuga-hashimoto@users.noreply.github.com>
This commit is contained in:
Teknium 2026-05-10 06:40:46 -07:00 committed by GitHub
parent 9cdcf31cae
commit 50f9fee988
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 2683 additions and 3 deletions

View file

@ -0,0 +1,3 @@
from .adapter import register
__all__ = ["register"]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,65 @@
name: line-platform
label: LINE
kind: platform
version: 1.0.0
description: >
LINE Messaging API gateway adapter for Hermes Agent.
Runs an aiohttp webhook server that receives LINE webhook events
(with HMAC-SHA256 signature verification) and relays messages between
LINE chats (1:1, groups, rooms) and the Hermes agent. Outbound replies
prefer the free reply token and fall back to the metered Push API
when the token has expired or is absent. Slow LLM responses surface a
Template Buttons postback bubble so the user can fetch the answer with
a fresh reply token (free) once it's ready.
author: Hermes Agent contributors
# ``requires_env`` and ``optional_env`` entries are surfaced in the
# ``hermes config`` UI via the platform-plugin env var injector in
# ``hermes_cli/config.py``.
requires_env:
- name: LINE_CHANNEL_ACCESS_TOKEN
description: "LINE channel long-lived access token (LINE Developers Console > Messaging API > Channel access token)"
prompt: "LINE channel access token"
url: "https://developers.line.biz/console/"
password: true
- name: LINE_CHANNEL_SECRET
description: "LINE channel secret (used for HMAC-SHA256 webhook signature verification)"
prompt: "LINE channel secret"
url: "https://developers.line.biz/console/"
password: true
optional_env:
- name: LINE_PORT
description: "Webhook listen port (default: 8646)"
prompt: "Webhook port"
password: false
- name: LINE_HOST
description: "Webhook bind host (default: 0.0.0.0)"
prompt: "Webhook host"
password: false
- name: LINE_PUBLIC_URL
description: "Public HTTPS base URL for serving images/audio/video to LINE (e.g. https://my-tunnel.example.com). Required for media sending when the bind address is not directly reachable."
prompt: "Public HTTPS base URL"
password: false
- name: LINE_ALLOWED_USERS
description: "Comma-separated LINE user IDs allowed to DM the bot (U-prefixed)"
prompt: "Allowed user IDs (comma-separated)"
password: false
- name: LINE_ALLOWED_GROUPS
description: "Comma-separated LINE group IDs the bot will respond in (C-prefixed)"
prompt: "Allowed group IDs (comma-separated)"
password: false
- name: LINE_ALLOWED_ROOMS
description: "Comma-separated LINE room IDs the bot will respond in (R-prefixed)"
prompt: "Allowed room IDs (comma-separated)"
password: false
- name: LINE_ALLOW_ALL_USERS
description: "Allow any LINE user to talk to the bot (dev only — disables allowlist)"
prompt: "Allow all users? (true/false)"
password: false
- name: LINE_HOME_CHANNEL
description: "Default user/group/room ID for cron / notification delivery"
prompt: "Home channel ID (or empty)"
password: false
- name: LINE_SLOW_RESPONSE_THRESHOLD
description: "Seconds before the slow-LLM postback button fires (default: 45; set 0 to disable and always Push-fallback)"
prompt: "Slow response threshold (seconds)"
password: false