├── README.assets ├── image-20240812230315063.png ├── image-20240812233032541.png ├── image-20240812233253703.png ├── img.png ├── img2.png ├── img3.png └── img4.png ├── README.md ├── SmsForwarder.json ├── build_exe.py ├── config.json ├── core ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── config_manager.cpython-38.pyc │ ├── message_processor.cpython-38.pyc │ └── server.cpython-38.pyc ├── config_manager.py ├── message_processor.py └── server.py ├── favicon.ico ├── main.py ├── main_tray.py ├── notification_manager.py ├── requirements.txt ├── tray_manager.py └── utils.py /README.assets/image-20240812230315063.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/README.assets/image-20240812230315063.png -------------------------------------------------------------------------------- /README.assets/image-20240812233032541.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/README.assets/image-20240812233032541.png -------------------------------------------------------------------------------- /README.assets/image-20240812233253703.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/README.assets/image-20240812233253703.png -------------------------------------------------------------------------------- /README.assets/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/README.assets/img.png -------------------------------------------------------------------------------- /README.assets/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/README.assets/img2.png -------------------------------------------------------------------------------- /README.assets/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/README.assets/img3.png -------------------------------------------------------------------------------- /README.assets/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/README.assets/img4.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 电脑接短信电话--实现短信验证码、来电提醒自动在PC端同步 2 | 3 | ## 效果展示 4 | 5 | 收到验证码后,自动拦截短信发送到电脑端,在电脑右下角有托盘消息提示,并自动复制到Windows剪贴板中,直接粘贴就可以。 6 | ![image](https://github.com/user-attachments/assets/88a51b44-0077-484e-968f-5a383ba102fd) 7 | 8 | ![image](https://github.com/user-attachments/assets/586f9608-cd40-40ed-9a6a-9e40d60b54bd) 9 | 10 | 11 | 12 | 13 | ## 安装使用步骤 14 | 15 | ### 1.安装SmsForwarder 16 | 17 | 在安卓手机上安装短信转发器SmsForwarder 18 | https://github.com/pppscn/SmsForwarder 19 | 已经测试支持的版本:https://github.com/pppscn/SmsForwarder/releases/tag/v3.3.2 20 | 21 | ### 2.SmsForwarder配置导入,通用设置开关要重新关闭打开一次 22 | 将[SmsForwarder.json](SmsForwarder.json)放在 23 | /storage/emulated/0/Download目录下,点击导入开始导入 24 | ![img.png](README.assets/img3.png) 25 | ![img.png](README.assets/img.png) 26 | ![img.png](README.assets/img2.png) 27 | 28 | ### 3.发送通道-修改Socket tcp配置 29 | 30 | 需要手机与电脑在同一局域网下,修改服务端ip为电脑自己的局域网ip 31 | ![img.png](README.assets/img4.png) 32 | 33 | ### 4.下载SmsCodeServer.exe 电脑上点击启动 34 | https://github.com/ddonano/SmsCodeServer/releases 35 | ![image](https://github.com/user-attachments/assets/0be44a1d-ddc7-4812-bb08-182add39778b) 36 | 37 | 38 | ### 5. 找个验证码网页开始测试 39 | 40 | 41 | ## 如何编译打包工程 42 | 43 | **将代码clone到本地部署运行** 44 | 45 | clone项目 46 | 47 | ```bash 48 | git clone git@github.com:ddonano/SmsCodeServer.git 49 | cd SmsCodeServer 50 | ``` 51 | 52 | 安装依赖 53 | 54 | ```bash 55 | pip install -r requirements.txt 56 | ``` 57 | 58 | 运行`main.py`,如果需要修改端口号 加启动参数 -p {port} 59 | 60 | ```bash 61 | python main.py 62 | ``` 63 | 64 | 打包成exe启动 65 | 66 | ```bash 67 | python build_exe.py 68 | ``` 69 | 启动,直接点击exe打开即可,或者在cmd命令行里修改端口号启动 70 | ```bash 71 | SmsCodeServer.exe -p 65431 72 | ``` 73 | 加入windows自启动,创建SmsCodeServer.exe 快捷方式,按win+R 输入shell:startup执行, 在打开的文件夹里拖入刚创建的快捷方式即可。 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /SmsForwarder.json: -------------------------------------------------------------------------------- 1 | {"frpc_list":[{"autorun":0,"config":"[common]\n#frps服务端公网IP\nserver_addr \u003d 88.88.88.88\n#frps服务端公网端口\nserver_port \u003d 8888\n#可选,建议启用\ntoken \u003d 88888888\n#连接服务端的超时时间(增大时间避免frpc在网络未就绪的情况下启动失败)\ndial_server_timeout \u003d 60\n#第一次登陆失败后是否退出\nlogin_fail_exit \u003d false\n\n#[二选一即可]每台机器不可重复,通过 http://88.88.88.88:5000 访问\n[SmsForwarder-TCP]\ntype \u003d tcp\nlocal_ip \u003d 127.0.0.1\nlocal_port \u003d 5000\n#只要修改下面这一行(frps所在服务器必须暴露的公网端口)\nremote_port \u003d 5000\n\n#[二选一即可]每台机器不可重复,通过 http://smsf.demo.com 访问\n[SmsForwarder-HTTP]\ntype \u003d http\nlocal_ip \u003d 127.0.0.1\nlocal_port \u003d 5000\n#只要修改下面这一行(在frps端将域名反代到vhost_http_port)\ncustom_domains \u003d smsf.demo.com\n","connecting":false,"name":"远程控制SmsForwarder","time":"May 1, 2022 00:00:00","uid":"830b0a0e-c2b3-4f95-b3c9-55db12923d2e"}],"rule_list":[{"check":"regex","filed":"msg_content","id":1,"regexReplace":"","senderId":1,"senderList":[{"id":1,"jsonSetting":"{\"address\":\"172.28.117.157\",\"clientId\":\"\",\"inCharset\":\"UTF-8\",\"inMessageTopic\":\"\",\"method\":\"TCP\",\"msgTemplate\":\"{[msg]}\",\"outCharset\":\"UTF-8\",\"outMessageTopic\":\"\",\"password\":\"\",\"path\":\"\",\"port\":65432,\"response\":\"\",\"secret\":\"\",\"uriType\":\"tcp\",\"username\":\"\"}","name":"tcp","status":1,"time":"Feb 6, 2025 19:53:38","type":15}],"senderLogic":"ALL","silentPeriodEnd":0,"silentPeriodStart":0,"simSlot":"ALL","smsTemplate":"SMS.{{SMS}}","status":1,"time":"Feb 6, 2025 19:54:58","type":"sms","value":".*密码.*|.*验证码.*"},{"check":"is","filed":"call_type","id":3,"regexReplace":"","senderId":1,"senderList":[{"id":1,"jsonSetting":"{\"address\":\"172.28.117.157\",\"clientId\":\"\",\"inCharset\":\"UTF-8\",\"inMessageTopic\":\"\",\"method\":\"TCP\",\"msgTemplate\":\"{[msg]}\",\"outCharset\":\"UTF-8\",\"outMessageTopic\":\"\",\"password\":\"\",\"path\":\"\",\"port\":65432,\"response\":\"\",\"secret\":\"\",\"uriType\":\"tcp\",\"username\":\"\"}","name":"tcp","status":1,"time":"Feb 6, 2025 19:53:38","type":15}],"senderLogic":"ALL","silentPeriodEnd":0,"silentPeriodStart":0,"simSlot":"ALL","smsTemplate":"CALL.{{FROM}}","status":1,"time":"Feb 6, 2025 19:54:35","type":"call","value":"4"}],"sender_list":[{"id":1,"jsonSetting":"{\"address\":\"172.28.117.157\",\"clientId\":\"\",\"inCharset\":\"UTF-8\",\"inMessageTopic\":\"\",\"method\":\"TCP\",\"msgTemplate\":\"{[msg]}\",\"outCharset\":\"UTF-8\",\"outMessageTopic\":\"\",\"password\":\"\",\"path\":\"\",\"port\":65432,\"response\":\"\",\"secret\":\"\",\"uriType\":\"tcp\",\"username\":\"\"}","name":"tcp","status":1,"time":"Feb 6, 2025 19:53:38","type":15}],"settings":"%C2%AC%C3%AD%00%05sr%00%11java.util.HashMap%05%07%C3%9A%C3%81%C3%83%16%60%C3%91%03%00%02F%00%0AloadFactorI%00%09thresholdxp%3F%40%00%00%00%00%000w%08%00%00%00%40%00%00%00%2Ft%00%0Aenable_smssr%00%11java.lang.Boolean%C3%8D+r%C2%80%C3%95%C2%9C%C3%BA%C3%AE%02%00%01Z%00%05valuexp%01t%00%0Frequest_timeoutsr%00%11java.lang.Integer%12%C3%A2%C2%A0%C2%A4%C3%B7%C2%81%C2%878%02%00%01I%00%05valuexr%00%10java.lang.Number%C2%86%C2%AC%C2%95%1D%0B%C2%94%C3%A0%C2%8B%02%00%00xp%00%00%00%0At%00%19enable_silent_period_logssq%00%7E%00%03%00t%00%0Ddata_sim_slotsq%00%7E%00%06%00%00%00%01t%00%0Enotify_contentt%00%15%C3%A8%C2%BD%C2%AC%C3%A5%C2%8F%C2%91%C3%A7%C2%9F%C2%AD%C3%A4%C2%BF%C2%A1%C3%A9%C2%AA%C2%8C%C3%A8%C2%AF%C2%81%C3%A7%C2%A0%C2%81t%00%12lock_screen_actiont%00%22android.intent.action.USER_PRESENTt%00%17enable_pure_client_modeq%00%7E%00%0At%00%11extra_device_markt%00%10Redmi+24117RK2CCt%00%07ip_listt%00i172.28.118.217%0A2409%3A8924%3Ac41%3A11c9%3A2494%3A7cff%3Afe22%3Ad761%0A10.118.4.171%0A2409%3A8124%3A425%3A177a%3Adcb2%3Abeff%3Afecd%3Ab8fft%00%19duplicate_messages_limitssq%00%7E%00%06%00%00%00%00t%00%11enable_debug_modeq%00%7E%00%0At%00%14is_agree_privacy_keyq%00%7E%00%04t%00%0Denable_cactusq%00%7E%00%0At%00%19enable_play_silence_musicq%00%7E%00%0At%00%12enable_call_type_1q%00%7E%00%0At%00%12enable_call_type_2q%00%7E%00%0At%00%16sms_command_safe_phonet%00%00t%00%13request_retry_timesq%00%7E%00%17t%00%12enable_call_type_3q%00%7E%00%0At%00%04ipv4q%00%7E%00%1Ft%00%12enable_call_type_4q%00%7E%00%04t%00%11enable_app_notifyq%00%7E%00%0At%00%12enable_call_type_5q%00%7E%00%0At%00%0Cbattery_infot%00%C2%93%0A%C3%A5%C2%89%C2%A9%C3%A4%C2%BD%C2%99%C3%A7%C2%94%C2%B5%C3%A9%C2%87%C2%8F%C3%AF%C2%BC%C2%9A76%25%0A%C3%A5%C2%85%C2%85%C3%A6%C2%BB%C2%A1%C3%A7%C2%94%C2%B5%C3%A9%C2%87%C2%8F%C3%AF%C2%BC%C2%9A100%25%0A%C3%A5%C2%BD%C2%93%C3%A5%C2%89%C2%8D%C3%A7%C2%94%C2%B5%C3%A5%C2%8E%C2%8B%C3%AF%C2%BC%C2%9A4.12V%0A%C3%A5%C2%BD%C2%93%C3%A5%C2%89%C2%8D%C3%A6%C2%B8%C2%A9%C3%A5%C2%BA%C2%A6%C3%AF%C2%BC%C2%9A25.00%C3%A2%C2%84%C2%83%0A%C3%A7%C2%94%C2%B5%C3%A6%C2%B1%C2%A0%C3%A7%C2%8A%C2%B6%C3%A6%C2%80%C2%81%C3%AF%C2%BC%C2%9A%C3%A6%C2%94%C2%BE%C3%A7%C2%94%C2%B5%C3%A4%C2%B8%C2%AD%0A%C3%A5%C2%81%C2%A5%C3%A5%C2%BA%C2%B7%C3%A5%C2%BA%C2%A6%C3%AF%C2%BC%C2%9A%C3%A8%C2%89%C2%AF%C3%A5%C2%A5%C2%BD%0A%C3%A5%C2%85%C2%85%C3%A7%C2%94%C2%B5%C3%A5%C2%99%C2%A8%C3%AF%C2%BC%C2%9A%C3%A6%C2%9C%C2%AA%C3%A7%C2%9F%C2%A5t%00%04ipv6t%00%262409%3A8924%3Ac41%3A11c9%3A2494%3A7cff%3Afe22%3Ad761t%00%12enable_call_type_6q%00%7E%00%0At%00%0Aextra_sim2q%00%7E%00%1Ft%00%0Aextra_sim1t%00%1B%C3%A4%C2%B8%C2%AD%C3%A5%C2%9B%C2%BD%C3%A7%C2%A7%C2%BB%C3%A5%C2%8A%C2%A8_%2B8613914764325t%00%18enable_cancel_app_notifyq%00%7E%00%04t%00%0Cenable_phoneq%00%7E%00%04t%00%17enable_not_user_presentq%00%7E%00%0At%00%09wifi_ssidq%00%7E%00%1Ft%009com.idormy.sms.forwarder.widget.key_is_ignore_tips_300053q%00%7E%00%04t%00%0Dbattery_levelsq%00%7E%00%06%00%00%00Lt%00%0Asubid_sim2q%00%7E%00%17t%00%0Fbluetooth_statesq%00%7E%00%06%00%00%00%0Ct%00%0Fbattery_pluggedq%00%7E%00%17t%00%0Asubid_sim1q%00%7E%00%0Ct%00%19enable_one_pixel_activityq%00%7E%00%0At%00%1Abluetooth_ignore_anonymousq%00%7E%00%04t%00%0Csms_templateq%00%7E%00%1Ft%00%1Ccancel_extra_app_notify_listq%00%7E%00%1Ft%00%0Ebattery_statussq%00%7E%00%06%00%00%00%03t%00%0Bbattery_pctsr%00%0Fjava.lang.Float%C3%9A%C3%AD%C3%89%C2%A2%C3%9B%3C%C3%B0%C3%AC%02%00%01F%00%05valuexq%00%7E%00%07B%C2%98%00%00t%00%1Benable_load_system_app_listq%00%7E%00%0At%00%0Dnetwork_statesq%00%7E%00%06%00%00%00%02t%00%19enable_load_user_app_listq%00%7E%00%0Ax","task_list":[],"version_code":300053,"version_name":"3.3.2.240815"} -------------------------------------------------------------------------------- /build_exe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | SmsCodeServer - EXE打包脚本 4 | 将项目打包成可执行文件 5 | """ 6 | 7 | import os 8 | import sys 9 | import subprocess 10 | import shutil 11 | from pathlib import Path 12 | 13 | def check_pyinstaller(): 14 | """检查PyInstaller是否已安装""" 15 | try: 16 | import PyInstaller 17 | print("✅ PyInstaller已安装") 18 | return True 19 | except ImportError: 20 | print("❌ PyInstaller未安装") 21 | print("请运行: pip install pyinstaller") 22 | return False 23 | 24 | def create_spec_file(): 25 | """创建PyInstaller规范文件""" 26 | spec_content = '''# -*- mode: python ; coding: utf-8 -*- 27 | 28 | block_cipher = None 29 | 30 | # 控制台版本 31 | a = Analysis( 32 | ['main.py'], 33 | pathex=[], 34 | binaries=[], 35 | datas=[ 36 | ('config.json', '.'), 37 | ('favicon.ico', '.'), 38 | ('SmsForwarder.json', '.'), 39 | ], 40 | hiddenimports=[ 41 | 'core', 42 | 'core.server', 43 | 'core.message_processor', 44 | 'core.config_manager', 45 | 'tray_manager', 46 | 'utils', 47 | 'notification_manager', 48 | 'pystray', 49 | 'PIL', 50 | 'winotify', 51 | 'pyperclip' 52 | ], 53 | hookspath=[], 54 | hooksconfig={}, 55 | runtime_hooks=[], 56 | excludes=[], 57 | win_no_prefer_redirects=False, 58 | win_private_assemblies=False, 59 | cipher=block_cipher, 60 | noarchive=False, 61 | ) 62 | 63 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 64 | 65 | exe = EXE( 66 | pyz, 67 | a.scripts, 68 | a.binaries, 69 | a.zipfiles, 70 | a.datas, 71 | [], 72 | name='SmsCodeServer', 73 | debug=False, 74 | bootloader_ignore_signals=False, 75 | strip=False, 76 | upx=True, 77 | upx_exclude=[], 78 | runtime_tmpdir=None, 79 | console=True, 80 | disable_windowed_traceback=False, 81 | argv_emulation=False, 82 | target_arch=None, 83 | codesign_identity=None, 84 | entitlements_file=None, 85 | icon='favicon.ico' 86 | ) 87 | 88 | # 托盘版本 89 | b = Analysis( 90 | ['main_tray.py'], 91 | pathex=[], 92 | binaries=[], 93 | datas=[ 94 | ('config.json', '.'), 95 | ('favicon.ico', '.'), 96 | ('SmsForwarder.json', '.'), 97 | ], 98 | hiddenimports=[ 99 | 'core', 100 | 'core.server', 101 | 'core.message_processor', 102 | 'core.config_manager', 103 | 'tray_manager', 104 | 'utils', 105 | 'notification_manager', 106 | 'pystray', 107 | 'PIL', 108 | 'winotify', 109 | 'pyperclip' 110 | ], 111 | hookspath=[], 112 | hooksconfig={}, 113 | runtime_hooks=[], 114 | excludes=[], 115 | win_no_prefer_redirects=False, 116 | win_private_assemblies=False, 117 | cipher=block_cipher, 118 | noarchive=False, 119 | ) 120 | 121 | pyz2 = PYZ(b.pure, b.zipped_data, cipher=block_cipher) 122 | 123 | exe2 = EXE( 124 | pyz2, 125 | b.scripts, 126 | b.binaries, 127 | b.zipfiles, 128 | b.datas, 129 | [], 130 | name='SmsCodeServer_Tray', 131 | debug=False, 132 | bootloader_ignore_signals=False, 133 | strip=False, 134 | upx=True, 135 | upx_exclude=[], 136 | runtime_tmpdir=None, 137 | console=False, 138 | disable_windowed_traceback=False, 139 | argv_emulation=False, 140 | target_arch=None, 141 | codesign_identity=None, 142 | entitlements_file=None, 143 | icon='favicon.ico' 144 | ) 145 | ''' 146 | 147 | with open('SmsCodeServer.spec', 'w', encoding='utf-8') as f: 148 | f.write(spec_content) 149 | 150 | print("✅ 规范文件创建成功: SmsCodeServer.spec") 151 | 152 | def clean_build_dirs(): 153 | """清理构建目录""" 154 | dirs_to_clean = ['build', 'dist', '__pycache__'] 155 | 156 | for dir_name in dirs_to_clean: 157 | if os.path.exists(dir_name): 158 | shutil.rmtree(dir_name) 159 | print(f"🗑️ 清理目录: {dir_name}") 160 | 161 | def build_exe(): 162 | """构建exe文件""" 163 | print("🔨 开始构建exe文件...") 164 | 165 | try: 166 | # 使用PyInstaller构建 167 | cmd = ['pyinstaller', '--clean', 'SmsCodeServer.spec'] 168 | result = subprocess.run(cmd, capture_output=True, text=True) 169 | 170 | if result.returncode == 0: 171 | print("✅ exe文件构建成功!") 172 | print("📁 输出目录: dist/") 173 | return True 174 | else: 175 | print("❌ 构建失败:") 176 | print(result.stderr) 177 | return False 178 | 179 | except Exception as e: 180 | print(f"❌ 构建过程中出错: {e}") 181 | return False 182 | 183 | def show_build_info(): 184 | """显示构建信息""" 185 | print("\n📋 构建信息:") 186 | print("=" * 50) 187 | 188 | # 检查输出文件 189 | dist_dir = Path('dist') 190 | if dist_dir.exists(): 191 | exe_files = list(dist_dir.glob('*.exe')) 192 | for exe_file in exe_files: 193 | size_mb = exe_file.stat().st_size / (1024 * 1024) 194 | print(f"📦 {exe_file.name} ({size_mb:.1f} MB)") 195 | 196 | print("\n🚀 使用说明:") 197 | print("1. 控制台版本: SmsCodeServer.exe") 198 | print("2. 托盘版本: SmsCodeServer_Tray.exe") 199 | print("3. 将exe文件复制到任意目录即可使用") 200 | 201 | def main(): 202 | """主函数""" 203 | print("🚀 SmsCodeServer - EXE打包工具") 204 | print("=" * 50) 205 | 206 | # 检查PyInstaller 207 | if not check_pyinstaller(): 208 | return 1 209 | 210 | # 清理构建目录 211 | clean_build_dirs() 212 | 213 | # 创建规范文件 214 | create_spec_file() 215 | 216 | # 构建exe 217 | if build_exe(): 218 | show_build_info() 219 | print("\n🎉 打包完成!") 220 | return 0 221 | else: 222 | print("\n💥 打包失败!") 223 | return 1 224 | 225 | if __name__ == "__main__": 226 | sys.exit(main()) -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 65432, 3 | "auto_hide": true, 4 | "enable_tray": true, 5 | "notification": { 6 | "enabled": true, 7 | "sound": true, 8 | "duration": "long" 9 | } 10 | } -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SmsCodeServer 核心模块 3 | 提供核心的短信验证码处理功能 4 | """ 5 | 6 | from .server import SMSServer 7 | from .message_processor import MessageProcessor 8 | from .config_manager import ConfigManager 9 | 10 | __all__ = ['SMSServer', 'MessageProcessor', 'ConfigManager'] -------------------------------------------------------------------------------- /core/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/core/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /core/__pycache__/config_manager.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/core/__pycache__/config_manager.cpython-38.pyc -------------------------------------------------------------------------------- /core/__pycache__/message_processor.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/core/__pycache__/message_processor.cpython-38.pyc -------------------------------------------------------------------------------- /core/__pycache__/server.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/core/__pycache__/server.cpython-38.pyc -------------------------------------------------------------------------------- /core/config_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | 配置管理模块 3 | 统一管理配置文件的读取和验证 4 | """ 5 | 6 | import json 7 | import os 8 | import sys 9 | import logging 10 | from typing import Dict, Any, Optional 11 | 12 | class ConfigManager: 13 | """配置管理器""" 14 | 15 | def __init__(self, config_file: str = "config.json"): 16 | self.config_file = config_file 17 | self.config = {} 18 | self._load_config() 19 | 20 | def _load_config(self): 21 | """加载配置文件""" 22 | try: 23 | config_path = self._get_config_path() 24 | if os.path.exists(config_path): 25 | with open(config_path, 'r', encoding='utf-8') as file: 26 | self.config = json.load(file) 27 | logging.info(f"配置文件加载成功: {config_path}") 28 | else: 29 | logging.warning(f"配置文件不存在: {config_path}") 30 | self._create_default_config() 31 | except Exception as e: 32 | logging.error(f"加载配置文件失败: {e}") 33 | self._create_default_config() 34 | 35 | def _get_config_path(self) -> str: 36 | """获取配置文件路径""" 37 | if getattr(sys, 'frozen', False): 38 | # 打包后的exe文件 39 | return os.path.join(sys._MEIPASS, self.config_file) 40 | else: 41 | # 开发环境 42 | return os.path.join(os.path.dirname(os.path.dirname(__file__)), self.config_file) 43 | 44 | def _create_default_config(self): 45 | """创建默认配置""" 46 | self.config = { 47 | 'port': 65432, 48 | 'auto_hide': False, 49 | 'log_level': 'INFO' 50 | } 51 | logging.info("使用默认配置") 52 | 53 | def get(self, key: str, default: Any = None) -> Any: 54 | """获取配置值""" 55 | return self.config.get(key, default) 56 | 57 | def set(self, key: str, value: Any): 58 | """设置配置值""" 59 | self.config[key] = value 60 | 61 | def save(self): 62 | """保存配置到文件""" 63 | try: 64 | config_path = self._get_config_path() 65 | with open(config_path, 'w', encoding='utf-8') as file: 66 | json.dump(self.config, file, indent=2, ensure_ascii=False) 67 | logging.info(f"配置文件保存成功: {config_path}") 68 | except Exception as e: 69 | logging.error(f"保存配置文件失败: {e}") 70 | 71 | def get_port(self) -> int: 72 | """获取端口号""" 73 | port = self.get('port', 65432) 74 | if not isinstance(port, int) or port < 1 or port > 65535: 75 | logging.warning(f"无效的端口号: {port},使用默认端口65432") 76 | return 65432 77 | return port 78 | 79 | def set_port(self, port: int): 80 | """设置端口号""" 81 | if isinstance(port, int) and 1 <= port <= 65535: 82 | self.set('port', port) 83 | else: 84 | raise ValueError(f"无效的端口号: {port}") 85 | 86 | def get_auto_hide(self) -> bool: 87 | """获取自动隐藏设置""" 88 | return self.get('auto_hide', False) 89 | 90 | def set_auto_hide(self, auto_hide: bool): 91 | """设置自动隐藏""" 92 | self.set('auto_hide', bool(auto_hide)) 93 | 94 | def validate_config(self) -> bool: 95 | """验证配置有效性""" 96 | try: 97 | port = self.get_port() 98 | auto_hide = self.get_auto_hide() 99 | 100 | logging.info(f"配置验证通过 - 端口: {port}, 自动隐藏: {auto_hide}") 101 | return True 102 | except Exception as e: 103 | logging.error(f"配置验证失败: {e}") 104 | return False -------------------------------------------------------------------------------- /core/message_processor.py: -------------------------------------------------------------------------------- 1 | """ 2 | 消息处理模块 3 | 处理SMS和CALL消息的核心逻辑 4 | """ 5 | 6 | import re 7 | import logging 8 | from typing import Dict, Any, Optional, Tuple 9 | 10 | from utils import copy_verification_code, caller_handler,show_handler 11 | 12 | class MessageProcessor: 13 | """消息处理器""" 14 | 15 | def __init__(self): 16 | pass 17 | 18 | def process_message(self, text: str, stats: Dict[str, Any]) -> bool: 19 | """ 20 | 处理接收到的消息 21 | 22 | Args: 23 | text: 消息文本 24 | stats: 统计信息字典 25 | 26 | Returns: 27 | bool: 处理是否成功 28 | """ 29 | try: 30 | # 清理消息格式 31 | text = text.strip() 32 | if text.startswith('{') and text.endswith('}'): 33 | text = text[1:-1] 34 | 35 | # 解析消息类型 36 | match = self._split_string_at_first_dot(text) 37 | if not match: 38 | logging.error("❌ 消息格式错误") 39 | show_handler(text) 40 | return False 41 | 42 | prefix, suffix = match 43 | return self._handle_message_type(prefix, suffix, stats) 44 | 45 | except Exception as e: 46 | logging.error(f"处理消息时出错: {e}") 47 | return False 48 | 49 | def _split_string_at_first_dot(self, text: str) -> Optional[Tuple[str, str]]: 50 | """ 51 | 分割字符串,以第一个"."为界限 52 | 53 | Args: 54 | text: 输入字符串 55 | 56 | Returns: 57 | Tuple[str, str]: (前缀, 后缀) 或 None 58 | """ 59 | if "." not in text: 60 | return None 61 | 62 | index = text.find(".") 63 | before_dot = text[:index] 64 | after_dot = text[index + 1:] 65 | return before_dot, after_dot 66 | 67 | def _handle_message_type(self, prefix: str, suffix: str, stats: Dict[str, Any]) -> bool: 68 | """ 69 | 根据消息类型处理消息 70 | 71 | Args: 72 | prefix: 消息类型前缀 73 | suffix: 消息内容 74 | stats: 统计信息 75 | 76 | Returns: 77 | bool: 处理是否成功 78 | """ 79 | if prefix == 'CALL': 80 | # 处理来电 81 | stats['call_count'] += 1 82 | logging.info(f"📞 处理来电: {suffix}") 83 | caller_handler(suffix) 84 | logging.info(f"处理来电消息,总计: {stats['call_count']}") 85 | return True 86 | 87 | elif prefix == 'SMS': 88 | # 处理短信 89 | stats['sms_count'] += 1 90 | logging.info(f"📱 处理短信: {suffix}") 91 | result = copy_verification_code(suffix) 92 | if result: 93 | logging.info(f"✅ 验证码已复制: {result}") 94 | logging.info(f"处理短信消息成功,总计: {stats['sms_count']}") 95 | return True 96 | else: 97 | logging.warning("❌ 验证码提取失败") 98 | logging.warning(f"处理短信消息失败,总计: {stats['sms_count']}") 99 | return False 100 | 101 | else: 102 | logging.warning(f"⚠️ 未知消息类型: {prefix}") 103 | show_handler(suffix) 104 | return False -------------------------------------------------------------------------------- /core/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | SMS服务器核心模块 3 | 提供基础的socket服务器功能 4 | """ 5 | 6 | import socket 7 | import threading 8 | import time 9 | import logging 10 | from typing import Optional, Dict, Any 11 | from .message_processor import MessageProcessor 12 | from .config_manager import ConfigManager 13 | 14 | class SMSServer: 15 | """SMS服务器核心类""" 16 | 17 | def __init__(self, port: int, config_manager: Optional[ConfigManager] = None): 18 | self.port = port 19 | self.server_socket = None 20 | self.running = False 21 | self.force_exit = False 22 | self.stats = { 23 | 'start_time': 0, 24 | 'last_activity': 0, 25 | 'sms_count': 0, 26 | 'call_count': 0 27 | } 28 | 29 | # 初始化组件 30 | self.config_manager = config_manager or ConfigManager() 31 | self.message_processor = MessageProcessor() 32 | 33 | def start(self): 34 | """启动服务器""" 35 | self.running = True 36 | self.stats['start_time'] = time.time() 37 | 38 | # 创建socket对象 39 | self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 40 | self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 41 | 42 | try: 43 | self.server_socket.bind(('0.0.0.0', self.port)) 44 | self.server_socket.listen(5) 45 | logging.info(f"服务器启动成功,监听端口 {self.port}") 46 | 47 | # 主服务器循环 48 | while self.running and not self.force_exit: 49 | try: 50 | # 设置socket超时,以便能够响应退出信号 51 | self.server_socket.settimeout(1.0) 52 | client_socket, client_address = self.server_socket.accept() 53 | logging.info(f"客户端连接: {client_address}") 54 | 55 | # 在新线程中处理客户端连接 56 | client_thread = threading.Thread( 57 | target=self._handle_client, 58 | args=(client_socket, client_address) 59 | ) 60 | client_thread.daemon = True 61 | client_thread.start() 62 | 63 | except socket.timeout: 64 | # 超时是正常的,继续循环 65 | continue 66 | except Exception as e: 67 | if self.running and not self.force_exit: 68 | logging.error(f"处理客户端连接时出错: {e}") 69 | continue 70 | 71 | except Exception as e: 72 | logging.error(f"服务器启动失败: {e}") 73 | finally: 74 | self.stop() 75 | 76 | def _handle_client(self, client_socket, client_address): 77 | """处理客户端连接""" 78 | try: 79 | data = client_socket.recv(1024) 80 | if not data: 81 | logging.warning(f"客户端 {client_address} 未发送数据") 82 | client_socket.close() 83 | return 84 | 85 | text = data.decode('utf-8') 86 | logging.info(f"收到数据: {text}") 87 | 88 | # 更新最后活动时间 89 | self.stats['last_activity'] = time.time() 90 | 91 | # 处理消息 92 | self.message_processor.process_message(text, self.stats) 93 | 94 | except Exception as e: 95 | logging.error(f"处理客户端 {client_address} 时出错: {e}") 96 | finally: 97 | try: 98 | client_socket.close() 99 | except: 100 | pass 101 | 102 | def stop(self): 103 | """停止服务器""" 104 | self.running = False 105 | 106 | # 关闭服务器socket 107 | if self.server_socket: 108 | try: 109 | self.server_socket.close() 110 | self.server_socket = None 111 | except: 112 | pass 113 | 114 | logging.info("服务器已停止") 115 | 116 | def restart(self): 117 | """重启服务器""" 118 | logging.info("正在重启服务器...") 119 | self.stop() 120 | time.sleep(1) # 等待资源释放 121 | self.start() 122 | 123 | def force_quit(self): 124 | """强制退出程序""" 125 | logging.info("收到强制退出信号") 126 | self.force_exit = True 127 | self.stop() 128 | 129 | def get_stats(self) -> Dict[str, Any]: 130 | """获取服务器统计信息""" 131 | return self.stats.copy() -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SmsCodeServer/ee963cf900debc99d78ef64e392d3e096979206d/favicon.ico -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | SmsCodeServer - 控制台版 4 | 短信验证码转发服务器(简化版) 5 | """ 6 | 7 | import sys 8 | import logging 9 | import argparse 10 | from core import SMSServer, ConfigManager 11 | 12 | def setup_logging(): 13 | """设置日志配置""" 14 | logging.basicConfig( 15 | level=logging.INFO, 16 | format='%(asctime)s - %(levelname)s - %(message)s' 17 | ) 18 | 19 | def print_banner(): 20 | """打印启动横幅""" 21 | banner = """ 22 | ╔══════════════════════════════════════════════════════════════╗ 23 | ║ SmsCodeServer ║ 24 | ║ 短信验证码转发服务器 - 控制台版 ║ 25 | ║ ║ 26 | ║ 功能: 自动提取短信验证码并复制到剪贴板 ║ 27 | ║ 特色: 控制台显示、实时日志、简单易用 ║ 28 | ║ 注意: 按 Ctrl+C 停止服务器 ║ 29 | ╚══════════════════════════════════════════════════════════════╝ 30 | """ 31 | print(banner) 32 | 33 | def parse_args(): 34 | """解析命令行参数""" 35 | parser = argparse.ArgumentParser( 36 | description="SmsCodeServer - 短信验证码转发服务器", 37 | formatter_class=argparse.RawDescriptionHelpFormatter, 38 | epilog=""" 39 | 使用示例: 40 | python main.py # 使用默认配置启动 41 | python main.py -p 65432 # 指定端口启动 42 | """ 43 | ) 44 | parser.add_argument('-p', '--port', type=int, help="监听端口号", default=None) 45 | return parser.parse_args() 46 | 47 | def main(): 48 | """主函数""" 49 | # 设置日志 50 | setup_logging() 51 | 52 | # 打印横幅 53 | print_banner() 54 | 55 | # 解析命令行参数 56 | args = parse_args() 57 | 58 | # 初始化配置管理器 59 | config_manager = ConfigManager() 60 | 61 | # 确定端口号 62 | if args.port: 63 | port = args.port 64 | logging.info(f"使用命令行参数指定的端口: {port}") 65 | else: 66 | port = config_manager.get_port() 67 | logging.info(f"使用配置文件中的端口: {port}") 68 | 69 | # 验证端口号 70 | if port < 1 or port > 65535: 71 | logging.error("端口号必须在1-65535之间") 72 | return 1 73 | 74 | # 创建并启动服务器 75 | server = SMSServer(port, config_manager) 76 | 77 | try: 78 | print(f"📱 请确保手机端SmsForwarder已正确配置") 79 | print("⏹️ 按 Ctrl+C 停止服务器\n") 80 | server.start() 81 | except KeyboardInterrupt: 82 | print("\n\n🛑 用户中断,正在关闭服务器...") 83 | logging.info("用户中断程序") 84 | except Exception as e: 85 | logging.error(f"程序运行出错: {e}") 86 | return 1 87 | 88 | return 0 89 | 90 | if __name__ == "__main__": 91 | sys.exit(main()) 92 | -------------------------------------------------------------------------------- /main_tray.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | SmsCodeServer with System Tray 4 | 带系统托盘功能的短信验证码转发服务器 5 | """ 6 | 7 | import re 8 | import socket 9 | import json 10 | import os 11 | import sys 12 | import logging 13 | import argparse 14 | import threading 15 | import time 16 | import utils 17 | from typing import Optional 18 | 19 | # 导入托盘管理器 20 | try: 21 | from tray_manager import create_tray_manager 22 | TRAY_AVAILABLE = True 23 | except ImportError: 24 | TRAY_AVAILABLE = False 25 | print("警告: 托盘管理器不可用,请安装依赖: pip install pystray pillow") 26 | 27 | # 配置日志记录 28 | class SafeStreamHandler(logging.StreamHandler): 29 | """安全的流处理器,处理控制台窗口隐藏的情况""" 30 | 31 | def emit(self, record): 32 | try: 33 | # 检查流是否可用 34 | if self.stream is None or self.stream.closed: 35 | return 36 | super().emit(record) 37 | except (AttributeError, OSError, ValueError): 38 | # 如果流不可用,静默忽略 39 | pass 40 | 41 | # 创建自定义的日志配置 42 | def setup_logging(): 43 | """设置安全的日志配置""" 44 | # 清除现有的处理器 45 | root_logger = logging.getLogger() 46 | for handler in root_logger.handlers[:]: 47 | root_logger.removeHandler(handler) 48 | 49 | # 创建格式化器 50 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 51 | 52 | # 文件处理器 53 | file_handler = logging.FileHandler('sms_server.log', encoding='utf-8') 54 | file_handler.setFormatter(formatter) 55 | 56 | # 安全的流处理器 57 | stream_handler = SafeStreamHandler() 58 | stream_handler.setFormatter(formatter) 59 | 60 | # 设置根日志记录器 61 | root_logger.setLevel(logging.INFO) 62 | root_logger.addHandler(file_handler) 63 | root_logger.addHandler(stream_handler) 64 | 65 | # 初始化日志配置 66 | setup_logging() 67 | 68 | def safe_logging(level, message, *args): 69 | """安全的日志记录函数,处理控制台窗口隐藏的情况""" 70 | try: 71 | if level == 'info': 72 | logging.info(message, *args) 73 | elif level == 'error': 74 | logging.error(message, *args) 75 | elif level == 'warning': 76 | logging.warning(message, *args) 77 | elif level == 'debug': 78 | logging.debug(message, *args) 79 | except (AttributeError, OSError, ValueError): 80 | # 如果日志记录失败,静默忽略 81 | pass 82 | 83 | def hide_console_window(): 84 | """隐藏控制台窗口""" 85 | try: 86 | import ctypes 87 | # 获取控制台窗口句柄 88 | console_window = ctypes.windll.kernel32.GetConsoleWindow() 89 | if console_window: 90 | # 隐藏控制台窗口 91 | ctypes.windll.user32.ShowWindow(console_window, 0) # SW_HIDE = 0 92 | 93 | # 更新日志处理器,移除可能失效的流处理器 94 | root_logger = logging.getLogger() 95 | for handler in root_logger.handlers[:]: 96 | if isinstance(handler, logging.StreamHandler) and not isinstance(handler, logging.FileHandler): 97 | # 检查流是否仍然有效 98 | try: 99 | if handler.stream is None or handler.stream.closed: 100 | root_logger.removeHandler(handler) 101 | except (AttributeError, OSError): 102 | root_logger.removeHandler(handler) 103 | 104 | logging.info("控制台窗口已隐藏") 105 | return True 106 | except Exception as e: 107 | # 使用文件日志记录错误,避免控制台输出 108 | try: 109 | logging.error(f"隐藏控制台窗口失败: {e}") 110 | except: 111 | pass 112 | return False 113 | 114 | def show_console_window(): 115 | """显示控制台窗口""" 116 | try: 117 | import ctypes 118 | # 获取控制台窗口句柄 119 | console_window = ctypes.windll.kernel32.GetConsoleWindow() 120 | if console_window: 121 | # 显示控制台窗口 122 | ctypes.windll.user32.ShowWindow(console_window, 1) # SW_SHOW = 1 123 | ctypes.windll.user32.SetForegroundWindow(console_window) 124 | 125 | # 恢复流处理器 126 | root_logger = logging.getLogger() 127 | has_stream_handler = any( 128 | isinstance(handler, logging.StreamHandler) and not isinstance(handler, logging.FileHandler) 129 | for handler in root_logger.handlers 130 | ) 131 | 132 | if not has_stream_handler: 133 | # 重新添加安全的流处理器 134 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 135 | stream_handler = SafeStreamHandler() 136 | stream_handler.setFormatter(formatter) 137 | root_logger.addHandler(stream_handler) 138 | 139 | logging.info("控制台窗口已显示") 140 | return True 141 | except Exception as e: 142 | # 使用文件日志记录错误,避免控制台输出 143 | try: 144 | logging.error(f"显示控制台窗口失败: {e}") 145 | except: 146 | pass 147 | return False 148 | 149 | def setup_console_close_handler(): 150 | """设置控制台关闭事件处理""" 151 | try: 152 | import ctypes 153 | from ctypes import wintypes 154 | 155 | # 保存全局引用,防止被垃圾回收 156 | global _console_handler 157 | 158 | # 定义控制台关闭事件处理函数 159 | def console_ctrl_handler(ctrl_type): 160 | if ctrl_type in [0, 2]: # CTRL_C_EVENT or CTRL_CLOSE_EVENT 161 | # 安全地记录日志,避免控制台输出错误 162 | try: 163 | logging.info("检测到控制台关闭事件,程序将继续在后台运行") 164 | except: 165 | pass 166 | # 隐藏控制台窗口而不是退出程序 167 | hide_console_window() 168 | return True # 返回True表示已处理事件 169 | return False 170 | 171 | # 设置控制台事件处理 172 | handler_func = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD) 173 | _console_handler = handler_func(console_ctrl_handler) 174 | ctypes.windll.kernel32.SetConsoleCtrlHandler(_console_handler, True) 175 | 176 | # 安全地记录日志 177 | try: 178 | logging.info("已设置控制台关闭事件处理") 179 | except: 180 | pass 181 | return True 182 | except Exception as e: 183 | # 安全地记录错误 184 | try: 185 | logging.error(f"设置控制台关闭事件处理失败: {e}") 186 | except: 187 | pass 188 | return False 189 | 190 | def prevent_console_close(): 191 | """防止控制台窗口被关闭""" 192 | try: 193 | import ctypes 194 | from ctypes import wintypes 195 | 196 | # 获取控制台窗口句柄 197 | console_window = ctypes.windll.kernel32.GetConsoleWindow() 198 | if console_window: 199 | # 禁用关闭按钮 200 | ctypes.windll.user32.EnableMenuItem( 201 | ctypes.windll.user32.GetSystemMenu(console_window, False), 202 | 0xF060, # SC_CLOSE 203 | 0x00000001 # MF_GRAYED 204 | ) 205 | # 安全地记录日志 206 | try: 207 | logging.info("已禁用控制台关闭按钮") 208 | except: 209 | pass 210 | return True 211 | except Exception as e: 212 | # 安全地记录错误 213 | try: 214 | logging.error(f"禁用控制台关闭按钮失败: {e}") 215 | except: 216 | pass 217 | return False 218 | 219 | class SMSServer: 220 | """SMS服务器类""" 221 | 222 | def __init__(self, port: int, enable_tray: bool = True, auto_hide: bool = False): 223 | self.port = port 224 | self.server_socket = None 225 | self.running = False 226 | self.tray_manager = None 227 | self.enable_tray = enable_tray and TRAY_AVAILABLE 228 | self.auto_hide = auto_hide 229 | self.force_exit = False # 强制退出标志 230 | 231 | # 统计信息 232 | self.stats = { 233 | 'sms_count': 0, 234 | 'call_count': 0, 235 | 'start_time': None, 236 | 'last_activity': None 237 | } 238 | 239 | def start(self): 240 | """启动服务器""" 241 | self.running = True 242 | self.stats['start_time'] = time.time() 243 | 244 | # 设置控制台关闭事件处理 245 | if self.enable_tray: 246 | setup_console_close_handler() 247 | # 可选:禁用控制台关闭按钮,强制用户通过托盘退出 248 | # prevent_console_close() 249 | 250 | # 创建socket对象 251 | self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 252 | self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 253 | 254 | try: 255 | self.server_socket.bind(('0.0.0.0', self.port)) 256 | self.server_socket.listen(5) 257 | safe_logging('info', f"服务器启动成功,监听端口 {self.port}") 258 | 259 | # 启动系统托盘 260 | if self.enable_tray: 261 | self.tray_manager = create_tray_manager(self) 262 | if self.tray_manager: 263 | self.tray_manager.start() 264 | safe_logging('info', "系统托盘已启动") 265 | 266 | # 如果启用自动隐藏,延迟隐藏控制台窗口 267 | if self.auto_hide: 268 | # 等待托盘图标完全启动后再隐藏窗口 269 | time.sleep(2) 270 | hide_console_window() 271 | 272 | # 显示托盘启动通知 273 | utils.show_toast_notification( 274 | "托盘已启动", 275 | "系统托盘图标已启动\n关闭控制台窗口程序将继续在后台运行" 276 | ) 277 | 278 | # 显示启动通知 279 | utils.show_toast_notification( 280 | "服务器启动", 281 | f"SmsCodeServer 已启动\n监听端口: {self.port}\n程序将继续在后台运行" 282 | ) 283 | 284 | # 主服务器循环 285 | while self.running and not self.force_exit: 286 | try: 287 | # 设置socket超时,以便能够响应退出信号 288 | self.server_socket.settimeout(1.0) 289 | client_socket, client_address = self.server_socket.accept() 290 | safe_logging('info', f"客户端连接: {client_address}") 291 | 292 | # 在新线程中处理客户端连接 293 | client_thread = threading.Thread( 294 | target=self.handle_client, 295 | args=(client_socket, client_address) 296 | ) 297 | client_thread.daemon = True 298 | client_thread.start() 299 | 300 | except socket.timeout: 301 | # 超时是正常的,继续循环 302 | continue 303 | except Exception as e: 304 | if self.running and not self.force_exit: 305 | safe_logging('error', f"处理客户端连接时出错: {e}") 306 | continue 307 | 308 | # 如果程序正常退出(非强制退出),等待托盘线程结束 309 | if not self.force_exit and self.tray_manager and self.tray_manager.is_running: 310 | safe_logging('info', "等待托盘线程结束...") 311 | # 给托盘线程一些时间来完成 312 | time.sleep(1) 313 | 314 | except Exception as e: 315 | safe_logging('error', f"服务器启动失败: {e}") 316 | if self.enable_tray and self.tray_manager: 317 | self.tray_manager.update_icon("error") 318 | finally: 319 | self.stop() 320 | 321 | def handle_client(self, client_socket, client_address): 322 | """处理客户端连接""" 323 | try: 324 | data = client_socket.recv(1024) 325 | if not data: 326 | safe_logging('warning', f"客户端 {client_address} 未发送数据") 327 | client_socket.close() 328 | return 329 | 330 | text = data.decode('utf-8') 331 | safe_logging('info', f"收到数据: {text}") 332 | 333 | # 更新最后活动时间 334 | self.stats['last_activity'] = time.time() 335 | 336 | # 处理消息 337 | self.process_message(text) 338 | 339 | except Exception as e: 340 | logging.error(f"处理客户端 {client_address} 时出错: {e}") 341 | finally: 342 | try: 343 | client_socket.close() 344 | except: 345 | pass 346 | 347 | def process_message(self, text): 348 | """处理消息""" 349 | # 清理消息格式 350 | text = text.strip() 351 | if text.startswith('{') and text.endswith('}'): 352 | text = text[1:-1] 353 | 354 | # 解析消息类型 355 | match = split_string_at_first_dot(text) 356 | 357 | if match: 358 | prefix, suffix = match 359 | if prefix == 'CALL': 360 | # 处理来电 361 | self.stats['call_count'] += 1 362 | utils.caller_handler(suffix) 363 | logging.info(f"处理来电消息,总计: {self.stats['call_count']}") 364 | 365 | elif prefix == 'SMS': 366 | # 处理短信 367 | self.stats['sms_count'] += 1 368 | result = utils.copy_verification_code(suffix) 369 | if result: 370 | logging.info(f"处理短信消息成功,总计: {self.stats['sms_count']}") 371 | else: 372 | logging.warning(f"处理短信消息失败,总计: {self.stats['sms_count']}") 373 | else: 374 | utils.show_handler(text) 375 | logging.warning(f"未知消息类型: {prefix}") 376 | else: 377 | utils.show_handler(text) 378 | logging.error("无法匹配到预期格式") 379 | 380 | def restart(self): 381 | """重启服务器""" 382 | safe_logging('info', "正在重启服务器...") 383 | 384 | # 先停止托盘图标 385 | if self.tray_manager: 386 | self.tray_manager.stop() 387 | # 等待托盘图标完全停止 388 | time.sleep(0.5) 389 | 390 | # 停止服务器 391 | self.stop() 392 | 393 | # 等待资源释放 394 | time.sleep(1) 395 | 396 | # 重新创建托盘管理器 397 | if self.enable_tray: 398 | self.tray_manager = create_tray_manager(self) 399 | 400 | # 重新启动服务器 401 | self.start() 402 | 403 | def force_quit(self): 404 | """强制退出程序""" 405 | logging.info("收到强制退出信号") 406 | self.force_exit = True 407 | self.stop() 408 | # 强制退出程序 409 | os._exit(0) 410 | 411 | def stop(self): 412 | """停止服务器""" 413 | self.running = False 414 | 415 | # 停止托盘图标 416 | if self.tray_manager: 417 | self.tray_manager.stop() 418 | # 清空托盘管理器引用 419 | self.tray_manager = None 420 | 421 | # 关闭服务器socket 422 | if self.server_socket: 423 | try: 424 | self.server_socket.close() 425 | self.server_socket = None 426 | except: 427 | pass 428 | 429 | safe_logging('info', "服务器已停止") 430 | 431 | def split_string_at_first_dot(text): 432 | """分割字符串,以第一个"."为界限""" 433 | if "." not in text: 434 | return None 435 | 436 | index = text.find(".") 437 | before_dot = text[:index] 438 | after_dot = text[index + 1:] 439 | return before_dot, after_dot 440 | 441 | def get_config_path(): 442 | """获取配置文件路径""" 443 | if getattr(sys, 'frozen', False): 444 | return os.path.join(sys._MEIPASS, 'config.json') 445 | else: 446 | return os.path.join(os.path.dirname(__file__), 'config.json') 447 | 448 | def parse_args(): 449 | """解析命令行参数""" 450 | # 检查stdout是否可用,如果不可用则重定向到文件 451 | if sys.stdout is None: 452 | # 重定向到日志文件 453 | sys.stdout = open('sms_server.log', 'a', encoding='utf-8') 454 | sys.stderr = sys.stdout 455 | 456 | parser = argparse.ArgumentParser( 457 | description="SmsCodeServer - 短信验证码转发服务器", 458 | formatter_class=argparse.RawDescriptionHelpFormatter, 459 | epilog=""" 460 | 使用示例: 461 | python main_tray.py # 使用默认配置启动 462 | python main_tray.py -p 65432 # 指定端口启动 463 | python main_tray.py --no-tray # 禁用系统托盘 464 | python main_tray.py --auto-hide # 自动隐藏控制台窗口 465 | """ 466 | ) 467 | parser.add_argument('-p', '--port', type=int, help="监听端口号", default=None) 468 | parser.add_argument('--no-tray', action='store_true', help="禁用系统托盘") 469 | parser.add_argument('--auto-hide', action='store_true', help="自动隐藏控制台窗口") 470 | 471 | try: 472 | return parser.parse_args() 473 | except Exception as e: 474 | # 如果解析失败,返回默认参数 475 | logging.error(f"参数解析失败: {e}") 476 | return argparse.Namespace(port=None, no_tray=False, auto_hide=False) 477 | 478 | def print_banner(): 479 | """打印启动横幅""" 480 | try: 481 | banner = """ 482 | ╔══════════════════════════════════════════════════════════════╗ 483 | ║ SmsCodeServer ║ 484 | ║ 短信验证码转发服务器 ║ 485 | ║ ║ 486 | ║ 功能: 自动提取短信验证码并复制到剪贴板 ║ 487 | ║ 特色: 系统托盘支持、复制历史、状态监控 ║ 488 | ║ 注意: 关闭控制台窗口程序将继续在后台运行 ║ 489 | ╚══════════════════════════════════════════════════════════════╝ 490 | """ 491 | print(banner) 492 | except Exception as e: 493 | # 如果打印失败,记录到日志 494 | logging.info("SmsCodeServer 启动") 495 | logging.info("功能: 自动提取短信验证码并复制到剪贴板") 496 | logging.info("特色: 系统托盘支持、复制历史、状态监控") 497 | 498 | def main(): 499 | """主函数""" 500 | print_banner() 501 | 502 | # 解析命令行参数 503 | args = parse_args() 504 | 505 | # 确定端口号 506 | if args.port: 507 | port = args.port 508 | logging.info(f"使用命令行参数指定的端口: {port}") 509 | else: 510 | try: 511 | config_path = get_config_path() 512 | with open(config_path, 'r', encoding='utf-8') as file: 513 | config = json.load(file) 514 | port = config['port'] 515 | logging.info(f"使用配置文件中的端口: {port}") 516 | except Exception as e: 517 | logging.error(f"加载配置失败: {e}") 518 | port = 65432 519 | logging.info(f"使用默认端口: {port}") 520 | 521 | # 验证端口号 522 | if port < 1 or port > 65535: 523 | logging.error("端口号必须在1-65535之间") 524 | return 1 525 | 526 | # 检查系统托盘可用性 527 | enable_tray = not args.no_tray and TRAY_AVAILABLE 528 | if not enable_tray and not args.no_tray: 529 | try: 530 | print("警告: 系统托盘功能不可用,请安装依赖: pip install pystray pillow") 531 | except: 532 | logging.warning("系统托盘功能不可用,请安装依赖: pip install pystray pillow") 533 | 534 | # 检查自动隐藏设置 535 | auto_hide = args.auto_hide 536 | if not auto_hide: 537 | # 如果没有命令行参数,尝试从配置文件读取 538 | try: 539 | config_path = get_config_path() 540 | with open(config_path, 'r', encoding='utf-8') as file: 541 | config = json.load(file) 542 | auto_hide = config.get('auto_hide', False) 543 | except: 544 | auto_hide = False 545 | 546 | # 自动隐藏需要托盘功能支持 547 | auto_hide = auto_hide and enable_tray 548 | if auto_hide: 549 | try: 550 | print("🔔 已启用自动隐藏模式,程序将最小化到系统托盘") 551 | except: 552 | logging.info("已启用自动隐藏模式,程序将最小化到系统托盘") 553 | 554 | # 创建并启动服务器 555 | server = SMSServer(port, enable_tray, auto_hide) 556 | 557 | try: 558 | try: 559 | print(f"\n✅ 服务器启动中,监听端口: {port}") 560 | if enable_tray: 561 | print("🔔 系统托盘功能已启用") 562 | print("💡 关闭控制台窗口程序将继续在后台运行") 563 | print("💡 只有通过托盘菜单的'退出'选项才能完全退出程序") 564 | if auto_hide: 565 | print("🪟 控制台窗口将在2秒后自动隐藏") 566 | print("📱 请确保手机端SmsForwarder已正确配置") 567 | print("⏹️ 按 Ctrl+C 停止服务器\n") 568 | except: 569 | logging.info(f"服务器启动中,监听端口: {port}") 570 | if enable_tray: 571 | logging.info("系统托盘功能已启用") 572 | if auto_hide: 573 | logging.info("控制台窗口将在2秒后自动隐藏") 574 | 575 | server.start() 576 | 577 | except KeyboardInterrupt: 578 | try: 579 | print("\n\n🛑 用户中断,正在关闭服务器...") 580 | except: 581 | safe_logging('info', "用户中断,正在关闭服务器...") 582 | safe_logging('info', "用户中断程序") 583 | except Exception as e: 584 | safe_logging('error', f"程序运行出错: {e}") 585 | try: 586 | print(f"\n❌ 程序运行出错: {e}") 587 | except: 588 | pass 589 | return 1 590 | 591 | return 0 592 | 593 | if __name__ == "__main__": 594 | sys.exit(main()) -------------------------------------------------------------------------------- /notification_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | 改进的通知管理器 3 | 解决Windows通知不显示的问题 4 | """ 5 | 6 | import os 7 | import sys 8 | import logging 9 | import time 10 | import threading 11 | from typing import Optional, Dict, Any 12 | from winotify import Notification, audio 13 | 14 | class NotificationManager: 15 | """改进的通知管理器""" 16 | 17 | def __init__(self): 18 | self.last_notification_time = 0 19 | self.notification_queue = [] 20 | self.is_processing = False 21 | self.min_interval = 1.0 # 最小通知间隔(秒) 22 | 23 | def get_icon_path(self) -> str: 24 | """获取图标路径""" 25 | if getattr(sys, 'frozen', False): 26 | icon_path = os.path.join(sys._MEIPASS, 'favicon.ico') 27 | else: 28 | icon_path = os.path.join(os.path.dirname(__file__), 'favicon.ico') 29 | 30 | return icon_path if os.path.exists(icon_path) else "" 31 | 32 | def show_notification(self, title: str, message: str, notification_type: str = "info", 33 | duration: str = "long", sound: bool = True) -> bool: 34 | """ 35 | 显示通知 36 | 37 | Args: 38 | title: 通知标题 39 | message: 通知内容 40 | notification_type: 通知类型 (info, success, error, warning) 41 | duration: 显示时长 (short, long) 42 | sound: 是否播放声音 43 | 44 | Returns: 45 | bool: 是否成功显示 46 | """ 47 | try: 48 | # 检查通知间隔 49 | current_time = time.time() 50 | if current_time - self.last_notification_time < self.min_interval: 51 | # 如果间隔太短,将通知加入队列 52 | self.notification_queue.append({ 53 | 'title': title, 54 | 'message': message, 55 | 'type': notification_type, 56 | 'duration': duration, 57 | 'sound': sound, 58 | 'timestamp': current_time 59 | }) 60 | 61 | # 启动队列处理线程 62 | if not self.is_processing: 63 | threading.Thread(target=self._process_queue, daemon=True).start() 64 | return True 65 | 66 | # 更新最后通知时间 67 | self.last_notification_time = current_time 68 | 69 | # 获取图标路径 70 | icon_path = self.get_icon_path() 71 | 72 | # 根据通知类型设置不同的应用ID 73 | app_id_map = { 74 | "info": "SmsCodeServer", 75 | "success": "SmsCodeServer.Success", 76 | "error": "SmsCodeServer.Error", 77 | "warning": "SmsCodeServer.Warning" 78 | } 79 | app_id = app_id_map.get(notification_type, "SmsCodeServer") 80 | 81 | # 创建通知 82 | toast = Notification( 83 | app_id=app_id, 84 | title=title, 85 | msg=message, 86 | icon=icon_path, 87 | duration=duration 88 | ) 89 | 90 | # 设置通知声音 91 | if sound: 92 | try: 93 | toast.set_audio(audio.Default, loop=False) 94 | except Exception as e: 95 | logging.warning(f"设置通知声音失败: {e}") 96 | 97 | # 显示通知 98 | toast.show() 99 | 100 | logging.info(f"通知已显示: {title} - {message}") 101 | return True 102 | 103 | except Exception as e: 104 | logging.error(f"通知显示失败: {str(e)}") 105 | return False 106 | 107 | def _process_queue(self): 108 | """处理通知队列""" 109 | self.is_processing = True 110 | 111 | while self.notification_queue: 112 | current_time = time.time() 113 | 114 | # 检查是否可以显示下一个通知 115 | if current_time - self.last_notification_time >= self.min_interval: 116 | notification = self.notification_queue.pop(0) 117 | 118 | # 显示通知 119 | self.show_notification( 120 | notification['title'], 121 | notification['message'], 122 | notification['type'], 123 | notification['duration'], 124 | notification['sound'] 125 | ) 126 | 127 | # 等待一小段时间 128 | time.sleep(0.5) 129 | else: 130 | # 等待到可以显示下一个通知 131 | time.sleep(0.1) 132 | 133 | self.is_processing = False 134 | 135 | def show_success_notification(self, title: str, message: str) -> bool: 136 | """显示成功通知""" 137 | return self.show_notification(title, message, "success", "long", True) 138 | 139 | def show_error_notification(self, title: str, message: str) -> bool: 140 | """显示错误通知""" 141 | return self.show_notification(title, message, "error", "long", True) 142 | 143 | def show_warning_notification(self, title: str, message: str) -> bool: 144 | """显示警告通知""" 145 | return self.show_notification(title, message, "warning", "long", True) 146 | 147 | def show_info_notification(self, title: str, message: str) -> bool: 148 | """显示信息通知""" 149 | return self.show_notification(title, message, "info", "long", True) 150 | 151 | def test_notification(self) -> bool: 152 | """测试通知功能""" 153 | return self.show_notification( 154 | "通知测试", 155 | "如果您看到这条通知,说明通知功能正常工作!", 156 | "info", 157 | "short", 158 | True 159 | ) 160 | 161 | # 全局通知管理器实例 162 | _notification_manager = NotificationManager() 163 | 164 | # 便捷函数 165 | def show_toast_notification(title: str, message: str, notification_type: str = "info") -> bool: 166 | """显示通知的便捷函数""" 167 | return _notification_manager.show_notification(title, message, notification_type) 168 | 169 | def show_success_notification(title: str, message: str) -> bool: 170 | """显示成功通知""" 171 | return _notification_manager.show_success_notification(title, message) 172 | 173 | def show_error_notification(title: str, message: str) -> bool: 174 | """显示错误通知""" 175 | return _notification_manager.show_error_notification(title, message) 176 | 177 | def show_warning_notification(title: str, message: str) -> bool: 178 | """显示警告通知""" 179 | return _notification_manager.show_warning_notification(title, message) 180 | 181 | def test_notification() -> bool: 182 | """测试通知功能""" 183 | return _notification_manager.test_notification() 184 | 185 | if __name__ == "__main__": 186 | # 测试通知功能 187 | print("测试通知功能...") 188 | 189 | # 测试不同类型的通知 190 | test_notification() 191 | time.sleep(2) 192 | 193 | show_success_notification("成功", "操作成功完成!") 194 | time.sleep(2) 195 | 196 | show_error_notification("错误", "发生了一个错误!") 197 | time.sleep(2) 198 | 199 | show_warning_notification("警告", "请注意这个警告!") 200 | 201 | print("通知测试完成!") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 核心依赖 2 | winotify>=1.1.0 3 | pyperclip>=1.8.2 4 | 5 | # 系统托盘功能 6 | pystray>=0.19.4 7 | pillow>=9.0.0 8 | 9 | # 开发依赖 10 | pyinstaller>=5.13.0 -------------------------------------------------------------------------------- /tray_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | 系统托盘管理器 3 | 提供简化的托盘功能和用户界面 4 | """ 5 | 6 | import threading 7 | import time 8 | import logging 9 | from typing import Optional, Callable, List, Dict, Any 10 | import utils 11 | 12 | # 尝试导入系统托盘相关库 13 | try: 14 | import pystray 15 | from PIL import Image, ImageDraw, ImageFont 16 | TRAY_AVAILABLE = True 17 | except ImportError: 18 | TRAY_AVAILABLE = False 19 | print("警告: pystray 或 PIL 未安装,系统托盘功能不可用") 20 | print("请运行: pip install pystray pillow") 21 | 22 | class TrayManager: 23 | """系统托盘管理器""" 24 | 25 | def __init__(self, server_instance=None): 26 | self.server = server_instance 27 | self.icon = None 28 | self.is_running = False 29 | self.menu_items = [] 30 | self.status_callback = None 31 | 32 | def set_status_callback(self, callback: Callable[[], str]): 33 | """设置状态回调函数""" 34 | self.status_callback = callback 35 | 36 | def create_icon_image(self, size=(64, 64), color=(0, 120, 212), status="running"): 37 | """创建托盘图标""" 38 | # 创建基础图像 39 | image = Image.new('RGBA', size, (0, 0, 0, 0)) 40 | draw = ImageDraw.Draw(image) 41 | 42 | # 根据状态选择颜色 43 | if status == "running": 44 | fill_color = (0, 120, 212) # 蓝色 45 | elif status == "stopped": 46 | fill_color = (200, 0, 0) # 红色 47 | elif status == "error": 48 | fill_color = (255, 165, 0) # 橙色 49 | else: 50 | fill_color = color 51 | 52 | # 绘制圆形背景 53 | margin = 4 54 | draw.ellipse([margin, margin, size[0]-margin, size[1]-margin], 55 | fill=fill_color, outline=(255, 255, 255, 255), width=2) 56 | 57 | # 添加文字 "SMS" 58 | try: 59 | # 尝试使用系统字体 60 | font_size = size[0] // 4 61 | try: 62 | font = ImageFont.truetype("arial.ttf", font_size) 63 | except: 64 | font = ImageFont.load_default() 65 | 66 | text = "SMS" 67 | text_bbox = draw.textbbox((0, 0), text, font=font) 68 | text_width = text_bbox[2] - text_bbox[0] 69 | text_height = text_bbox[3] - text_bbox[1] 70 | 71 | x = (size[0] - text_width) // 2 72 | y = (size[1] - text_height) // 2 73 | 74 | draw.text((x, y), text, fill=(255, 255, 255, 255), font=font) 75 | except Exception as e: 76 | logging.warning(f"绘制图标文字失败: {e}") 77 | 78 | return image 79 | 80 | def create_menu(self): 81 | """创建托盘菜单""" 82 | menu_items = [] 83 | 84 | # 状态信息 85 | status_text = "状态: 运行中" if self.server and self.server.running else "状态: 已停止" 86 | menu_items.append(pystray.MenuItem(status_text, self.show_status, enabled=False)) 87 | 88 | # 端口信息 89 | if self.server: 90 | port_text = f"端口: {self.server.port}" 91 | menu_items.append(pystray.MenuItem(port_text, self.show_port, enabled=False)) 92 | 93 | # 使用分隔符(兼容不同版本的pystray) 94 | try: 95 | menu_items.append(pystray.MenuSeparator()) 96 | except AttributeError: 97 | # 如果MenuSeparator不存在,使用空字符串作为分隔符 98 | menu_items.append(pystray.MenuItem("─" * 20, lambda: None, enabled=False)) 99 | 100 | # 功能菜单 101 | menu_items.extend([ 102 | pystray.MenuItem("重启服务器", self.restart_server), 103 | pystray.MenuItem("停止服务器", self.stop_server), 104 | ]) 105 | 106 | # 第二个分隔符 107 | try: 108 | menu_items.append(pystray.MenuSeparator()) 109 | except AttributeError: 110 | menu_items.append(pystray.MenuItem("─" * 20, lambda: None, enabled=False)) 111 | 112 | menu_items.extend([ 113 | pystray.MenuItem("关于", self.show_about), 114 | pystray.MenuItem("退出程序", self.quit_application) 115 | ]) 116 | 117 | return pystray.Menu(*menu_items) 118 | 119 | def show_status(self, icon, item): 120 | """显示状态信息""" 121 | pass # 只读菜单项 122 | 123 | def show_port(self, icon, item): 124 | """显示端口信息""" 125 | pass # 只读菜单项 126 | 127 | def restart_server(self, icon, item): 128 | """重启服务器""" 129 | if not self.server: 130 | utils.show_toast_notification("错误", "服务器实例不可用") 131 | return 132 | 133 | try: 134 | utils.show_toast_notification("服务器重启", "正在重启服务器...") 135 | self.server.restart() 136 | except Exception as e: 137 | logging.error(f"重启服务器失败: {e}") 138 | utils.show_toast_notification("重启失败", f"错误: {str(e)}") 139 | 140 | def stop_server(self, icon, item): 141 | """停止服务器""" 142 | if not self.server: 143 | utils.show_toast_notification("错误", "服务器实例不可用") 144 | return 145 | 146 | try: 147 | utils.show_toast_notification("服务器停止", "正在停止服务器...") 148 | self.server.stop() 149 | except Exception as e: 150 | logging.error(f"停止服务器失败: {e}") 151 | utils.show_toast_notification("停止失败", f"错误: {str(e)}") 152 | 153 | def show_about(self, icon, item): 154 | """显示关于信息""" 155 | try: 156 | # 使用多个通知来显示完整的关于信息 157 | utils.show_toast_notification( 158 | "SmsCodeServer", 159 | "短信验证码转发服务器\n版本: 2.0.0" 160 | ) 161 | 162 | # 延迟显示功能说明 163 | import threading 164 | def show_features(): 165 | time.sleep(1) 166 | utils.show_toast_notification( 167 | "功能特性", 168 | "• 自动提取短信验证码\n• 复制到剪贴板\n• 来电提醒\n• 系统托盘支持" 169 | ) 170 | 171 | threading.Thread(target=show_features, daemon=True).start() 172 | 173 | # 延迟显示使用说明 174 | def show_usage(): 175 | time.sleep(2) 176 | utils.show_toast_notification( 177 | "使用说明", 178 | "• 关闭控制台窗口程序将继续在后台运行\n• 只有通过托盘菜单的'退出程序'才能完全退出" 179 | ) 180 | 181 | threading.Thread(target=show_usage, daemon=True).start() 182 | 183 | except Exception as e: 184 | logging.error(f"显示关于信息失败: {e}") 185 | # 备用方案:显示简单的关于信息 186 | utils.show_toast_notification("关于", "SmsCodeServer v1.0.0") 187 | 188 | def quit_application(self, icon, item): 189 | """退出应用程序""" 190 | utils.show_toast_notification("退出", "正在关闭服务器...") 191 | self.stop() 192 | if self.server: 193 | # 调用服务器的强制退出方法 194 | self.server.force_quit() 195 | if self.icon: 196 | self.icon.stop() 197 | 198 | def update_menu(self): 199 | """更新菜单""" 200 | if self.icon: 201 | try: 202 | self.icon.menu = self.create_menu() 203 | except Exception as e: 204 | logging.error(f"更新菜单失败: {e}") 205 | 206 | def update_icon(self, status="running"): 207 | """更新图标""" 208 | if self.icon: 209 | try: 210 | new_image = self.create_icon_image(status=status) 211 | self.icon.icon = new_image 212 | except Exception as e: 213 | logging.error(f"更新图标失败: {e}") 214 | 215 | def start(self): 216 | """启动托盘图标""" 217 | if not TRAY_AVAILABLE: 218 | logging.warning("系统托盘功能不可用") 219 | return False 220 | 221 | # 如果已经运行,先停止 222 | if self.is_running and self.icon: 223 | self.stop() 224 | 225 | try: 226 | # 创建图标 227 | icon_image = self.create_icon_image() 228 | menu = self.create_menu() 229 | 230 | # 创建托盘图标 231 | self.icon = pystray.Icon("sms_server", icon_image, "SmsCodeServer", menu) 232 | self.is_running = True 233 | 234 | # 在新线程中运行托盘图标,设置为非守护线程 235 | tray_thread = threading.Thread(target=self.icon.run, daemon=False) 236 | tray_thread.start() 237 | 238 | logging.info("系统托盘图标已启动") 239 | return True 240 | 241 | except Exception as e: 242 | logging.error(f"启动系统托盘失败: {e}") 243 | return False 244 | 245 | def stop(self): 246 | """停止托盘图标""" 247 | self.is_running = False 248 | if self.icon: 249 | try: 250 | self.icon.stop() 251 | # 等待图标完全停止 252 | time.sleep(0.2) 253 | except: 254 | pass 255 | finally: 256 | # 清空图标引用 257 | self.icon = None 258 | 259 | def is_available(self): 260 | """检查系统托盘是否可用""" 261 | return TRAY_AVAILABLE 262 | 263 | 264 | # 便捷函数 265 | def create_tray_manager(server_instance=None) -> Optional[TrayManager]: 266 | """创建托盘管理器""" 267 | if not TRAY_AVAILABLE: 268 | return None 269 | 270 | return TrayManager(server_instance) 271 | 272 | 273 | def show_tray_notification(title: str, message: str, duration: str = "long"): 274 | """显示托盘通知""" 275 | try: 276 | utils.show_toast_notification(title, message) 277 | except Exception as e: 278 | logging.error(f"显示托盘通知失败: {e}") 279 | 280 | 281 | if __name__ == "__main__": 282 | # 测试托盘功能 283 | if TRAY_AVAILABLE: 284 | tray = create_tray_manager() 285 | if tray: 286 | tray.start() 287 | print("托盘图标已启动,按 Ctrl+C 退出") 288 | try: 289 | while True: 290 | time.sleep(1) 291 | except KeyboardInterrupt: 292 | tray.stop() 293 | print("托盘图标已停止") 294 | else: 295 | print("系统托盘功能不可用") -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import os 4 | import pyperclip 5 | import logging 6 | import time 7 | from typing import List, Dict, Any, Optional 8 | 9 | # 导入改进的通知管理器 10 | try: 11 | from notification_manager import show_toast_notification, show_success_notification, show_error_notification 12 | 13 | NOTIFICATION_AVAILABLE = True 14 | except ImportError: 15 | # 如果导入失败,使用原始的通知函数 16 | from winotify import Notification, audio 17 | 18 | NOTIFICATION_AVAILABLE = False 19 | 20 | # 配置日志记录 21 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 22 | 23 | # 全局变量存储最后复制的验证码 24 | _last_copied = None 25 | 26 | 27 | def extract_first_long_number(text): 28 | # 匹配长度大于等于4的数字字符串 29 | pattern = r'\d{4,}' 30 | match = re.search(pattern, text) 31 | if match: 32 | return match.group(0) 33 | return None 34 | 35 | 36 | def get_icon_path(): 37 | # When running from a packaged .exe 38 | if getattr(sys, 'frozen', False): 39 | return os.path.join(sys._MEIPASS, 'favicon.ico') 40 | else: 41 | return os.path.join(os.path.dirname(__file__), 'favicon.ico') 42 | 43 | 44 | def show_toast_notification(title, message): 45 | """显示通知(兼容性函数)""" 46 | if NOTIFICATION_AVAILABLE: 47 | # 使用改进的通知管理器 48 | from notification_manager import show_toast_notification as show_notification 49 | return show_notification(title, message, "info") 50 | else: 51 | # 使用原始的通知函数 52 | try: 53 | # 获取图标路径 54 | icon_path = get_icon_path() 55 | if not os.path.exists(icon_path): 56 | icon_path = "" # 如果图标不存在则使用默认图标 57 | 58 | # 创建通知 59 | toast = Notification( 60 | app_id="SmsCodeServer", # 应用标识名称 61 | title=title, 62 | msg=message, 63 | icon=icon_path, 64 | duration="long" # short约为4.5秒,long约为9秒 65 | ) 66 | 67 | # 设置通知声音 68 | toast.set_audio(audio.Default, loop=False) 69 | 70 | # 显示通知 71 | toast.show() 72 | return True 73 | 74 | except Exception as e: 75 | logging.error(f"通知显示失败: {str(e)}") 76 | return False 77 | 78 | 79 | def caller_handler(text): 80 | # 显示通知 81 | show_toast_notification( 82 | f"联系电话: {text} 来了", 83 | f"原文: {text}" 84 | ) 85 | 86 | 87 | def show_handler(text): 88 | # 显示通知 89 | show_toast_notification( 90 | f"其他消息", 91 | f"原文: {text}" 92 | ) 93 | 94 | 95 | def copy_verification_code(text): 96 | global _last_copied 97 | 98 | number = extract_first_long_number(text) 99 | if number: 100 | # 复制到剪贴板 101 | try: 102 | pyperclip.copy(number) 103 | _last_copied = number 104 | 105 | logging.info(f"已复制到剪贴板: {number}") 106 | except Exception as e: 107 | logging.error(f"复制到剪贴板失败: {str(e)}") 108 | if NOTIFICATION_AVAILABLE: 109 | show_error_notification("复制失败", f"错误: {str(e)}") 110 | else: 111 | show_toast_notification("复制失败", f"错误: {str(e)}") 112 | return None 113 | 114 | # 处理文本,确保索引访问安全 115 | display_text = text 116 | if text.startswith('{') and text.endswith('}'): 117 | display_text = text[1:-1] 118 | 119 | # 显示通知 120 | if NOTIFICATION_AVAILABLE: 121 | show_success_notification( 122 | f"验证码: {number} 复制成功", 123 | f"短信原文: {display_text}" 124 | ) 125 | else: 126 | show_toast_notification( 127 | f"验证码: {number} 复制成功", 128 | f"短信原文: {display_text}" 129 | ) 130 | return number 131 | else: 132 | logging.warning("未找到符合条件的数字字符串") 133 | if NOTIFICATION_AVAILABLE: 134 | show_error_notification("复制失败", "请检查短信验证码") 135 | else: 136 | show_toast_notification("复制失败", "请检查短信验证码") 137 | return None 138 | 139 | 140 | def get_last_copied_code() -> Optional[str]: 141 | """获取最后复制的验证码""" 142 | return _last_copied 143 | 144 | 145 | if __name__ == "__main__": 146 | test_text = "{这是一段包含验证码737363的测试文本}" 147 | copy_verification_code(test_text) 148 | --------------------------------------------------------------------------------