- 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
115 lines
3.7 KiB
Python
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)
|