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内嵌在积木里,直观清晰;
- 魔法变量,积木的输出可以直接 连接到/插入到 后续积木的输入,意味着数据流的传递完全不需要依赖变量,只需要长按、选择变量即可;
- 内容图,弱类型,变量类型隐式自动转换,可以把复杂对象传入几乎任意积木,符合直觉,用户友好。
但是快捷指令也存在一些问题。对于内容图,官方的举例是:
例如,你可以创建包含“获取当前歌曲”操作、“获取音乐的详细信息”操作和“存储到相簿”操作的快捷指令。运行时,此操作系列会自动提取音乐资料库中当前播放歌曲的专辑插图,并将该插图存储到相簿。但是如果后面是无法进行自动类型推导的快速查看,那么用户反倒没法准确控制工作流了。
此外,快捷指令的逻辑控制存在设计上的**“特性”**,比如不允许使用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,因为它是一个简单的独立模块。
// 文件: .../module/notification/SendNotificationModule.kt
package com.chaomixian.vflow.core.workflow.module.notification
import com.chaomixian.vflow.core.module.BaseModule// ... 其他 imports
class SendNotificationModule : BaseModule() { // 模块代码将在这里填充}第 3 步: 定义模块 ID 和元数据
id: 模块的唯一标识符,必须全局唯一,格式为vflow.分类.名称。metadata: 定义模块在 UI 上的表现。
import com.chaomixian.vflow.Rimport com.chaomixian.vflow.core.module.ActionMetadata
// ...override val id = "vflow.notification.send_notification"override val metadata = ActionMetadata( name = "发送通知", description = "在系统通知栏中创建一个自定义通知。", iconRes = R.drawable.rounded_notifications_unread_24, // 使用一个合适的图标 category = "应用与系统" // 这会决定它在动作选择器中的分组)// ...第 4 步: 定义输入参数 (Inputs)
我们的模块需要用户提供通知的“标题”和“内容”。我们通过重写 getInputs() 方法来定义它们。
import com.chaomixian.vflow.core.module.InputDefinitionimport com.chaomixian.vflow.core.module.ParameterTypeimport com.chaomixian.vflow.core.module.TextVariable
// ...override fun getInputs(): List<InputDefinition> = listOf( InputDefinition( id = "title", // 参数的唯一ID name = "标题", // 显示在编辑器中的名称 staticType = ParameterType.STRING, // 参数的基本类型 defaultValue = "vFlow 通知", // 默认值 acceptsMagicVariable = true, // 允许用户连接“魔法变量” acceptedMagicVariableTypes = setOf(TextVariable.TYPE_NAME) // 只接受文本类型的变量 ), InputDefinition( id = "message", name = "内容", staticType = ParameterType.STRING, defaultValue = "这是一条来自 vFlow 的消息。", acceptsMagicVariable = true, acceptedMagicVariableTypes = setOf(TextVariable.TYPE_NAME) ))// ...第 5 步: 定义输出参数 (Outputs)
模块执行后可以产生结果,供后续模块使用。我们的通知模块可以输出一个“是否成功”的布尔值。
import com.chaomixian.vflow.core.module.OutputDefinitionimport com.chaomixian.vflow.core.module.BooleanVariable
// ...override fun getOutputs(step: ActionStep?): List<OutputDefinition> = listOf( OutputDefinition( id = "success", // 输出值的唯一ID name = "是否成功", // 在魔法变量选择器中显示的名称 typeName = BooleanVariable.TYPE_NAME // 输出值的数据类型 ))// ...注意:
getOutputs方法可以接收一个step参数。这意味着你可以根据用户在编辑器里设置的参数,动态地决定模块有哪些输出。
第 6 步: 定义摘要 (Summary)
摘要是显示在工作流卡片上的那段描述性文本,它能让用户一眼看出这个步骤是做什么的。我们使用 PillUtil 来创建带“药丸”效果的富文本。
import android.content.Contextimport com.chaomixian.vflow.core.workflow.model.ActionStepimport com.chaomixian.vflow.ui.workflow_editor.PillUtil
// ...override fun getSummary(context: Context, step: ActionStep): CharSequence { val inputs = getInputs() val titlePill = PillUtil.createPillFromParam( step.parameters["title"], inputs.find { it.id == "title" } ) val messagePill = PillUtil.createPillFromParam( step.parameters["message"], inputs.find { it.id == "message" } )
return PillUtil.buildSpannable(context, "发送通知: ", titlePill, " - ", messagePill)}// ...第 7 步: 实现核心执行逻辑 (execute)
这是模块最核心的部分。execute 是一个 suspend 函数,意味着你可以在其中执行耗时操作。
import android.app.NotificationChannelimport android.app.NotificationManagerimport android.os.Buildimport androidx.core.app.NotificationCompatimport com.chaomixian.vflow.core.execution.ExecutionContextimport com.chaomixian.vflow.core.execution.ExecutionResultimport com.chaomixian.vflow.core.module.ProgressUpdate
// ...override suspend fun execute( context: ExecutionContext, onProgress: suspend (ProgressUpdate) -> Unit): ExecutionResult { // 1. 从 ExecutionContext 获取解析后的参数值 // 如果用户连接了魔法变量,它会存在于 magicVariables 中,否则在 variables 中 val title = (context.magicVariables["title"] as? TextVariable)?.value ?: context.variables["title"] as? String ?: "vFlow 通知"
val message = (context.magicVariables["message"] as? TextVariable)?.value ?: context.variables["message"] as? String ?: ""
// 2. 报告进度,这对于调试很有帮助 onProgress(ProgressUpdate("准备发送通知: $title"))
// 3. 执行核心逻辑 try { val appContext = context.applicationContext val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val channelId = "vflow_custom_notifications"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel(channelId, "自定义通知", NotificationManager.IMPORTANCE_DEFAULT) notificationManager.createNotificationChannel(channel) }
val notification = NotificationCompat.Builder(appContext, channelId) .setContentTitle(title) .setContentText(message) .setSmallIcon(R.drawable.ic_workflows) // 使用一个已有的图标 .setAutoCancel(true) .build()
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
// 4. 返回成功结果,并附带输出值 return ExecutionResult.Success(outputs = mapOf("success" to BooleanVariable(true)))
} catch (e: Exception) { // 5. 如果发生错误,返回失败结果 return ExecutionResult.Failure("执行失败", e.localizedMessage ?: "未知错误") }}// ...第 8 步: 注册模块
最后一步,也是最关键的一步!打开 ModuleRegistry.kt 文件,在 initialize() 方法中,将你的新模块添加进去。
// 文件: .../core/module/ModuleRegistry.kt
import com.chaomixian.vflow.core.workflow.module.notification.SendNotificationModule // 导入你的新模块
// ...object ModuleRegistry { // ... fun initialize() { modules.clear() // ... 其他模块
// 应用与系统 register(SendNotificationModule()) // 在这里注册!
// ... }}恭喜! 你已经成功创建并集成了一个全新的模块。现在重新运行应用,你应该就能在“应用与系统”分类下找到并使用“发送通知”模块了。
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:
{ "id": "user.batch_rename", "name": "批量重命名文件", "description": "为文件名添加前缀", "category": "用户脚本", "author": "YourName", "version": "1.0.0", "inputs": [ { "id": "files", "name": "文件列表", "type": "any", "magic_variable": true }, { "id": "prefix", "name": "前缀", "type": "string", "defaultValue": "new_" } ], "outputs": [ { "id": "renamed_count", "name": "重命名数量", "type": "number" } ], "permissions": ["vflow.permission.SHIZUKU"]}然后是具体的执行。 script.lua:
local files = inputs.fileslocal prefix = inputs.prefixlocal count = 0
-- 遍历文件列表for i, filepath in ipairs(files) do -- 提取文件名 local filename = filepath:match("/([^/]+)$") local new_name = prefix .. filename
-- 执行重命名命令 local result = vflow.shizuku.shell_command({ command = "mv '" .. filepath .. "' /sdcard/" .. new_name })
if result.success then count = count + 1 endend
-- 显示结果vflow.device.toast({ message = "已重命名 " .. count .. " 个文件"})
return { renamed_count = count}怎么跑起来的?
呃,实现了一个vm。
先写这么多吧,后面还想写写学习心得和一些我认为还蛮巧妙的设计
0x03 后续计划?
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时












