通过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切换显示器输入源
前言 最近倒腾Java，发现JVM并行收集器在还有大量堆内存的时候就开始触发老年代的回收，通过GC日志发现每小时都会执行GC和Full GC回收，回收是通过System.gc()调用。 那每小时都调用一次Full GC，我添加启动参数-Xms11g -Xmx11g分配11G堆内存只是为了好看吗？每次Full GC耗时在600ms，并行收集器老年代内存使用还不足5%，真是发神经了！ 过程就不做记录了，无聊又蛋疼。以下是在使用了RMI或者其他NIO对象的情况。 RMI DGC每小时触发一次Full GC 原因：DGC代码中调用System.gc()导致并行收集器触发Full GC。 解决办法： 修改DGC调用System.gc()执行时间，默认一小时，单位ms。启动参数添加： -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 禁止程序中的System.gc()，添加启动参数： -XX:+DisableExplicitGC 使用支持并发的收集器（CMS、G1、ZGC或Shenandoah），并且添加启动参数： -XX:+ExplicitGCInvokesConcurrent 方法1，治标不治本，延长时间触发Full GC会导致STW时间更长，可以配合并行收集器使用-XX:+ExplicitGCInvokesConcurrent来控制频率。 方法3，启用-XX:+ExplicitGCInvokesConcurrent参数可以尝试在垃圾收集时使用并发收集来减少STW时间，从而提高应用程序的响应性。 不建议使用-XX:+DisableExplicitGC来限制System.gc()。 因为从Java 7开始，Java提供了基于NIO的RMI实现，称为NIO-based RMI。这种实现利用了NIO的非阻塞I/O模型，能够更高效地处理大量的并发连接，减少了线程资源的消耗，并提高了性能和可伸缩性。通过使用NIO，NIO-based RMI能够更好地适应高并发和大规模的网络通信场景。 DirectByteBuffer是一种在堆外内存中分配的ByteBuffer，它与NIO密切相关，因为它可以直接映射到操作系统的内存中，从而避免了在Java堆和本地内存之间进行复制。在NIO编程中，使用DirectByteBuffer可以提高I/O操作的性能和效率，特别是在处理大量数据时。在基于NIO的RMI实现中，使用NioServerSocketChannel和NioEventLoopGroup通常涉及到DirectByteBuffer的使用。 虽然 DirectByteBuffer 对象本身是由 JVM 管理的，它们存储在 Java 堆内存中，但是 DirectByteBuffer 对象所持有的实际数据存储在堆外内存中。 JVM 通过 DirectByteBuffer 对象来管理对堆外内存的访问和操作。当 DirectByteBuffer 对象被垃圾回收时，它所持有的堆外内存也会随之被释放。 因此，触发 Full GC 通常是为了回收已经失去引用的 DirectByteBuffer 对象，进而释放掉它们所占用的堆外内存。 Full GC 在回收过程中会扫描整个堆内存，包括其中的对象和引用。当发现 DirectByteBuffer 对象已经没有引用指向时，JVM 就会将其标记为可回收， 待下次 Full GC 执行时进行回收。这样，间接地通过 Full GC 回收了 DirectByteBuffer 对象，也就释放了相应的堆外内存。 如果禁用了System.gc()，那么不会及时的清理 DirectByteBuffer 或者其他 NIO 对象导致堆外内存也不会释放。虽然最后也可能因为JVM堆内内存不足触发Full GC 来释放，但没必要冒险。参阅 监控程序调用System.gc()堆栈 可以使用 async-profiler 跟踪System.gc调用者： 预先开始分析： profiler.sh start -e java.lang.System.gc <pid> 发生一种或多种System.gc情况后，停止分析并打印堆栈跟踪： --- Execution profile --- Total samples : 6 Frame buffer usage : 0.0007% --- 4 calls (66.67%), 4 samples [ 0] java.lang.System.gc [ 1] java.nio.Bits.reserveMemory [ 2] java.nio.DirectByteBuffer.<init> [ 3] java.nio.ByteBuffer.allocateDirect [ 4] Allocate.main --- 2 calls (33.33%), 2 samples [ 0] java.lang.System.gc [ 1] sun.misc.GC$Daemon.run 在上面的示例中，System.gc从两个地方调用了 6 次。这两种情况都是 JDK 内部强制进行垃圾回收的典型情况。第一个来自java.nio.Bits.reserveMemory.当没有足够的可用内存来分配新的直接 ByteBuffer 时（由于-XX:MaxDirectMemorySize限制）， JDK 会强制进行 Full GC 回收无法访问的直接 ByteBuffer。 第二个来自 GC Daemon 线程。这由 Java RMI 运行时定期调用。例如，如果您使用 JMX 远程，则每小时自动启用一次定期 GC。 这可以 通过-Dsun.rmi.dgc.client.gcInterval系统属性进行调整。 结语 起初我很不明白为什么DGC代码里面会有System.gc()，我一直以为堆外内存不归JVM管理，那调用System.gc()又有什么用呢？又是问ChatGPT又是各种搜索，才发现虽然JVM不管理堆外内存，但是管理使用堆外内存的对象，System.gc()是为了及时释放掉不再使用的堆外内存持有对象，堆内释放了，操作系统就会释放堆外内存了！
探索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弄挂了，还好有备份！