Files
sino-mci/channel-service/app/api/monitor.py
marsal bd0ac464cf 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
2026-07-07 12:36:54 +08:00

115 lines
3.7 KiB
Python

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)