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:
114
channel-service/app/api/monitor.py
Normal file
114
channel-service/app/api/monitor.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.clients.pywechat_client import registry
|
||||
from app.services.heartbeat_service import heartbeat_service
|
||||
|
||||
router = APIRouter(tags=["monitor"])
|
||||
|
||||
|
||||
class HeartbeatRequest(BaseModel):
|
||||
account_id: str = Field(..., description="渠道账号唯一标识")
|
||||
account_name: str | None = Field(None)
|
||||
risk_level: int = Field(0, ge=0, le=100)
|
||||
sent_count_today: int = Field(0, ge=0)
|
||||
|
||||
|
||||
@router.post("/v1/heartbeat")
|
||||
def heartbeat(request: HeartbeatRequest):
|
||||
return heartbeat_service.heartbeat(request.account_id, request.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
@router.get("/v1/monitor/status")
|
||||
def monitor_status():
|
||||
clients = []
|
||||
for client in registry.list_all():
|
||||
state = client.get_state()
|
||||
clients.append({
|
||||
"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,
|
||||
"error_message": state.error_message,
|
||||
})
|
||||
return {"clients": clients, "count": len(clients)}
|
||||
|
||||
|
||||
@router.get("/monitor", response_class=HTMLResponse)
|
||||
def monitor_page(request: Request):
|
||||
html = """<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>pywechat 客户端监控</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; margin: 20px; background: #f5f5f5; }
|
||||
h1 { font-size: 20px; }
|
||||
table { width: 100%; border-collapse: collapse; background: #fff; }
|
||||
th, td { padding: 10px; border: 1px solid #ddd; text-align: left; }
|
||||
th { background: #f0f0f0; }
|
||||
.online { color: green; }
|
||||
.offline { color: red; }
|
||||
.pending { color: orange; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>pywechat 客户端监控</h1>
|
||||
<channel-status-table></channel-status-table>
|
||||
|
||||
<script>
|
||||
class ChannelStatusTable extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.render();
|
||||
setInterval(() => this.refresh(), 5000);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
const res = await fetch('/v1/monitor/status');
|
||||
const body = await res.json();
|
||||
this.render(body.clients || []);
|
||||
} catch (e) {
|
||||
this.innerHTML = '<p>加载失败:' + e.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
render(clients = []) {
|
||||
if (!clients.length) {
|
||||
this.innerHTML = '<p>暂无客户端</p>';
|
||||
return;
|
||||
}
|
||||
const rows = clients.map(c => `
|
||||
<tr>
|
||||
<td>${c.account_id}</td>
|
||||
<td>${c.account_name || '-'}</td>
|
||||
<td class="${c.login_status}">${c.login_status}</td>
|
||||
<td class="${c.online_status}">${c.online_status}</td>
|
||||
<td>${c.risk_level}</td>
|
||||
<td>${c.sent_count_today}</td>
|
||||
<td>${c.last_heartbeat_at ? new Date(c.last_heartbeat_at * 1000).toLocaleString() : '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
this.innerHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>账号ID</th><th>名称</th><th>登录状态</th><th>在线状态</th><th>风控等级</th><th>今日发送</th><th>最后心跳</th></tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('channel-status-table', ChannelStatusTable);
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
return HTMLResponse(content=html)
|
||||
Reference in New Issue
Block a user