初始版本
This commit is contained in:
commit
9de9186977
60
.gitignore
vendored
Normal file
60
.gitignore
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# 虚拟环境
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
wowfish/
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
*.log.*
|
||||
notification_client.log*
|
||||
|
||||
# 客户端特定文件
|
||||
client_key.txt
|
||||
notification_history.json
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
|
||||
# 操作系统
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# 配置文件备份
|
||||
*.yml.bak
|
||||
*.yaml.bak
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
*.bak
|
45
client/NotifyWin.spec
Normal file
45
client/NotifyWin.spec
Normal file
@ -0,0 +1,45 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['notification_client.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('config.yml', '.')],
|
||||
hiddenimports=['websockets', 'winotify', 'yaml', 'PIL', 'pystray', 'asyncio', 'logging', 'logging.handlers', 'pyperclip'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='NotifyWin',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=['icons\\notify.ico'],
|
||||
)
|
58
client/build.bat
Normal file
58
client/build.bat
Normal file
@ -0,0 +1,58 @@
|
||||
@echo off
|
||||
echo 开始打包 NotifyWin...
|
||||
|
||||
:: 创建图标目录
|
||||
if not exist icons mkdir icons
|
||||
|
||||
:: 生成图标
|
||||
echo 生成图标...
|
||||
python create_icon.py
|
||||
|
||||
:: 清理旧的构建文件
|
||||
if exist build rmdir /s /q build
|
||||
if exist dist rmdir /s /q dist
|
||||
|
||||
:: 临时删除 typing 包
|
||||
echo 删除 typing 包...
|
||||
pip uninstall -y typing
|
||||
|
||||
:: 打包
|
||||
echo 开始打包...
|
||||
pyinstaller ^
|
||||
--name=NotifyWin ^
|
||||
--noconsole ^
|
||||
--onefile ^
|
||||
--icon=icons/notify.ico ^
|
||||
--add-data="config.yml;." ^
|
||||
--hidden-import=websockets ^
|
||||
--hidden-import=winotify ^
|
||||
--hidden-import=yaml ^
|
||||
--hidden-import=PIL ^
|
||||
--hidden-import=pystray ^
|
||||
--hidden-import=asyncio ^
|
||||
--hidden-import=logging ^
|
||||
--hidden-import=logging.handlers ^
|
||||
--hidden-import=pyperclip ^
|
||||
notification_client.py
|
||||
|
||||
:: 复制必要文件
|
||||
echo 复制配置文件...
|
||||
if not exist dist mkdir dist
|
||||
copy config.yml dist\config.yml
|
||||
|
||||
:: 复制图标目录
|
||||
if exist icons (
|
||||
echo 复制图标...
|
||||
xcopy /s /i icons dist\icons
|
||||
)
|
||||
|
||||
:: 复制声音目录
|
||||
if exist sounds (
|
||||
echo 复制声音...
|
||||
xcopy /s /i sounds dist\sounds
|
||||
)
|
||||
|
||||
echo 打包完成!
|
||||
echo 生成的文件在 dist 目录中
|
||||
|
||||
pause
|
36
client/build.py
Normal file
36
client/build.py
Normal file
@ -0,0 +1,36 @@
|
||||
import PyInstaller.__main__
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# 清理旧的构建文件
|
||||
if os.path.exists('build'):
|
||||
shutil.rmtree('build')
|
||||
if os.path.exists('dist'):
|
||||
shutil.rmtree('dist')
|
||||
|
||||
# 打包配置
|
||||
PyInstaller.__main__.run([
|
||||
'notification_client.py', # 主程序
|
||||
'--name=NotifyWin', # 生成的exe名称
|
||||
'--noconsole', # 不显示控制台
|
||||
'--onefile', # 打包成单个文件
|
||||
'--icon=icons/notify.ico', # 图标文件
|
||||
'--add-data=config.yml;.', # 添加配置文件
|
||||
'--hidden-import=websockets',
|
||||
'--hidden-import=winotify',
|
||||
'--hidden-import=yaml',
|
||||
'--hidden-import=PIL',
|
||||
'--hidden-import=pystray',
|
||||
'--hidden-import=typing_extensions',
|
||||
'--hidden-import=asyncio',
|
||||
'--hidden-import=logging',
|
||||
'--hidden-import=logging.handlers',
|
||||
'--hidden-import=pyperclip',
|
||||
])
|
||||
|
||||
# 复制必要的文件到dist目录
|
||||
shutil.copy('config.yml', 'dist/config.yml')
|
||||
if os.path.exists('icons'):
|
||||
shutil.copytree('icons', 'dist/icons')
|
||||
if os.path.exists('sounds'):
|
||||
shutil.copytree('sounds', 'dist/sounds')
|
16
client/config.yml
Normal file
16
client/config.yml
Normal file
@ -0,0 +1,16 @@
|
||||
client:
|
||||
app_id: Windows App
|
||||
device_name: 我的电脑
|
||||
icon_path: null
|
||||
notification:
|
||||
default_duration: short
|
||||
mode: system # 可选值: system/custom
|
||||
duration:
|
||||
long: 10
|
||||
short: 3
|
||||
sound: default
|
||||
security:
|
||||
use_ssl: true
|
||||
server:
|
||||
host: notify.xj.rs
|
||||
port: 443
|
13
client/create_icon.py
Normal file
13
client/create_icon.py
Normal file
@ -0,0 +1,13 @@
|
||||
from PIL import Image
|
||||
|
||||
# 创建一个渐变色图标
|
||||
icon = Image.new('RGBA', (256, 256))
|
||||
for y in range(256):
|
||||
for x in range(256):
|
||||
r = int(x/256 * 255)
|
||||
g = int(y/256 * 255)
|
||||
b = 255
|
||||
icon.putpixel((x, y), (r, g, b, 255))
|
||||
|
||||
# 保存为 ICO 文件
|
||||
icon.save('icons/notify.ico')
|
BIN
client/icons/notify.ico
Normal file
BIN
client/icons/notify.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
693
client/notification_client.py
Normal file
693
client/notification_client.py
Normal file
@ -0,0 +1,693 @@
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
import pystray
|
||||
from PIL import Image
|
||||
import sys
|
||||
import yaml
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import os
|
||||
import argparse
|
||||
import logging.handlers
|
||||
from collections import deque, defaultdict, Counter
|
||||
from winotify import Notification # 添加到文件开头的导入部分
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import threading
|
||||
|
||||
try:
|
||||
from win10toast import ToastNotifier
|
||||
USE_WIN10TOAST = True
|
||||
toaster = ToastNotifier()
|
||||
except ImportError:
|
||||
USE_WIN10TOAST = False
|
||||
|
||||
# 设置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# 控制台处理器
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s - %(levelname)s - %(message)s'
|
||||
))
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# 文件处理器
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
'notification_client.log',
|
||||
maxBytes=1024*1024, # 1MB
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s - %(levelname)s - %(message)s'
|
||||
))
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# 添加自定义通知框类
|
||||
class CustomNotificationWindow:
|
||||
def __init__(self, title, message, duration=3000, position=None, client=None):
|
||||
# 保存客户端引用和位置信息
|
||||
self.client = client
|
||||
self.position = position
|
||||
|
||||
# 确保在主线程中运行 tkinter
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
return
|
||||
|
||||
self.window = tk.Tk()
|
||||
self.window.withdraw() # 先隐藏窗口
|
||||
|
||||
# 设置窗口样式
|
||||
self.window.overrideredirect(True)
|
||||
self.window.attributes('-topmost', True)
|
||||
self.window.configure(bg='#f0f0f0')
|
||||
|
||||
# 创建主框架
|
||||
self.frame = tk.Frame(self.window, bg='#f0f0f0')
|
||||
self.frame.pack(fill='both', expand=True)
|
||||
|
||||
# 创建标题栏框架
|
||||
self.title_frame = tk.Frame(self.frame, bg='#f0f0f0', cursor='hand2')
|
||||
self.title_frame.pack(fill='x', expand=True, padx=10, pady=(10,0))
|
||||
|
||||
# 标题
|
||||
self.title_label = tk.Label(self.title_frame,
|
||||
text=title,
|
||||
font=('Arial', 10, 'bold'),
|
||||
bg='#f0f0f0',
|
||||
fg='#333333')
|
||||
self.title_label.pack(side='left', anchor='w')
|
||||
|
||||
# 关闭按钮
|
||||
self.close_button = tk.Label(self.title_frame,
|
||||
text='×',
|
||||
font=('Arial', 12, 'bold'),
|
||||
bg='#f0f0f0',
|
||||
fg='#666666',
|
||||
cursor='hand2',
|
||||
padx=5)
|
||||
self.close_button.pack(side='right', anchor='e')
|
||||
|
||||
# 消息内容框架
|
||||
self.message_frame = tk.Frame(self.frame, bg='#f0f0f0')
|
||||
self.message_frame.pack(fill='both', expand=True, padx=10, pady=10)
|
||||
|
||||
# 消息内容
|
||||
self.message_label = tk.Label(self.message_frame,
|
||||
text=message,
|
||||
wraplength=300,
|
||||
justify='left',
|
||||
bg='#f0f0f0',
|
||||
fg='#666666')
|
||||
self.message_label.pack(anchor='w')
|
||||
|
||||
# 获取屏幕尺寸
|
||||
screen_width = self.window.winfo_screenwidth()
|
||||
screen_height = self.window.winfo_screenheight()
|
||||
|
||||
# 设置窗口大小和位置
|
||||
self.window.update_idletasks()
|
||||
self.width = self.window.winfo_width()
|
||||
self.height = self.window.winfo_height()
|
||||
|
||||
# 如果有指定位置就使用指定位置,否则使用默认位置
|
||||
if self.position:
|
||||
x, y = self.position
|
||||
else:
|
||||
x = screen_width - self.width - 20
|
||||
y = screen_height - self.height - 40
|
||||
|
||||
# 调整Y坐标以避免重叠
|
||||
if self.client:
|
||||
y = self.client.get_next_notification_y(self.height)
|
||||
|
||||
self.window.geometry(f'+{x}+{y}')
|
||||
|
||||
# 将自己添加到客户端的通知窗口列表
|
||||
if self.client:
|
||||
self.client.notification_windows.append(self)
|
||||
|
||||
# 绑定事件
|
||||
self._bind_events()
|
||||
|
||||
# 显示窗口
|
||||
self.window.deiconify()
|
||||
|
||||
# 运行主循环
|
||||
self.window.mainloop()
|
||||
|
||||
def _bind_events(self):
|
||||
"""绑定所有事件"""
|
||||
# 关闭按钮事件
|
||||
self.close_button.bind('<Button-1>', lambda e: self.close())
|
||||
self.close_button.bind('<Enter>',
|
||||
lambda e: self.close_button.configure(fg='#ff0000'))
|
||||
self.close_button.bind('<Leave>',
|
||||
lambda e: self.close_button.configure(fg='#666666'))
|
||||
|
||||
# 拖动事件
|
||||
self.title_frame.bind('<Button-1>', self.start_move)
|
||||
self.title_frame.bind('<B1-Motion>', self.on_move)
|
||||
self.title_label.bind('<Button-1>', self.start_move)
|
||||
self.title_label.bind('<B1-Motion>', self.on_move)
|
||||
|
||||
# 窗口事件
|
||||
self.window.bind('<Escape>', lambda e: self.close())
|
||||
|
||||
def start_move(self, event):
|
||||
"""开始拖动窗口"""
|
||||
self._drag_start_x = event.x_root - self.window.winfo_x()
|
||||
self._drag_start_y = event.y_root - self.window.winfo_y()
|
||||
|
||||
def on_move(self, event):
|
||||
"""处理窗口拖动"""
|
||||
x = event.x_root - self._drag_start_x
|
||||
y = event.y_root - self._drag_start_y
|
||||
self.window.geometry(f'+{x}+{y}')
|
||||
# 更新客户端中记录的位置
|
||||
if self.client:
|
||||
self.client.last_notification_pos = (x, y)
|
||||
|
||||
def close(self, event=None):
|
||||
"""关闭窗口"""
|
||||
try:
|
||||
if self.window:
|
||||
# 保存最后的位置
|
||||
if self.client:
|
||||
self.client.last_notification_pos = (
|
||||
self.window.winfo_x(),
|
||||
self.window.winfo_y()
|
||||
)
|
||||
# 从通知窗口列表中移除自己
|
||||
if self in self.client.notification_windows:
|
||||
self.client.notification_windows.remove(self)
|
||||
# 重新排列其他通知窗口
|
||||
self.client.rearrange_notifications()
|
||||
|
||||
self.window.destroy()
|
||||
self.window = None
|
||||
except Exception as e:
|
||||
print(f"关闭窗口时出错: {e}")
|
||||
|
||||
class NotificationClient:
|
||||
def __init__(self, config_path='config.yml'):
|
||||
self.logger = logger # 先初始化logger
|
||||
self.config_path = config_path
|
||||
self.config_mtime = 0
|
||||
self.load_config()
|
||||
self.running = True
|
||||
self.key_file = 'client_key.txt'
|
||||
self.key = self.load_key()
|
||||
self.connected = False
|
||||
self.status_text = "未连接" # 添加状态文本变量
|
||||
self.status_item = None
|
||||
self.history_file = 'notification_history.json'
|
||||
self.history = deque(maxlen=100) # 最多保存100条记录
|
||||
self.load_history()
|
||||
self.notification_filters = []
|
||||
self.notification_groups = defaultdict(list)
|
||||
self.notification_stats = {
|
||||
'total': 0,
|
||||
'by_type': Counter(),
|
||||
'by_hour': Counter(),
|
||||
'filtered': 0
|
||||
}
|
||||
self.notification_mode = 'system' # 默认使用系统通知
|
||||
self.notification_windows = [] # 存储当前显示的通知窗口
|
||||
self.last_notification_pos = None # 记录最后一个通知的位置
|
||||
self.notification_spacing = 10 # 通知窗口之间的间距
|
||||
|
||||
def load_config(self):
|
||||
"""加载配置文件"""
|
||||
try:
|
||||
if os.path.exists(self.config_path):
|
||||
mtime = os.path.getmtime(self.config_path)
|
||||
if mtime > self.config_mtime:
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
self.config = yaml.safe_load(f)
|
||||
self.config_mtime = mtime
|
||||
self.logger.info("配置已重新加载")
|
||||
return True
|
||||
else:
|
||||
# 如果配置文件不存在,创建默认配置
|
||||
self.config = {
|
||||
'server': {
|
||||
'host': '127.0.0.1',
|
||||
'port': 8080
|
||||
},
|
||||
'client': {
|
||||
'device_name': '我的设备',
|
||||
'app_id': 'Python.Notification'
|
||||
},
|
||||
'security': {
|
||||
'use_ssl': False
|
||||
},
|
||||
'notification': {
|
||||
'duration': {
|
||||
'info': "short",
|
||||
'warning': "long",
|
||||
'error': "long"
|
||||
},
|
||||
'sounds': {
|
||||
'enabled': True,
|
||||
'info': "default",
|
||||
'warning': "warning.wav",
|
||||
'error': "error.wav"
|
||||
}
|
||||
}
|
||||
}
|
||||
# 保存默认配置
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.safe_dump(self.config, f, allow_unicode=True)
|
||||
self.logger.info("已创建默认配置文件")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"加载配置失败: {e}")
|
||||
return False
|
||||
|
||||
def load_key(self):
|
||||
"""从文件加载密钥"""
|
||||
try:
|
||||
if os.path.exists(self.key_file):
|
||||
with open(self.key_file, 'r', encoding='utf-8') as f:
|
||||
return f.read().strip()
|
||||
except Exception as e:
|
||||
self.logger.error(f"加载密钥失败: {e}")
|
||||
return None
|
||||
|
||||
def save_key(self, key):
|
||||
"""保存密钥到文件"""
|
||||
try:
|
||||
with open(self.key_file, 'w', encoding='utf-8') as f:
|
||||
f.write(key)
|
||||
except Exception as e:
|
||||
self.logger.error(f"保存密钥失败: {e}")
|
||||
|
||||
def load_history(self):
|
||||
"""加载通知历史"""
|
||||
try:
|
||||
if os.path.exists(self.history_file):
|
||||
with open(self.history_file, 'r', encoding='utf-8') as f:
|
||||
self.history = deque(json.load(f), maxlen=100)
|
||||
except Exception as e:
|
||||
self.logger.error(f"加载历史记录失败: {e}")
|
||||
|
||||
def save_history(self):
|
||||
"""保存通知历史"""
|
||||
try:
|
||||
with open(self.history_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(list(self.history), f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
self.logger.error(f"保存历史记录失败: {e}")
|
||||
|
||||
def add_filter(self, filter_func):
|
||||
"""添加通知过滤器"""
|
||||
self.notification_filters.append(filter_func)
|
||||
|
||||
def should_show_notification(self, title, message, notification_type):
|
||||
"""检查是否应该显示通知"""
|
||||
for filter_func in self.notification_filters:
|
||||
if not filter_func(title, message, notification_type):
|
||||
return False
|
||||
return True
|
||||
|
||||
def group_notification(self, title, message, notification_type):
|
||||
"""对通知进行分组"""
|
||||
group_key = title # 可以根据需要修改分组规则
|
||||
self.notification_groups[group_key].append({
|
||||
'message': message,
|
||||
'type': notification_type,
|
||||
'time': datetime.now()
|
||||
})
|
||||
|
||||
# 如果同一组有多条消息,合并显示
|
||||
if len(self.notification_groups[group_key]) > 1:
|
||||
messages = [n['message'] for n in self.notification_groups[group_key][-5:]]
|
||||
return f"{message}\n\n还有 {len(messages)-1} 条相似消息"
|
||||
return message
|
||||
|
||||
def show_custom_notification(self, title, message, duration):
|
||||
"""在主线程中显示自定义通知"""
|
||||
if hasattr(self, 'icon') and self.icon:
|
||||
# 如果在主线程中,直接创建通知窗口
|
||||
CustomNotificationWindow(title, message, duration,
|
||||
position=self.last_notification_pos,
|
||||
client=self)
|
||||
else:
|
||||
# 如果不在主线程中,使用新线程创建通知窗口
|
||||
self.logger.info("在主线程中创建通知窗口")
|
||||
threading.Thread(target=lambda: CustomNotificationWindow(
|
||||
title, message, duration,
|
||||
position=self.last_notification_pos,
|
||||
client=self
|
||||
), daemon=True).start()
|
||||
|
||||
def show_notification(self, title, message, notification_type='info'):
|
||||
try:
|
||||
self.logger.info(f"准备显示通知: {title} - {message} (类型: {notification_type})")
|
||||
|
||||
# 更新统计和历史记录
|
||||
self.update_stats(notification_type)
|
||||
self.history.append({
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'title': title,
|
||||
'message': message,
|
||||
'type': notification_type
|
||||
})
|
||||
self.save_history()
|
||||
|
||||
if self.notification_mode == 'system':
|
||||
# 使用系统通知
|
||||
duration_type = self.config['notification']['default_duration']
|
||||
duration = "long" if duration_type == "long" else "short"
|
||||
|
||||
notification = Notification(
|
||||
app_id="Windows App",
|
||||
title=title,
|
||||
msg=message,
|
||||
duration=duration
|
||||
)
|
||||
notification.show()
|
||||
self.logger.info(f"系统通知已发送 (持续时间: {duration})")
|
||||
else:
|
||||
# 使用自定义通知框
|
||||
duration_ms = 10000 if self.config['notification']['default_duration'] == 'long' else 3000
|
||||
self.show_custom_notification(title, message, duration_ms)
|
||||
self.logger.info("自定义通知已发送")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"通知处理失败: {e}")
|
||||
self.logger.error(f"错误类型: {type(e)}")
|
||||
import traceback
|
||||
self.logger.error(f"详细错误: {traceback.format_exc()}")
|
||||
|
||||
async def connect_websocket(self):
|
||||
retry_delays = [5, 10, 30, 60] # 重试间隔(秒)
|
||||
retry_count = 0
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
self.update_status("正在连接...", False)
|
||||
ws_url = f"{'wss' if self.config['security']['use_ssl'] else 'ws'}://{self.config['server']['host']}:{self.config['server']['port']}/ws"
|
||||
self.logger.info(f"正在连接到服务器: {ws_url}")
|
||||
|
||||
async with websockets.connect(ws_url, ping_interval=20, ping_timeout=10) as websocket:
|
||||
if self.key:
|
||||
self.logger.info("使用已有密钥连接")
|
||||
await websocket.send(json.dumps({
|
||||
'action': 'connect',
|
||||
'key': self.key
|
||||
}))
|
||||
else:
|
||||
self.logger.info("注册新客户端")
|
||||
await websocket.send(json.dumps({
|
||||
'action': 'register',
|
||||
'device_name': self.config['client']['device_name']
|
||||
}))
|
||||
|
||||
response = await websocket.recv()
|
||||
data = json.loads(response)
|
||||
self.logger.info(f"收到服务器响应: {data}")
|
||||
|
||||
if data.get('status') == 'success':
|
||||
self.update_status("已连接", True)
|
||||
if 'key' in data:
|
||||
self.key = data['key']
|
||||
self.save_key(self.key)
|
||||
self.logger.info(f"获取到新密钥: {self.key[:8]}...")
|
||||
|
||||
self.show_notification(
|
||||
"设备注册成功",
|
||||
f"你的推送密钥: {self.key}\n请保管好此密钥"
|
||||
)
|
||||
|
||||
self.logger.info("已连接到服务器,等待消息...")
|
||||
retry_count = 0 # 连接成功后重置重试计数
|
||||
|
||||
# 处理消息
|
||||
while self.running:
|
||||
try:
|
||||
message = await websocket.recv()
|
||||
if message == "ping":
|
||||
await websocket.send("pong")
|
||||
continue
|
||||
elif message == "pong":
|
||||
continue
|
||||
|
||||
data = json.loads(message)
|
||||
self.logger.info(f"收到新消息: {data}")
|
||||
self.show_notification(
|
||||
data["title"],
|
||||
data["message"],
|
||||
data.get("type", "info")
|
||||
)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
break
|
||||
else:
|
||||
self.logger.error(f"连接失败: {data.get('message')}")
|
||||
if self.key:
|
||||
self.key = None
|
||||
if os.path.exists(self.key_file):
|
||||
os.remove(self.key_file)
|
||||
|
||||
except Exception as e:
|
||||
self.update_status(f"连接错误: {str(e)}", False)
|
||||
|
||||
# 计算重试延迟
|
||||
delay = retry_delays[min(retry_count, len(retry_delays)-1)]
|
||||
self.logger.info(f"{delay} 秒后重试连接...")
|
||||
await asyncio.sleep(delay)
|
||||
retry_count += 1
|
||||
|
||||
def update_status(self, status, connected=None):
|
||||
"""更新托盘图标状态"""
|
||||
if connected is not None:
|
||||
self.connected = connected
|
||||
self.status_text = f"状态: {'已连接' if self.connected else '未连接'} - {status}"
|
||||
# 重建菜单
|
||||
if hasattr(self, 'icon') and self.icon:
|
||||
self.icon.menu = self.create_menu()
|
||||
|
||||
def create_menu(self):
|
||||
"""创建托盘菜单"""
|
||||
duration_menu = pystray.Menu(
|
||||
pystray.MenuItem("短 (3秒)",
|
||||
lambda: self.set_notification_duration("short")),
|
||||
pystray.MenuItem("长 (10秒)",
|
||||
lambda: self.set_notification_duration("long"))
|
||||
)
|
||||
|
||||
notification_mode_text = "切换到自定义通知" if self.notification_mode == 'system' else "切换到系统通知"
|
||||
|
||||
return pystray.Menu(
|
||||
pystray.MenuItem(self.status_text, lambda: None, enabled=False),
|
||||
pystray.MenuItem("复制密钥", self.copy_key_to_clipboard),
|
||||
pystray.MenuItem("查看历史", self.show_history),
|
||||
pystray.MenuItem("查看统计", self.show_stats),
|
||||
pystray.MenuItem("通知持续时间", duration_menu),
|
||||
pystray.MenuItem(notification_mode_text, self.toggle_notification_mode),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("重新连接", self.reconnect),
|
||||
pystray.MenuItem("退出", self.exit_app)
|
||||
)
|
||||
|
||||
def create_tray_icon(self):
|
||||
try:
|
||||
# 尝试加载自定义图标
|
||||
icon_path = self.config['client'].get('icon_path')
|
||||
if icon_path and os.path.exists(icon_path):
|
||||
image = Image.open(icon_path)
|
||||
else:
|
||||
# 创建默认图标(使用渐变色而不是纯白)
|
||||
image = Image.new('RGB', (64, 64))
|
||||
for y in range(64):
|
||||
for x in range(64):
|
||||
image.putpixel((x, y), (x*4, y*4, 255))
|
||||
|
||||
self.icon = pystray.Icon(
|
||||
"notification_client",
|
||||
image,
|
||||
"通知客户端",
|
||||
self.create_menu()
|
||||
)
|
||||
return self.icon
|
||||
except Exception as e:
|
||||
self.logger.error(f"创建托盘图标失败: {e}")
|
||||
# 创建一个简单的白色图标作为后备
|
||||
image = Image.new('RGB', (64, 64), color='white')
|
||||
menu = pystray.Menu(pystray.MenuItem("退出", self.exit_app))
|
||||
self.icon = pystray.Icon("notification_client", image, "通知客户端", menu)
|
||||
return self.icon
|
||||
|
||||
def copy_key_to_clipboard(self):
|
||||
"""复制密钥到剪贴板"""
|
||||
if self.key:
|
||||
import pyperclip
|
||||
pyperclip.copy(self.key)
|
||||
self.show_notification("复制成功", "密钥已复制到剪贴板")
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理资源"""
|
||||
self.running = False
|
||||
self.save_history()
|
||||
# 等待其他任务完成
|
||||
await asyncio.sleep(1)
|
||||
|
||||
def exit_app(self, icon):
|
||||
"""优雅退出"""
|
||||
self.logger.info("正在关闭应用...")
|
||||
# 运行清理
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(self.cleanup())
|
||||
# 停止图标
|
||||
icon.stop()
|
||||
sys.exit(0)
|
||||
|
||||
def reconnect(self):
|
||||
"""手动触发重连"""
|
||||
if not self.connected:
|
||||
self.logger.info("手动触发重连...")
|
||||
# 创建新的事件循环来处理重连
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(self.connect_websocket())
|
||||
|
||||
async def config_watch(self):
|
||||
"""监控配置文件变化"""
|
||||
while self.running:
|
||||
if self.load_config():
|
||||
# 如果配置改变,可以在这里处理
|
||||
pass
|
||||
await asyncio.sleep(5) # 每5秒检查一次
|
||||
|
||||
def show_history(self):
|
||||
"""显示通知历史"""
|
||||
if self.history:
|
||||
latest = list(self.history)[-5:] # 显示最近5条
|
||||
message = "\n".join(
|
||||
f"{item['timestamp'][:19]} - {item['title']}: {item['message']}"
|
||||
for item in latest
|
||||
)
|
||||
self.show_notification("最近通知记录", message)
|
||||
else:
|
||||
self.show_notification("通知历史", "暂无通知记录")
|
||||
|
||||
def update_stats(self, notification_type, shown=True):
|
||||
"""更新通知统计"""
|
||||
if shown:
|
||||
self.notification_stats['total'] += 1
|
||||
self.notification_stats['by_type'][notification_type] += 1
|
||||
hour = datetime.now().hour
|
||||
self.notification_stats['by_hour'][hour] += 1
|
||||
else:
|
||||
self.notification_stats['filtered'] += 1
|
||||
|
||||
def show_stats(self):
|
||||
"""显示通知统计"""
|
||||
stats = (
|
||||
f"总通知数: {self.notification_stats['total']}\n"
|
||||
f"已过滤: {self.notification_stats['filtered']}\n\n"
|
||||
f"类型统计:\n"
|
||||
+ "\n".join(f"- {t}: {c}" for t, c in self.notification_stats['by_type'].most_common())
|
||||
)
|
||||
self.show_notification("通知统计", stats)
|
||||
|
||||
def set_notification_duration(self, duration_type):
|
||||
"""设置通知持续时间"""
|
||||
self.config['notification']['default_duration'] = duration_type
|
||||
self.show_notification(
|
||||
"设置已更新",
|
||||
f"通知持续时间已设置为: {duration_type}"
|
||||
)
|
||||
# 保存配置
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.safe_dump(self.config, f, allow_unicode=True)
|
||||
|
||||
def toggle_notification_mode(self):
|
||||
"""切换通知模式"""
|
||||
self.notification_mode = 'system' if self.notification_mode == 'custom' else 'custom'
|
||||
mode_text = "系统通知" if self.notification_mode == 'system' else "自定义通知"
|
||||
self.show_notification("通知模式已更改", f"已切换为{mode_text}")
|
||||
|
||||
def get_next_notification_y(self, window_height):
|
||||
"""计算下一个通知窗口的Y坐标"""
|
||||
# 创建一个临时的 Tk 实例来获取屏幕高度
|
||||
root = tk.Tk()
|
||||
root.withdraw() # 隐藏窗口
|
||||
screen_height = root.winfo_screenheight()
|
||||
root.destroy()
|
||||
|
||||
if not self.notification_windows:
|
||||
# 如果没有其他通知,使用默认位置或最后记录的位置
|
||||
if self.last_notification_pos:
|
||||
return self.last_notification_pos[1]
|
||||
return screen_height - window_height - 40
|
||||
|
||||
# 获取最下方通知窗口的位置
|
||||
last_window = max(self.notification_windows,
|
||||
key=lambda w: w.window.winfo_y())
|
||||
return last_window.window.winfo_y() + last_window.height + self.notification_spacing
|
||||
|
||||
def rearrange_notifications(self):
|
||||
"""重新排列所有通知窗口"""
|
||||
if not self.notification_windows:
|
||||
return
|
||||
|
||||
# 按Y坐标排序
|
||||
windows = sorted(self.notification_windows,
|
||||
key=lambda w: w.window.winfo_y())
|
||||
|
||||
# 重新设置位置
|
||||
base_y = windows[0].window.winfo_y()
|
||||
for i, win in enumerate(windows):
|
||||
if i == 0:
|
||||
continue # 保持第一个窗口位置不变
|
||||
new_y = base_y + sum(w.height + self.notification_spacing
|
||||
for w in windows[:i])
|
||||
x = win.window.winfo_x()
|
||||
win.window.geometry(f'+{x}+{new_y}')
|
||||
|
||||
def run(self):
|
||||
# 创建并运行系统托盘图标
|
||||
icon = self.create_tray_icon()
|
||||
|
||||
# 创建事件循环
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# 启动WebSocket客户端和配置监控
|
||||
loop.create_task(self.connect_websocket())
|
||||
loop.create_task(self.config_watch())
|
||||
|
||||
# 在新线程中运行事件循环
|
||||
import threading
|
||||
threading.Thread(target=lambda: loop.run_forever(), daemon=True).start()
|
||||
|
||||
# 运行系统托盘
|
||||
icon.run()
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='Windows 通知客户端')
|
||||
parser.add_argument('--config', default='config.yml', help='配置文件路径')
|
||||
parser.add_argument('--server', help='服务器地址 (例如: localhost:8080)')
|
||||
parser.add_argument('--device', help='设备名称')
|
||||
return parser.parse_args()
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
|
||||
client = NotificationClient(config_path=args.config)
|
||||
|
||||
# 命令行参数覆盖配置文件
|
||||
if args.server:
|
||||
host, port = args.server.split(':')
|
||||
client.config['server']['host'] = host
|
||||
client.config['server']['port'] = int(port)
|
||||
if args.device:
|
||||
client.config['client']['device_name'] = args.device
|
||||
|
||||
client.run()
|
8
client/requirements-dev.txt
Normal file
8
client/requirements-dev.txt
Normal file
@ -0,0 +1,8 @@
|
||||
websockets==12.0
|
||||
winotify==1.1.0
|
||||
PyYAML==6.0.1
|
||||
Pillow==10.2.0
|
||||
pystray==0.19.5
|
||||
pyperclip==1.8.2
|
||||
pyinstaller==6.3.0
|
||||
typing-extensions>=4.0.0
|
21
server/.dockerignore
Normal file
21
server/.dockerignore
Normal file
@ -0,0 +1,21 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
env
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
.tox
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.log
|
||||
.pytest_cache
|
||||
.env
|
||||
.venv
|
||||
.DS_Store
|
||||
data/
|
30
server/Dockerfile
Normal file
30
server/Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
# 使用多架构基础镜像
|
||||
FROM --platform=$TARGETPLATFORM python:3.9-slim
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONIOENCODING=utf-8
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制服务器代码和配置
|
||||
COPY server.py .
|
||||
COPY config.yml .
|
||||
|
||||
# 创建数据目录
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
# 设置启动命令
|
||||
CMD ["python", "server.py"]
|
12
server/config.yml
Normal file
12
server/config.yml
Normal file
@ -0,0 +1,12 @@
|
||||
websocket:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
|
||||
security:
|
||||
use_ssl: false
|
||||
ssl_cert: "path/to/cert.pem"
|
||||
ssl_key: "path/to/key.pem"
|
||||
|
||||
# 数据存储路径
|
||||
data:
|
||||
keys_file: "/app/data/registered_keys.json"
|
13
server/docker-compose.yml
Normal file
13
server/docker-compose.yml
Normal file
@ -0,0 +1,13 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
notify-server:
|
||||
image: your-username/notify-server:latest # 替换为你的镜像名
|
||||
container_name: notify-server
|
||||
ports:
|
||||
- "31182:8080" # 统一使用一个端口
|
||||
volumes:
|
||||
- ./data:/app/data # 数据持久化
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Shanghai # 设置时区
|
7
server/registered_keys.json
Normal file
7
server/registered_keys.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"oxvHH-PdW8tdT6lnigYw_YKRjwWzyl9tn3qhnTLGYXA": {
|
||||
"device_name": "\u6211\u7684\u7535\u8111",
|
||||
"created_at": "2025-02-21T10:52:59.871115",
|
||||
"last_seen": "2025-02-21T10:52:59.872385"
|
||||
}
|
||||
}
|
3
server/requirements.txt
Normal file
3
server/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
websockets==12.0
|
||||
aiohttp==3.9.3
|
||||
PyYAML==6.0.1
|
274
server/server.py
Normal file
274
server/server.py
Normal file
@ -0,0 +1,274 @@
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
import ssl
|
||||
import secrets
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
import yaml
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
import os
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NotificationServer:
|
||||
def __init__(self, config_path='config.yml'):
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
self.config = yaml.safe_load(f)
|
||||
except FileNotFoundError:
|
||||
# 如果配置文件不存在,创建默认配置
|
||||
self.config = {
|
||||
'websocket': {
|
||||
'host': '0.0.0.0',
|
||||
'port': 8080
|
||||
},
|
||||
'security': {
|
||||
'use_ssl': False,
|
||||
'ssl_cert': 'path/to/cert.pem',
|
||||
'ssl_key': 'path/to/key.pem'
|
||||
},
|
||||
'data': {
|
||||
'keys_file': '/app/data/registered_keys.json'
|
||||
}
|
||||
}
|
||||
# 保存默认配置
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.safe_dump(self.config, f, allow_unicode=True)
|
||||
|
||||
# 存储客户端连接
|
||||
self.client_connections = defaultdict(set)
|
||||
# 从配置文件获取数据存储路径
|
||||
self.keys_file = self.config['data']['keys_file']
|
||||
self.registered_keys = {}
|
||||
self.load_registered_keys()
|
||||
|
||||
def load_registered_keys(self):
|
||||
"""从文件加载已注册的密钥"""
|
||||
try:
|
||||
if os.path.exists(self.keys_file):
|
||||
with open(self.keys_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
self.registered_keys = data
|
||||
logger.info(f"已加载 {len(self.registered_keys)} 个注册密钥")
|
||||
except Exception as e:
|
||||
logger.error(f"加载密钥文件失败: {e}")
|
||||
|
||||
def save_registered_keys(self):
|
||||
"""保存密钥到文件"""
|
||||
try:
|
||||
with open(self.keys_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.registered_keys, f, indent=2)
|
||||
except Exception as e:
|
||||
logger.error(f"保存密钥文件失败: {e}")
|
||||
|
||||
def generate_key(self):
|
||||
"""生成唯一的客户端密钥"""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
async def register_client(self, websocket):
|
||||
"""注册新客户端并生成密钥"""
|
||||
try:
|
||||
# 等待客户端发送注册请求
|
||||
msg = await websocket.recv()
|
||||
data = json.loads(msg)
|
||||
|
||||
if data.get('action') == 'register':
|
||||
device_name = data.get('device_name', 'unknown')
|
||||
key = self.generate_key()
|
||||
|
||||
client_info = {
|
||||
'device_name': device_name,
|
||||
'created_at': datetime.now().isoformat(),
|
||||
'last_seen': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
self.registered_keys[key] = client_info
|
||||
self.client_connections[key].add(websocket)
|
||||
|
||||
# 发送密钥给客户端
|
||||
await websocket.send(json.dumps({
|
||||
'status': 'success',
|
||||
'key': key,
|
||||
'message': '注册成功'
|
||||
}))
|
||||
|
||||
logger.info(f"新客户端注册: {device_name}, key: {key[:8]}...")
|
||||
self.save_registered_keys() # 保存新注册的密钥
|
||||
return key
|
||||
|
||||
elif data.get('action') == 'connect':
|
||||
key = data.get('key')
|
||||
if key in self.registered_keys:
|
||||
self.client_connections[key].add(websocket)
|
||||
self.registered_keys[key]['last_seen'] = datetime.now().isoformat()
|
||||
await websocket.send(json.dumps({
|
||||
'status': 'success',
|
||||
'message': '连接成功'
|
||||
}))
|
||||
logger.info(f"客户端重连成功: {key[:8]}...")
|
||||
return key
|
||||
else:
|
||||
await websocket.send(json.dumps({
|
||||
'status': 'error',
|
||||
'message': '无效的密钥'
|
||||
}))
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"注册失败: {e}")
|
||||
await websocket.close()
|
||||
return None
|
||||
|
||||
async def handle_websocket(self, websocket):
|
||||
key = await self.register_client(websocket)
|
||||
if not key:
|
||||
return
|
||||
|
||||
try:
|
||||
# 添加心跳检测
|
||||
while True:
|
||||
try:
|
||||
message = await asyncio.wait_for(websocket.recv(), timeout=30)
|
||||
if message == "ping":
|
||||
await websocket.send("pong")
|
||||
except asyncio.TimeoutError:
|
||||
# 30秒没有收到消息,发送 ping
|
||||
try:
|
||||
await websocket.send("ping")
|
||||
await asyncio.wait_for(websocket.recv(), timeout=10)
|
||||
except:
|
||||
# ping 失败,关闭连接
|
||||
break
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
break
|
||||
finally:
|
||||
self.client_connections[key].discard(websocket)
|
||||
if not self.client_connections[key]:
|
||||
logger.info(f"客户端完全断开: {key[:8]}...")
|
||||
|
||||
async def send_notification(self, key, notification):
|
||||
"""向指定key的所有连接发送通知"""
|
||||
if key not in self.client_connections:
|
||||
return False
|
||||
|
||||
connections = self.client_connections[key].copy()
|
||||
if not connections:
|
||||
return False
|
||||
|
||||
for websocket in connections:
|
||||
try:
|
||||
await websocket.send(json.dumps(notification))
|
||||
except Exception as e:
|
||||
logger.error(f"发送通知失败: {e}")
|
||||
self.client_connections[key].discard(websocket)
|
||||
|
||||
return True
|
||||
|
||||
async def handle_http_push(self, request):
|
||||
try:
|
||||
# 从URL路径中获取key
|
||||
key = request.match_info['key']
|
||||
|
||||
if key not in self.registered_keys:
|
||||
return web.Response(status=404, text='Invalid key')
|
||||
|
||||
data = await request.json()
|
||||
if not data.get('title') or not data.get('message'):
|
||||
return web.Response(status=400, text='Missing title or message')
|
||||
|
||||
notification = {
|
||||
'title': data['title'],
|
||||
'message': data['message'],
|
||||
'type': data.get('type', 'info'),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
success = await self.send_notification(key, notification)
|
||||
if success:
|
||||
return web.Response(text='Notification sent')
|
||||
else:
|
||||
return web.Response(status=404, text='No active connections')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理HTTP请求失败: {e}")
|
||||
return web.Response(status=500, text='Internal server error')
|
||||
|
||||
async def start(self):
|
||||
# 设置SSL
|
||||
ssl_context = None
|
||||
if self.config['security']['use_ssl']:
|
||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ssl_context.load_cert_chain(
|
||||
self.config['security']['ssl_cert'],
|
||||
self.config['security']['ssl_key']
|
||||
)
|
||||
|
||||
# 创建 aiohttp 应用
|
||||
app = web.Application()
|
||||
|
||||
# 添加 WebSocket 处理
|
||||
async def websocket_handler(request):
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
|
||||
# 包装 aiohttp WebSocketResponse 为 websockets 兼容的接口
|
||||
class WebSocketWrapper:
|
||||
async def send(self, message):
|
||||
await ws.send_str(message)
|
||||
|
||||
async def recv(self):
|
||||
msg = await ws.receive()
|
||||
if msg.type == web.WSMsgType.TEXT:
|
||||
return msg.data
|
||||
elif msg.type == web.WSMsgType.CLOSE:
|
||||
raise websockets.exceptions.ConnectionClosed(None, None)
|
||||
|
||||
async def close(self):
|
||||
await ws.close()
|
||||
|
||||
wrapped_ws = WebSocketWrapper()
|
||||
await self.handle_websocket(wrapped_ws)
|
||||
return ws
|
||||
|
||||
# 注册路由
|
||||
app.router.add_get('/ws', websocket_handler) # WebSocket 端点
|
||||
app.router.add_post('/push/{key}', self.handle_http_push) # HTTP 推送端点
|
||||
|
||||
# 启动服务器
|
||||
runner = web.AppRunner(app)
|
||||
await runner.setup()
|
||||
|
||||
# 根据是否使用 SSL 创建不同的站点
|
||||
if ssl_context:
|
||||
site = web.SSLSite(
|
||||
runner,
|
||||
self.config['websocket']['host'],
|
||||
self.config['websocket']['port'],
|
||||
ssl_context=ssl_context
|
||||
)
|
||||
else:
|
||||
site = web.TCPSite(
|
||||
runner,
|
||||
self.config['websocket']['host'],
|
||||
self.config['websocket']['port']
|
||||
)
|
||||
|
||||
await site.start()
|
||||
|
||||
logger.info(f"服务器运行在 {self.config['websocket']['host']}:{self.config['websocket']['port']}")
|
||||
logger.info("WebSocket 路径: /ws")
|
||||
logger.info("HTTP 推送路径: /push/{key}")
|
||||
|
||||
await asyncio.Future() # 持续运行
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = NotificationServer()
|
||||
asyncio.run(server.start())
|
Loading…
Reference in New Issue
Block a user