- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions, schedules, executions, team_members) and unbind goals/tasks before delete - Remove hardcoded personality templates in Android, replace with dynamic system prompt generation from name + description - Set promptSectionsEnabled=false to bypass PromptComposer for personality - Add Tencent Cloud Linux deployment guide (Docker Compose) - Accumulated backend service updates, frontend UI fixes, Android app changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
3.3 KiB
Python
125 lines
3.3 KiB
Python
"""浏览器推送通知服务 — 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
|