├── README.assets ├── image-20240812230315063.png ├── image-20240812233032541.png ├── image-20240812233253703.png ├── img.png ├── img2.png ├── img3.png └── img4.png ├── README.md ├── SmsForwarder.json ├── config.json ├── favicon.ico ├── main.py ├── requirements.txt └── utils.py /README.assets/image-20240812230315063.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SMSOTPServer/dfa21f236f2551a18ce0ec73bc3442d5fce55db8/README.assets/image-20240812230315063.png -------------------------------------------------------------------------------- /README.assets/image-20240812233032541.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SMSOTPServer/dfa21f236f2551a18ce0ec73bc3442d5fce55db8/README.assets/image-20240812233032541.png -------------------------------------------------------------------------------- /README.assets/image-20240812233253703.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SMSOTPServer/dfa21f236f2551a18ce0ec73bc3442d5fce55db8/README.assets/image-20240812233253703.png -------------------------------------------------------------------------------- /README.assets/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SMSOTPServer/dfa21f236f2551a18ce0ec73bc3442d5fce55db8/README.assets/img.png -------------------------------------------------------------------------------- /README.assets/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SMSOTPServer/dfa21f236f2551a18ce0ec73bc3442d5fce55db8/README.assets/img2.png -------------------------------------------------------------------------------- /README.assets/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SMSOTPServer/dfa21f236f2551a18ce0ec73bc3442d5fce55db8/README.assets/img3.png -------------------------------------------------------------------------------- /README.assets/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SMSOTPServer/dfa21f236f2551a18ce0ec73bc3442d5fce55db8/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 | 20 | ### 2.SmsForwarder配置导入,通用设置开关要重新关闭打开一次 21 | 将[SmsForwarder.json](SmsForwarder.json)放在 22 | /storage/emulated/0/Download目录下,点击导入开始导入 23 | ![img.png](README.assets/img3.png) 24 | ![img.png](README.assets/img.png) 25 | ![img.png](README.assets/img2.png) 26 | 27 | ### 3.发送通道-修改Socket tcp配置 28 | 29 | 需要手机与电脑在同一局域网下,修改服务端ip为电脑自己的局域网ip 30 | ![img.png](README.assets/img4.png) 31 | 32 | ### 4.下载SMSOTPServer.exe 电脑上点击启动 33 | https://github.com/ddonano/SMSOTPServer/releases 34 | ![image](https://github.com/user-attachments/assets/0be44a1d-ddc7-4812-bb08-182add39778b) 35 | 36 | 37 | ### 5. 找个验证码网页开始测试 38 | 39 | 40 | ## 如何编译打包工程 41 | 42 | **将代码clone到本地部署运行** 43 | 44 | clone项目 45 | 46 | ```bash 47 | git clone git@github.com:ddonano/SMSOTPServer.git 48 | cd SMSOTPServer 49 | ``` 50 | 51 | 安装依赖 52 | 53 | ```bash 54 | pip install -r requirements.txt 55 | ``` 56 | 57 | 运行`main.py`,如果需要修改端口号 加启动参数 -p {port} 58 | 59 | ```bash 60 | python main.py 61 | ``` 62 | 63 | 打包成exe启动 64 | 65 | ```bash 66 | pyinstaller -F --add-data "config.json;." --add-data "favicon.ico;." --icon="favicon.ico" --name="SMSOTPServer" main.py 67 | ``` 68 | 启动,直接点击exe打开即可,或者在cmd命令行里修改端口号启动 69 | ```bash 70 | SMSOTPServer.exe -p 65431 71 | ``` 72 | 加入windows自启动,创建SMSOTPServer.exe 快捷方式,按win+R 输入shell:startup执行, 在打开的文件夹里拖入刚创建的快捷方式即可。 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /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"} -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 65432 3 | } 4 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddonano/SMSOTPServer/dfa21f236f2551a18ce0ec73bc3442d5fce55db8/favicon.ico -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import re 2 | import socket 3 | import json 4 | import os 5 | import sys 6 | import logging 7 | import argparse 8 | import utils 9 | 10 | # 配置日志记录 11 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 12 | 13 | 14 | def receive_message(port): 15 | # 创建socket对象 16 | server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 | 18 | # 允许地址重用 19 | server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 20 | 21 | try: 22 | server_socket.bind(('0.0.0.0', port)) 23 | server_socket.listen(5) 24 | logging.info(f"Server is listening on port {port}...") 25 | except Exception as e: 26 | logging.error(f"Failed to bind or listen on port {port}: {e}") 27 | return 28 | 29 | while True: 30 | try: 31 | client_socket, client_address = server_socket.accept() 32 | logging.info(f"Connection from {client_address}") 33 | 34 | data = client_socket.recv(1024) 35 | if not data: 36 | logging.warning("No data received, closing connection.") 37 | client_socket.close() 38 | continue 39 | 40 | text = data.decode('utf-8') 41 | logging.info(f"Received data: {text}") 42 | # 如果数据是以大括号包围的,去掉首尾的大括号 43 | text = text.strip() 44 | if text.startswith('{') and text.endswith('}'): 45 | text = text[1:-1] 46 | logging.info(f"text data: {text}") 47 | # 正则表达式匹配 . 前面的部分 48 | match = split_string_at_first_dot(text) 49 | 50 | if match: 51 | prefix, suffix = match # 提取 . 前的部分 52 | if prefix == 'CALL': 53 | # 处理 CALL 的情况 54 | utils.caller_handler(suffix) 55 | elif prefix == 'SMS': 56 | # 处理 SMS 的情况 57 | # 调用外部函数来处理验证码 58 | utils.copy_verification_code(suffix) 59 | else: 60 | logging.warning(f"处理其他类型: {prefix}") 61 | else: 62 | logging.error("无法匹配到预期格式") 63 | 64 | client_socket.close() 65 | except Exception as e: 66 | logging.error(f"Error handling client connection: {e}") 67 | continue 68 | 69 | 70 | def split_string_at_first_dot(text): 71 | """ 72 | 分割字符串,以第一个"."为界限。 73 | 74 | Args: 75 | text: 输入字符串。 76 | 77 | Returns: 78 | 一个包含两个子字符串的元组,或者 None 如果字符串中没有"."。 79 | """ 80 | if "." not in text: 81 | return None # 或者抛出异常,取决于你的需求 82 | 83 | index = text.find(".") 84 | before_dot = text[:index] 85 | after_dot = text[index + 1:] 86 | return before_dot, after_dot 87 | 88 | 89 | def get_config_path(): 90 | # When running from a packaged .exe 91 | if getattr(sys, 'frozen', False): 92 | return os.path.join(sys._MEIPASS, 'config.json') # Path to the extracted config.json 93 | else: 94 | return os.path.join(os.path.dirname(__file__), 'config.json') # Normal script execution 95 | 96 | 97 | def parse_args(): 98 | # 解析命令行参数 99 | parser = argparse.ArgumentParser(description="Start the server and listen on a specified port.") 100 | parser.add_argument('-p', '--port', type=int, help="Port to listen on", default=None) 101 | return parser.parse_args() 102 | 103 | 104 | def main(): 105 | # 解析命令行参数 106 | args = parse_args() 107 | 108 | # 如果命令行参数中有端口,优先使用命令行端口 109 | if args.port: 110 | port = args.port 111 | logging.info(f"Using port {port} from command line argument.") 112 | else: 113 | # 如果没有传递端口参数,读取配置文件中的端口 114 | try: 115 | config_path = get_config_path() 116 | with open(config_path, 'r') as file: 117 | config = json.load(file) 118 | 119 | port = config['port'] 120 | logging.info(f"Using port {port} from config file.") 121 | except FileNotFoundError: 122 | logging.error("Configuration file 'config.json' not found.") 123 | return 124 | except json.JSONDecodeError: 125 | logging.error("Error decoding the configuration file.") 126 | return 127 | except KeyError: 128 | logging.error("Missing 'port' key in the configuration file.") 129 | return 130 | except Exception as e: 131 | logging.error(f"An unexpected error occurred: {e}") 132 | return 133 | 134 | receive_message(port) 135 | 136 | 137 | if __name__ == "__main__": 138 | main() 139 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | winotify 2 | pyperclip 3 | pyinstaller -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import os 4 | import pyperclip 5 | from winotify import Notification, audio 6 | import logging 7 | 8 | # 配置日志记录 9 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 10 | 11 | 12 | def extract_first_long_number(text): 13 | # 匹配长度大于等于4的数字字符串 14 | pattern = r'\d{4,}' 15 | match = re.search(pattern, text) 16 | if match: 17 | return match.group(0) 18 | return None 19 | 20 | 21 | def get_icon_path(): 22 | # When running from a packaged .exe 23 | if getattr(sys, 'frozen', False): 24 | return os.path.join(sys._MEIPASS, 'favicon.ico') 25 | else: 26 | return os.path.join(os.path.dirname(__file__), 'favicon.ico') 27 | 28 | 29 | def show_toast_notification(title, message): 30 | try: 31 | # 获取图标路径 32 | icon_path = get_icon_path() 33 | if not os.path.exists(icon_path): 34 | icon_path = "" # 如果图标不存在则使用默认图标 35 | 36 | # 创建通知 37 | toast = Notification( 38 | app_id="SMSOTPServer", # 应用标识名称 39 | title=title, 40 | msg=message, 41 | icon=icon_path, 42 | duration="long" # short约为4.5秒,long约为9秒 43 | ) 44 | 45 | # 设置通知声音 46 | toast.set_audio(audio.Default, loop=False) 47 | 48 | # 显示通知 49 | toast.show() 50 | 51 | except Exception as e: 52 | logging.error(f"通知显示失败: {str(e)}") 53 | 54 | 55 | def caller_handler(text): 56 | # 显示通知 57 | show_toast_notification( 58 | f"联系电话: {text} 来了", 59 | f"原文: {text}" 60 | ) 61 | 62 | 63 | def copy_verification_code(text): 64 | number = extract_first_long_number(text) 65 | if number: 66 | # 复制到剪贴板 67 | pyperclip.copy(number) 68 | logging.info(f"已复制到剪贴板: {number}") 69 | 70 | # 处理文本,确保索引访问安全 71 | display_text = text 72 | if text.startswith('{') and text.endswith('}'): 73 | display_text = text[1:-1] 74 | 75 | # 显示通知 76 | show_toast_notification( 77 | f"验证码: {number} 复制成功", 78 | f"短信原文: {display_text}" 79 | ) 80 | return number 81 | else: 82 | logging.warning("未找到符合条件的数字字符串") 83 | show_toast_notification("复制失败", "请检查短信验证码") 84 | return None 85 | 86 | 87 | if __name__ == "__main__": 88 | test_text = "{这是一段包含验证码737363的测试文本}" 89 | copy_verification_code(test_text) 90 | --------------------------------------------------------------------------------