Skip to content

feat(telegram): expose channel policies and bind native choices - #2086

Closed
ideshun wants to merge 18 commits into
TencentCloud:developfrom
ideshun:feat/telegram-channel-ab
Closed

ideshun wants to merge 18 commits into
TencentCloud:developfrom
ideshun:feat/telegram-channel-ab

Conversation

@ideshun

@ideshun ideshun commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

已拆分并由以下 PR 替代:

  • octop-gateway #44:通用渲染、引用、提及与按钮收尾。
  • Octop #2335:干净的配置与业务集成草稿,待 gateway 正式发版后更新发布版本依赖。

原分支及历史讨论保留。以下为原 PR 说明。


Behavior

Expose Telegram group activation, allowlists, topic isolation, reply/edited-message policy, streaming, reactions, delivery options and media settings in the channel drawer. Defaults preserve existing group activation; users can explicitly choose stricter modes.

Topic-isolated sessions include channel instance, chat and topic identity, with routing metadata retained for scheduled delivery. Single-choice ask_user_question prompts and paginated model menus use native callbacks bound to sender/chat/topic and recheck agent ownership, thread identity and model eligibility. Stopping a Telegram response also signals the active Harness stream.

Dependency and scope

Requires octop-gateway>=1.0.3 (companion PR octop-gateway#34, released in 1.0.3); the pin is part of this PR. Channel save/probe rejects unsupported options on older Gateway builds instead of silently ignoring them.

Agent switching is explicitly unsupported. Multi-select/free-text questions retain text handling. This does not introduce Telegram security-approval buttons or grant group administrators Octop administrative authority.

Validation

  • CI is green on the PR head (full pytest -m "not live" on Python 3.12 and Windows, make lint, make typecheck).
  • Local: ruff, mypy src/octop, tests/unit/gateway + tests/unit/i18n (694 passed), dashboard tsc and the Telegram channel tests.
  • Verified live on a Telegram non-forum supergroup: native tables, own-bot mention stripping, reply quote, and ask_user_question choice buttons.

Channel messaging improvements (before → after)

Screenshots: see PR comments / mockups — before/after captured on Telegram & QQ.

1. Choice cards: tap feedback & keyboard settlement

Before

  • Tapping a choice produced no confirmation — only the default button-press animation. The button list stayed expanded and tappable, and repeat taps were silently dropped by validation (dead-end UX).
  • /model additionally pushed two identical menu messages (the bridge processor was wrapped twice), and expired choices kept live-looking buttons forever (zombie buttons).
image

After

  • A valid tap toasts ✓ <label> at the top and settles the original message: the selection is appended and the inline keyboard is removed.
  • Expired/invalid choices toast "This option is invalid or has been processed." and also collapse the keyboard — no more zombie buttons.
  • Paging arrows (←/→) collapse only the old keyboard; Stop keeps its existing streaming cleanup path.
  • Exactly one menu message per /model invocation.
image

2. Table rendering across channels

Before

  • Telegram: GFM pipe tables were forwarded verbatim — | column | column | lines with a |---|---| separator row — unreadable in proportional fonts (scrambled columns, noisy separator).
  • QQ / WeChat iLink: the same raw pipes. WeChat iLink delivers plain text; QQ's official-bot markdown subset does not guarantee table support.
image

After

  • Telegram: tables ride Bot API 10.1 sendRichMessage (rich_message.markdown) for native table rendering (real borders, column alignment, up to 20 columns). Any failure — older client, API rejection — falls back to a CJK-double-width-aware column-aligned code block.
  • QQ / WeChat iLink: tables are flattened to keyed list rows (- 名称=CPU | 数值=80%) — the only layout that survives proportional fonts and line wrapping.
  • Tables inside fenced code blocks are left untouched on all channels.
image

Group chat reliability and review follow-ups

Fixes found while using the Telegram integration in group chats. They only touch the Telegram channel and its local gateway patches; nothing here changes plugin, dashboard or non-Telegram behavior.

Behavior

Rich tables (telegram_table_format.py)

  • Before: after editing the group preview placeholder into a native table, the stream state still pointed at that message, so the next segment of the same turn kept editing it and overwrote the table. sendRichMessage also dropped message_thread_id, reply parameters and the silent/protect-content options, so forum-topic tables landed in General. Any exception fell back to a resend, including ambiguous timeouts.
  • After: the placeholder is edited in place only while the preview is finalizing and its text matches, and then released the same way the gateway's _finish_preview does. Rich messages go through the gateway request scheduler with _send_options. Only explicit rejections (BadRequest, InvalidToken from older Bot API servers) fall back to the aligned code block; network errors set delivery_failed and are not resent.

Reply quotes (telegram_reply_quote.py)

  • The gateway only forwards reply_to_message_id, so the agent never saw what the user replied to. _extract_message (async) now adds reply_to_text / reply_quote_text, and parse_inbound (sync) passes exactly those two keys through. Passing every unknown payload key through leaked content into metadata, so the list is explicit.
  • Quotes are capped at 2000 characters, and a reply to the bot's own message is not injected because it is already in the history. The prompt prefix is localized.

Mentions (telegram_bridge.py)

  • Only this bot's own @username is removed, using the username the channel already knows. Other bots, e-mail addresses (john@abbot.com) and words such as @robot are kept. Indentation and repeated spaces are left untouched; the previous whitespace folding stripped indentation from every message. Without a known username nothing is stripped.

Choice buttons

  • In groups the thread binding changes with every new message, so the thread identity check made still-valid taps fail silently. Group buttons now rely on the gateway's user + chat check, the owner/model checks and, for HITL, the pending record (session + user + question index). Private chats keep the strict thread checks, which is what the existing tests cover.
  • A rejected tap sends a short localized notice instead of being dropped. The gateway has already answered the callback query before invoke, so a toast is not possible.
  • The HITL text card is held back while native buttons are sent and released again if sending the buttons fails.
  • Buttons pass ttl=1800 to send_buttons (aligned with the 30 minute HITL record); the previous Interactions.register patch is gone.
  • Interactions.invoke ignores the topic for callbacks registered without one. User and chat are still checked. A mismatch is logged at INFO (see Open question).

Bridge wiring (gateway.py)

  • Gateway._register_channel wraps Telegram channels in TelegramBridge once. An earlier commit in this branch had dropped the wrap, so native choice buttons, reject notices and the bridge-level mention/quote handling silently did not run (only the plain text card reached the user). test_telegram_bridge_wiring.py registers a channel through _register_channel and fails without the wrap.

Patch layer

  • All local channel patches install from patch_entry.install().
  • test_patch_audit.py finds every wrapper through __wrapped_base__ and checks that coroutine-ness matches the replaced method and that the wrapper accepts all of its parameters. patch_entry.verify() logs a warning when a patch target is missing or no longer wrapped, so a gateway upgrade cannot disable a patch silently.
  • Background: a sync wrapper around the async _extract_message once made every inbound Telegram message go unanswered (coroutine was never awaited).

i18n

  • Rejected-button notices and the quote prefix live in channel.telegram.* in en.json / zh.json instead of being hard-coded in infra/. Users without a stored locale get the channel default (en for Telegram).

Gateway follow-ups

message_thread_id means "message thread or forum topic" in the Bot API; only is_topic_message tells them apart. Confirmed on a live non-forum supergroup: buttons were registered with topic None (user message) while the tap carried the bot message's thread id (registered=None actual=397). The invoke patch keeps taps working until the gateway fix ships; once it does, the patch and its two diagnostic logs can be removed.

  • octop-gateway: issue #41, fix in PR #42 (use the thread id only when is_topic_message).
  • octop-harness: issue #89. Early stream close leaves nested generators open, which logs ContextVar ... created in a different Context after each ask_user_question pause. Log noise only, pre-existing, not addressed here.

Validation

  • ruff check, ruff format --check and mypy on src/octop/infra/gateway and src/octop/i18n: clean.
  • tests/unit/gateway + tests/unit/i18n: 694 passed against octop-gateway 1.0.3, octop-harness 1.0.4, octop-memory 1.0.2.
  • constants.test.ts (11 tests) and tsc --noEmit pass after merging develop.
  • New or reworked tests: test_rich_table_delivery.py, test_group_chat_regressions.py, test_patch_audit.py, tests/unit/i18n/test_channel.py.
  • CI is green on the PR head. Live group behavior was checked on a non-forum supergroup: native tables, own-bot mention stripping, reply quote and choice buttons work.

Screenshots

Before/after screenshots are in the sections above and in the PR comment (channel configuration UI).

Checklist

  • Base is develop
  • Added/updated tests
  • Updated CHANGELOG.md
  • Screenshots attached
  • CI passing
  • Live group verification

@ideshun

ideshun commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

This update adds Telegram channel settings for group activation, user and chat allowlists, topic isolation, streaming responses, and media delivery. It also adds native buttons for model selection and single-choice questions.

Screenshots below show the updated Telegram channel configuration UI.
image

@ideshun
ideshun marked this pull request as ready for review October 15, 2026 16:16

@Bosheng0422 Bosheng0422 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues should be fixed before merge. Button actions stall Telegram polling for the whole agent turn, and topic isolation rewrites private-chat and non-topic session keys.

Comment thread src/octop/infra/gateway/channels/telegram_bridge.py Outdated
Comment thread src/octop/infra/gateway/process/message_keys.py
Chovy and others added 16 commits October 16, 2026 08:01
Telegram 无表格语法, GFM 竖线表格在比例字体下排版错乱. 发送前把表格重排 CJK 双宽感知的列对齐代码块, 输出 <pre> 等宽渲染.
QQ weixin pingdao wu kekao dengkuan xuanyan, biao ge jiang wei jian-zhi lie-biao xing; fuyong telegram bu ding de biao ge jie xi (renderer can shu hua), chuai dan ce 6 xiang
GeneratorExit chuan guo async-for bu hui guan bi nei ceng sheng cheng qi; ge ceng bu finally-aclose hou, zheng tiao liu jiao gei GC zai yi yuan Context qing li, zhi octop_hitl_thread/octop_turn_model reset beng kui, resume zhong tu diu shi (dian ji an niu wu fan ying cheng yin zhi yi). jia gu liang ge scope de kua Context reset; an niu yan zheng shi bai luo WARNING yin gu. hui gui ce shi 3 xiang.
…allback

GFM tables ride Bot API 10.1 sendRichMessage (rich_message.markdown) for native client rendering; any failure falls back to the aligned code-block conversion.
toast + settle keyboard after choice; tests 4
close_event_stream comes from gateway 1.0.3; a local fallback keeps installs on older resolver versions working.
Co-authored-by: Cursor <cursoragent@cursor.com>
…h tables

The rich-table path edited the group preview placeholder in place but
skipped the preview bookkeeping that the gateway's _finish_preview does.
The message id stayed in the stream state, so the next segment of the
same turn kept editing that message and overwrote the rendered table.
sendRichMessage also dropped message_thread_id, reply parameters and the
silent/protect-content options, which put forum-topic tables in General.

- Edit in place only when the preview is finalizing and its text matches,
  then release message_id/finalizing/stop token like _finish_preview.
- Send through the gateway request scheduler with _send_options so rate
  limiting, 429 retries, topics and reply threading apply.
- Fall back to the aligned code block only on explicit rejections
  (BadRequest, InvalidToken from older Bot API servers). Ambiguous network
  errors mark delivery_failed and are not resent.

Tests: tests/unit/gateway/test_rich_table_delivery.py covers in-place
edit, state release, non-finalizing previews, topic/reply options,
fallback and the no-resend rule.

Co-authored-by: Cursor <cursoragent@cursor.com>
Rejected-button notices and the quoted-message prompt prefix are host
generated copy and belong in the backend bundles, not hard-coded in
infra/.

- channel.telegram.reject.* (with a default) and channel.telegram.quote.*
  in en.json and zh.json.
- telegram_reject_notice() and telegram_quote_prompt() in
  i18n/domains/channel.py; unknown reasons use the default notice.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reply quotes
- The gateway only forwards reply_to_message_id. Wrap _extract_message
  (async, kept async) to add reply_to_text/reply_quote_text and
  parse_inbound (sync) to pass exactly those two keys through; the whole
  payload is not passed through because that leaked `content` into
  metadata. Quotes are capped at 2000 characters and a reply to the bot's
  own message is not injected, since it is already in the history.
- The bridge prefixes the prompt with the localized quote header.

Mentions
- Strip only this bot's own @username (taken from the channel). Other
  mentions, other bots, e-mail addresses and words such as @Robot stay.
  Indentation and repeated spaces in the message are left untouched.

Choice buttons
- In group chats the thread binding moves with every new message, so the
  thread identity check made still-valid taps fail silently. Group
  buttons now rely on the gateway's user+chat check, owner/model checks
  and, for HITL, the pending record (session + user + question index).
  Private chats keep the strict thread checks.
- Rejected taps send a localized notice instead of being dropped.
- The text card is held back while native buttons are sent and is
  released again if sending the buttons fails.
- Buttons pass ttl=1800 to send_buttons (aligned with the 30 minute HITL
  record) instead of patching Interactions.register.
- Interactions.invoke ignores the topic for callbacks registered without
  one; user and chat are still checked. Mismatches are logged at INFO to
  confirm why the topics differ.

All local channel patches are installed from patch_entry.install().

Tests: test_group_chat_regressions.py plus adjusted bridge and
stream-close tests.
…liveness

The local patch layer wraps private gateway methods. A sync wrapper around
an async method once made every inbound Telegram message go unanswered,
and a gateway rename would disable a patch without any signal.

- Every wrapper records __wrapped_base__; the audit discovers them and
  checks that coroutine-ness matches the replaced method and that the
  wrapper accepts all of the base method's parameters.
- patch_entry.verify() checks that each patch target still exists and is
  wrapped; install() logs a warning naming the inactive patches.
- Clean up the mypy ignores in the Telegram patch modules.

Co-authored-by: Cursor <cursoragent@cursor.com>
User-facing behavior changes from the Telegram group chat fixes: rich
tables, reply quotes, own-bot mention stripping and choice buttons.

Co-authored-by: Cursor <cursoragent@cursor.com>
…import

With octop-gateway>=1.0.3 pinned, the imported close_event_stream matches
the fallback signature, so mypy reports the assignment ignore as unused and
`make typecheck` fails on Linux and Windows.

Co-authored-by: Cursor <cursoragent@cursor.com>
The bridge wrapper was dropped from Gateway._register_channel, so native
choice buttons, own-bot mention stripping, quote injection and button
rejection notices never ran. Restore the wrap (once, idempotent) and cover
it with a registration test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants