Harness IM Bridge
Multi-platform IM channel bridge — unified message abstraction for AI agents and bots.
English · 中文
Highlights
- 9 Platforms, One Processor — Write one async generator function, run your bot on Feishu, QQ, WeChat Work, DingTalk, Discord, WeChat, and more
- Streaming-First — Delta events accumulate token-by-token;
COMPLETEDtriggers merge,<think>cleanup, and send - Platform Constraints Handled — Reply timeouts, rate limits, and typing indicators managed transparently per platform
- Proactive Push — Bot-initiated messages for notifications, webhooks, and scheduled tasks
- LLM Integration Ready — Works with OpenAI streaming, LangGraph agents, and any async generator pattern
Overview
Harness IM Bridge lets you write one processor function and deploy your AI bot across 9 messaging platforms simultaneously. The library handles platform-specific transport (WebSocket, HTTP, Socket.IO), message format translation, media upload/download, rate limiting, and reply timeout strategies — so your bot logic stays clean and platform-agnostic.
async def my_bot(message: InboundMessage) -> AsyncIterator[MessageEvent]:
yield MessageEvent.text(f"Received: {message.text}")
yield MessageEvent.completed()
Core Technology
| Component | Technology | Purpose |
|---|---|---|
| Message Model | Pydantic v2 | Typed InboundMessage / MessageEvent / ContentPart |
| Transport | Platform SDKs + aiohttp | WebSocket, HTTP callback, Socket.IO, streaming |
| Orchestration | asyncio + Worker pools | Multi-channel queues with session-aware batching |
| Constraints | Sliding window + timeout guards | Transparent rate limiting and reply timeout handling |
| Media | Per-platform abstraction | Unified upload/download across IM platforms |
| Registry | Decorator + filesystem discovery | @channel auto-registration for custom channels |
Features
- Unified Model —
InboundMessage/MessageEvent/ContentPartacross all platforms - Async Generator Pattern — Simple processor: receive messages, yield responses
- Multi-Channel — Run multiple platforms simultaneously with session-aware batching
- Streaming Support — DELTA events for token-by-token LLM output, merged on COMPLETED
<think>Auto-Filtering — Reasoning model thinking content stripped automatically- Platform Constraints — Reply timeouts, rate limits, typing indicators per platform
- Proactive Push — Bot-initiated messages for notifications and scheduled tasks
- Media Handling — Upload/download abstraction per platform (images, files, audio, video)
- Session Awareness — Same-session serial, cross-session parallel, prevents message reordering
- Message Batching — Rapid consecutive messages from the same user are auto-batched
- Extensible —
@channeldecorator + filesystem discovery for custom channels
Supported Platforms
| Platform | channel_type | Transport | Text | Image | File | Push |
|---|---|---|---|---|---|---|
| Feishu (Lark) | feishu |
WebSocket + REST | ✅ | ✅ | ✅ | ✅ |
qq |
WebSocket + REST | ✅ | ✅ | ✅ | ✅ | |
| WeChat Work | wecom |
WebSocket + REST | ✅ | ✅ | ✅ | ✅ |
| DingTalk | dingtalk |
Stream + REST | ✅ | ✅ | ✅ | ✅ |
| Discord | discord |
discord.py | ✅ | ✅ | ✅ | ✅ |
| WeChat iLink | weixin |
HTTP Long-Poll | ✅ | ✅ | ✅ | ✅ |
| AgentChat | agentchat |
Socket.IO | ✅ | ✅ | ✅ | ✅ |
| Yuanbao | yuanbao |
HTTP API | ✅ | ✅ | ✅ | ✅ |
| Dashboard | dashboard |
Console (stdout) | ✅ | — | — | — |
Quick Start
Installation
pip install harness-im-bridge
Minimal Echo Bot
import asyncio
from collections.abc import AsyncIterator
from harness_im_bridge import ChannelManager, InboundMessage, MessageEvent
from harness_im_bridge.channels.dashboard import DashboardConfig
async def echo(message: InboundMessage) -> AsyncIterator[MessageEvent]:
yield MessageEvent.text(f"Echo: {message.text}")
yield MessageEvent.completed()
async def main():
manager = ChannelManager(processor=echo)
await manager.start()
channel_id = await manager.add_channel("dashboard", DashboardConfig())
manager.enqueue(channel_id, "Hello World")
await asyncio.sleep(0.5)
await manager.stop()
asyncio.run(main())
Real Platform (QQ Bot)
import asyncio
from collections.abc import AsyncIterator
from harness_im_bridge import ChannelManager, InboundMessage, MessageEvent
from harness_im_bridge.channels.qq import QQConfig
async def bot(msg: InboundMessage) -> AsyncIterator[MessageEvent]:
yield MessageEvent.text(f"Echo: {msg.text}")
yield MessageEvent.completed()
async def main():
manager = ChannelManager(processor=bot)
await manager.start()
channel_id = await manager.add_qq_channel(
QQConfig(app_id="YOUR_APP_ID", token="YOUR_TOKEN", secret="YOUR_SECRET")
)
await asyncio.Event().wait()
asyncio.run(main())
Multi-Platform
import asyncio, os
from collections.abc import AsyncIterator
from harness_im_bridge import ChannelManager, InboundMessage, MessageEvent
from harness_im_bridge.channels.feishu import FeishuConfig
from harness_im_bridge.channels.qq import QQConfig
from harness_im_bridge.channels.dingtalk import DingTalkConfig
async def unified_bot(msg: InboundMessage) -> AsyncIterator[MessageEvent]:
yield MessageEvent.text(f"[{msg.channel_type}] {msg.text}")
yield MessageEvent.completed()
async def main():
manager = ChannelManager(processor=unified_bot, workers_per_channel=4)
await manager.start()
await manager.add_feishu_channel(
FeishuConfig(app_id=os.environ["FEISHU_APP_ID"], app_secret=os.environ["FEISHU_APP_SECRET"])
)
await manager.add_qq_channel(
QQConfig(app_id=os.environ["QQ_APP_ID"], token=os.environ["QQ_TOKEN"], secret=os.environ["QQ_SECRET"])
)
await manager.add_dingtalk_channel(
DingTalkConfig(app_key=os.environ["DINGTALK_APP_KEY"], app_secret=os.environ["DINGTALK_APP_SECRET"])
)
await asyncio.Event().wait()
asyncio.run(main())
Streaming + LLM Integration
Event Types
| Event | Purpose | Channel Behavior |
|---|---|---|
MESSAGE |
Complete message | Send immediately |
DELTA |
Token-by-token LLM output | Accumulate, merge on COMPLETED |
TOOL_START |
Tool call starting | Show status hint (configurable) |
TOOL_END |
Tool call finished | Silent by default |
TYPING |
Typing indicator | Platform typing status |
ERROR |
Error occurred | Send error message |
COMPLETED |
Stream ended | Flush delta buffer → clean <think> → send |
OpenAI Streaming
from openai import AsyncOpenAI
from harness_im_bridge import InboundMessage, MessageEvent
client = AsyncOpenAI(api_key="sk-xxx")
async def chat_bot(msg: InboundMessage) -> AsyncIterator[MessageEvent]:
yield MessageEvent.typing()
stream = await client.chat.completions.create(
model="gpt-4o", messages=[{"role": "user", "content": msg.text}], stream=True
)
async for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
yield MessageEvent.delta(chunk.choices[0].delta.content)
yield MessageEvent.completed()
<think> Auto-Filtering
Reasoning models (DeepSeek-R1, QwQ, etc.) output <think>...</think> blocks. Harness IM Bridge strips these automatically — users see only the final answer.
Platform Constraints
Different IM platforms have reply timeouts and rate limits. Harness IM Bridge handles them transparently:
| Platform | Reply Timeout | Rate Limit | Strategy |
|---|---|---|---|
| WeChat Work | 10s | ≤2 per 5s | Placeholder on timeout |
| WeChat Official | 5s | ≤5 per min | Placeholder + typing refresh |
| None | ≤20 per min | Rate-limit wait | |
| Feishu | None | Relaxed | No constraint |
| Discord | None | ≤5 per 5s | Rate-limit wait |
Configure constraints per channel:
from harness_im_bridge import ChannelConstraints
constraints = ChannelConstraints(
reply_timeout=10.0,
send_rate_limit=(2, 5.0),
timeout_strategy="placeholder",
show_thinking=False,
show_tool_hints=True,
)
Proactive Push
# channel_id is the UUID returned by add_*_channel()
await manager.push_text(channel_id, "user_open_id", "Your build completed ✅")
# Push rich content
from harness_im_bridge import TextContent, ImageContent
await manager.push_content(channel_id, "user_openid", [
TextContent(text="📊 Daily Report:"),
ImageContent(url="https://charts.example.com/daily.png"),
])
Multi-Tenant Support
Run multiple instances of the same platform — each with its own tenant_id for session isolation:
from harness_im_bridge.channels.feishu import FeishuConfig
# Two Feishu tenants, session keys auto-isolated as feishu-{tenant_id}-{sender_id}
t1_id = await manager.add_feishu_channel(
FeishuConfig(app_id="cli_t1", app_secret="secret1", tenant_id="tenant1")
)
t2_id = await manager.add_feishu_channel(
FeishuConfig(app_id="cli_t2", app_secret="secret2", tenant_id="tenant2")
)
# InboundMessage carries tenant context
async def bot(msg: InboundMessage):
print(msg.channel_type) # "feishu"
print(msg.tenant_id) # "tenant1" or "tenant2"
print(msg.channel_id) # UUID instance ID
yield MessageEvent.text(f"[{msg.tenant_id}] {msg.text}")
yield MessageEvent.completed()
---
## Custom Channels
```python
from dataclasses import dataclass
from harness_im_bridge import BaseChannel, ChannelConfig, InboundMessage, ContentPart, MessageProcessor
from harness_im_bridge.registry import channel
@dataclass
class TelegramConfig(ChannelConfig):
bot_token: str = ""
@channel("telegram")
class TelegramChannel(BaseChannel):
channel_type = "telegram"
def __init__(self, processor: MessageProcessor, config: TelegramConfig, *,
channel_id: str | None = None, tenant_id: str | None = None):
super().__init__(processor, channel_id=channel_id, tenant_id=tenant_id)
self._bot_token = config.bot_token
async def start(self) -> None: ...
async def stop(self) -> None: ...
async def _send_text(self, to_handle: str, text: str, meta=None) -> None: ...
async def _send_content(self, to_handle: str, parts: list[ContentPart], meta=None) -> None: ...
async def _send_media(self, to_handle: str, media: ContentPart, meta=None) -> None: ...
def parse_inbound(self, raw_payload) -> InboundMessage: ...
Architecture
┌─────────────────┐
│ IM Platform │ Feishu / QQ / WeChat Work / DingTalk / Discord / ...
└────────┬────────┘
│ WebSocket / HTTP / Socket.IO
▼
┌─────────────────┐
│ BaseChannel │ parse_inbound() → InboundMessage
│ (impl) │ send_text() / send_content()
└────────┬────────┘
│ enqueue
▼
┌─────────────────┐
│ ChannelManager │ Multi-channel queues + Worker pool + Session lock
└────────┬────────┘
│ InboundMessage
▼
┌─────────────────┐
│ Your Processor │ async def(InboundMessage) -> AsyncIterator[MessageEvent]
└────────┬────────┘
│ MessageEvent (MESSAGE / DELTA / TOOL_START / ...)
▼
┌─────────────────┐
│ BaseChannel │ Constraints: timeout guard + rate limiter + typing
│ (send reply) │ Delta merge → platform API delivery
└─────────────────┘
Development
make install # Install dependencies
make test # Run tests
make test-quick # Unit tests only (fast)
make lint # Lint + type check
make format # Format code
make build # Build wheel
Related Projects
| Project | Description |
|---|---|
| harness-agent | Production-grade AI agent platform built on LangChain Deep Agents |
| harness-memory | Pluggable memory system with hierarchical recall and FTS |
| harness-browser | AI-friendly browser automation via CDP |
License
Metadata
Release files for harness-im-bridge 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| harness_im_bridge-0.1.0.tar.gz | 428.4 kB | Without provenance | Details for harness_im_bridge-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| harness_im_bridge-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for harness_im_bridge-0.1.0-py3-none-any.whl |
Total release size: 523.4 kB
Release files / harness_im_bridge-0.1.0.tar.gz
| Download URL | harness_im_bridge-0.1.0.tar.gz |
|---|---|
| Size | 428.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f742e1aa6770c66df4ce4c74ccfaba5433ee7bd6acfb7a57483480822a277451
|
|
BLAKE2b-256 checksum How to use checksums |
1e077db1a815d5081f049d15b1e5a3617b00dc6e2830eaef9822815d2961792a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Release files / harness_im_bridge-0.1.0-py3-none-any.whl
| Download URL | harness_im_bridge-0.1.0-py3-none-any.whl |
|---|---|
| Size | 95.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f247a111347ec164dc4f771ab3a9d63b2002a9633e827f65918818871cef6850
|
|
BLAKE2b-256 checksum How to use checksums |
3dba32a3bc43328420c724f159dc87162920058fa4088689c4536b62481f400c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Security for harness-im-bridge 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.