Skip to content

Commit c98f770

Browse files
committed
Merge feat/v0.2-transport-layer: two-layer architecture (Transport + Bot)
2 parents 499f7f0 + 4eac5f2 commit c98f770

29 files changed

+2878
-552
lines changed

README.md

Lines changed: 229 additions & 212 deletions
Large diffs are not rendered by default.

README_zh.md

Lines changed: 226 additions & 210 deletions
Large diffs are not rendered by default.

examples/builder_bot.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""
2+
Builder 模式 + 中间件示例。
3+
4+
演示:
5+
- WeChatBot.builder() 流式构建
6+
- 自定义中间件(日志、限流)
7+
- 自定义错误处理
8+
9+
Usage:
10+
python examples/builder_bot.py
11+
"""
12+
13+
import asyncio
14+
import time
15+
16+
from wechat_agent_sdk import (
17+
Agent,
18+
ChatRequest,
19+
ChatResponse,
20+
Context,
21+
WeChatBot,
22+
make_error_middleware,
23+
)
24+
25+
26+
class SmartAgent(Agent):
27+
async def chat(self, request: ChatRequest) -> ChatResponse:
28+
if "error" in request.text:
29+
raise ValueError("Simulated error for testing")
30+
return ChatResponse(text=f"Reply: {request.text}")
31+
32+
33+
# ── Middleware examples ──
34+
35+
async def logging_middleware(ctx: Context, next_fn):
36+
"""Log inbound and outbound messages."""
37+
print(f"[LOG] ← {ctx.request.conversation_id}: {ctx.request.text[:50]}")
38+
start = time.time()
39+
await next_fn()
40+
elapsed = time.time() - start
41+
reply = ctx.response.text[:50] if ctx.response and ctx.response.text else "None"
42+
print(f"[LOG] → {reply} ({elapsed:.2f}s)")
43+
44+
45+
_last_message_time: dict[str, float] = {}
46+
47+
async def rate_limit_middleware(ctx: Context, next_fn):
48+
"""Simple per-user rate limiter (1 message per second)."""
49+
user = ctx.request.conversation_id
50+
now = time.time()
51+
last = _last_message_time.get(user, 0)
52+
if now - last < 1.0:
53+
ctx.response = ChatResponse(text="Too fast, please wait a moment.")
54+
return # short-circuit
55+
_last_message_time[user] = now
56+
await next_fn()
57+
58+
59+
async def custom_error_handler(ctx: Context, error: Exception):
60+
"""Custom error handler: log and return friendly message."""
61+
print(f"[ERROR] {error}")
62+
return ChatResponse(text=f"Oops: {error}")
63+
64+
65+
# ── Main ──
66+
67+
async def main():
68+
bot = (
69+
WeChatBot.builder()
70+
.agent(SmartAgent())
71+
.on_error(custom_error_handler)
72+
.middleware(logging_middleware)
73+
.middleware(rate_limit_middleware)
74+
.build()
75+
)
76+
77+
try:
78+
await bot.run()
79+
except KeyboardInterrupt:
80+
await bot.stop()
81+
82+
83+
if __name__ == "__main__":
84+
asyncio.run(main())

examples/multi_bot.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
多账号管理示例。
3+
4+
WeChatBotManager 管理多个 bot 实例,支持独立启停和自动重启。
5+
6+
Usage:
7+
python examples/multi_bot.py
8+
"""
9+
10+
import asyncio
11+
12+
from wechat_agent_sdk import (
13+
Agent,
14+
ChatRequest,
15+
ChatResponse,
16+
WeChatBotManager,
17+
)
18+
19+
20+
class TaggedAgent(Agent):
21+
"""An agent that prefixes replies with its tag."""
22+
23+
def __init__(self, tag: str):
24+
self._tag = tag
25+
26+
async def chat(self, request: ChatRequest) -> ChatResponse:
27+
return ChatResponse(text=f"[{self._tag}] {request.text}")
28+
29+
30+
async def main():
31+
manager = WeChatBotManager(auto_restart=True, max_restart_attempts=3)
32+
33+
# Register multiple bots
34+
manager.add_bot("bot_alice", agent=TaggedAgent("Alice"))
35+
manager.add_bot("bot_bob", agent=TaggedAgent("Bob"))
36+
37+
print(f"Registered {manager.bot_count} bots")
38+
print(f"Status: {manager.get_status()}")
39+
40+
# Each bot needs to login separately
41+
# In a real scenario, you'd call bot.login() or use the web login API
42+
43+
try:
44+
await manager.start_all()
45+
except KeyboardInterrupt:
46+
pass
47+
finally:
48+
await manager.stop_all()
49+
50+
51+
if __name__ == "__main__":
52+
asyncio.run(main())

