feat(channel-service): add Python FastAPI pywechat channel service
- Add FastAPI project under channel-service/ - Implement mock pywechat client adapter (login, send, heartbeat) - Add /v1/wechat-personal/login/*, /v1/wechat-personal/send/* APIs - Add /v1/heartbeat and /v1/monitor/status for client health - Add /monitor HTML page with Web Components - Add pytest suite and Dockerfile - Update .gitignore for Python/Node artifacts
This commit is contained in:
0
channel-service/app/services/__init__.py
Normal file
0
channel-service/app/services/__init__.py
Normal file
32
channel-service/app/services/heartbeat_service.py
Normal file
32
channel-service/app/services/heartbeat_service.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from app.clients.pywechat_client import ClientRegistry, OnlineStatus, registry
|
||||
|
||||
|
||||
class HeartbeatService:
|
||||
def __init__(self, registry: ClientRegistry):
|
||||
self.registry = registry
|
||||
|
||||
def heartbeat(self, account_id: str, payload: dict) -> dict:
|
||||
client = self.registry.get_or_create(account_id, payload.get("account_name"))
|
||||
client.heartbeat()
|
||||
if "risk_level" in payload:
|
||||
client.state.risk_level = payload["risk_level"]
|
||||
if "sent_count_today" in payload:
|
||||
client.state.sent_count_today = payload["sent_count_today"]
|
||||
return {"success": True, "account_id": account_id}
|
||||
|
||||
def check_offline_accounts(self, timeout_seconds: int = 120) -> list[dict]:
|
||||
now = time.time()
|
||||
offline = []
|
||||
for client in self.registry.list_all():
|
||||
last = client.state.last_heartbeat_at
|
||||
if client.state.online_status == OnlineStatus.ONLINE and (last is None or now - last > timeout_seconds):
|
||||
client.state.online_status = OnlineStatus.OFFLINE
|
||||
offline.append({"account_id": client.state.account_id, "last_heartbeat_at": last})
|
||||
return offline
|
||||
|
||||
|
||||
heartbeat_service = HeartbeatService(registry)
|
||||
53
channel-service/app/services/login_service.py
Normal file
53
channel-service/app/services/login_service.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from app.clients.pywechat_client import ClientRegistry, LoginStatus, OnlineStatus, registry
|
||||
|
||||
|
||||
class LoginService:
|
||||
def __init__(self, registry: ClientRegistry):
|
||||
self.registry = registry
|
||||
|
||||
def start_login(self, account_id: str, account_name: Optional[str] = None) -> dict:
|
||||
client = self.registry.get_or_create(account_id, account_name)
|
||||
login_id = client.start_login()
|
||||
return {
|
||||
"login_id": login_id,
|
||||
"qr_code_url": client.state.qr_code_url,
|
||||
"status": client.state.login_status.value,
|
||||
}
|
||||
|
||||
def get_status(self, account_id: str) -> dict:
|
||||
client = self.registry.get(account_id)
|
||||
if client is None:
|
||||
return {"found": False, "account_id": account_id}
|
||||
state = client.get_state()
|
||||
return {
|
||||
"found": True,
|
||||
"account_id": state.account_id,
|
||||
"account_name": state.account_name,
|
||||
"login_status": state.login_status.value,
|
||||
"online_status": state.online_status.value,
|
||||
"last_heartbeat_at": state.last_heartbeat_at,
|
||||
"risk_level": state.risk_level,
|
||||
"sent_count_today": state.sent_count_today,
|
||||
}
|
||||
|
||||
def confirm_login(self, account_id: str) -> dict:
|
||||
client = self.registry.get(account_id)
|
||||
if client is None:
|
||||
return {"success": False, "error": "account not found"}
|
||||
client.confirm_login()
|
||||
return {"success": True, "status": client.state.login_status.value}
|
||||
|
||||
def expire_login(self, account_id: str) -> dict:
|
||||
client = self.registry.get(account_id)
|
||||
if client is None:
|
||||
return {"success": False, "error": "account not found"}
|
||||
client.expire_login()
|
||||
return {"success": True, "status": client.state.login_status.value}
|
||||
|
||||
|
||||
login_service = LoginService(registry)
|
||||
33
channel-service/app/services/send_service.py
Normal file
33
channel-service/app/services/send_service.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.clients.pywechat_client import ClientRegistry, OnlineStatus, registry
|
||||
|
||||
|
||||
class SendService:
|
||||
def __init__(self, registry: ClientRegistry):
|
||||
self.registry = registry
|
||||
|
||||
def send_text(self, account_id: str, conversation_id: str, text: str) -> dict:
|
||||
client = self.registry.get(account_id)
|
||||
if client is None:
|
||||
return {"success": False, "error": f"account {account_id} not registered"}
|
||||
if client.state.online_status != OnlineStatus.ONLINE:
|
||||
return {"success": False, "error": "account not online"}
|
||||
return client.send_text(conversation_id, text)
|
||||
|
||||
def send_image(self, account_id: str, conversation_id: str, image_url: str) -> dict:
|
||||
# Placeholder for image sending via pywechat.
|
||||
client = self.registry.get(account_id)
|
||||
if client is None:
|
||||
return {"success": False, "error": f"account {account_id} not registered"}
|
||||
if client.state.online_status != OnlineStatus.ONLINE:
|
||||
return {"success": False, "error": "account not online"}
|
||||
return {
|
||||
"success": True,
|
||||
"message_id": f"mock-img-{image_url.split('/')[-1]}",
|
||||
"conversation_id": conversation_id,
|
||||
"type": "image",
|
||||
}
|
||||
|
||||
|
||||
send_service = SendService(registry)
|
||||
Reference in New Issue
Block a user