taojy123 / KeymouseGo

类似按键精灵的鼠标键盘录制和自动化操作 模拟点击和键入 | automate mouse clicks and keyboard input
http://taojy123.github.io/KeymouseGo
GNU General Public License v2.0
7.15k stars 1.04k forks source link

自定义功能扩展(Cont'd) #119

Closed Monomux closed 2 years ago

Monomux commented 2 years ago
  1. 将文本编码为'utf-8'保存防止保存时将unicode字符转为ascii码 #118
  2. 增加自定义扩展对程序流程控制功能 #113
zhsunlight commented 2 years ago

效率真的高,昨晚才讨论完,今天就实现了。

zhsunlight commented 2 years ago

刚刚看到,这个PR已经支持分支流程了。期待早日合并... 这两天看了 按键精灵 的视频教程(本想安装使用一下,无奈从安装开始,直到运行,杀软一直报警,只好放弃),里面有通过查找颜色来匹配屏幕上位置,也有通过提供图片来查找屏幕位置的功能,这两项功能可以为很多应用场景提供便利。后来搜索python有没有类似的库,还真找到一个 PyAutoGUI,这个库不但提供了颜色、图像匹配查找,还提供几乎所有自动化控制电脑的功能,有了这个库作为补充,KeymouseGo 的控制功能就非常全面了,编写插件常用的库都全了。

参考资料: 1、pypi 网址 https://pypi.org/project/PyAutoGUI/ 2、详解Python中pyautogui库的最全使用方法 https://cloud.tencent.com/developer/article/1741066 3、自动给微信好友发信息示例 pyautogui自动化操作脚本

我模仿参考资料3做了个 自动给微信好友发信息 的功能,确实可行。一行代码就实现按所提供图片在屏幕上查找、且在找到后自动点击的功能。比如:pyautogui.click('send.png')

但如果要按图片查找并返回找到的具体位置,比如:location = pyautogui.locateOnScreen(avatar),则还需要安装 opencv-python 库,这个库比较庞大,会让编译后的 exe 文件增加几乎一倍,达到95M。如果只使用 pyautogui.click('send.png') 这样的图片查找功能,则只需要 pillow 的支持,编译后的exe文件50M(只比现在增加大约5M)。

在新PR的基础上,加上 pyautogui,发现 issue 中提出的所有功能都能在扩展插件中实现。初步评估在扩展能力上,已经全面超越 按键精灵。要是同时加上 requests 库的支持,类似搜索引擎、人工智能之类的接口在插件中随便调用,未来各种能力非凡的插件可期。

以上并不需要修改代码,只在 requirements.txt 文件中添加库就行。是否添加由楼主定夺。

zhsunlight commented 2 years ago

另外一个特性希望能考虑一下:在上一个PR中,插件名称是写在 scripts 中的第一行,但新版中已经不支持这样写,而是直接在 KeymouseGo 界面中选择插件。我建议除了可在KeymouseGo界面上选择插件外,还兼容原有的 scripts 中第一行写插件名称的特性(scripts 中写了插件时,界面上的插件列表自动选中 scripts中的插件,除非用户自己另行选择)。理由是:scripts 跟着插件走,这样每次选择不同的 scripts 运行时,不必再去选择一次插件(如果有多个插件,时间长了之后,自己都不知道该scripts配哪个插件)。而且现在支持分支流程,如果分支流程也有插件,会让分支流程运行变得复杂。

还有一点:分支流程应该在另一个thread 中运行才行,否则父流程插件中的变量可能会被修改而导致混乱。不知道现在是否已经设计成在一个独立的 thread 中运行分支流程。

Monomux commented 2 years ago

但如果要按图片查找并返回找到的具体位置,比如:location = pyautogui.locateOnScreen(avatar),则还需要安装 opencv-python 库,这个库比较庞大,会让编译后的 exe 文件增加几乎一倍,达到95M。如果只使用 pyautogui.click('send.png') 这样的图片查找功能,则只需要 pillow 的支持,编译后的exe文件50M(只比现在增加大约5M)。 在新PR的基础上,加上 pyautogui,发现 issue 中提出的所有功能都能在扩展插件中实现。初步评估在扩展能力上,已经全面超越 按键精灵。要是同时加上 requests 库的支持,类似搜索引擎、人工智能之类的接口在插件中随便调用,未来各种能力非凡的插件可期。

也许可以在发布新版本时提供一个包含上述库的extended版供选择。

我建议除了可在KeymouseGo界面上选择插件外,还兼容原有的 scripts 中第一行写插件名称的特性(scripts 中写了插件时,界面上的插件列表自动选中 scripts中的插件,除非用户自己另行选择)。

这样会涉及插件选择的优先级问题,要兼容之前的特性的话优先级我想是: scripts中指定>gui界面指定=命令行指定>默认

还有一点:分支流程应该在另一个thread 中运行才行,否则父流程插件中的变量可能会被修改而导致混乱。不知道现在是否已经设计成在一个独立的 thread 中运行分支流程。

目前都是在一个线程中执行的,运行子流程时会实例化一个新的插件对象配合子脚本的执行,父流程插件中仅有swap内容会传递给子流程,理论上不会影响父流程插件内容。不过开新线程隔离运行的想法也挺好,之后加上。

这样说来我设计时忽略了一个问题,子流程结束后没有反馈,如果子流程结束后需要将一些数据传递给父流程插件只能通过文件IO实现。

zhsunlight commented 2 years ago

也许可以在发布新版本时提供一个包含上述库的extended版供选择。

这样也挺好,喜欢简单的就下基础版;喜欢折腾的就下 Extended 版。

这样会涉及插件选择的优先级问题,要兼容之前的特性的话优先级我想是: scripts中指定>gui界面指定=命令行指定>默认

这个方案正是我期待的。

目前都是在一个线程中执行的,运行子流程时会实例化一个新的插件对象配合子脚本的执行,父流程插件中仅有swap内容会传递给子流程,理论上不会影响父流程插件内容。不过开新线程隔离运行的想法也挺好,之后加上。

如果执行子流程时,同时也只执行子流程的插件代码,而不会执行父流程的插件代码,反之亦然,那这样的隔离度已经足够了。

zhsunlight commented 2 years ago

从你的 fork 下载了最新版本测试,除了分支流程测试没通过,其它各项改进都测试通过。 奇怪的是,分支执行失败后,提示信息只有一行,看你的代码,明明后续还有更详细提示,但没有打印出来,是否Exception里面又发生了新的 Exception导致的? 附图: image image

zhsunlight commented 2 years ago

启动分支流程的代码: image 我的写法是否有误?sub.txt 是否需要写全路径?变成: path=os.path.join(os.getcwd(),'scripts','sub.txt') raise PushProcess(path, speed=self.speed, runtimes=1, extension='Extension') 不过按这种写法同样提示运行错误。

这个分支流程本身没有自定义的扩展插件,extension='Extension' 是否可以不写?

Monomux commented 2 years ago

onendp方法没有增加相应的try-catch块,这个挺严重的,之后加上。

这个分支流程本身没有自定义的扩展插件,extension='Extension' 是否可以不写?

在构造函数定义里设置了默认值,如果只要使用默认值的话这些参数可以都不写。

zhsunlight commented 2 years ago

把代码放在 onrunbefore 那里执行成功了(其实想放在 onrunafter中,但不成功,原因随后探讨)。 分支流程脚本使用相对路径或绝对路径都可以。

还有一个问题探讨一下:如果一行脚本在 onrunbefore 中返回 False,那这行脚本就不会被执行,所以随后的 onrunafter 也不会被触发。这样设计也合情理,但存在一种情况,以我这个附图为例,我在录制脚本时,自动将鼠标左键点击位置附件区域保存为图片,而在执行脚本时,再调出录制时保存的图片搜索屏幕,搜索到之后,再点击图片,这样处理后,原来录制时点击的脚本行就不用再执行了。这样即使窗口大小和位置与录制时不一致,脚本也能正确运行。但因为原来的脚本没有被执行(返回了 False),导致 onrunafter 不被触发,如果在代码中另行触发该事件,会否导致其它不良后果? image

Monomux commented 2 years ago

如果在代码中另行触发该事件,会否导致其它不良后果?

不会吧,异常可以照常捕获,从整体流程上看也没什么影响。

Monomux commented 2 years ago

等一下,现在的onrunbeforeonrunafter互不影响,onrunbefore返回False只会跳过本行事件的执行,onrunafter还会照常触发的。

zhsunlight commented 2 years ago

等一下,这里的onrunbeforeonrunafter互不影响,onrunbefore返回False只会跳过本行事件的执行,onrunafter还会照常触发的。

重新试了几次,确实放在 onrunafter也执行了。之前只测试一次,有可能某个地方弄错了。 感谢!

Monomux commented 2 years ago

当前的各接口对流程控制的支持情况是这样的:

- onbeforeeachloop onrunbefore onrunafter onafterloop onendp onrecord
Jump × × × ×
Push × × × ×
Add × × × ×
Break × ×
End × ×

计划将支持情况变为:

- onbeforeeachloop onrunbefore onrunafter onafterloop onendp onrecord
Jump × × × ×
Push ×
Add ×
Break × ×
End × ×

不过现在的逻辑里还存在一个问题:像onbeforeeachlooponrunbefore都有返回值,现在在其中raise Exception会导致之后的执行被跳过,相当于返回False,对于这两个方法在其中raise Exception还需要传递返回值以决定是否执行本行脚本。

zhsunlight commented 2 years ago

分支流程已测试完成,包括自带扩展的分支流程,均通过测试。

onendp方法没有增加相应的try-catch块,这个挺严重的,之后加上。

把这个也处理好,这个版本就很满意了。

zhsunlight commented 2 years ago

当前的各接口对流程控制的支持情况是这样的:

  • onbeforeeachloop onrunbefore onrunafter onafterloop onendp onrecord Jump × √ √ × × × Push × √ √ × × × Add × √ √ × × × Break √ √ √ √ × × End √ √ √ √ × × 计划将支持情况变为:

  • onbeforeeachloop onrunbefore onrunafter onafterloop onendp onrecord Jump × √ √ × × × Push √ √ √ √ √ × Add √ √ √ √ √ × Break √ √ √ √ × × End √ √ √ √ × × 不过现在的逻辑里还存在一个问题:像onbeforeeachlooponrunbefore都有返回值,现在在其中raise Exception会导致之后的执行被跳过,相当于返回False,对于这两个方法在其中raise Exception还需要传递返回值以决定是否执行本行脚本。

你很专业,这么快就做好表格,一目了然。

不过现在的逻辑里还存在一个问题:像onbeforeeachlooponrunbefore都有返回值,现在在其中raise Exception会导致之后的执行被跳过,相当于返回False,对于这两个方法在其中raise Exception还需要传递返回值以决定是否执行本行脚本。

你说的是 onbeforeeachlooponrunbefore 两个事件内部 raise的后续代码没被执行吗?这个确实有一点缺陷。可能要通过传递参数解决,比如 resume=True之类的。

Monomux commented 2 years ago

仔细想了想现在的PushAdd本来是想做成类似于压栈执行子流程的效果,但在raise异常后方法即被终止,没有返回值同时也跳过了一些内容,不如直接取消这两个异常而直接在扩展中实现相关功能。 现在的RunScriptClass定义了一个run_sub_script方法,传入的是PushProcess对象和父扩展对象用于执行相应的子流程,可以修改一下直接供自定义扩展内调用。至于AdditionProcess的功能,如果封装好了ScriptEvent对象,可以直接调用其execute方法执行操作,现在的处理方式也只是在catch Exception后遍历一遍列表来执行每个事件,完全可以放在自定义扩展内。 这样的话就不会有因raise异常导致执行过程不连贯的问题了。

zhsunlight commented 2 years ago

我测试一下新的流程控制功能。 另外,建议readme 里下图示例代码修改一下init里变量赋值。我昨晚就掉坑里,录制的脚本明明在GUI设置了运行一次,结果每次都执行了三次,百思不得其解。直到今早才查出问题所在。建议给那些变量赋予传递过来的值,或者把代码注释掉,在其前面增加一行提示:可直接修改此值覆盖GUI设置值。 image

Monomux commented 2 years ago

那我给示例的代码都加上注释,示例代码上面说明了限制执行次数为3次,在这里是想体现这个功能。

zhsunlight commented 2 years ago

那我给示例的代码都加上注释

那最好不过了,有注释一看就明白。 我觉得还是把代码注释掉,再加以说明比较好,初学者一下子掌握不了那么多。 image

zhsunlight commented 2 years ago

复制示例扩展代码保存到文件后,不能正常执行,也不报错。(在GUI中选择MyExtension扩展后,按录制脚本快捷键,KeymouseGo画面没有出现正在录制的信息,画面没有任何变化。) image image

分析原因后,是由于示例代码的注释缩进不一致导致,如下图: image 处理缩进后,运行正常: image 这种错误应该是在加载模块时出现,由于代码缩进错误而导致不能正常加载,不知道能否监控到,如能获知加载状态,在 logger中提示一下,比如:加载xx扩展插件失败,请检查代码缩进是否正确,命名是否规范。这些提示都有助于修正错误。

zhsunlight commented 2 years ago

测试过分支流程,没什么问题了。也测试过另外几项功能 raise BreakProcess(),raise EndProcess(),raise JumpProcess(),均正常工作。

有个疑问:如果子流程还没有执行结束,终止执行快捷键是不起作用的。我测试时,让子流程执行到第5行后,又 raise JumpProcess(2),回到第2行执行,这样就进入无限循环中,看见鼠标在屏幕上反复在某个区域内移动,想终止执行也没有办法,只好重启电脑。我这个例子没有什么实际意义,只是为了测试而故意让它进入无限循环。

Monomux commented 2 years ago

这种错误应该是在加载模块时出现,由于代码缩进错误而导致不能正常加载,不知道能否监控到,如能获知加载状态,在 logger中提示一下,比如:加载xx扩展插件失败,请检查代码缩进是否正确,命名是否规范。这些提示都有助于修正错误。

给实例化扩展的方法加了装饰器@logger.catch,现在可以从日志中查看加载失败的原因了。

Monomux commented 2 years ago

如果子流程还没有执行结束,终止执行快捷键是不起作用的。

run_sub_script方法还有一个参数,传入当前线程对象,在执行过程中如果线程对象不为None则会有快捷键的检测过程,示例里没有写,导致快捷键不起作用。

zhsunlight commented 2 years ago

给实例化扩展的方法加了装饰器@logger.catch,现在可以从日志中查看加载失败的原因了

run_sub_script方法还有一个参数,传入当前线程对象,在执行过程中如果线程对象不为None则会有快捷键的检测过程,示例里没有写,导致快捷键不起作用。

你太强了!我找到的所有问题你都有解决办法。

zhsunlight commented 2 years ago

run_sub_script方法还有一个参数,传入当前线程对象,在执行过程中如果线程对象不为None则会有快捷键的检测过程,示例里没有写,导致快捷键不起作用。

刚刚找了一圈,没有在扩展变量中找到当前线程对象,查看UIFunc也不明白如何传递。正想请你在方便的时候更新一下示例,这么快你就完成了修改。

zhsunlight commented 2 years ago

经测试,快捷键也可以在子流程内生效,通过快捷键,可让子流程中止执行。

目前PR版本已经很完善,从录制脚本到执行脚本,各个环节都可以通过插件进行干预,甚至完全取代内置的运行逻辑,具备实现完全按个性需求运行的机制。issues列表中提到的个性需求,除了游戏类需求由于反外挂原因没办法实现外,其它绝大部分都可以通过插件实现。

ZutJoe commented 2 years ago

插件的使用是通过自己编写程序来实现吗, 还是说是通过下载其他人写的接入到里面

Monomux commented 2 years ago

插件的使用是通过自己编写程序来实现吗, 还是说是通过下载其他人写的接入到里面

可以自己写,也可以下载他人提供的。

ZutJoe commented 2 years ago

可以自己写,也可以下载他人提供的。

刚刚看看了一下您fork的仓库的readme, 我不知道我的理解对不对, 就是我们如果要使用插件, 就需要将源码clone下来, 然后在plugins/目录下编写相应的拓展程序是嘛?

如果插件是按照上面的方式的话, 我觉得或许可以提供已经打包好的程序, 通过加载拓展程序文件的方式来增添拓展会不会更好, 但是我不知道这种方式的难度大不大, 因为可能的问题会包括:

  1. 文件的查找
  2. 程序中没有的import的类的类的加载
  3. 如果拓展程序还包含其他文件应该怎么做

我现在所能想到的只有这几点, 我认为如果使用这种加载拓展程序文件的方式, 可能能让使用的人更多???或是更加方便????

Monomux commented 2 years ago

就是我们如果要使用插件, 就需要将源码clone下来, 然后在plugins/目录下编写相应的拓展程序是嘛?

无论是否打包都可以使用这个功能。插件功能是通过动态加载plugins/目录下的扩展取得相应模块实例,并在程序执行的相应阶段调用实例相关方法实现的。

  1. 文件的查找
  2. 如果拓展程序还包含其他文件应该怎么做

这些坑其实都踩过了,在前两个PR和这个PR内有涉及。

2.程序中没有的import的类的类的加载

这是个比较麻烦的问题,毕竟程序一旦打包,包含的库就确定了,一种方案是打包时提供一个包含更多可能使用的库的extended版供更多功能的实现。另一种方案是下载相应库的源码(文件多,比较麻烦)或动态链接库文件(pyc,pyd等),在插件内使用动态加载的方法引入。

ZutJoe commented 2 years ago

无论是否打包都可以使用这个功能。插件功能是通过动态加载plugins/目录下的扩展取得相应模块实例,并在程序执行的相应阶段调用实例相关方法实现的。

我记得打包完之后只有一个exe文件呀😂, 可能是我太长时间没用, 忘记了, 不好意思

这些坑其实都踩过了,在前两个PR和这个PR内有讨论。

好的, 我去学习一下

这是个比较麻烦的问题,毕竟程序一旦打包,包含的库就确定了,一种方案是打包时提供一个包含更多可能使用的库的extended版供更多功能的实现。另一种方案是下载相应库的源码(文件多,比较麻烦)或动态链接库文件(pyc,pyd等),在插件内使用动态加载的方法引入。

这些只是我的想法啦, 有点纸上谈兵了可能

Monomux commented 2 years ago

我记得打包完之后只有一个exe文件呀😂

打包后是只有一个exe文件,在第一次启动时会在当前目录下生成scriptsplugins文件夹。如果用户有自定义的插件,就可以放在plugins目录下供程序加载;如果没有自定义插件,程序就会加载默认的插件(在assets/plugins/目录下,按照pyinstaller的机制会存放在C盘的临时文件夹中),默认什么扩展功能都不做,效果与不加载插件一样。

ZutJoe commented 2 years ago

打包后是只有一个exe文件,在第一次启动时会在当前目录下生成scriptsplugins文件夹。如果用户有自定义的插件,就可以放在plugins目录下供程序加载;如果没有自定义插件,程序就会加载默认的插件(在assets/plugins/目录下,按照pyinstaller的机制会存放在C盘的临时文件夹中),默认什么扩展功能都不做,效果与不加载插件一样。

好的, 太长时间没使用了, 不好意思😂

taojy123 commented 2 years ago

@Monomux
这是个伟大的PR~ 我准备合入了,还有细节代码需要补充的吗? 合入后,我找时间打包发布 5.0 版本。

Monomux commented 2 years ago

我再加一条,针对 #124 的问题把提示音播放也放到插件中执行,其余的问题可以日后解决。

zhsunlight commented 2 years ago

今天试了把一些插件常用的工具类函数放在一个单独的tools.py文件中,想在各个插件中可以共用。 试了在插件的开头使用 from .tools import 不成功; 接着试了 from .plugins.tools import 也不成功。 image

参考UIFunc.py中加载模块的方式,最后这样搞定。 image

可否在Extension中增加一个函数 load_modual,这样在插件中只需要 tools=load_modual("tools.py) 就行。不过这好像也只能解决单个文件的加载,如果要通过这种方式加载一个库,比如 pytesseract (文字识别库),还是不行。只能自己编译把这种库打包进exe文件中。 @Monomux 有其他解决方案么?

Monomux commented 2 years ago

可否在Extension中增加一个函数 load_modual,这样在插件中只需要 tools=load_modual("tools.py) 就行。不过这好像也只能解决单个文件的加载,如果要通过这种方式加载一个库,比如 pytesseract (文字识别库),还是不行。只能自己编译把这种库打包进exe文件中。

好问题,对于打包过的程序其包含的模块已经确定,要再引入额外模块需要提供相应模块编译过的.pyc文件或是动态链接库文件(.pyd, .so),然后通过importlib动态加载。

zhsunlight commented 2 years ago

好问题,对于打包过的程序其包含的模块已经确定,要再引入额外模块需要提供相应模块编译过的.pyc文件或是动态链接库文件(.pyd, .so),然后通过importlib动态加载。

如果动态修改 python的库路径,是否可行?在原有的库路径基础上,增加一些库路径。 参考资料 https://blog.csdn.net/tycoon1988/article/details/39502505

zhsunlight commented 2 years ago

有可能 importlib.import_module(...) 也能实现整个库的动态加载。

Monomux commented 2 years ago

如果动态修改 python的库路径,是否可行?在原有的库路径基础上,增加一些库路径。

尝试了一下在程序启动时添加了到plugins文件夹的库路径,在插件中使用import导入自己写的测试库,可以实现加载。

有可能 importlib.import_module(...) 也能实现整个库的动态加载。

这个也可以,操作相对麻烦一点。

但是这样操作下来加载库还是很麻烦,我尝试在程序目录下添加libs文件夹,从pip安装路径下复制了pytesseract库放在libs下。在插件内使用import引入模块,存在依赖库不存在的问题,在放入packagingPIL等库后又引入了新的依赖问题,整体效果不如重新打包。

Monomux commented 2 years ago

KeymouseGo模块加入了add_lib_path函数,可以添加库搜索路径。

示例,添加程序目录下的plugins文件夹到搜索路径

import os
from KeymouseGo import add_lib_path
add_lib_path([
    os.path.join(os.getcwd(), "plugins")
])

程序启动时已默认添加plugins文件夹到搜索路径,无需重复添加。

zhsunlight commented 2 years ago

程序启动时已默认添加plugins文件夹到搜索路径,无需重复添加。

现在如果要在插件中添加 plugins/tools.py 文件,可以使用 from plugins.tools import * 这种写法吗?或者麻烦你在readme里写一下正确的写法。

zhsunlight commented 2 years ago

已经下载最新版测试,完全按readme方式工作,超赞。这一改动非常合理地满足插件之间共用代码的需求。

如上所述,大型的库,由用户自己导入编译也不难。使用pip安装好库,再编译打包,比编译好之后再找齐库的各种依赖放在自已的目录下要方便得多。所以这个需求就不考虑了。

另外一点小小的请求,不知道是否方便实现。就是我看到 KeymouseGo 界面上的大多数参数都记录在 config.ini中,唯独当前的脚本文件名没有记录,所以每次打开,都要进行一次选择才能找到上次运行的脚本。能否也象其它参数一样将当前运行的脚本文件名记录在 config.ini中?然后每次打开时,自动选择该脚本文件。如果比较麻烦或其它原因不便改动,保持现状也没多大麻烦。 image

zhsunlight commented 2 years ago

还有一个隐藏得比较深的bug,不知道是否缓存造成。 我有一个插件,按下图方式导入 plugins/tools.py 后,一切功能正常运行,日志显示也正常。 image image 然后我故意注释掉, from tools import * 这一行,并且 logger 也改成: logger.info('Import ClearCookies3') 变成如下所示代码: image

保存插件后,在不退出 KeymouseGo前提下,按快捷键执行,插件居然执行正常! 输出日志如下: image 红框表示确实执行的是修改后的插件(修改前日志输出的是 Import ClearCookies2,修改后输出的是 Import ClearCookies3),

但是,注释掉 from tools import * ,确实是不正确的(会导致onrunbefore中的fix_index函数找不到),重启 KeymouseGo 后,无法执行,日志显示如下: image

有可能是 importlib 的缓存导致,但奇怪的是,为何只缓存正确的代码,对出错的代码却不缓存。

这个问题隐藏得比较深,如按上述方式无法重现,请告诉我,我把示例代码发你。

Monomux commented 2 years ago

有可能是 importlib 的缓存导致,但奇怪的是,为何只缓存正确的代码,对出错的代码却不缓存。

查了一下python的文档the-module-cache,是导入的缓存机制导致的,第一次加载扩展后运行环境中已包含tools模块,第二次加载扩展时即使不标明导入tools亦可调用其中的内容。

zhsunlight commented 2 years ago

查了一下python的文档the-module-cache,是导入的缓存机制导致的,第一次加载扩展后运行环境中已包含tools模块,第二次加载扩展时即使不标明导入tools亦可调用其中的内容。

如果是这样,那这个问题仅限于对 import 语句的影响,在readme适当提醒即可。热加载运行方式相当于在python的命令行中工作。

taojy123 commented 2 years ago

我先合入了,其他问题后面再解决✌️