examples/transport_demo.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
Transport 层独立使用示例 — 平台集成场景。
3+
4+
Transport 层不管 Agent、不管中间件,只做传输:
5+
连接、收消息、解析、发送。
6+
7+
平台自己管权限、会话、Agent 调用、SSE 流式。
8+
9+
Usage:
10+
python examples/transport_demo.py
11+
"""
12+
13+
import asyncio
14+
15+
from wechat_agent_sdk import WeChatTransport
16+
17+
18+
async def main():
19+
transport = WeChatTransport(account_id="demo")
20+
21+
# Terminal login (or use request_login() + check_login() for web UI)
22+
await transport.login_terminal()
23+
24+
# Connect and receive messages
25+
await transport.connect()
26+
print("[transport] Connected, waiting for messages...")
27+
28+
try:
29+
async for raw_msg in transport.messages():
30+
parsed = transport.parse(raw_msg)
31+
if not parsed:
32+
continue
33+
34+
print(f"[transport] From: {parsed.conversation_id}")
35+
print(f"[transport] Text: {parsed.text}")
36+
print(f"[transport] Media count: {len(parsed.media)}")
37+
38+
# Echo reply via transport
39+
await transport.send_text(
40+
parsed.conversation_id,
41+
f"[Transport 模式] 收到: {parsed.text}",
42+
parsed.context_token,
43+
)
44+
except KeyboardInterrupt:
45+
pass
46+
finally:
47+
await transport.disconnect()
48+
49+
50+
if __name__ == "__main__":
51+
asyncio.run(main())

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ classifiers = [
2626
]
2727
dependencies = [
2828
"httpx>=0.27",
29+
"cryptography>=42.0",
2930
]
3031

3132
[project.optional-dependencies]
3233
qr = ["qrcode[pil]>=7.0"]
3334
acp = ["agent-client-protocol>=0.8"]
3435
openai = ["openai>=1.0"]
36+
redis = ["redis>=5.0"]
37+
sqlite = ["aiosqlite>=0.20"]
38+
all = ["qrcode[pil]>=7.0", "agent-client-protocol>=0.8", "openai>=1.0", "redis>=5.0", "aiosqlite>=0.20"]
3539
dev = [
3640
"pytest>=8.0",
3741
"pytest-asyncio>=0.24",

src/wechat_agent_sdk/__init__.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
"""
22
wechat-agent-sdk — WeChat AI Agent bridge framework.
33
4-
Provides a simple Agent interface to connect any AI backend to WeChat
5-
via the iLink Bot API, with optional ACP (Agent Client Protocol) support.
4+
Two-layer architecture:
65
7-
Quick start::
6+
**Transport layer** (platform integration)::
7+
8+
from wechat_agent_sdk import WeChatTransport, ParsedMessage
9+
10+
transport = WeChatTransport(account_id="bot_1")
11+
async for msg in transport.messages():
12+
parsed = transport.parse(msg)
13+
await transport.send_text(parsed.conversation_id, "reply")
14+
15+
**Bot layer** (standalone developers)::
816
917
from wechat_agent_sdk import Agent, ChatRequest, ChatResponse, WeChatBot
1018
@@ -16,18 +24,43 @@ async def chat(self, request: ChatRequest) -> ChatResponse:
1624
await bot.run()
1725
"""
1826

27+
# === Transport layer (platform integration) ===
28+
from .transport import WeChatTransport, ParsedMessage, LoginRequiredError
29+
30+
# === Bot layer (standalone developers) ===
1931
from .agent import Agent
2032
from .types import ChatRequest, ChatResponse, MediaInfo, MediaResponseInfo
21-
from .account.manager import WeChatBot
33+
from .account.manager import WeChatBot, WeChatBotBuilder
34+
from .middleware import Context, Middleware, MiddlewareChain, make_error_middleware
35+
from .account.bot_manager import WeChatBotManager, BotStatus
36+
37+
# === Shared infrastructure ===
2238
from .account.storage import AccountStorage, JsonFileStorage
39+
from .api.auth import LoginSession, LoginResult, LoginStatus
2340

2441
__all__ = [
42+
# Transport
43+
"WeChatTransport",
44+
"ParsedMessage",
45+
"LoginRequiredError",
46+
# Bot
2547
"Agent",
2648
"ChatRequest",
2749
"ChatResponse",
2850
"MediaInfo",
2951
"MediaResponseInfo",
3052
"WeChatBot",
53+
"WeChatBotBuilder",
54+
"WeChatBotManager",
55+
"BotStatus",
56+
"Context",
57+
"Middleware",
58+
"MiddlewareChain",
59+
"make_error_middleware",
60+
# Shared
3161
"AccountStorage",
3262
"JsonFileStorage",
63+
"LoginSession",
64+
"LoginResult",
65+
"LoginStatus",
3366
]

0 commit comments

Comments
 (0)