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

125 lines
3.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""浏览器推送通知服务 — 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