Skip to content

feat(portal): channel-agnostic channels portal (WeChat + Telegram)#55

Merged
Germey merged 1 commit into
mainfrom
feat/portal-channel-agnostic
Jul 3, 2026
Merged

feat(portal): channel-agnostic channels portal (WeChat + Telegram)#55
Germey merged 1 commit into
mainfrom
feat/portal-channel-agnostic

Conversation

@acedatacloud-dev

Copy link
Copy Markdown
Member

What & why

The coding-bridge channels portal opened straight into a WeChat-only editor ("Channels · WeChat") — it proxied WeChat gateway endpoints and had no entry point for any other channel. With Telegram now a first-class channel (#54), the portal needs to be channel-agnostic.

This redesigns the portal into a channel overview that lists every configured instance (WeChat + Telegram) with a type badge and enabled state, and opens a type-appropriate editor when you click one.

Changes

Backend (portal.py)

  • public_config() now emits both WeChat and Telegram instances, each tagged with a type ("wechat" / "telegram"). New _telegram_instance_public().
  • save() splits posted instances by type into [[channels.wechat]] / [[channels.telegram]] blocks and round-trips through the real loader (unknown type → 400).
  • dump_channels_toml() + new _dump_telegram_instance() serialize Telegram blocks; require_approval is now serialized for both types (it was silently dropped on WeChat portal saves before).
  • New telegram_status() — a getMe probe (sync httpx). The bot token lives only in the request URL path and is never logged or surfaced; errors reduce to the exception class name only.
  • New route GET /api/telegram/status; _telegram_instance() lookup.

Frontend (portal_html.py)

  • Header is now "Channels"; a channel overview card lists all instances (/ icon + type badge + enabled dot), click to select.
  • render() branches on it.typerenderWeChatEditor (unchanged: account/contacts search/groups/QR) vs new renderTelegramEditor (token status via getMe, numeric-id allowlists as textareas, dedup field, provider, rate limit).
  • Shared behaviorCard / safetyCard (the Safety card exposes require_approval for both types now).
  • save() payload carries type + api_base (Telegram) / base_url (WeChat) + require_approval.

Tests

  • +Telegram/type coverage in test_channels_portal.py (type in public_config, save round-trips both types, require_approval preserved for both, unknown-type rejected, telegram_status ok/401-no-leak/404, live /api/telegram/status route, TOML round-trip) + a static guard asserting the client save() carries type.

Testing

  • ruff check clean. Portal suite 34 passed; full suite 481 passed, 1 skipped (3 pre-existing Windows-only failures unrelated to this change).
  • Playwright, end-to-end against a config with a WeChat and a Telegram instance: overview lists both; the Telegram editor renders (token-status, senders/groups textareas, dedup, require_approval reflecting config); editing + enabling + Save rewrote channels.toml with both blocks intact (WeChat preserved, Telegram enabled/require_approval/allowlists persisted).

🤖 对抗评审

Reviewer: Claude general-purpose subagent (Codex unavailable on this host), read-only, one round. It returned VERDICT: ISSUES FOUND with 2 RISKs:

  • RISK 1 — per-render document click listener accumulates (portal_html.py). Real (a latent issue amplified by the new per-select re-render). FIXED: hoisted the contact-search dropdown-close listener to a single global registration at bootstrap; removed the per-render addEventListener. Verified with Playwright — after 6 channel switches the dropdown still closes on an outside click (results showresults), with one listener.
  • RISK 2 — save() defaults a missing type to "wechat" (portal.py). REBUTTED (won't fix): this is deliberate backward-compatibility. The legacy WeChat-only payload (and 6 existing tests) post instances with no type; defaulting to wechat keeps them working. Telegram is brand-new so no type-less Telegram payload can exist; the client always sends type (enforced by a static guard test); and unknown non-empty types are already rejected with 400. Forcing type required would break the documented backward-compatible payload for no real-world safety gain.

A separate save bug was found during Playwright testing and fixed before review: the client save() mapper dropped the type field (and sent base_url for Telegram), so a Telegram instance was mis-serialized as a [[channels.wechat]] block → base_url missing 400. The mapper now sends type/api_base/require_approval; a backend round-trip test + a static HTML guard prevent regression.

…egram)

The portal opened straight into a WeChat-only editor with no entry point for other channels. It now lists every configured channel in an overview and opens a type-appropriate editor for each.

- portal.py: public_config emits both wechat + telegram instances (each tagged with a type); save() splits posted instances by type; new telegram_status() getMe probe (token stays server-side, never logged); require_approval now serialized for both types (previously silently dropped on WeChat save).

- portal_html.py: channel overview list + per-type editors (WeChat keeps contacts/groups/QR; Telegram gets token status, id-list allowlists, dedup); shared behavior/safety cards; save() payload now carries type/api_base/require_approval.

- Fixed a save bug found via Playwright: the client dropped the type field, mis-serializing a Telegram instance as a wechat block. Added a static guard + backend round-trip tests.

- Hoisted the contact-search dropdown listener so it registers once instead of per render. 34 portal tests (full suite 481 pass/1 skip); Playwright-verified both editors + save round-trip.
@Germey Germey merged commit 3d2d383 into main Jul 3, 2026
3 checks passed
@Germey Germey deleted the feat/portal-channel-agnostic branch July 3, 2026 18:40
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