vFlow 幕后故事

vFlow 幕后故事
不是炒米线Timmy给我发了他的博客编译器幕后,woc好帅,我也要写,写vFlow的。
vFlow 是一款为 Android 平台设计的、强大且高度可扩展的自动化工具。它允许你通过图形化界面,将一系列“动作模块”自由组合成强大的“工作流”,从而自动完成各种日常的、重复性的屏幕操作任务。
![]() | ![]() | ![]() |
| 首页 | 工作流编辑 | 模块管理器 |
0x00. 起因
2025年4月,在Apple Watch S5上的浏览器上看见一加13T,顿时被它的美貌所吸引:哇!完美的Pixel!完美的粉色!完美的Deco设计!
2025年6月份购入一部一加13T,折腾完root,又感觉索然无味…
2025年7月,机缘巧合下接了一个外包,要求开发一款安卓端自动化宏工具,这就是vFlow的前身,
vClick。在开发vClick的过程中,产生了一些vFlow的最初构思,但由于与vClick需求不符,手头的开发开始偏离我的设想。2025年8月30日,vClick补齐OCR相关功能,vFlow项目新建文件夹。考虑的vClick本身的包袱和局限,没有选择分支,而是完全从头开始。希望打造一个 模块化的、低耦合高内聚的、具备拓展能力的、手机触屏友好的 安卓自动化工具。
0x01. 为什么 vFlow
我的主力是iPhone,因此快捷指令的使用已经习以为常。连接Wi-Fi自动关闭小火箭🚀,断开Wi-Fi自动启动小火箭🚀。但当我尝试把主力机切换到这部一加13T时,我发现没有平替。
安卓现存的自动化软件里,Tasker过于负责&陈旧,MacroDroid节点式的编辑体验不适合手机,ShortX依赖XPosed框架且不开源,AutoCMD+不开源&流程编辑嵌套很深。
总结下来,都没有快捷指令用的舒服。我思考了很久,从vClick思考到vFlow,我认为快捷指令好用,主要有三个原因:
- 积木式编程,摘要内容符合自然语义,而且参数会以Pill药丸💊(暂且这么叫吧)的形式inline内嵌在积木里,直观清晰;
- 魔法变量,积木的输出可以直接 连接到/插入到 后续积木的输入,意味着数据流的传递完全不需要依赖变量,只需要长按、选择变量即可;
- 内容图,弱类型,变量类型隐式自动转换,可以把复杂对象传入几乎任意积木,符合直觉,用户友好。
但是快捷指令也存在一些问题。对于内容图,官方的举例是:
1 | 例如,你可以创建包含“获取当前歌曲”操作、“获取音乐的详细信息”操作和“存储到相簿”操作的快捷指令。运行时,此操作系列会自动提取音乐资料库中当前播放歌曲的专辑插图,并将该插图存储到相簿。 |
但是如果后面是无法进行自动类型推导的快速查看,那么用户反倒没法准确控制工作流了。
此外,快捷指令的逻辑控制存在设计上的“特性”,比如不允许使用While。同时,对于如果积木,难以一眼区分层级。
那么vFlow是怎么做的呢?
- 积木式编程,摘要内容符合自然语义,而且参数会以Pill药丸💊(暂且这么叫吧)的形式inline内嵌在积木里,通过缩进区分执行层级,直观清晰;
- 魔法变量,积木的Editor可以通过变量选择器选择魔法变量&命名变量;
- VObject,采用面向对象思想,对于复杂对象(比如VImage),声明其拥有的属性(比如raw, path, width, length, size, etc.),并且属性本身也是VObject,可以实现
{{image.path.uppercase.lenth}}这样的链式调用
0x02 Talk is cheap
架构设计
从v1.4.x开始,vFlow由两部分构成:App和Core。
App负责工作流管理、工作流编辑,以及主要功能的实现;Core是一个由app_process启动的**.dex,负责反射hidden api以及高权限功能的具体执行。App与Core通过Socket + JSON-RPC 2.0**实现通讯。
其中Core端为主从架构,app_process直接启动的是Master,Master会Fork出ShellWorker和RootWorker(如果Master以Root权限启动)。
从Root(uid=0)权限drop到Shell(uid=2000),并不是件简单的事情,也不只是setuid。如果Master以Root权限启动,则会通过vflow_shell_exec这个native二进制包装ShellWorker,此时ShellWorker的大部分Context会与shell一致。然后ShellWorker会使用scrcpy的Workaround相关代码,实现com.android.shell的伪装,这之后,ShellWorker就拥有了com.android.shell的manifest里定义的全部权限。
怎么个模块化?
vFlow的所有模块都定义在App端。只需要override一个BaseModule就可以轻松实现一个新的模块(AI很适合干这个事情)。这里直接应用GitHub仓库里的CONTRIBUTION.md:
vFlow 模块开发指南
欢迎来到 vFlow 的世界!本指南将带你从零开始,一步步学习如何为 vFlow 创建一个全新的功能模块。vFlow 的核心魅力在于其高度的可扩展性,而你开发的每一个模块都将成为这个生态系统的一部分。
0. 模块是什么?
在 vFlow 中,模块 (Module) 是自动化的最小功能单元。它封装了一个具体的操作,比如“延迟 1 秒”、“点击屏幕上的某个位置”或“判断一个条件是否成立”。用户在编辑器中看到的每一个可拖拽的卡片,背后都对应着一个模块。
一个模块的职责包括:
- 自我描述: 告诉系统它的名字、图标、分类等信息。
- 定义参数: 声明它需要哪些输入(Inputs)才能工作。
- 声明产出: 声明它执行后会产生哪些输出(Outputs)。
- 执行核心逻辑: 在工作流运行时,执行真正的自动化任务。
- (可选)提供自定义 UI: 为参数编辑提供比标准输入框更丰富的界面。
1. 项目结构概览
理解项目结构是开始贡献的第一步。vFlow 项目主要分为以下几个核心目录:
main/java/com/chaomixian/vflow/core/: 项目的核心逻辑。execution/: 工作流执行器 (WorkflowExecutor)、执行上下文 (ExecutionContext) 和 Lua 脚本执行器 (LuaExecutor)。logging/: 日志管理器,包括面向用户的执行日志 (LogManager) 和开发者调试日志 (DebugLogger)。module/: 模块系统的基础定义,如ActionModule接口、BaseModule基类和各种数据类型 (VariableTypes.kt)。workflow/: 工作流的核心管理 (WorkflowManager) 和模块的具体实现。module/: 所有模块的源代码,按功能分类(data,file,logic,system,triggers等)。
services/: 后台服务,如无障碍服务 (AccessibilityService)、触发器服务 (TriggerService) 和 Shizuku 服务 (ShizukuUserService,ShizukuManager)。ui/: 应用的所有用户界面(Activity 和 Fragment),按功能划分。main/: 主界面,包含底部导航和首页、设置等。workflow_editor/: 工作流编辑器界面。workflow_list/: 工作流列表界面。
permissions/: 权限管理相关的逻辑和界面 (PermissionManager,PermissionActivity)。
2. 准备工作:理解核心概念
在开始编码前,我们先了解几个关键的类和接口:
ActionModule.kt: 所有模块都必须实现的核心接口。它定义了模块的“契约”,规定了模块必须具备的所有能力。BaseModule.kt: 一个抽象基类,提供了ActionModule接口的默认实现。对于大多数简单的、独立的模块(如“延迟”、“显示 Toast”),直接继承它会非常方便。BaseBlockModule.kt: 专用于创建“积木块”类型模块的基类(如If...EndIf,Loop...EndLoop)。它自动处理了创建和删除整个代码块的复杂逻辑。definitions.kt: 这个文件包含了所有重要的数据类,是你开发模块时一定会用到的:ActionMetadata: 模块的元数据(名称、描述、图标、分类)。InputDefinition: 定义一个输入参数(ID、名称、类型、默认值等)。OutputDefinition: 定义一个输出参数(ID、名称、类型)。ExecutionContext: 模块执行时获取所有上下文信息(如参数值、服务实例)的“上帝对象”。
ModuleRegistry.kt: 模块注册表。你开发完的模块需要在这里“登记”,应用才能发现并使用它。
3. 实战:创建一个“发送通知”模块
让我们通过一个具体的例子来学习。目标是创建一个新模块,它可以在系统通知栏发送一条指定内容的通知。
第 1 步: 创建模块文件
在 main/java/com/chaomixian/vflow/core/workflow/module/notification/ 目录下创建一个新的 Kotlin 文件,命名为 SendNotificationModule.kt。
第 2 步: 继承 BaseModule
让我们的新类继承自 BaseModule,因为它是一个简单的独立模块。
1 | // 文件: .../module/notification/SendNotificationModule.kt |
第 3 步: 定义模块 ID 和元数据
id: 模块的唯一标识符,必须全局唯一,格式为vflow.分类.名称。metadata: 定义模块在 UI 上的表现。
1 | import com.chaomixian.vflow.R |
第 4 步: 定义输入参数 (Inputs)
我们的模块需要用户提供通知的“标题”和“内容”。我们通过重写 getInputs() 方法来定义它们。
1 | import com.chaomixian.vflow.core.module.InputDefinition |
第 5 步: 定义输出参数 (Outputs)
模块执行后可以产生结果,供后续模块使用。我们的通知模块可以输出一个“是否成功”的布尔值。
1 | import com.chaomixian.vflow.core.module.OutputDefinition |
注意:
getOutputs方法可以接收一个step参数。这意味着你可以根据用户在编辑器里设置的参数,动态地决定模块有哪些输出。
第 6 步: 定义摘要 (Summary)
摘要是显示在工作流卡片上的那段描述性文本,它能让用户一眼看出这个步骤是做什么的。我们使用 PillUtil 来创建带“药丸”效果的富文本。
1 | import android.content.Context |
第 7 步: 实现核心执行逻辑 (execute)
这是模块最核心的部分。execute 是一个 suspend 函数,意味着你可以在其中执行耗时操作。
1 | import android.app.NotificationChannel |
第 8 步: 注册模块
最后一步,也是最关键的一步!打开 ModuleRegistry.kt 文件,在 initialize() 方法中,将你的新模块添加进去。
1 | // 文件: .../core/module/ModuleRegistry.kt |
恭喜! 你已经成功创建并集成了一个全新的模块。现在重新运行应用,你应该就能在“应用与系统”分类下找到并使用“发送通知”模块了。
4. 进阶主题
积木块模块 (Block Module)
对于需要包含其他模块的逻辑块(如 If 和 Loop),你应该继承 BaseBlockModule。
stepIdsInBlock: 定义组成这个积木块的所有模块 ID 列表(开始、中间、结束)。pairingId: 定义一个唯一的配对 ID,用于将这些模块关联起来。
vFlow 会自动处理积木块的创建(一次性添加所有部分)和删除(一次性删除整个块)。
动态输入 (getDynamicInputs)
IfModule 是一个很好的例子。它的输入参数会根据第一个“输入”连接的变量类型而改变,从而只显示适用的比较条件。如果你需要这种动态行为,可以重写 getDynamicInputs 方法。
自定义 UI (ModuleUIProvider)
对于需要复杂编辑界面的模块(例如“HTTP 请求”模块中的字典编辑器),你可以实现 ModuleUIProvider 接口,并重写模块的 uiProvider 属性。这允许你完全控制参数的编辑界面,实现标准控件无法完成的功能。
怎么拓展?
vFlow支持像KernelSU/Magisk那样通过zip包来安装模块拓展(用户模块)。当前支持使用Lua来编写拓展(即将支持JavaScript)。
所有的vFlow模块都会注入到Lua的运行时,这意味着你可以像调用builtin函数一样调用vFlow的功能,比如vflow.device.toast({ message = "Hello, World! "}),就可以显示一个Toast。(是不是很像Auto.js?)
举个例子:批量重命名文件
首先定义模块的元数据,这包括模块信息和输入输出的定义:
manifest.json:
1 | { |
然后是具体的执行。
script.lua:
1 | local files = inputs.files |
怎么跑起来的?
呃,实现了一个vm。






