Skip to content

第 1 章:从自动化到智能化

我遇到了什么问题

2025 年初,我想做一个本地的"扣子"。

为什么是"本地"?因为 Coze 很好,但数据要上传到云端。对于企业用户来说,这不可接受——你的客户数据、内部流程、账号密码,都要经过别人的服务器。

我想做的是:同样的拖拽编排体验,但跑在你自己的电脑上,数据不出本机。

这个想法听起来简单,但一上来就面临一个选择:从头造一个,还是站在别人肩膀上?

我做了什么决策

第一个决策:前端不造轮子,直接用扣子

做本地扣子,第一件事是画布——用户拖拽编排流程的那个界面。

这个轮子太大了。节点拖拽、连线、参数配置、实时预览……一套完整的可视化编辑器,光前端就得干几个月。

我的判断是:画布是体验层,不是核心壁垒。Coze 的画布已经做得很好了,没必要重写。

所以第一个决策很明确——前端直接用扣子。用户在 Coze 上画好工作流,导出 JSON 定义,这就是"前端"给我的全部东西。

用户在扣子画布上拖拽编排
  ↓ 导出
工作流 JSON 定义
  ↓ 交给
本地后端执行

这个决策的核心逻辑是:我要解决的问题不是"怎么画流程图",而是"怎么在本地跑流程图"。 画布是入口,执行才是价值。

第二个决策:后端用 C#,嵌入式架构

前端定了,接下来是核心问题:后端怎么接住 Coze 导出的 JSON,在本地把它跑起来?

我选了 C#。原因不是"C# 最好",而是:

  • 我的技术栈是 .NET,效率最高
  • 需要嵌入式运行(不依赖外部服务)
  • 需要跟 Windows 桌面端深度集成

后端的核心任务只有一个:把 Coze 的工作流 JSON 变成可执行的程序。

我选了 WorkflowCore 作为工作流引擎。原因很简单:

  • 轻量,嵌入式运行,不需要额外部署
  • 支持 SQLite 持久化,适合本地场景
  • 支持暂停/恢复/中断,适合长时间运行的流程

但 WorkflowCore 本身是个"裸引擎"——它只负责按定义执行流程,不管流程定义从哪来,不管执行状态怎么查,不管节点怎么扩展。

我做的事情是给它包了一层,让它能"听懂" Coze 的 JSON:

csharp
public interface IWorkflowService
{
    // 注册工作流定义(从 Coze JSON 解析 → 引擎可执行)
    Task<WorkflowRegisterResult> RegisterDefinitionAsync(CozeWorkflowDefinition definition);
    
    // 启动工作流(传入定义ID + 输入数据 → 返回实例ID)
    Task<WorkflowStartResult> StartWorkflowAsync(
        string workflowDefineId, string version, 
        Dictionary<string, object> input, ExecuteConfig config = null);
    
    // 暂停/恢复/停止(控制正在运行的工作流)
    Task<WorkflowResult> SuspendWorkflowAsync(string workflowId);
    Task<WorkflowResult> ResumeWorkflowAsync(string workflowId);
    Task<WorkflowResult> TerminateWorkflowAsync(string workflowId);
    
    // 单节点调试(绕过工作流,直接跑一个节点)
    Task<NodeDebugResult> InvokeNodeDebugDirect(string nodeConfig, Dictionary<string, object> input);
}

这层包装解决了一个核心问题:Coze 的 JSON 格式和 WorkflowCore 的执行模型之间,需要一个翻译层。

扣子 JSON 本地执行链路

扣子画布导出 JSON → 翻译层解析 → WorkflowCore 注册 → 本地执行

第三个决策:桌面端用 WPF,一体化交付

后端有了,怎么交付给用户?

我的场景是"本地运行"——不是 SaaS,不是 Docker,是用户装在自己电脑上就能用。

WPF 是最直接的选择:

  • Windows 原生,不需要额外运行时
  • 可以内嵌 WebView2 来加载 Coze 画布
  • 可以直接调用本地后端服务
  • 打包成一个 .exe,双击就能用

整体架构就变成了:

WPF 桌面客户端一体化架构

┌─────────────────────────────────────────┐
│            WPF 桌面客户端                 │
│  ┌─────────────┐  ┌──────────────────┐  │
│  │  WebView2   │  │   C# 后端服务     │  │
│  │  加载扣子画布 │  │  WorkflowCore    │  │
│  │  用户拖拽编排 │  │  触发器引擎      │  │
│  │  导出 JSON   │──│  代码运行器      │  │
│  └─────────────┘  └──────────────────┘  │
│         ↑                ↓              │
│         └──── SQLite 持久化 ────────────┘│
└─────────────────────────────────────────┘

前端是扣子的画布(WebView2 内嵌),后端是 C# 的执行引擎,中间用 JSON 对接。 用户感知到的是"跟 Coze 一样的体验",但数据全程在本地。

先搭原型,验证核心链路

架构定了,我没有一上来就写完整系统。先搭原型,验证最关键的那条链路:扣子 JSON → 本地引擎 → 跑通。

原型只做三件事:

  1. 能导入 Coze 导出的工作流 JSON
  2. 能在本地启动这个工作流
  3. 能看到执行结果

其他所有东西——触发器、代码沙箱、版本管理、错误处理——原型阶段都不做。

