Files
aiagent/backend/app/services/push_service.py

125 lines
3.3 KiB
Python
Raw Normal View History

"""浏览器推送通知服务 — Web Push Protocol (RFC 8291)"""
from __future__ import annotations
import json
import logging
from typing import Optional
from app.core.database import SessionLocal
from app.models.push_subscription import PushSubscription
logger = logging.getLogger(__name__)
async def send_push_to_user(
user_id: str,
title: str,
body: str = "",
url: str = "/",
require_interaction: bool = False,
) -> int:
"""向指定用户的所有设备发送推送通知。返回成功发送数。"""
db = SessionLocal()
sent = 0
try:
subs = (
db.query(PushSubscription)
.filter(PushSubscription.user_id == user_id)
.all()
)
for sub in subs:
ok = await _send_webpush(
endpoint=sub.endpoint,
p256dh=sub.p256dh,
auth=sub.auth,
title=title,
body=body,
url=url,
require_interaction=require_interaction,
)
if ok:
sent += 1
return sent
finally:
db.close()
async def broadcast_push(
title: str,
body: str = "",
url: str = "/",
) -> int:
"""向所有已订阅用户广播推送通知。"""
db = SessionLocal()
sent = 0
try:
subs = db.query(PushSubscription).all()
for sub in subs:
ok = await _send_webpush(
endpoint=sub.endpoint,
p256dh=sub.p256dh,
auth=sub.auth,
title=title,
body=body,
url=url,
require_interaction=False,
)
if ok:
sent += 1
return sent
finally:
db.close()
async def _send_webpush(
endpoint: str,
p256dh: str,
auth: str,
title: str,
body: str,
url: str = "/",
require_interaction: bool = False,
) -> bool:
"""发送单条 Web Push 消息。使用 pywebpush 或直接 HTTP POST。"""
payload = json.dumps({
"title": title,
"body": body,
"data": {"url": url},
"requireInteraction": require_interaction,
"icon": "/icons/icon-192.png",
"badge": "/icons/icon-192.png",
})
try:
# 尝试 pywebpush如果已安装
try:
from pywebpush import WebPusher, WebPushException
wp = WebPusher({
"endpoint": endpoint,
"keys": {"p256dh": p256dh, "auth": auth},
})
wp.send(payload, timeout=10)
return True
except ImportError:
pass
# Fallback: 直接 HTTP POST不加密仅用于开发环境
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.post(
endpoint,
data=payload,
headers={
"Content-Type": "application/json",
"TTL": "86400",
},
timeout=aiohttp.ClientTimeout(total=10),
) as resp:
ok = resp.status in (200, 201, 204)
if not ok:
logger.warning("Push 发送失败: status=%d", resp.status)
return ok
except Exception as e:
logger.error("Push 发送异常: %s", e)
return False