通过USB扩展坞代替KVM切换显示器输入源
前言 入了台新电脑，揪心事情就来了。两台显示器、一套键鼠怎么在都开机的情况共用两台主机呢？而且还有来回切换的场景。 上网查，发现有东西叫KVM切换器，原理就是两台主机显示接口接入KVM的显示输入接口，USB输入接口，然后KVM在出两根视频线给显示器。切换的时候应把A主机视频信号切断，通过硬件让显示器接入B主机输出的视频信号。线多点无所谓，切换过程耗时也能接受，继续查各网友使用反馈。得到的结论就是国内这东西无异于工业垃圾... 二进二出KVM对高帧率支持有限，EDID（在Windows下切换屏幕后系统桌面程序窗口还是之前一样，没有EDID切回就乱了，mac就算拔掉一个显示器，插回去还会恢复...）寥寥无几，更别提各种黑屏闪屏乱七八糟的问题，倒是价格三五百也还好。反正网上没有说非常满意的，最多也就是个能用。 灵机一动，我买个USB切换器，写个程序监测鼠标键盘插入拔出，通过DDC/CI直接切换显示器输入源不久OJBK了吗？买个KVM我实在都没真心满意的。两台4K显示器，LG的144HZ，另一台60HZ。要放弃144HZ选择还多点，不然没办法。而且支持EDID只有一款双HDMI的支持，其他不支持。 搞起 显示器必须支持DDC/CI，不支持都不用搞了。大厂应该都支持，小作坊就不保证了。Dell、LG两个在查资料过程还是看到不少关于这两显示器问题的。下个control_my_monitor打开看看吧。 我本来想用go来写的，结果go在winodws监听USB输入输出事件没有很简单的方法，不成熟。不过找了这么多资料总有点结果，nodejs有usb包支持usb热插拔事件。既然能支持，还管它什么语言，能解决问题就是好语言。 当USB切换器切换当前主机的键盘和鼠标USB会拔出，反之另一台会插入。基本逻辑就是插入鼠标键盘，DDC/CI切换显示器输入源为当前主机视频接口，两台主机一边一套，这样理论上支持无限台主机数量。其实只要有切换器，不一定要鼠标或者键盘，随便插个什么USB都行。 LG显示器DDC/CI切换输入源比较麻烦，可以看我上一篇文章解决LG显示器无法通过DDC/CL切换输入源，其他显示器可以看看输入源端口代码。 windows这边好弄，但是mac有点麻烦。因为macBetterDisplay只能登陆后启动，不能开机直接启动（可能有办法解决，没深入研究）。然后通过USB扩展坞扩展的视频接口是个残缺DDC/CI接，没法切换输入源，调节他亮度声音倒没什么问题。难道又买一根type-C to HDMI的线？另一台直连HDMI接口的显示没有问题。 干脆就到windows上跑nodejs好了，反正只有两台主机，插入就切windows主机，拔出切回去。 我用到了winddcutil（control_my_monitor也可以，无非就是换下命令）。nodejs需要安装usb包。 直接上代码： const usb = require('usb').usb; const { exec } = require('child_process'); const os = require('os'); // const fs = require('fs'); // const path = require('path'); const platform = os.platform(); // const LOG_FILE = path.join(__dirname, 'usb_switch_monitor.log'); const DEVICE_VENDOR_ID = 1507; const DEVICE_PRODUCT_ID = 1552; const WINDOWS_DIR = 'D:\\xxx\\xxx\\'; // console.log('USB hot plug switch monitor service started...'); // console.log(usb.getDeviceList()); // 写入日志函数 // function writeLog(message) { // const timestamp = new Date().toISOString(); // const logMessage = `[${timestamp}] ${message}\n`; // fs.appendFile(LOG_FILE, logMessage, (err) => { // if (err) { // console.error(`无法写入日志: ${err}`); // } // }); // } //执行命令函数 async function executeCommand(command) { exec(command, (error, stdout, stderr) => { if (error) { console.error(`执行错误: ${error}`); // writeLog(`执行错误: ${error}`); // return; } console.log(`输出: ${stdout}`); // writeLog(`执行命令: ${command}`); // writeLog(`输出: ${stdout}`); }); } // Listen for attach event usb.on('attach', (device) => { // console.log('USB attached:', device); console.log('attach Vendor ID:', device.deviceDescriptor.idVendor); console.log('attach Product ID:', device.deviceDescriptor.idProduct); if (device.deviceDescriptor.idVendor === DEVICE_VENDOR_ID && device.deviceDescriptor.idProduct === DEVICE_PRODUCT_ID) { console.log('检测到设备插入'); // writeLog('检测到设备插入'); if (platform === 'win32') { // 模拟鼠标移动唤醒屏幕 executeCommand('cmd /c '+ WINDOWS_DIR + 'nircmd-x64\\nircmd.exe sendmouse move 10 10'); // writeLog('移动鼠标唤醒屏幕'); //切换LG显示器 DP端口 executeCommand('cmd /c '+ WINDOWS_DIR +'amdddc-windows.exe --i2c-source-addr 0x50 setvcp 5 0 0xD0'); // writeLog('切换LG屏幕已执行'); // 查看所有显示器 // executeCommand('cmd /c '+ WINDOWS_DIR +'winddcutil.exe detect'); executeCommand('cmd /c '+ WINDOWS_DIR +'winddcutil.exe setvcp 1 0x60 15'); // writeLog('切换屏幕已执行'); } // else if (platform === 'darwin') { // // macOS 的唤醒命令 // executeCommand('caffeinate -u -t 1'); // //切换HDMI-2 // executeCommand('ddcutil -d 1 setvcp xF4 x0091 --i2c-source-addr=x50 --noverify'); // //切换HDMI-1 // executeCommand('ddcutil -d 2 setvcp 60 0x11'); // // macOS 的熄屏命令 // // executeCommand('pmset displaysleepnow'); // } else { console.log('不支持的操作系统'); } } }); // Listen for detach event usb.on('detach', (device) => { // console.log('USB detached:', device); console.log('detach Vendor ID:', device.deviceDescriptor.idVendor); console.log('detach Product ID:', device.deviceDescriptor.idProduct); if (device.deviceDescriptor.idVendor === DEVICE_VENDOR_ID && device.deviceDescriptor.idProduct === DEVICE_PRODUCT_ID) { console.log('检测到设备拔出'); // writeLog('检测到设备插入'); if (platform === 'win32') { // writeLog('移动鼠标唤醒屏幕'); //切换DP executeCommand('cmd /c '+ WINDOWS_DIR +'amdddc-windows.exe --i2c-source-addr 0x50 setvcp 5 0 0x91'); // writeLog('切换LG屏幕已执行'); // executeCommand('cmd /c '+ WINDOWS_DIR +'winddcutil.exe detect'); executeCommand('cmd /c '+ WINDOWS_DIR +'winddcutil.exe setvcp 1 0x60 17'); // writeLog('切换屏幕已执行'); } // else if (platform === 'darwin') { // // macOS 的唤醒命令 // executeCommand('caffeinate -u -t 1'); // //切换HDMI-2 // executeCommand('ddcutil -d 1 setvcp xF4 x0091 --i2c-source-addr=x50 --noverify'); // //切换HDMI-1 // executeCommand('ddcutil -d 2 setvcp 60 0x11'); // // macOS 的熄屏命令 // // executeCommand('pmset displaysleepnow'); // } else { console.log('不支持的操作系统'); } } }); 开机启动 接下来就是开机启动脚本了。windows自定义开机启动还是比较方便的，有很多种方法。注册服务、任务调度、放到启动目录，很可惜都不行。前两个开机启动后后台运行，没有黑窗口，但是检测不到显示器了...放到启动目录只能开机登陆后启动，万一在另一台主机上，这台才开机切回来屏幕还是上一台主机的。 新建个文本文档： @echo off start /min cmd /k "node D:\xx\xx\脚本名字.js" 改名为xxx.bat 找到了个改注册表开机启动的方法（原文）： 用户登陆时运行: 计算机\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run 不管用户是否登录都要运行: 计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run 两者配置方式相同，只是路径不同，以下是示例： 右键创建 "字符串值" 数值名称 随意 数值数据 指定文件路径（把bat路径填进去） 完美解决，就是会一直有一个黑窗口。不过也好，不用单独也日志，出现问题方便查看。 结语 终于都实现了，不用KVM，也不用手动切换显示器。总算完美了，切换过程丝和KVM一样会黑屏，全靠显示器切换速度。比KVM少两跟线，没有奇奇怪怪的显示问题，USB切换器也比KVM便宜，也支持高分辨率。windows屏幕没有断开，不需要EDID维持原窗口，随便怎么切都是原来的样子。缺点就是只支持两台主机，如果没有mac，那完全可以一台主机跑一套，只监听usb插入事件，支持<=USB扩展坞支持数量。 研究了好几天，中途都想放弃了，LG显示器太磨人了。不过好在还是找到了解决方法，简直大海捞针...
前言 LG虽然是大厂，但是显示器DDC/CL却无法设置输入源。声音亮度等设置却没有问题，这是我目前最想要的功能。我有两个系统，共用两个显示器，不想买二进二出KVM切换器。国内这些KVM也就是个能用的程度。高刷和EDID几乎没有，就算有也不是所有接口都支持。 所以我有个想法，让系统来通过显示器DDC/CI控制切换，我再买个USB二进二出切换器，共用键盘和鼠标。写个程序监听USB设备，插入就通过电脑执行DDC/CI切换显示器输入源。 这样比KVM二进二出切换器多了很多优点： 少了连根视频线 显示器和主机直连兼容性更好 USB切换器便宜 支持高刷等显示器特性（EDID的话我目前切换没有乱桌面布局，我不确定） 我的两台显示器，一台是AOC，好多年了，DDC/CI没有任何问题。还有台LG 27GP95RP，这两年入的，DDC/CL能控制大部分功能，唯独输入源控制不了。国内LG DDC/CI相关讨论都很少，国外还是有讨论关于LG显示器DDC/CI的问题。 开搞 DDC/CI不支持，国外有大佬通过GPU来控制显示器切换。我不太懂，你可以看看这篇文章。虽然里面只列出少部分LG显示器型号，但是自己试试说不定也支持，27GP95RP上面就没有列出，但是确实可行。 先上输入源代码，其实很好区分，你先确定你的LG显示器有什么接口，例如两个HDMI和一个DP，HDMI就是0x90开始递增，DP接口就是0xD0递增，至于type-c就要自己找找了，应该是通过0xD0来递增，我这台显示器没有。 Input Source Value MONITOR_AUTO 0x0 MONITOR_DP1 0xD0 MONITOR_DP2 0xD1 MONITOR_DP3 0xD2 MONITOR_USB_C 0xD2 MONITOR_HDMI1 0x90 MONITOR_HDMI2 0x91 英伟达显卡可以通过NVapi-write-value-to-monitor项目来执行输入源切换。 你只需要修改两个参数，第一个参数0来选择显示器的序号，第二个参数是显示器的输入源（这里是切换到HDMI1，你要修改成自己想切换的输入源，看上面代码）。后面的0xF4 0x50不能动。我的是AMD显卡，具体显示器序号和他功能可以去项目里看作者的说明。 writeValueToDisplay.exe 0 0x90 0xF4 0x50 ADM显示可以通过amdddc-windows项目来执行输入源切换。可以执行文件在issue中找到，作者上传的txt文件，下载下来修改后缀exe即可。 先通过detect命令来查看显示器位置： amdddc-windows.exe detect 我这里适配器索引5，LG显示器是0，要切换的端口DP1 0xD0。你只用修改最后三个参数。下面通过命令来切换输入源到DP1接口： amdddc-windows.exe --i2c-source-addr 0x50 setvcp 5 0 0xD0 intel可以看看这个issue MAC OS mac os可以使用BetterDisplay或者m1ddc来控制，同一个作者。m1ddc我在M4 pro上使用过了没用，不管是不是LG显示器都无法切换，没有做过多研究。 BetterDisplay没有问题，但是发现了一个坑。如果使用扩展坞带的视频接口，那么大概率是残血DDC/CI，或者干脆不支持的。网上资料说是由于usb占用了两个引脚，导致功能残缺。我这边是发现不能切换，但可以调亮度和声音，这还是另一台支持DDC/CI切换输入源的显示上测试的。所以要转视频接口建议买质量好的type-C直接转HDMP或者DP，不要附带其他扩展接口。LG用HDMI直连所有功能都正常。 BetterDisplayLG显示器输入源：DisplayPort 1: 208, DisplayPort 2: 209, HDMI 1: 144, HDMI 2: 145, USB-C / DP 3: 210。CLI文档可以看文档。 显示器设备名称 UUID等信息去APP中可以看到，命令行的话看文档找找。 除了namelike，还有name uuid都可以指定显示器，ddcAlt就是输入源端口代码了。非LG显示器切换去掉两个参数Alt字母就好了--ddc=15 --vcp=inputSelect。 /Applications/BetterDisplay.app/Contents/MacOS/BetterDisplay set --namelike=ABC --ddcAlt=208 --vcp=inputSelectAlt 如果安装了betterdisplaycli可以简化命令，但是BetterDisplay不仅要装，还要启动，否则不行： betterdisplaycli set --namelike=ABC --ddcAlt=0x91 --vcp=inputSelectAlt Linux 我就是ddcutil的issues中找到windows切换LG输入源的，没有Linux需求，没有尝试，命令行参数对着上面的改。 -d就是显示器顺序号，x0090就是输入源端口代码，其他不动。 ddcutil -d 1 setvcp xF4 x0090 --i2c-source-addr=x50 --noverify 顺便把DDL/CI代码也贴一下好了： Input Source Value VGA-1 1 VGA-2 2 DVI-1 3 DVI-2 4 Composite video 1 5 Composite video 2 6 S-Video-1 7 S-Video-2 8 Tuner-1 9 Tuner-2 10 Tuner-3 11 Component video (YPrPb/YCrCb) 1 12 Component video (YPrPb/YCrCb) 2 13 Component video (YPrPb/YCrCb) 3 14 DisplayPort-1 15 DisplayPort-2 16 HDMI-1 17 HDMI-2 18 USB-C 27 DDC/CL切换用winddcutil或者ControlMyMonitor都行。 winddcutil查看显示2的60 输入源可能值，ControlMyMonitor有时候不准。 .\winddcutil.exe capabilities 2 (prot(monitor)type(LCD)model(U2790PQU)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 08 0C 10 12 14(01 05 06 08 0B) 16 18 1A 52 60(0F 11 12 ) 86(01 02 05 0B 0C 0D 0E 0F 10 11 12 13 14) AC AE B2 B6 C6 C8 CA CC(01 02 03 04 05 06 07 09 0A 0B 0D 0E 12 14 16 1E) D6(01 04 05) DC(00 0B 0C 0D 0E 0F 10) DF ED FF)mswhql(1)asset_eep(40)mccs_ver(2.2)) 60(0F 11 12 ) 代表十进制15 17 18,对应DP1、HDMI1、HDMI2 DDC/CI切换就要简单些，setvcp和/SetValue后面分别是 显示器、输入源切换代码不变、显示器输入源端口，可以看上面DDL/CI代码来尝试切换。 .\ControlMyMonitor.exe /SetValue "\\.\DISPLAY2\Monitor0" 60 17 或 .\winddcutil.exe setvcp 1 0x60 17 结语 好了，差不多就这么多，看起来不复杂，但是找资料确实花了我好几个小时。 下面我就要开始写代码通过USB切换器来同时控制显示器切换了。目前已经在windows上成功，另一个系统还在路上。等都到齐了我再来多测测，到时候再分享出来! 2025-03-04 切换搞定了，效果不错，通过USB扩展坞代替KVM切换显示器输入源
探索ChatGPT、NewBing和Google Bard：我与大型语言模型的初体验
前言 最近ChatGPT很火爆，不过我还是近几天才知道，平时也没怎么看新闻，有点孤陋寡闻了！既然都发布了那我也来试试看人工智能到底有多智能！这不，一口气体验三款AI，chatGP、Bing AI，还有Google Bard。 一个ChatGPT出来，后面就会有千千万万个ChatGPT如脱缰野马般涌现，那么那些跟趋势蹭流量的ChatGPT到底是人工智能还是"人工智障"？ 注册ChatGPT 刚接触到ChatGPT那就不得不提注册了。虽然注册也没什么难点，但是还是有限制，例如你的IP、手机号码等。当然伟大的国人总是能想到各种方法来解决问题。 IP问题科学上网就行了，我还是推荐多使用欧美地区节点，香港都不行，和大陆一视同仁了，唉~~~！ 注册也没什么难点，主要是短信验证，对号码有要求，国内、香港的反正是不行的，只能用其他国家。电话验证接码我使用的是sms-activate 【链接包含我的Ref,介意的话不要点击】，当然有很多选择，这个倒是无所谓。openai好像也只是用来验证你是真人，也没看见那里能设置或修改电话，目前来说应该是没有什么其他用途。 sms-activate必须要充值后才能使用，充值支持支付宝、银联卡等。 点击右上角充值： 选择支付宝即可，当然也可以用银联卡，什么方便用什么： 充值完了就可以选号码开始注册了，在右边搜索openai： 选择印度尼西亚（你可以试试其他的，但是不保证能收到验证码，没收到码一般也不会扣费），一般在openai验证手机号时等个3-5分钟，如果还没有接收到验证码，就可以X掉再换一个号码了，没收到验证码是会把扣费返还的： 他们甚至都为其出了官方教程：sms-activate注册openAI。 注册openai后就可以正常使用了，chatGPT-web版是免费的。听说是有次数限制的，这两天使用还是没有触碰到上限。 体验ChatGPT chatGPT-web版在对话中经常出现错误的情况，之前我以为是访问人数太多导致，这两天换了一个相对稳定的翻墙节点后稳定许多，否则会一直出错，需要刷新网页才能恢复，非常难受。 ChatGPT自然语言处理非常出色，精通多国语言，非常流利。最让人惊讶的是上下文关联，每次对话都会结合本轮对话上下文做出回答。 打个比方，你先问它"乔布斯是谁"，再问它"他对苹果公司有什么贡献"，这里的【他】是指我上一次问题里面的"乔布斯"。别小看了上下文关联以为很简单，其实对于提取上下文里面与本次对话有关联的关键词是非常困难的。这就类似于人类的记忆功能，你和刚会说话的小朋友对话时，他就可能经常出现"丢失上下文关联"，不理解你说的是指什么。 它甚至都能写论文、诗词、程序，在编程开发中，遇到BUG、异常它都能给出可能涉及的问题。这可比在搜索引擎里面一个个翻找答案的效率可快多了。何况中文垃圾站真是一片一片的，大大降低效率。提供给它的任何日志，让它总结分析，它都说得头头是道，有理有据。这确实在某些情况下对我有很大的帮助。 目前网上也出现了很多Chrome浏览器的ChatGPT扩展，总体来说体验还是不够好。 一些基于web获取chatGPT-web版的Cookie和Token，在chatGPT-web版中对话都经常断线要刷新网页才能继续，放到扩展中也一样断线，体验非常不好。 还有一种基于OpenAI-API，这种体验非常好，但是OpenAI-API收费。目前注册ChatGPT账号有5美金的体验金，而OpenAI-API的收费是按照字数来收取的，称之为Token，以下是价格： 关于token消耗，以下引用ChatGPT3.5回答： 在 OpenAI API 中，每个模型的 Token 数量限制是不同的，因此“1,000 tokens 约等于 750 个单词”这个说法的具体计算方式也可能因模型而异。一般来说，这种说法是根据一个平均单词长度为五个字符的假设得出的。在这种情况下，一个包含 1,000 个 Token 的输入文本大约包含 200 个单词。而根据英语写作的经验，每个单词的平均长度约为 4-6 个字符，因此约 200 个单词的文本约有 750 到 1,200 个字符，这就是通常将 1,000 个 Token 与 750 个单词联系起来的原因。但需要注意的是，这只是一个大致的估计，并不适用于所有情况。 这个token是双向计费，你提问的字数和AI回答的字数都是会消耗掉token的（包括标点符号等一系列其他符号）。虽然GPT3.5 1000Token只需要0.002美金，但是1000Token消耗非常快，特别是中文字符。如果使用上下文关联，需要用到会话ID，那本次对话所关联的会话也会再算一次token，你想想这个消耗有多大。个人使用都能感觉到肉疼，但是公开使用那就更不用说了。你看现在网上大把的免翻墙免费用ChatGPT的那些，如果真是ChatGPT API搭建，然后免费给你用，你觉得现在还有这种"好心人"吗？如果不是ChatGPT，那它是个什么东西相信大家都应该想得到！ 目前我在使用的Chrome浏览器扩展只有一款，chatGPTBox，相对于其他的体验比较好，而且开源。目前划词功能、配合搜索引擎比较实用，其他的我觉得也还好，使用情况不多。有兴趣可以点进去看看。 ChatGPT还有Plus版，可以抢先体验新功能，目前plus版可以使用GPT4： 我目前还没有去体验Plus版，chatGPT-web版使用的比较少，但是我准备使用openAI API，所以弄了张美国虚拟信用卡用来绑定。目前国内推荐的虚拟信用卡大多都是depay和nobepay。免费版在高峰期会提示We're experiencing exceptionally high demand. Please hang tight as we work on scaling our systems.，需要刷新网页更频繁，plus应该会好许多，但是我没用过也不能确定。 起初我想使用depay，无奈连个像样的官网都没有，注册也有BUG，我发送验证码始终没有反应。而且充值有点麻烦。有手续费、月费、不想实名需要10美金卡费。 nobepay注册需要邀请码，这个邀请码自己上网找找，我也是网上找的 （989B08，用不了的话自己上网找找吧），没有还注册不了。 需要实名认证，注册时微信ID那一栏一定要已经实名认证的微信，到时候充值和实名认证都是要用该微信。 注册时最低充值500RMB，支付宝充值有1%手续费，开卡有手续费和最低充值金额（每张卡可能不一样）。充值的RMB，开卡是扣费也要选择RMB账户，汇率偏高，我开卡时汇率时6.8，他那边显示7.0！556766卡段开卡要充值最低30美金，需要扣除手续费、开卡费、汇率转换差不多2.X美金。556766卡段能绑定openai，开卡时地址找找美国免税州地址，网上搜搜美国地址生成器，生成一个即可，信用卡姓名可自定义。 openai使用的是Stripe收款，开卡时需要注意，你的ip最好是信用卡开卡国的，否则大概率被拒绝。如果你使用nobepay 556766卡段，选美国节点去绑定，一般是没有什么问题的。 目前支持卡段（已验证）： OpenAI/ChatGPT Plus 支持的卡段： 591393 （ DePay ） 556766 （ NobePay ） OpenAI/ChatGPT Plus 不支持的信用卡： 国内招行 VISA/MASTERCARD 、全币卡 全球付香港虚拟卡 万里汇香港虚拟卡 绑定成功后一般是从信用卡 PENDING 5美金，然后每月有账单，用多少扣多少！先用后付。 体验NEW BING Bing在众多搜索引擎中一直不怎么起眼，毕竟Google才是大头。虽然Google搜索引擎早已涉及自然语言AI领域，但是却是微软的Bing AI先出来。 微软在早期忽略搜索引擎，自家的IE也是爱用不用的态度，直到Google的出现，无论是搜索引擎还是浏览器，那个曾经有巨大优势的微软反应过来才发现已经被人家甩得太远。而微软近几年的操作一直在布局，从收购Github，组建Azure云服务, 投资OpenAI实验室，直到今天ChatGPT，微软在AI领域已经走在了前面。 Bing AI相对于ChatGPT最大的优势就是拥有互联网能力，目前的Bing AI使用GPT4模型，并且结合自家搜索引擎能更快更好的找到答案。Google虽然借助自然语言处理来完善搜索引擎，但是返回的大量搜索结果中去找答案还是非常耗时的，而Bing AI却已经缩短了从搜索结果中到找到答案之间的时间，这让Google搜索引擎感受到了压力。 在和Bing AI对话时发现，对于已知问题它会直接给你答案，未知问题，它会通过搜索引擎去结果中总结答案。可惜也需要科学上网才能使用。总体体验相对于chatGPT-web版反应稍慢，有可能时因为使用GPT4模型的关系。 Bing AI必须使用自家的Edge浏览器，不过可以通过修改请求User Agent来模拟Edge。Chrome浏览器可以使用ModHeader扩展来修改： Edge浏览器user-agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.57 你甚至可以添加X-Forwarded-For头部然后指定国外IP来免除科学上网，但毕竟不是长久之计。 Bing AI虽然可以联网进行搜索，但是表现起来比ChatGPT要呆，很多时候一直给出重复答案，就算你强调关键字也是如此，甚至它会结束当前对话，脾气还不小。像是给它一份报告，让它分析，他就完全不如ChatGPT那样会给出总结和分析。看来Bing AI可能被屏蔽某些功能。 体验Google Bard 其实，我还是很看好Google的Bard AI，用了这么多年Google搜索引擎，Google搜索引擎的关键词纠错能力还是很亮眼的。我相信很多人在打字输入比较快的的时候，容易出现很多谐音词，当它输入到Google的搜索引擎的时候，你会发现基本上已经被Google纠错，并且在搜索引擎结果上面返回已经纠正的关键词，你是不是要找 xxxxx，红色高亮。ChatGPT和Bing也是有纠错能力的，只是我更想试试Bard。 2023-04-05 我已经获得了Bard AI的体验资格，很遗憾，它目前支持英文... Bard它说它支持关键词纠错，并且善于互联网搜索（我英语一塌糊度，这都是我用Google翻译的...） 你甚至可以将文章链接丢给它，它去帮你分析总结。我先是发生文章地址让它帮我做SEO优化，然后让它分析总结这篇文章。但是我发现它的回答有些问题，我把我这篇文章丢给它分析，它把New Bing说成是Google Ai的新搜索引擎... 当然它的中文还在训练中，这都可以理解。 Bard回复体验不像ChatGPT和Bing两个AI，它是一次返回全部答案，而其他两个是一个个字输出。Bard什么都敢说，什么都敢答，只要你敢问！ 有时候回答能明显比之前回答慢，我估计它是自己在互联网搜索，并且分析搜索结果。Bard虽然自己说会用到各种搜索引擎，但是我认为它多数时候都是使用Google，毕竟是自家的，不可能放着自家世界上最大的搜索引擎不用，去用其他搜索引擎。配合Google搜索引擎简直如虎添翼，这也让我更看好Bard未来的发展。 我立即去问Bard来验证，果不其然它回答确实优先使用Google，但是他这回答总感觉有点王婆卖瓜的感觉... 随即我又问它出现的原因是不是因为ChatGPT，连Google两位创始人都回来了，虽然我也认为它的潜力很大，希望它自己的回答也不是在讲大话！ 就目前来看，它没有像chatGPT-web版一样频繁断线需要刷新网页，体验感稍微好点，但是它的中文还在训练中，不知道还要多久。没有发现禁止某些地区使用，只是体验需要申请。 结语 目前我已经简单体验了ChatGPT、Bing AI和Google的AI Bard。目前人类在人工智能领域已经走出了一大步，相信在人工智能的帮助下，进步会越来越快。openai API价格比较贵，就个人而言勉强能接受，但是如果用它来做公共服务前期投入还是比较大的。ChatGPT和Bing AI可能是两个完全不同的产品，Bing AI明显更偏向解决关于搜索类型问题，虽然基于GPT4，但表现出来连GPT3.5都不如（阿三的锅？）。而Google AI Bard虽然还没发布，但是消息模型功能上感觉比其他两款要强大，看看Bard的后续进展吧！ 我这两天只玩了下消息模型，还有很多图片、语音等模型还没机会，就拿图片模型来说，提取图片中的文字功能就可能绕过目前大部分简单图片验证码。 AI是新鲜事物，能力有限，目前最多也是辅助使用，远没有达到代替的程度。它无论是写代码，还是写论文，都基于大数据。 但这仅仅是目前...
前言 之前写过一篇文章Emby公益服-上万部电影电视剧免费看，如今账号快到期了，需要考核后才能延长账号有效期。正准备去考核时发现不是三天一次考核机会了，而是每日签到获取积分后才能考核，一次考核需要40积分，每日签到获取的积分不固定，好像1-5分都有！考核失败还是需要72小时后才能再次考试！ 这就有点蛋疼了，他们bot签到是需要回答问题验证的，之前好像没有。本想实现自动签到，就用telegram-cli试了一下，发现根本不支持内联键盘消息！！！毕竟好几年前的代码了，作者也没有在维护。那么现在只能找找其他的telegram client api集成包玩玩了！ 皇天不负有心人，找到了一个python包python-telegram，赶紧上手试试看支不支持内联键盘消息！ 开搞 python-telegram命名空间和python-telegram-bot有冲突，如果装了两者没法用。 有两种解决办法，要么卸载掉python-telegram-bot，要么使用virtualenv环境隔离。 python-telegram没办法在windows下使用，还不支持！ 先来将环境隔离,创建一个工作目录，然后在目录下安装隔离venv是隔离环境名称： mkdir python-telegram-client cd python-telegram-client virtualenv venv # 进入隔离环境 source venv/bin/activate # 安装python-telegram pip install python-telegram # 建个文件写代码 nano server.py 退出隔离环境使用: # 退出隔离环境 deactivate 安装python-telegram 在Github看了下作者的例子和文档，随便找了个试一试。tg.get_chats()返回的都是chat_id，找不到哪个chat_id对应的群组/bot。想了个办法，查看每个chat_id最后一条消息，然后自己一个个找： from telegram.client import Telegram import re,time ... tg = Telegram(...) tg.login() result = tg.get_chats() result.wait() if result.error: print(f'get chats error: {result.error_info}') else: print(f'chats: {result.update}') # 查看所有会话的最后一条消息，用来查找对应的chat_id chat_ids = result.update['chat_ids'] for chat_id in chat_ids: chat_history = tg.get_chat_history(chat_id=chat_id,limit=1) chat_history.wait() print(f'chat id:{chat_id},message:{chat_history.update["messages"][0]["content"]}\n') ... 找到了对应的chat_id就可以和签到bot交互了。在发送签到命令之前，还需要监听新消息： tg.add_update_handler('updateNewMessage', send_verification_code) def send_verification_code(update): # 所有的新消息都会被监听，增加判断只监听自己感兴趣的 if 123456789 == update['message']['chat_id']: # print(update) # 提取问题并且计算 question = update['message']['content']['text']['text'] print(question) a = re.findall(r"\s\s(.+?)\+", question, re.M) b = re.findall(r"\+(.+?)=", question, re.M) if a and b: print(a, b) c = int(a[-1].strip()) + int(b[-1].strip()) answers = update['message']['reply_markup']['rows'][0] print(f'{a} + {b} = {c}') # 用答案和内联键盘值做匹配，一旦匹配执行按钮点击效果 for answer in answers: print(f'答案：{answer["text"]}') if int(answer['text']) == c: payload = { '@type': 'callbackQueryPayloadData', 'data': answer['type']['data'], ##每一个内联键盘都带有data数据 } # 发送答案（点击内联键盘） result = tg.call_method(method_name='getCallbackQueryAnswer', params={'chat_id': update['message']['chat_id'], 'message_id': update['message']['id'], 'payload': payload}) result.wait() if result.error: print(f'getCallbackQueryAnswer error: {result.error_info}') else: print(f'getCallbackQueryAnswer: {result.update}') break 以下是签到验证内联键盘消息部分报文： ... 'content': {'@type': 'messageText', 'text': {'@type': 'formattedText', 'text': '签到需要确认问题并选择您认为正确的答案：\n\n25 + 1 = ?\n\n请在 30 秒内作答', 'entities': [{'@type': 'textEntity', 'offset': 22, 'length': 10, 'type': {'@type': 'textEntityTypeBold'}}, {'@type': 'textEntity', 'offset': 37, 'length': 2, 'type': {'@type': 'textEntityTypeBold'}}]}}, 'reply_markup': {'@type': 'replyMarkupInlineKeyboard', 'rows': [ [ {'@type': 'inlineKeyboardButton', 'text': '26', 'type': {'@type': 'inlineKeyboardButtonTypeCallback', 'data': 'Y2hlY2tpbi1RbWFYT3Z5SzlKb0VMZmtmUURMN3ZYcVZMeFIxNk1CRQ=='}}, {'@type': 'inlineKeyboardButton', 'text': '22', 'type': {'@type': 'inlineKeyboardButtonTypeCallback', 'data': 'Y2hlY2tpbi1PVkdOSjliZTJncXgxdDdmQTFQdnc5b2RtTXoxNlJ4WA=='}}, {'@type': 'inlineKeyboardButton', 'text': '85', 'type': {'@type': 'inlineKeyboardButtonTypeCallback', 'data': 'Y2hlY2tpbi1EV1gwTDYxTzRvWmxqRjFmYTlkMThBcFZ4OW1ZbmR5Sg=='}}, {'@type': 'inlineKeyboardButton', 'text': '66', 'type': {'@type': 'inlineKeyboardButtonTypeCallback', 'data': 'Y2hlY2tpbi1WR05KOWJlMmdxeEUxaTdmQTFQdnc5b2RtTXoxNlJ4WA=='}}, {'@type': 'inlineKeyboardButton', 'text': '68', 'type': {'@type': 'inlineKeyboardButtonTypeCallback', 'data': 'Y2hlY2tpbi1sNUJrbVBOd1hxQnhWZnpmMmJBUVZMclFKN0RSOXlqeA=='}}]]}}} 最后发送签到命令： result = tg.send_message( chat_id=123456789, ##chat_id自己找一找，虽然都一样！ text="/checkin", ) result.wait() tg.idle() 这就完成了一个账号的签到，如果是多个账号使用tg.idle()就不行了，tg.idle()会堵塞当前进程，需要终止信号终止。要跑多个账号只能等待一段时候后自动停止，然后再执行下一个账号。需要修改下代码，让他可以跑多个账号： accounts = ['+123456789', '+456789123', '+789456123' ...] for account in accounts: tg = Telegram(... phone=account, ...) tg.add_update_handler('updateNewMessage', send_verification_code) tg.login() ... print(f'当前账号：{account}') print(f'等待15秒，接收签到验证信息并且验证签到') time.sleep(15) tg.stop() python-telegram是支持多账户的，会保存用户凭证，所以登陆一次即可，新账户登陆还是需要输入验证码的。 现在我们将他打包成可执行程序： pip install pyinstaller 打包 pyinstaller -F server.py 打包完成在dist文件夹下。 如果直接跑起来会报错：..../libtdjson.so的话可以下载作者编译好的libtdjson.so，也可以自己编译td build。libtdjson.so的位置可以在代码中指定: tg = Telegram(...,library_path='/path/to/libtdjson.so') 这时候退出隔离环境deactivate，跑一下../dist/server，没有其他问题的话加入crontab, 设定每天执行一次即可自动签到！ 结语 我个人感觉python-telegram在可编程上比telegram-cli要好用，之前LXK0301京东签到脚本-自动提交互助码一文中使用telegram-cli虽然也可以达到效果，但是不如直接敲代码来得舒服。telegram-cli更多的还是作为一个linux的telegram客户端。 python-telegram用来做签到只是个引子，更多的还是各种自动化非常简单了，毕竟官方提供了所有API。本来以为要自己集成实现API，还好有大佬做好了！
前言 前几天随手撸了个自动上传京东open token（open token其实就是用户cookie）的脚本（有兴趣可以看看：京东cookie自动更新维护），虽然可以自动上传了，但是每天都得去打开京东APP才行，而且两次打开时间不能超过24小时，不然open token就过期了。这方法怎么看都不是很省事！ 最近一直在找替代方法，发现Zy143L大佬脚本中提到wskey可以申请open token，wskey有效期应该比较长，我使用好几天之前抓包到的wskey依然可以获取到open token。wskey在APP抓包中经常看到，使用wskey的请求里面都有签名，感觉签名方法应该是一样的。通过genToken的接口获取open token，那个签名方法虽然短时间可以重复使用（请求只需要变动wskey就好，签名没有加入wskey做计算），但是还不清楚有效期，如果一直靠抓包获取签名的话那比上面直接抓包open token还恶心！ 分析 wskey很重要，京东APP很多敏感接口都是使用wskey，不要泄露！！！ 既然可以通过wskey获取open token，wskey有效期有比较长，那么它是一个很好代替每天上传open token的方法。 我的思路： 不定期通过手机app抓包wskey上传到服务器，服务器接收到后写入wskeys.list。 每天定时通过wskey换取open token（open token 24小时有效期，应该每天获取两次比较保险），并且写入cookies.list。 目前还不清楚wskey有效期，所以在第二步中wskey过期及时发送通知以便重新上传wskey。 经过一下午折腾，确实可行。但是看Zy143L大佬源码发现genToken接口中的签名是请求coding.net返回的（应该是大佬自建部署的吧！）！这个动作引起了我的好奇心，没有公布签名算法，究竟是何等高级的玩意儿？请求没有任何数据，就一个get请求即可返回所有签名参数，难道签名参与计算的值都是固定的吗？ 这类资料比较少，不过还是有乐于分享的大佬。找到几篇文章： JD app sign 加密参数破解 - unidbg 逆向工具之unidbg（在pc端模拟执行so文件中的函数） Unidbg使用指南(一) Unidbg模拟执行大厂so实操教程(一) 先把框架搭起来 Unidbg模拟执行大厂so实操教程(二) 借鸡生蛋之SandHook的使用(一) 借鸡生蛋之某电商App签名so的使用(二) 借鸡生蛋之某电商App签名so的使用(三) 对于我来说难度比较大，涉及知识面广，不过如果有信心那就没问题。 我总结了一下经过步骤： 首先得搭建一个android开发环境。 下载京东安卓APP，apk文件可以当作压缩文件打开，从lib里面提取出libjdbitmapkit.so。 用ida修改libjdbitmapkit.so文件，需要修改一些地方屏蔽错误。 使用unidbg载入libjdbitmapkit.so，调用签名方法签名。 打包编译，部署到服务器。 步骤虽然简单，但是操作过程对于一个从来没有接触过andriod、汇编的人来说每一步都掉坑！！！ 过大坑纪要 Exception in thread "main" java.lang.IllegalStateException: Illegal JNI version: 0xffffffff，这个问题坑了我好久，起初我以为自己环境有问题，因为根本找不到什么资料。其实就是so文件问题，如果你没有使用unidbg-0.9.1版本的话会出现这个错误，引发错误的根本原因就是先按借鸡生蛋之某电商App签名so的使用(三)对so文件打补丁，如果按上文推荐的文章顺序阅读那么就会被卡住，建议全部读一遍后再动手。 打补丁使用的ida软件，对于汇编是一脸懵逼的，借鸡生蛋之某电商App签名so的使用(三)一文中把修改位置写得很明白了，但是ida真心不会用，其中包括安装ida插件: keystone中下载msi安装文件，Version 0.9.1有提供32位和64位装一个就行。安装过程一定要勾选安装pthon2.7，我就取消了，最后插件无法使用，还是自己手动去安装的，如果你也是手动安装记得安装six模块：pip install six。 把keypatch代码复制，到ida plugins目录建一个新文件Keypatch.py，把代码复制进去。 ida中Options-General-Disassembly-Number of opcode bytes设置成16，以十六进制显示字节码： 使用keypatch插件修改sourceDir部分，JNI_OnLoad部分使用ida功能Edit-Patch program-Change byte就好了。000037CC 04 25 C0 F2 01 05 MOVS R5, #0x10004这段我是用keypatch一直报错，浪费一两个小时，使用Patch program替换中间字节就行了： 修改摘要： 00002D08 40 F0 40 81 BNE.W loc_2F8C -> 00002D08 40 E1 B loc_2F8C 00002F8C 6F F0 02 00 MOV R0, #0xFFFFFFFD -> 00002F8C 4F F0 01 00 MOV.W R0, \#1 000037C8 FF F7 96 F9 BL check_status -> 000037C8 FF F7 96 F9 BL check_status 000037C8 FF F7 96 F9 BL check_status 000037CC 80 46 MOV R8, R0 000037CE 00 28 CMP R0, \#0 000037D0 67 D1 BNE loc_38A2 -> 000037C8 FF F7 96 F9 BL check_status 000037CC 04 25 C0 F2 01 05 MOVS R5, #0x10004 000037D2 2A E0 B loc_382A; 打包一定要看Unidbg使用指南(一)，所有操作都是在test文件夹完成的，不然会缺少包，装又装不上！打包也要载入test文件夹！ 搭建服务 如果你能坚持到这步就已经成功了，只要能获取到签名参数想怎么玩就怎么玩！我还是用http服务比较顺手，网上找了个java socket代码一顿改！！！ 在JD app sign 加密参数破解 - unidbg文中代码基础上修改，加入socket模拟http： ... public static void service(final Socket socket, final JingDong jd) { InputStream inSocket; try { //获取HTTP请求头 inSocket = socket.getInputStream(); int size = inSocket.available(); byte[] buffer = new byte[size]; inSocket.read(buffer); String request = new String(buffer); System.out.println("ClientBrowser:\n" + request + "\n" + "------------------------------------------------------------------"); String httpCode = "200 OK"; String result = ""; if (request.length() > 0) { try { JSONObject data = JSON.parseObject(request.substring(request.lastIndexOf("\r\n")).trim()); if (data != null) { String uuid = data.getString("uuid"); String functionId = data.getString("functionId"); try { String body = new URLCodec().decode(data.getString("body")); String version = data.getString("version"); if (!uuid.isEmpty() && !functionId.isEmpty() && !body.isEmpty() && !version.isEmpty()) { result = convert(jd.runJni(uuid, functionId, body, version)); System.out.println(result); System.out.println(convert(result)); } } catch (DecoderException e) { e.printStackTrace(); } } } catch (JSONException e) { e.printStackTrace(); result = "{\"code\": \"400\", \"message\": \"Invalid param\"}"; httpCode = "400 Bad Request"; } } //将响应头发送给客户端 String responseFirstLine = "HTTP/1.1 " + httpCode + "\r\n"; String responseHead = "Content-Type:application/json\r\n"; OutputStream outSocket = socket.getOutputStream(); System.out.println("ServerResponse:\n" + responseFirstLine + "\n" + responseHead + "\n" + result + "\n" + "--------------------------------------------------------------------"); System.out.println("current thread：" + Thread.currentThread().getName()); System.out.println("threads count：" + Thread.activeCount()); outSocket.write(responseFirstLine.getBytes()); outSocket.write(responseHead.getBytes()); outSocket.write("\r\n".getBytes()); outSocket.write(result.getBytes()); outSocket.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { //獲取參數，服務監聽地址和端口 //java -Daddress=0.0.0.0 -Dport=9999 -jar unidbg-android.jar address = System.getProperty("address", "127.0.0.1"); port = Integer.parseInt(System.getProperty("port", "8668")); threadCount = Integer.parseInt(System.getProperty("thread", "10")); //綫程池 ExecutorService executorService = Executors.newFixedThreadPool(threadCount); final ServerSocket serverSocket; final JingDong jd = new JingDong(); try { //建立服务器Socket,监听客户端请求 serverSocket = new ServerSocket(port, 50, InetAddress.getByName(address)); System.out.println("Server is running on " + serverSocket.getLocalSocketAddress()); //死循环不间断监听客户端请求 while (true) { final Socket socket = serverSocket.accept(); System.out.println("biuld a new tcp link with client,the cient address:" + socket.getInetAddress() + ":" + socket.getPort()); //并发处理HTTP客户端请求 executorService.execute(new Runnable() { @Override public void run() { service(socket, jd); } }); } } catch (Exception e) { e.printStackTrace(); } finally { jd.destroy(); executorService.shutdown(); } } ... 网上七拼八凑折腾出来这么个玩意，使用： java -Daddress=127.0.0.1 -Dport=8668 -DthreadCount=5 -jar xxx.jar address：绑定地址，port：绑定端口，threadCount：线城数量。 请求参数: //没有限制请求方法，GET POST随意，以下是请求body {"uuid":"随机字符串，随机生成就行了","functionId":"接口（genToken）","body":"URI编码的json，不要带任何多余东西","version":"安卓版本"} //返回结果 { "uuid": "9d53afe389f6ae5f", "st": "1630922926986", "sign": "0a9b72a4fc6ffd5d799642dd295623f8", "sv": "111" } 然后外面再套一层nginx反代，或者和自己jd_script同服务器，内网请求。你要是绑定0.0.0.0暴漏出去也行！ 结语 为了个签名折腾了好久，整个签名使用了functionId，body，uuid，client，clientVersion五个参数，client基本可以固定android了。使用ida汇编长了不少知识。 至此应该是不用在操心京东登陆方面的问题了，只要使用京东APP全部都自动更新了！
前言 薅羊毛门槛越来越高了，京东cookie目前有效期只有一天，有些甚至更短。之前扫码登陆目前看来已经获取不到cookie了,现在获取cookie相对麻烦不少。 前段时间很忙，最近闲下来折腾下。看到github上有些脚本，可以抓包获取京东APP请求中cookie，突然有个大胆的想法。如果我让脚本把获取的cookie发送到自己的服务器，服务器自动更新维护cookies.list文件，那么每天打开下APP即可。像IOS有快捷指令APP，让它自动化，每天按时打开京东APP更新cookie,让后服务器自动维护岂不美哉！缺点就是每个用户都得有个可以执行脚本的APP。 废话不多说，干就完了 由于我用的是软路由，openwrt上跑docker（没错，还是LXK0301大佬的那套，自不过修修补补勉强用着，不过目前蛮多用什么面板的！），所以要复杂一点，服务器上部署可以少很多步骤。在openwrt上需要开启frp客户端，然后要映射docker容器端口到本地。还需要一台服务器搭建frp，主要是服务端会搭建在openwrt上，方便外网访问API。在openwrt上部署服务端，接收请求更新本地cookie文件就好了。 服务端 服务端不需要太复杂，用node.js弄个简单的就好了，根据cookie中的pt_pin来替换整行cookie（没有新增哦，只能提换已存在的）： const http = require('http') const fs = require('fs'); const path = require('path') const file = path.resolve(__dirname, '../logs/cookies.list') //路径不同需要改改，你当期间脚本文件相对路径 function writeCookie(pt_key, pt_pin) { const re = new RegExp('.*'+ pt_pin +'.*', 'g') const cookie = `${pt_key};${pt_pin};` // console.log(re) fs.readFile(file,'utf8',function(err,data){ // console.log('替換前' + data) let result = data.replace(re, cookie); // console.log('替換後' + result) fs.writeFile(file, result, 'utf8', function (err) { if (err){ console.log(err) } }); }) } const server = http.createServer(function(request, response) { const contentType = request.headers['content-type'] // response.setHeader('Content-type', 'application/json') // && contentType === 'application/json' if (request.method === 'POST' && request.url === '/cookie/update') { let body = []; request.on('data', (chunk) => { body.push(chunk); }).on('end', () => { // console.log(Buffer.concat(body).toString()) try { body = JSON.parse(Buffer.concat(body).toString()); if (body.pt_key && body.pt_pin) { writeCookie(body.pt_key, body.pt_pin) // console.log(body.pt_key) response.end('OK'); } else { response.statusCode = 400; response.end('Invalid param'); } } catch (e) { response.statusCode = 400; response.end('Invalid param'); } }); } else { response.statusCode = 404; response.end(); } }) const port = 8888 const host = '0.0.0.0' server.listen(port, host) console.log(`Listening at http://${host}:${port}`) 运行 node 脚本名即可启动服务，对于服务器上部署，我还是推荐使用 pm2 来管理 （至于有什么有点，点进去官网了解下吧！）： # 安装 $ npm install pm2@latest -g # or $ yarn global add pm2 # 运行 pm2 start 脚本名 客户端 脚本在此 jd_appopen.js， 只需要稍作修改即可： /* 杀掉后台后打开京东app获取app_open 在脚本日志查看值 [MITM] hostname = api.m.jd.com ===========Surge================= [Script] jd_appopen = type=http-request,pattern=^https:\/\/api\.m\.jd\.com\/openUpgrade, max-size=0, script-path=jd_appopen.js ===================Quantumult X===================== [rewrite_local] # jd_appopen ^https:\/\/api\.m\.jd\.com\/openUpgrade url script-request-header jd_appopen.js =====================Loon===================== [Script] http-request ^https:\/\/api\.m\.jd\.com\/openUpgrade script-path=jd_appopen.js, timeout=3600, tag=jd_appopen */ const $ = new Env("app_open") if ($request.headers) { let cookie = ($request.headers.Cookie || $request.headers['Cookie'] || $request.headers['cookie'] || '') let pt_key = cookie.match(/(pt_key=[^;]*)/)[1] let pt_pin = cookie.match(/(pt_pin=[^;]*)/)[1] if (pt_key && pt_pin) { console.log('================') console.log(`${pt_key};${pt_pin};`) console.log('================') const options = { "url": `换成你的域名/cookie/update`, 'body': JSON.stringify({ "pt_key": pt_key, "pt_pin": pt_pin }) } $.post(options, (err, resp, data) => { try { if (err) { $.msg("app_open获取成功！", `更新cookie失败: ${JSON.stringify(err)}`) console.log(`pp_open获取成功,更新cookie失败: ${JSON.stringify(err)}`) } else { console.log(`更新cookie: ${data}`) } } catch (e) { $.logErr(e, resp) } }) } } $.done($request.headers) ... 然后使用你手机APP（Surge、Quantumult X、Loon、shadowrocket）添加即可，shadowrocket我试过也可以，成本相对其他app应该便宜点！ IOS快捷指令 在快捷指令APP中添加个自动化，运行前询问看自己情况开启，这里的shadowrocket可以换成其他执行脚本APP，如果你跑脚本的APP常驻打开那么就可以删掉shadowrocket部分，启动此自动化需要打开： 快捷指令有个缺点，不是所有自动化都可以锁屏运行，像这个也是，但是会发送通知，点击通知就可以，或者自己不使用，每天自己手动也是一样的！ 结语 没有写很详细，这些也就是大概思路，还有很多值得优化的地方。京东这次动作能刷下不少代挂了，操作成本太高了。对于三五个好友玩玩还行。如果喜欢折腾的应该能按照思路完善吧，写得太详细有时候真不是件好事！
前言 Xray已经出来一段时间了，之前一直使用VLESS+tls+ws方式科学上网，其实都够用，没什么问题。但是了解了下XTLS黑科技，再看看Xray性能测试，这货简直爆炸，性能是TLS的数倍。 其实之前折腾过一次，但是Xray要监听443端口（不监听也行，但是不完美啊），影响做站。虽然有解决方法，但是不够完整，我一不小心就跳坑里面去了。Xray已经带了SNI回落功能（教程看这里：通过 SNI 回落功能实现伪装与按域名分流），Nginx也带SNI功能，我还是觉得用Nginx的比较好，免得折腾Xray影响网站运行。 这里我不会写如何安装Xray，因为教程实在是太多了，嫌麻烦还有一键脚本：Xray_onekey（V2ray脚本别用这个作者的哈,特别是服务器有站点的！！！Xray脚本这个大佬的还不错）。 坑 因为服务器上有几个站点，装完Xray后实现SNI分流，过程非常顺利，但是chrome上有问题，几个二级域名访问的内容全部和第一个访问的域名一样。 例如：我先访问 a.example.com 然后在访问 b.example.com，b.example.com 响应内容和 a.example.com 一模一样，然后再访问 c.example.com 还是和 a.example.com 一样。开chrome隐身模式直接访问 c.example.com 正常，然后访问 a.example.com 或 b.example.com 内容又全是 c.example.com 的（不一定会每次都这样，要看浏览器是否使用现有连接或服务器是否断开了之前的连接）。 这个问题折腾我很长时间，最后发现和HTTP/2有关，我所有站点不使用HTTP/2就会正常，只要有一个站点使用了HTTP/2，你先访问HTTP/2站点，那么后面的其他子域名都会变成它的内容。这个情况没有仔细测试证明，无法确定。 最开始我使用Xray的SNI也是这样，我还以为是Xray SNI的问题，之后换成Nginx使用SNI还是这样。chrome中有这个问题，用手机safari浏览器完全正常。 经过网上一番搜刮，终于找到了问题根源：通配符域名证书。 首先你得了解什么是SNI，个人理解：SNI就是通过证书区分服务。多个二级域名使用通配符域名证书的话，相同的证书无法区分具体想访问的服务，因为它们的证书都一样。 用下人家的图（原作者文章：Multiplex TLS Traffic with SNI Routing）： *由于原始服务器10.0.3.2使用的TLS证书具有通配符名称 .example.com，因此Web浏览器可以建立服务器名称为 b.example.com 的 TLS连接，并对HTTP/2请求使用相同的连接到 a.example.com。这可能会在网站和应用程序中导致未定义的行为。** 因为HTTP/2会保持TCP连接，导致你访问了 a.example.com 后，在访问 b.example.com 的时候使用的同一个连接，SNI跟失效了一样，直接使用该连接访问之前的服务。我在Xray的调试日志中发现访问错误的域名没有获取到，是空的！ 两个域名使用同一个连接（红框内是连接ID），可以很清楚的看到两个域名使用同一个连接，返回的内容也是一模一样： 解决办法两种： 不使用HTTP/2。不确定是否HTTP2的问题，但是你可以试一试 不使用通配符域名证书，站点域名证书一个个申请。 配置Nginx SNI 使用Nginx首先得安装（如果服务器自带就不用折腾了），不会安装Nginx可以看我之前的文章：Ubuntu 安装nginx（如果使用Xray_onekey脚本，那他已经帮你全部弄好了，你都不用折腾）。 nginx.conf配置追加： stream { map $ssl_preread_server_name $orzlee { a.example.com a; b.example.com b; c.example.com c; } upstream a { server 127.0.0.1:60088; ### A站点监听端口，xray xtls伪装站点 } upstream xtls { server 127.0.0.1:4443; ### 你的Xray端口 } upstream b { server 127.0.0.1:60001; ### B站点监听端口，业务站点 } upstream c { server 127.0.0.1:60002; ### C站点监听端口，业务站点 } server { listen 443 reuseport; listen [::]:443 reuseport; proxy_pass $orzlee; ssl_preread on; proxy_protocol on; } server { ### Xray XTLS listen 127.0.0.1:60088 proxy_protocol; proxy_pass xtls; } } a.example.com.conf配置（伪装站点）: server { listen 80; listen [::]:80; server_name a.example.com; return 301 https://$http_host$request_uri; } server { ### 伪造站点由Xray处理SSL listen 127.0.0.1:60003 proxy_protocol; ### xray http/1.1 listen 127.0.0.1:60004 http2 proxy_protocol; ### xray http/2 set_real_ip_from 127.0.0.1; real_ip_header proxy_protocol; port_in_redirect off; ###重定向去掉端口号 server_name a.example.com; index index.html index.htm index.php default.php default.htm default.html; root /var/www/a.example.com; ... } b.example.com.conf配置（其他业务站点配置照葫芦画瓢）: server { listen 80; server_name b.example.com; rewrite ^(.*) https://b.example.com$1 permanent; } server { listen 127.0.0.1:60001 proxy_protocol ssl http2; set_real_ip_from 127.0.0.1; ### 获取真实客户IP，不然全是127.0.0.1 real_ip_header proxy_protocol; port_in_redirect off; ###重定向去掉端口号。不然类似 try_files nginx帮你重定向时，你浏览器上都带了60003这样的端口 ssl_certificate b.example.com.cer; ### 证书换成你的 ssl_certificate_key b.example.com.key; ### 证书换成你的 server_name b.example.com; root /var/www/b.example.com; index index.php index.html index.htm; ... } 配置Xray 服务端配置： { "log": { "access": "/var/log/xray/access.log", "error": "/var/log/xray/error.log", "loglevel": "warning" }, "inbounds": [ { "listen": "127.0.0.1", "port": 4443, "protocol": "vless", "settings": { "clients": [ { "id": "你的UUID", "flow": "xtls-rprx-direct" } ], "decryption": "none", "fallbacks": [ { "dest": 60003, "xver": 1 }, { "dest": 60004, "alpn": "h2", "xver": 1 } ] }, "streamSettings": { "network": "tcp", "security": "xtls", "xtlsSettings": { "alpn": [ "h2", "http/1.1" ], "minVersion": "1.3", "certificates": [ { "certificateFile": "b.example.com.cer 记得换证书", "keyFile": "b.example.com.key 记得换证书" } ] } }, "sniffing": { "enabled": true, "destOverride": [ "http", "tls" ] } } ], "outbounds": [ { "protocol": "freedom" } ] } 客户端看作者说明吧，值得一看：Outbounds 可用协议列表-VLESS，flow仔细看看。 结语 本来折腾Xray的，结果Nginx折腾大半天。虽然掉坑里了，总算是爬出来了，受益匪浅！ 通配符证书用起来省事，碰到坑了一下还不好找问题。就今天这个坑上网找都不知道怎么描述，基本上找不到相关问题，比较冷门。之前参考其他博主Nginx SNI配置发现有其他同学也有这样的问题，但是无从查起，不了了之！ 遇到这个坑不长记性都难。
前言 openwrt的shell连上次会话历史记录都没有，很是蛋疼。不如换成oh-my-zsh,还有很多插件可以玩。 VPS上安装可以看之前的文章：oh-my-zsh强大的zsh配置管理 安装 操作前记得备份你得openwrt配置，现在很多用的都是改版openwrt，不确定会发生什么奇奇怪怪的问题，但是大部分是没有问题的，备份一下保险。 登陆到openwrt ssh,首先安装依赖： opkg update && opkg install wget unzip zsh ca-certificates 安装oh-my-zsh: sh -c "$(wget -O- https://raw.githubusercontent.com/felix-fly/openwrt-ohmyzsh/master/install.sh)" 这时候已经安装完成了，还差设置默认shell为zsh: which zsh && sed -i -- 's:/bin/ash:'`which zsh`':g' /etc/passwd 到此结束，退出当前会话然后重新登陆ssh看看是不是已经默认为zsh了。 有了历史记录再配合命令补全、命令高亮插件简直不能太爽！！！ 自动补全插件或其他插件安装方法还是请看：oh-my-zsh强大的zsh配置管理 自动补全插件： 命令高亮插件： 如果你要卸载oh-my-zsh的话： sh -c "$(wget -O- https://raw.githubusercontent.com/felix-fly/openwrt-ohmyzsh/master/uninstall.sh)" 结语 装完oh-my-zsh感觉没有束缚了，这样的终端爽的飞起。之前所有的服务器都装了oh-my-zsh，习惯了，没有oh-my-zsh总感觉太单调了，敲起来不顺手。
openwrt-通过telegram bot自动创建启动jd-scripts docker容器
前言 使用lxk0301大佬的jd_scripts-docker脚本后突发奇想，如果利用telegram bot获取用户京东cookie，然后自动创建docker容器并且启动容器跑脚本，这个流程一气呵成，以后京东cookie过期就可以很方便的处理，然后自己朋友也可以扫京东登陆二维码就部署整个签到脚本了。 在openwrt折腾可以看我之前的文章：openwrt-docker部署lxk0301京东自动签到脚本 2021-03-22更新：作者已经更新docker容器，详细说明：增加bot交互，spnode指令（jd_docker仓库被Gitee封了,最新地址jd_docker）。容器已经自带telegram bot,大量与docker容器配套的命令以及获取cookie功能。有兴趣可以玩玩看。 运行环境和思路 我使用的是openwrt软路由(CPU J4105, 8G, 128G SSD)，由于性能过剩，不折腾点东西不舒服，所以没有放VPS上。软路由固件使用的是eSir大佬的高大全版本，eSir大佬固件盘。记得扩容overlay分区，然后opt容量也要注意下,我在openwrt-docker部署lxk0301京东自动签到脚本文章中提到过。 由于部署在路由器上，telegram bot webhook发到路由可能会遇到GFW的干扰（不确定是否会干扰入境流量，可以试试看），我使用frp（eSir openwrt自带）做内网穿透，frp服务端需挂在境外服务器上，这样外网问题就能解决。但是路由器还是需要科学上网的。 telegram bot开发使用python,还有少部分lua，因为openwrt上的dockerman插件语言也是lua语言，启动docker时还是希望能在dockerman中管理，所以还是折腾了lua。 整个流程： 用户在telegram bot发送命令 telegram bot返回一个京东登陆二维码（以下图片中的login命令是测试时候的） 用户扫码二维码，服务器获取用户京东cookie 服务器拿到cookie创建（或删除重建）一个jd_scripts-docker容器，并且启动 提示用户启动成功或失败 成功后可以在openwrt web端docker-容器看到新加入的用户docker容器，还可以在openwrt的dockerman插件中管理容器。 思路： 用户发送命令给telegram bot，服务器接收到后进入配置好的容器，执行/scripts/getJDCookies.js脚本获取脚本生成的京东登陆链接，用链接生成二维码，通过telegram bot返回给用户。二维码有效期三分钟，脚本也会持续三分钟。用户扫码后服务器会捕获到脚本回显，提取京东cookie。 拿到cooki后判断用户是否已经创建容器，创建了多久，超过N天的删除旧容器，然后再创建一个新的容器启动，没有使用过的用户直接创建一个新的。由于我想兼容dockerman，所以用lua语言折腾了一个简版创建和管理docker命令行脚本。懒得折腾了，服务器直接复制一份已存在的jd_scripts-docker脚本配置新建，环境变量和脚本可以自行配置就行了。启动成功捕获命令行回显容器ID，容器ID和用户信息，京东cookie存Sqlite数据库。 由于防止用户频繁提交，使用reids存下已经获取二维码的telegram用户ID,必须等待三分钟或者jd_scripts-docker成功后再次获取。 本来是想使用一个容器，然后在容器里面修改JDCOOKIE环境变量，配置多个cookie。但是目前有两个缺点： 单容器多用户跑脚本时会一个个跑，并不是并发跑，人数一多脚本执行时间会很长（如果限制单容器用户人数也许是个办法，比如说一个容器只允许5个用户，多出来的再建个新容器，以后再说吧！）。 脚本通知（多账号）只能发给一个环境变量配置好的接收用户。 不过最近lxk0301大佬更新了单容器多用户脚本会自动互助的功能，只能自己再想办法折腾下了。 脚本说明： 2021-03-16更新:添加单容器追加cookie功能，这样VPS应该能用了（我没有测试，但是不涉及到dockerman容器管理应该是没有问题的！）： 仓库地址：Github env.config文件添加两个配置，主要是add_user_mode添加用户模式，默认是create，单容器是attach： [telegram_bot] ... [jd_scripts] ... auto_delete_docker_by_days=30 //目前没有实现自动删除过期用户容器功能，只是attach模式中过滤超过N天的用户cookie add_user_mode=attach //创建容易方式：attach 附加，create 创建，附加只会追加cookie到existed_docker_container_name配置容器里，create每一个用户都会创建一个新的docker容器 [sqlite] ... 如果使用attach模式，会在当前目录生成一个cookies.sh文件，执行该文件会导入所有通过telegram bot加入的有效用户cookie到环境变量。 使用attach模式方法： 现在开始准备新建一个容器，挂载cookies.sh文件到jd_scripts_docker容器（export_jd_cookies_script_orz.sh挂载文件名要改的话下面的脚本内容也要跟着修改）： 你的目录/cookies.sh:/scripts/docker/export_jd_cookies_script_orz.sh 添加一个自定义脚本： 先挂载本地脚本（名称目录自定，但是挂载后的不要改了，不然又要改脚本内容。如果远程脚本就不用挂载了）： 你的目录/docker_shell.sh:/scripts/docker/docker_shell.sh 添加环境变量（远程脚本不用上面的挂载docker_shell.sh步骤）： CUSTOM_SHELL_FILE=/scripts/docker/docker_shell.sh 脚本内容，意思是每分钟执行update_crontab.sh脚本： if ! [ ! -f "/scripts/docker/export_jd_cookies_script_orz.sh" ]; then echo "* * * * * sh +x /scripts/docker/update_crontab.sh 2>&1" >> /scripts/docker/merged_list_file.sh fi 挂载update_crontab.sh脚本 你的目录/update_crontab.sh:/scripts/docker/update_crontab.sh 脚本内容： FIND_FILE="/scripts/docker/merged_list_file.sh" FIND_STR="export_jd_cookies_script_orz.sh" # merged_list_file.sh定时任务加入cookies环境变量脚本 if ! [ `grep -c "$FIND_STR" $FIND_FILE` -ne '0' ];then sed -i 's/\/scripts\/logs\/auto_help_export\.log/\/scripts\/logs\/auto_help_export\.log \&\& \. \/scripts\/docker\/export_jd_cookies_script_orz\.sh/g' $FIND_FILE ## 更新定时任务列表 crontab $FIND_FILE fi CRZAY_JOY_COIN_FILE="/scripts/docker/proc_file.sh" # joy coin脚本加入cookies环境变量脚本 if ! [ `grep -c "$FIND_STR" $CRZAY_JOY_COIN_FILE` -ne '0' ];then sed -i 's/node \/scripts\/jd_crazy_joy_coin\.js/\. \/scripts\/docker\/export_jd_cookies_script_orz\.sh \&\& node \/scripts\/jd_crazy_joy_coin\.js/g' $CRZAY_JOY_COIN_FILE sh -x /scripts/docker/proc_file.sh fi 该脚本主要是每分钟执行，但是只修改一次merged_list_file.sh定时任务文件，并且立即更新crontab list，为每个京东任务定时脚本执行前先更新JD_COOKIE环境变量,这样你得脚本执行才能正确获取cookie。第二个是joy coin脚本，这个脚本没有在定时任务列表中，每天只会执行一次，所以要单独修改后并启动。 因为是每分钟执行，所以可以很大程度上避免cookie没有导入的尴尬。 你过你还需要其他环境变量或者磁盘挂载记得自己加。 单容器只有创建容器时配置好的通知环境变量接收通知，没法多个用户分发的。而且人数一多执行单个脚本时间会延长。优点就是可以添加环境变量配置ENABLE_AUTO_HELP=true开启单容器多用户互助。 2021-03-16更新结束。 脚本地址：jd_scripts telegram bot python3.8, python3.9都没问题，其他环境没测试，我使用python3.9开发，openwrt上是3.8没有发现问题。 openwrt上安装pip请点击get-pip。 之前python用的qrcode包生成二维码，但是默认只有svg格式，想生成png,jpg就需要Pillow包，但是这个包在openwrt上安装一直报错，确少zlib库，这个好解决，安装zlib-dev就好了，然后又提示缺少jpeg，这个库openwrt已经用libjpeg-turbo替换了libjpeg库，就算安装了libjpeg-turbo也还是不行，所以换成segno包了。 需要安装包(不知道是不是有遗漏)： pip install segno python-telegram-bot peewee redis 脚本已经撸出来了，做个说明吧！自己用也没太多讲究(记得删除注释)： [telegram_bot] token = 88888888:AAAAAAAAAAAAAAAA //这是telegram bot token port = 8080 //监听端口， frp请填写web端本地端口(local_port)，openwrt别配置80端口，不然都无法启动telegram脚本，因为openwrt web端已经占用了80端口 admins=88888888,8888888 //管理员用户，如果指定了管理员就只能管理员使用京东docker命令，获取自己的userid看这里https://gitee.com/lxk0301/jd_docker/blob/master/backUp/TG_PUSH.md url=https://telegram.bot/ // telegram bot webhook发送域名 redis_cache_prefix=telegram_bot: //redis缓存前缀，可以随便改 commands=jd_script_start:登陆京东并且创建docker容器 //命令，可以自行添加，命令格式 命令:命令说明,命令:命令说明,命令:命令说明 多命令逗号隔开 [jd_scripts] existed_docker_container_name = jd_scripts_orzlee //已经创建好的容器名称，主要用于执行`/scripts/getJDCookies.js`脚本给用户登陆 existed_docker_container_id=1919c3ff9e45ee6d1ca58xxxxxxxxxxxxxx5a8d //已经创建好的容器id，主要用于复制配置文件 docker_container_prefix = jd_scripts_ //新建docker容器名称前缀 tg_bot_token=88888888:AAAAAAAAAAAAAAAA //jd_scirpts 脚本推送通知bot token，可以和上面一样，可以使用其他token，推送用户就是谁请求的二维码，通知就会推送给谁 min_login_days=10 //最少登陆多少天才能重新使用，这个是怕已经成功创建容器后，用户一直用同一个京东账户扫码，然后服务器不停的删除容器创建容器，单位是天，可自行修改，0就是不设置 max_docker_num=50 //最大用户数量，这是判断Sqlite数据库用户数量决定的，不是你本地docker容器数量 auto_delete_docker_by_days=30 //目前没有实现自动删除过期用户容器功能，只是attach模式中过滤超过N天的用户cookie add_user_mode=attach //创建容易方式：attach 附加，create 创建，附加只会追加cookie到existed_docker_container_name配置容器里，create每一个用户都会创建一个新的docker容器 [sqlite] database=telegram_bot_jd_scripts.db //数据库文件名称 环境变量和挂载我给写死了，需要自己修改下吧! 命令由于有"号，所以要转义，在shell中要转义\",代码中就必须转义\和",所以环境变量和挂载磁盘都需要\\\"环境变量=xxx(挂载参数:xxx)\\\"包含起来，因为这个基本不会改变，所以没有放入配置文件，你可以继续加或者删掉部分，像JDCOOKIE和TG_USER_ID这些都是用户的变量，所以还是留下吧： from src.config import * from src import common def createAndStartContainer(cookie, tgUserId, name): ##lua newcontainer.lua -n jd_scripts_test -e \"JD_COOKIE=pt_key=AAJgSIA0ADC-1mV_7uCjZK2kIBxYN4sdb1L9PyAktQewf5Hse7QHaFJVBE3egdRZugQF0FeiWvI\;pt_pin=fangxueyidao\;\",\"RANDOM_DELAY_MAX= ##600\",\"TG_BOT_TOKEN=644204874\:AAETxq7Wr2-rXEijjKYJqn3vXsCijG6xm-w\",\"TG_USER_ID=490884842\",\"CUSTOM_SHELL_FILE=https://raw.githubusercontent.com/jianminLee/jd_scripts/main/docker_shell.sh\" -m \"/opt/jd_scri ##pts/logs:/scripts/logs\" ## 拼接参数 n = env['jd_scripts']['docker_container_prefix'] + name e = '\\\"JD_COOKIE='+cookie.strip()+'\\\",\\\"RANDOM_DELAY_MAX=600\\\",\\\"TG_BOT_TOKEN=' \ +env['jd_scripts']['tg_bot_token'] \ +'\\\",\\\"TG_USER_ID='+str(tgUserId)+'\\\"' m = '\\\"/opt/jd_scripts/logs/'+ str(tgUserId) +':/scripts/logs\\\"' command = ("cd "+ os.path.abspath('lua/') +" && lua "+ os.path.abspath('lua/dockerman.lua') +" -n " + n + ' -e ' + e + ' -m ' + m + ' -d ' + env['jd_scripts']['existed_docker_container_id']).replace(';','\\;') print(n+'\n'+e+'\n'+m+'\ncommand:'+command) result = common.run_command(command) for res in result: if res.startswith('id:'): #返回容器ID return res[3:] return -1 def stopAndDeleteContainer(containerId): command = 'cd '+os.path.abspath('lua/')+' && lua '+os.path.abspath('lua/dockerman.lua')+' -D '+containerId result = common.run_command(command) for res in result: if res == 0: #返回容器ID return 0 return -1 lua管理docker命令参数说明： { "-m, --mount", help = "容器挂载目录" }, { "-d, --duplicate", help = "容器ID 复制容易配置，复制后依然可以使用其他选项覆盖" }, { "-D, --delete", help = "容器ID 删除容器" }, { "-n, --name", help = "容器名称" }, { "-e, --env", help = "容器环境变量" }, 运行的话使用python serve.py。 frp服务配置 eSir的openwrt高大全版本固件中服务-frp内网穿透可以看到frp工具（服务器下面的端口是1234，和frps.ini中的bind_port一致）： 添加一个客户端，自定义域名随便起，不用注册，内网使用： 服务器安装可以看之前的文章frp内网穿透，但是我这里使用nginx反向代理安装。 frps.ini配置： [common] bind_port = 1234 vhost_http_port = 8080 token = AAAAAAAAAA type = http custom_domains = orzlee.test //这个域名和客户端自定义域名一致 nginx反向代理配置： server { listen 80; server_name 你的域名; ###rewrite ^(.*) https://$server_name$1 permanent; charset utf-8; location / { proxy_pass http://127.0.0.1:8080; ##端口是frps.ini中的vhost_http_port端口 proxy_set_header Host orzlee.test; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header REMOTE-HOST $remote_addr; } ##location = /favicon.ico { access_log off; log_not_found off; } ##location = /robots.txt { access_log off; log_not_found off; } access_log off; error_log off; sendfile off; location ~ /\.ht { deny all; } } server { listen 443 ssl http2; server_name 你的域名; ssl_certificate 证书目录/xxx.cer; ssl_certificate_key 证书私钥xxx.key; ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; #屏蔽不安全的加密方式 ssl_prefer_server_ciphers on; index index.html index.htm index.php; charset utf-8; client_max_body_size 5M; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host orzlee.test; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header REMOTE-HOST $remote_addr; } ##location = /favicon.ico { access_log off; log_not_found off; } ##location = /robots.txt { access_log off; log_not_found off; } access_log off; error_log off; sendfile off; location ~ /\.ht { deny all; } } 这样frp就通了，然后本地telegram-bot就可以接收webhook通知了。 结语 后续还有很多需要实现的功能，例如自动删除一个月以后的docker容器，因为可能cookie过期了，再继续跑下去也没有什么意义，还占用资源。等之后有时间在弄吧，折腾这个把openwrt弄挂了，还好有备份！
LXK0301京东签到脚本-京喜工厂解决无法签到异常
前言 LXK0301大佬的京喜工厂自动签到脚本不知怎么一直提示：亲,活动太火爆了,请稍后再试吧!,基本无法使用，任务也不自动做了。 上网查了一下，全是说黑号，真蛋疼。因为脚本无法做任务什么的，但是手动完全没有问题，根本不是黑号，网上一股脑说黑号。 我就不信邪，app能做任务脚本就不行，肯定是脚本有问题。 2021-03-25 第二次更新，已经不需要自己抓包获取签名参数了，脚本会在每次执行时自动获取签名参数。已经联系作者，等作者更新吧！(推荐使用作者脚本，修复了参团问题) 2021-03-25更新，今天更新了下脚本，应该可以获取自己的签名参数使用了，有时间的朋友可以测试一下！仓库地址：jd_scripts-orzlee 2021-03-23更新，自己获取签名的方法可能无效，作者已经跟新脚本，目前使用我提供的签名参数可以正常做任务和领取电力，后续可能还需要研究。更新作者脚本即可！ 原因 最根本的原因就是脚本发送请求链接签名有问题，这个好像只针对了部分用户，有的用户没问题，有的就会一直提示：亲,活动太火爆了,请稍后再试吧!。 起初发现问题是在手机上用QuantumultX抓包京喜APP，点击地上的零件，再抓包脚本，然后找出usermaterial/PickUpComponent请求来对比。开始我还以为是cookie问题，我替换cookie重试请求还是太火爆错误，再重发一下抓包京喜APP的请求，依然成功。再替换替换脚本请求的url参数为抓包app的，莫名奇妙的成功了。这个锅必须脚本背了！ 脚本中decrypt函数是负责请求链接签名的，但是目前脚本有问题，很多签名key都是没有值的，因为根本没有获取值，只有少部分定义了默认值。来看一下代码(目前是1535行)： function decrypt(time, stk, type) { if (stk) { const random = 'pmUmA8IyRcDp'; //这些常量其实每个用户都可能不一样 const token = `tk01wd4571d58a8nT0tkdXczeW94f5x4qjWs44kcPCTXeWKa2xXY+ZxHaOtbRxmyw6vrIF4RDFwwTUfwy1pIqNE0oyWW`; //这些常量其实每个用户都可能不一样 const fingerprint = 8410347712257161; //这些常量其实每个用户都可能不一样 //这个时间转换和现在的不一样，现在是new Date(time).Format("yyyyMMddhhmmssSSS");，转换结果不一样，加密出来也不一样 const timestamp = new Date(time).Format("yyyyMMddhhmmssS"); const appId = 10001; //这个是活动id 是固定的 const str = `${token}${fingerprint}${timestamp}${appId}${random}`; const hash1 = $.CryptoJS.HmacSHA512(str, token).toString($.CryptoJS.enc.Hex); //这里加密算法有可能不一样 let st = ''; stk.split(',').map((item, index) => { //下面的签名参数只有些默认值，很多都是空的，但是链接上又带了有效值，服务器验签估计不合法(但是有的用户却没有问题，搞不懂) st += `${item}:${item === '_time' ? time : item === 'zone' ? 'dream_factory' : item === 'type' ? type || '1' : ''}${index === stk.split(',').length -1 ? '' : '&'}`; }) const hash2 = $.CryptoJS.HmacSHA256(st, hash1).toString($.CryptoJS.enc.Hex); console.log(`st:${st}\n`) // console.log(`hash2:${JSON.stringify(["".concat(timestamp.toString()), "".concat(fingerprint.toString()), "".concat(appId.toString()), "".concat(token), "".concat(hash2)])}\n`) console.log(`h5st:${["".concat(timestamp.toString()), "".concat(fingerprint.toString()), "".concat(appId.toString()), "".concat(token), "".concat(hash2)].join(";")}\n`) return ["".concat(timestamp.toString()), "".concat(fingerprint.toString()), "".concat(appId.toString()), "".concat(token), "".concat(hash2)].join(";") } else { return '20210121201915905;8410347712257161;10001;tk01wa5bd1b5fa8nK2drQ3o3azhyhItRUb1DBNK57SQnGlXj9kmaV/iQlhKdXuz1RME5H/+NboJj8FAS9N+FcoAbf6cB;3c567a551a8e1c905a8d676d69e873c0bc7adbd8277957f90e95ab231e1800f2' } } 研究过程我就不再多说了，京东那边全是混淆的JS，真的很头疼，我弄了一下午才摸清楚他的逻辑。 整个流程大概是这样（官方的js混淆了，乱七八糟的就不贴代码了）： 第一次进入活动页面会发送POST请求：https://cactus.jd.com/request_algo?g_ty=ajax，将返回结果储存在本地（这个默认应该没有有效期，但是京喜工厂里面的JS会设置有效期，过期会重新获取）。已经更新脚本自动获取签名参数了。 请求参数： { appId: "10001" expandParams: "" //fingerprint //pc浏览器我试过，就算拿到所有数据跑脚本依然不行，还是太火爆错误。所以保险还是去京喜APP抓包吧！ fp: "5525701123405161" platform: "web" timestamp: 1614687205087 version: "1.0" } 接口会返回： { data: { result: { //加密函数 algo: "function test(token,fingerprint,timestamp,appId,algo){const random='jQ1SS0VI2Vuw';const str=`${token}${fingerprint}${timestamp}${appId}${random}`;return algo.SHA512(str)}" //加密token tk: "tk01w91541b51a8nWURUM3hiZk9HHRKep5j4FeOg/jHOQoGSDcsasdSOZ+83iz4uf/YBXHcK6xMIUqqv5/iJv6F0aooT5Nd3" } version: "1.0" message: "" status: 200 } } 这个接口会返回所有签名需要的参数，也就是脚本中decrypt中的参数，其实照着值样子都知道怎么替换了。 当前时间格式转换new Date(time).Format("yyyyMMddhhmmssSSS")这个时间是和链接参数中的'_time匹配'。 字符串拼接${token}${fingerprint}${timestamp}${appId}${random}。 第一次签名，将第三步的字符串用SHA或者MD5加密，这个主要参考第一步中的机密函数: function test(token,fingerprint,timestamp,appId,algo){ const random='jQ1SS0VI2Vuw'; const str=`${token}${fingerprint}${timestamp}${appId}${random}`; return algo.SHA512(str) } 这个函数提供了加密方法和随机字符串，官方直接用它做闭包函数第一次签名。 第一次签名完成后，开始第二次了，这个参数每个接口可能不同，它会按照请求url中的_stk参数来提供签名参数，例如&_stk=_time,pin,placeId,zone,那么就需要将参数拼接成time:xxxx&pin:xxxx&placeId:xxxx&zone:xxx这样子。stk参数的值也会包含在url中,例如： https://m.jingxi.com/dreamfactory/usermaterial/PickUpComponent?zone=dream_factory&placeId=1&pin=91DCBu8VvYt6rEdezHZ_VQ%3D%3D&_time=1614622722731&_stk=_time%2Cpin%2CplaceId%2Czone&_ste=1&h5st=20210302021842732%3B6099600123439161%3B10001%3Btk01wa66691cafa8nSmV1UDN2SWtZAiPI1uR1EAuPwYDaw36v9gQGhJz5Ac4h2cjfkeCQexVweqwa6%2BsjvERwkVU6wxTK%3Ba795we798qwe789789qe8262584d49f9449a1ce0194bb26c72049eef428f5e23e1043a641d&_=1614622722744&sceneval=2&g_login_type=1&callback=jsonpCBKR&g_ty=ls 第二次签名加密的算法是固定的，SHA256。 最后拼接： ["".concat(timestamp.toString()), "".concat(fingerprint.toString()), "".concat(appId.toString()), "".concat(token), "".concat(hash2)].join(";") 抓包 2021-03-04 更新可以免去抓包和修改脚本操作了： 一旦有一个设备的url签名参数更新，其他设备都会失效，如果多设备跑同一个账号要保持一直，否则还是会失败的！！！ 我更新了脚本，不需要手动抓包了，QuantumultX可以添加url_sign_params.conf重写引用，其他平台可能需要自己写一下。然后删除京喜APP，重新下载后进入京喜工厂（最好重下APP，不然有可能还是太火爆异常，如果没有提示退出京喜工厂再进一次，多试试！）: 折腾好了就只使用京喜APP进入京喜工厂（url签名参数会被京喜工厂里面的JS设置过期时间，过期会重新获取），以免从京东APP进入京喜工厂跳转到京喜APP重新获取覆盖，京东APP里面的京喜工厂获取的url签名参数也不能执行成功！只能使用京喜APP的url签名参数！ 日志可以查看url签名参数： 以下为未更新之前的方法： 找到了问题就好解决了，抓包京喜APP一个请求就行了，抓包之前建议先删除APP，因为签名参数会存在本地，基本上弄不到了。删掉APP再进入京喜工厂之前，准备好抓包工具，IOS可以用QuantumultX、Surge等,PC抓包太多了，一搜一大把。 准备好抓包工具之后，进入京喜工厂，等待完全进入工厂，最好再捡下零件。没有报错就可以关掉抓包工具了。 搜索algo,找到签名参数请求： 请求JSON和响应JSON都要看，不然你不知道fingerprint参数，是一串数字，在请求JSON里： 复制返回请求中的参数： 修改脚本 脚本我修改了很多处，但是很简单： 新增一个获取url参数的函数： /** * 新增url参数获取函数 * @param url_string * @param param * @returns {string|string} */ function getUrlQueryParams(url_string, param) { let url = new URL(url_string); let data = url.searchParams.get(param); return data ? data : ''; } 时间函数与官方不一致，会导致签名结果错误（这函数是官方混淆JS里面扒出来的）： /* 修改时间戳转换函数，京喜工厂原版修改 */ Date.prototype.Format = function (fmt) { var e, n = this, d = fmt, l = { "M+": n.getMonth() + 1, "d+": n.getDate(), "D+": n.getDate(), "h+": n.getHours(), "H+": n.getHours(), "m+": n.getMinutes(), "s+": n.getSeconds(), "w+": n.getDay(), "q+": Math.floor((n.getMonth() + 3) / 3), "S+": n.getMilliseconds() }; /(y+)/i.test(d) && (d = d.replace(RegExp.$1, "".concat(n.getFullYear()).substr(4 - RegExp.$1.length))); for (var k in l) { if (new RegExp("(".concat(k, ")")).test(d)) { var t, a = "S+" === k ? "000" : "00"; d = d.replace(RegExp.$1, 1 == RegExp.$1.length ? l[k] : ("".concat(a) + l[k]).substr("".concat(l[k]).length)) } } return d; } decrypt函数也要修改，我加了一个url参数，在拼接签名参数需要的值可以在url中拿（实在不想动太多代码，怎么方便怎么来吧）： /* 签名函数 签名信息获取地址：https://cactus.jd.com/request_algo?g_ty=ajax 方法： POST 参数：{ appId: "10001" expandParams: "" fp: "5525701123405161" //fingerprint platform: "web" timestamp: 1614687205087 version: "1.0" } 返回结果：{data: { result: { algo: "function test(token,fingerprint,timestamp,appId,algo){const random='jQ1SS0VI2Vuw';const str=`${token}${fingerprint}${timestamp}${appId}${random}`;return algo.SHA512(str)}" //加密函数 tk: "tk01w91541b51a8nWURUM3hiZk9HHRKep5j4FeOg/jHOQoGSDcsasdSOZ+83iz4uf/YBXHcK6xMIUqqv5/iJv6F0aooT5Nd3" //token } version: "1.0" message: "" status: 200 } } 将返回参数替换： const random : data.result.algo字符串中的const random='xxxxxx' const token : data.result.tk const fingerprint : 通过抓包APP获取 const hash1加密方法（例子：algo.SHA512、algo.MD5，主要看签名信息获取地址返回结果data.result.algo） */ function decrypt(time, stk, type, url) { stk = stk || (url ? getUrlQueryParams(url, '_stk') : '') if (stk) { const random = '1gy8nc5oDM+y'; const token = `tk01w9a771c1aa8nd1k2NUo4UmNaahBe0/qh61gMNV9222JdJwlR0wyO9gko9dFTogSpvPIpqxstZJ9tl8JM9ONKNJWk`; const fingerprint = 5525701217505161; const timestamp = new Date(time).Format("yyyyMMddhhmmssSSS"); const appId = 10001; const str = `${token}${fingerprint}${timestamp}${appId}${random}`; const hash1 = $.CryptoJS.HmacSHA512(str, token).toString($.CryptoJS.enc.Hex); let st = ''; stk.split(',').map((item, index) => { // st += `${item}:${item === '_time' ? time : item === 'zone' ? 'dream_factory' : item === 'type' ? type || '1' : ''}${index === stk.split(',').length -1 ? '' : '&'}`; st += `${item}:${getUrlQueryParams(url, item)}${index === stk.split(',').length -1 ? '' : '&'}`; }) const hash2 = $.CryptoJS.HmacSHA256(st, hash1).toString($.CryptoJS.enc.Hex); console.log(`st:${st}\n`) // console.log(`hash2:${JSON.stringify(["".concat(timestamp.toString()), "".concat(fingerprint.toString()), "".concat(appId.toString()), "".concat(token), "".concat(hash2)])}\n`) console.log(`h5st:${["".concat(timestamp.toString()), "".concat(fingerprint.toString()), "".concat(appId.toString()), "".concat(token), "".concat(hash2)].join(";")}\n`) return ["".concat(timestamp.toString()), "".concat(fingerprint.toString()), "".concat(appId.toString()), "".concat(token), "".concat(hash2)].join(";") } else { return '20210121201915905;8410347712257161;10001;tk01wa5bd1b5fa8nK2drQ3o3azhyhItRUb1DBNK57SQnGlXj9kmaV/iQlhKdXuz1RME5H/+NboJj8FAS9N+FcoAbf6cB;3c567a551a8e1c905a8d676d69e873c0bc7adbd8277957f90e95ab231e1800f2' } } 最后一步了，搜索${decrypt(关键字，大概有8处要修改，主要是团长这部分多，主要还是没有解耦： 参团部分（每一个api不一样，只要剥离出&h5st=${decrypt(xxxxx就行了，单独拼接）： //可获取开团后的团ID，如果团ID为空并且surplusOpenTuanNum>0，则可继续开团 //如果团ID不为空，则查询QueryTuan() function QueryActiveConfig() { return new Promise((resolve) => { //option中的url拉出来替换如下 let url = `https://m.jingxi.com/dreamfactory/tuan/QueryActiveConfig?activeId=${escape(tuanActiveId)}&_time=${Date.now()}&_=${Date.now()}&sceneval=2&g_login_type=1&_ste=1` url += `&h5st=${decrypt(Date.now(), '', '', url)}` const options = { 'url': url, //改成变量 "headers": { ... taskurl和newtasksysUrl函数部分: function taskurl(functionId, body = '', stk) { let url = `${JD_API_HOST}/dreamfactory/${functionId}?zone=dream_factory&${body}&sceneval=2&g_login_type=1&_time=${Date.now()}&_=${Date.now()}&_ste=1` url += `&h5st=${decrypt(Date.now(), stk, '', url)}` if (stk) { url += `&_stk=${stk}`; } ... function newtasksysUrl(functionId, taskId, stk) { let url = `${JD_API_HOST}/newtasksys/newtasksys_front/${functionId}?source=dreamfactory&bizCode=dream_factory&sceneval=2&g_login_type=1&_time=${Date.now()}&_=${Date.now()}&_ste=1`; if (taskId) { url += `&taskId=${taskId}`; } if (stk) { url += `&_stk=${stk}`; } //传入url进行签名,加入位置不要错了，不然有些签名参数获取不到值 url += `&h5st=${decrypt(Date.now(), stk, '', url)}` ... 大功告成，发到手机上或者node跑一下试试： 结果非常完美。 docker处理 手机端好办，docker麻烦一点，放到远程或者本地都行，具体看说明jd_scripts docker。docker提供了一个环境变量CUSTOM_SHELL_FILE（你也可以用CUSTOM_LIST_FILE挂载本地定时任务列表,只是更新要手动，要自动还是需要脚本的）可以跑远程shell脚本，jd_scripts docker说明中还有示例，照葫芦画瓢即可。我在github上弄一份自用，你可以参考或者fork一份自己用,地址：jd_scripts-orzlee。只需替换decrypt函数部分常量就好了，shell脚本最好看看我写的注释。然后在环境变量中加入shell脚本地址CUSTOM_SHELL_FILE=https://raw.githubusercontent.com/xxx/xxx.sh即可。 如果用我仓库的远程脚本，务必docker配置环境变量RANDOM_DELAY_MAX=600, 600的值可以自己设置非零正整数，意思是所有脚本每天随机延迟0-600秒执行。 结语 经过不懈努力，终于还是折腾好了，这半个月都是手动点击脚本，人家升级都比我快，没办法手动哪里有脚本那么按时呢！混淆JS浪费不少时间，眼睛都能看瞎。