原型验证通过了,才往下走。

原型验证通过后,长成了什么样

原型跑通之后,开始补全自动化的三件套:工作流引擎 + 触发器 + 代码运行器

触发器:让流程自己跑起来

有了工作流引擎,第二个问题是:谁来启动工作流?

用户手动点"运行"当然可以,但自动化的价值就在于"不用人点"。

我设计了 7 种触发器:

7 种触发器类型一览

csharp
public enum TriggerType
{
    Schedule  = 1,   // 定时触发(Cron 表达式)
    FileWatch = 2,   // 文件变化触发(监听目录)
    Socket    = 3,   // Socket 消息触发
    Hotkey    = 4,   // 快捷键触发
    Clipboard = 5,   // 剪贴板变化触发
    System    = 6,   // 系统事件触发
    WebHook   = 7,   // HTTP 回调触发
    Custom    = 99   // 自定义触发
}

这些触发器的设计原则是无状态——触发器本身不存数据,它只负责"检测到条件满足 → 发出事件 → 启动对应的工作流"。

触发器检测到事件 → 查找绑定的工作流 → 传入事件数据 → 启动工作流

比如"每天早上 8 点从数据库拉数据生成报表"这个场景:

  1. 用户创建一个 Schedule 触发器,配 Cron 为 0 8 * * *
  2. 用户在扣子画一个工作流:数据库查询 → 数据处理 → 生成 Excel → 发邮件
  3. 绑定触发器到工作流
  4. 每天 8 点,触发器发出事件,工作流自动启动

代码运行器:让流程能执行任意代码

工作流中的节点大部分是预定义的——HTTP 请求、数据转换、条件判断。但用户总会遇到"现有节点不够用"的情况,需要自己写代码。

这就是 CodeRunner 的价值:在工作流节点里安全地执行用户代码。

csharp
public interface ICodeRunnerService
{
    // 执行命令(基础版:传入命令字符串,返回结果)
    Task<ExecutionResult> ExecuteAsync(
        RuntimeType runtimeType, string command, ExecutionOptions? options = null);
    
    // 沙箱执行(隔离环境:传入沙箱句柄 + 脚本,返回结果)
    Task<SandboxResult> ExecuteInSandboxAsync(
        SandboxEnvironment environment);
}

关键设计是沙箱隔离。用户代码不能访问宿主机的文件系统,不能访问网络,不能影响主进程。每种语言(Python、Node.js、PowerShell)有自己的沙箱环境。

三件套的协作

自动化三件套协作

把三个模块放在一起看,自动化的全貌就清楚了:

触发器(什么时候跑)
  ↓ 触发
工作流引擎(怎么跑)← 扣子 JSON 定义
  ↓ 执行节点
代码运行器(节点里跑什么)
  ↓ 返回结果
下一个节点...

一个完整的例子:

场景:每天 8 点,从数据库拉订单数据,用 Python 脚本做分析,生成报表发邮件。

  1. 扣子画布:用户拖拽画出 SQL查询 → Python分析 → 邮件发送 的工作流
  2. 导出 JSON:工作流定义存到本地 SQLite
  3. Schedule 触发器0 8 * * *,每天 8 点触发
  4. 本地执行:WorkflowCore 加载定义,Python 节点通过 CodeRunner 在沙箱里执行

数据全程在本地,不经过任何云端。

回过头看这几个决策

回头看这个阶段的架构,有几个决策值得复盘:

决策 1:前端不造轮子

如果当时选择自己写画布,光前端就得干 3 个月,还不一定做得好。用扣子导出 JSON 的方式,让我把 100% 的精力放在执行引擎上——这才是真正的壁垒。

核心判断:区分"体验层"和"能力层"。体验层能用现成的就用,能力层必须自己建。

决策 2:嵌入式而非微服务

没有搞"前端一个服务、后端一个服务、数据库一个服务"的微服务架构。所有东西跑在一个进程里:WPF 宿主 + WebView2 画布 + C# 后端 + SQLite 存储。

核心判断:本地工具不需要分布式架构。一个 .exe 能解决的问题,不要拆成五个服务。

决策 3:先原型后系统

没有一上来就设计"完整架构"。先验证"扣子 JSON 能不能在本地跑通"这个核心假设,验证通过了再补全触发器、代码运行器、版本管理。

核心判断:架构不是设计出来的,是长出来的。先证明路走得通,再修路。

关键认知

自动化 vs 智能化

这个阶段做完,我有了第一个关键认知:

自动化是"人告诉机器每一步怎么走",智能化是"人告诉机器要去哪里"。

工作流引擎是确定性的——你画好流程图,它按图执行。每一步做什么、走哪个分支、什么时候停,都是你预先定义好的。

但真实场景中,很多事情是"不确定"的:

  • 用户说"帮我查一下最近的订单"——"最近"是多久?"查"是查数据库还是查 API?
  • 用户说"把这个文件整理一下"——"整理"是重命名?分类?还是压缩?
  • 用户说"写一封回复邮件"——回复什么内容?语气怎么样?

这些问题,工作流引擎回答不了。因为它需要你预先定义所有分支,而"用户意图"的分支是无穷的。

这就是下一章节要解决的问题:当工作流遇到 LLM。