跳到主要内容

Flow 源码可视化

dexcli visualize 可以把一个 Go、Python 或 TypeScript Flow 源文件转换为静态 Flow Definition Graph。图中包含能够从源码确定的所有可能路径,而不是某一次 Flow 运行及其历史。

dexcli visualize ./order_flow.go

默认 schema version 是 1.0。Go、Python 和 TypeScript 源码都可以选择 Version 2.0,以启用 Dex Web v2:

dexcli visualize ./refund_flow.go --schema-version 2.0

该命令会打开本地的 Flow Rendering 页面,并持续提供图形直到按下 Ctrl+C。需要 Flow Definition Graph 文件时使用 --json。未指定 --out 时,JSON 会写入标准输出;使用 --out 可以选择文件前缀:

dexcli visualize ./order_flow.py --json --out ./build/order-flow

使用 --json 可以把 JSON 写入标准输出:

dexcli visualize ./order_flow.py --json

在 Dex Web 中渲染定义图​

启动 Dex 时指定包含生成后 JSON 文件的目录:

dexcli dev --flow-rendering-dir ./build

在 Dex Web 的 /v1/rendering 打开 Flow Rendering。该页面独立于 Flow 搜索页和运行详情页,用于交互式渲染所选定义图。通过图例可以显示或隐藏控制流、WaitFor、RPC、Attribute、Channel、Stream、SubFlow 及诊断。Flow timeout handler 始终作为 Flow 的一部分显示。Stream 默认隐藏,其他图层默认显示。Version 2.0 文件则使用 v2 画布绘制,随后列出其 diagnostic。参见 Dex Web。

当 Dex 带着 JSON 目录启动时,/v2 也会在 run 列表右侧渲染当前 Flow type 的 definition。见 Dex Web。

Dex 会递归扫描 JSON 文件,并在启动时创建一次快照。文件改变后需要重启 Dex。无效 JSON、不支持的 schema 版本或非目录路径都会使启动失败并返回错误。

图中包含的内容​

主 Flow 显示为浅紫色大框,每个 Step 是其中的浅蓝色中框。WaitFor 路径位于 Execute decision 上方。WaitFor 或 Execute 存在条件返回时,先经过一个棱形,再根据源码条件连接到不同卡片。

定义图包括:

  • Flow 和起始 Step
  • 已注册的 Step,以及按 Execute decision 分组的静态跳转
  • 条件标签,以及固定数量或运行时数量的并行分支
  • 有序的 WaitFor 条件,包括 Channel 数量、Timer 时长和外部 SubFlow
  • RPC 和 timeout decision 及其后续 Step
  • Attribute 名称、源码语言原生 value type 和 map 标记
  • Channel、Attribute 和可选 Stream 的访问关系
  • Execute 卡中的完成、失败、Channel-empty 检查和取消信息
  • Execute 失败后的恢复路径

终止 decision 不再生成额外 terminal 节点。取消信息直接显示在 Execute 卡中,不再绘制重复箭头。起始 Step 使用 START 标记,不再生成合成的 start 边。Timer 作为 WaitFor 中的一行显示,不再生成独立节点。

错误返回或抛出的异常并不能确定另一个 Step,因此不会产生跳转。显式的 ProceedToOnExecuteFailure 或 on_execute_failure_proceed_to 策略会生成一条指向恢复 Step 的紫红色实线。恢复跳过 WaitFor 时,边的标签会明确说明。

资源箭头使用统一方向:

  • publisher → Channel → consuming WaitFor
  • Attribute writer → Attribute 框 → Attribute reader
  • WaitFor → 折叠的 SubFlow

资源和 SubFlow 关系使用虚线,视觉上弱于 Step 之间的实线跳转。Channel 集中在 Flow 左上区域,Attribute 合并为一个框,RPC 六边形横跨 Flow 左边界。SubFlow 只显示为包含 Flow type 的紫色虚线小框,不展开内部 Step。

Renderer 默认会把完整 Flow 缩放到当前 viewport。Channel 和 Attribute 箭头默认隐藏;选择对应资源,或相关的 Step、WaitFor、Execute decision、RPC、timeout handler 后才会显示。默认视图因此优先呈现 control flow,同时仍能按需查看资源访问关系。

Attribute 关系包括读取、写入和加锁。Python analyzer 会追踪 Flow class 上的静态资源 alias,并记录 RPC 或 Step options 声明的 Attribute lock。Go analyzer 会把同一 package 中辅助函数的读写归到调用它的处理方法上,对应的边会在 metadata 中记录该辅助函数。

Stream 关系描述的是应用可观察输出,而不是控制流。Flow Rendering 默认隐藏 Stream,用户可以通过图例开启。JSON 会标记 Step Stream 写入可重复且为 best effort。Heartbeat 调用和 checkpoint 读取属于运行时可靠性细节,因此不会进入定义图。

当 RPC 或 Flow timeout handler 调用 Stream.Write 或通过 NewBufferedTextStream 创建 writer 时,Go analyzer 会报 step_progress_outside_step 错误。检查也会追踪同一 package 的辅助函数和 buffered writer alias。错误会指出处理方法、Stream 和辅助函数位置。将输出移到 Step 中;RPC 可以发布 Channel 消息或返回 NextSteps 来调度该 Step。

支持的源码约束​

首版要求每个源文件只包含一个 Flow。以下内容必须直接出现在该文件中:

  • 静态 Step 注册
  • 每个已注册 Step 的处理方法
  • Dex 跳转和 WaitFor 定义
  • RPC 的后续 Step
  • Execute 失败后的恢复目标
  • 持久化资源定义
  • Step Stream progress 操作

可以使用普通业务辅助函数和导入的数据模型,但辅助函数不能隐藏 Dex 跳转、WaitFor 定义、资源定义或恢复目标。资源名称必须能够静态确定。Flow type 和 Step type 遵循 Flow 与 Step 名称 中的规则。

Go 的资源访问也可以位于处理方法调用的同一 package 函数和方法中。Analyzer 最多沿调用追踪六层,包括循环和函数字面量中的调用。作为参数传入的资源会绑定到对应形参,因此通过该形参的访问也算作对该资源的访问。

Go 分析需要本地 Go toolchain,输入必须属于能够完成类型检查的 module 和 package。Python 分析要求 Python 3.11 或更高版本。Python analyzer 使用标准库 AST。它以隔离模式运行解释器,并禁用 site 的自动初始化,因此 .pth 中的 import 行和 sitecustomize 都不会运行。它绝不会导入或执行应用模块。

对于 Python,analyzer 可以识别同步 Step generator 通过 yield 发出的 Stream output,也可以识别异步 Step handler 中同步入队的 Stream 写入。在 RPC 或 Flow timeout handler 中使用 Step Stream progress 属于无效代码,会产生阻断诊断。

TypeScript 分析需要 Node.js,并且源文件所在项目必须能通过 node_modules 或 NODE_PATH 解析到 typescript package。Analyzer 绝不会从 dexcli 的工作目录加载编译器。它读取编译器 AST,不会导入应用模块。它识别以下源码形态:

  • 通过 StepList.startStep、StepList.withoutStartStep、otherSteps 或 StepList.empty 注册的本地 Step class 和静态 Flow field。
  • 直接返回的 Execute decision:goTo、配合 StepMovement.of 的 goToMany、gracefulComplete、forceComplete、forceFail、deadEnd 和 forceCompleteIfChannelsEmpty。withCancelingSteps 和 withCancelingSiblingSteps 会加入取消关系。从 @superdurable/dex 具名导入、别名导入和 namespace 导入都受支持。
  • 来自 if、switch、条件表达式、循环和 try 块的条件。
  • getStepOptions 中的 ExecuteFailure.proceedTo,可以直接写出,也可以位于同一文件的 helper method 或 function 中。
  • Attribute、Channel、Stream 定义,以及 manifest 声明的 Connector factory。

TypeScript analyzer 暂不支持以下功能:

  • WaitFor 方法。带有 waitFor 的 Step 会产生 blocking diagnostic typescript_wait_for_unsupported。
  • 单个文件中的多个 Flow。这会产生 multiple_flows。
  • RPC handler 和 handleTimeout。它们的 decision 不会显示,因此只由 RPC 调度的 Step 可能报告 unreachable_step。
  • 本地 handler 中的资源读取、写入、锁、state load 和 Stream 写入。定义图不包含本地 Step 的资源边,也不会报告未使用资源的 warning。
  • SubFlow,以及 Version 2.0 的 Indexed Attribute、Summary、Display、Action 和 start-input schema。

把 decision 隐藏在 helper 中的 Execute 方法会产生 hidden_dex_decision。在 TypeScript 中,Execute 的每个返回值只要不是可识别的 decision(例如 helper 调用或变量),都会单独产生 hidden_dex_decision,即使其他返回值可以识别。暂不支持 Java、Rust 和递归多文件分析。

Version 1 不支持单文件中的多个 Flow、反射、动态 class、通配符导入、基于 getattr 的目标、monkeypatch,以及逃逸出处理方法的 movement 集合。

Version 2.0 保留所有 Version 1 字段,并要求 Step group 和 explanation 与 Flow 位于同一文件。同一行内的具名 directive 参数可以任意排列。多条 directive 行保留源码顺序,并成为 UI 顺序。未知、重复、缺失或非法参数都会产生 blocking diagnostic。Go analyzer 还会读取 Indexed Attribute、Summary、Display、Action 和 start-input schema;规则见 Dex Web。

Version 2.0 会从通过 DefineStartStep 注册的 Step 输入类型生成 optional start-input schema。它遵循 JSON field name、忽略字段、pointer 和 omitempty。支持嵌套与 imported struct、具名 string/integer constant、time.Time、普通数值宽度、slice、固定数组和 string-key map。Integer bound 与 integer enum value 在 schema 中保持为十进制字符串。递归类型、超过 32 层的嵌套、非 string map key、interface、function、channel、byte slice、有冲突的匿名字段、JSON string encoding tag 和自定义 JSON codec 会产生 warning 并省略 start schema,但不会使图的其余部分失效。

Version 2.0 会通过 config 中 canonical Connector SDK marker 与生成的字段 metadata 识别 openai.NewCreateResponseStep 等 operation-specific Connector factory,而不维护 connector 或函数名列表。Dex Web 会用应用名称的缩写显示这些 Step。llm Connector 可以连接任意模型服务商,它的 Step 会显示为 AI 模型步骤。通用 SDK 的 MustNewQueryStep 和 MustNewMutationStep escape hatch 仍然受支持。Factory 调用必须直接位于 DefineStep 或 DefineStartStep 中,并使用内联 config literal。Step type、annotations、具名 GoTo branch target(或通用 GoToBranch target)、StepRef 名称、Result Attribute、Stream 和 ProceedToOnExecuteFailure target 都必须能够静态确定。Factory annotations 直接提供 Step group 和 explanation,因此生成的 Step 不需要本地 directive。每个 Connector Step 只把当前 operation result 传给 branch target;进入 Connector Step 前,应把业务上下文持久化到 application Attribute。定义图会为每个 Connector branch 生成一条带标签的 transition,为 Result Attribute 生成 write relation,并为 structured/text Stream 生成 best-effort progress relation。动态 config、branch slice、target、annotation 或 resource reference 会产生 blocking diagnostic。参见 Connector factory 示例。

Python 与 TypeScript Step annotations​

每个本地 Step class 上方必须各有一条 group directive 和 explanation directive。Python 使用 # 注释行;注释与 class 之间可以有 decorator。TypeScript 使用紧贴 class 上方的 // 注释行。注释与声明之间不能有空行。语法与 Go 相同:

# dex:group group-id:lookup group-label:"Lookup"
# dex:explanation text:"Look up a catalog record."
// dex:group group-id:lookup group-label:"Lookup"
// dex:explanation text:"Look up a catalog record."

Group ID 必须为 kebab-case。共用同一 ID 的 Step 必须使用相同的非空 label。Explanation 用一个非空句子说明 Step 的作用。FDG 2.0 将其写入 metadata.explanation。缺失、重复或格式错误的 directive 都会阻断分析,启用 --lint app 时也一样。

Connector package manifest​

Python 或 npm Connector package 需要包含 dex-connector.json。Python 将它放在 import package 目录内。Analyzer 使用所选解释器已安装的 package 以及 Flow 源码目录解析 import;通过 --python /path/to/venv/bin/python 选择虚拟环境。它读取 site-packages 目录及其 .pth 文件中的普通目录行,因此支持基于路径的 editable install。它绝不会运行 .pth 中的 import 行,所以依赖 import hook 的 editable install 无法被找到。npm 将它与 package.json 放在同一目录。TypeScript 从 Flow 源码目录开始,逐级向文件系统根目录查找 node_modules 中被导入的 package。支持具名 import alias 和 package namespace alias。分析过程不会执行导入的 Connector。

Manifest JSON schema 定义 manifest 结构。Manifest 包含 connectorId、精确的 releaseTag(例如 connectors/catalog/v1.2.3)、含 ecosystem、name、version 的 package 对象,以及非空 factories 数组。Package ecosystem 可以是 pypi、npm 或 go;Python 和 TypeScript analyzer 分别要求 pypi 与 npm。Package version 是安装的精确版本,不必与 Connector release tag 相同。CLI 还会执行 JSON Schema 无法表达的规则:factory name、branch ID 和 ConfigurationUI unit ID 必须唯一;映射的 config 字段名必须互不相同;同一 unit 内的 binding port 必须唯一。

每个导出的 factory 声明 name、kind(query、mutation 或 trigger),以及把语义名称映射到源码 config 字段名的 fields。Query 和 mutation factory 必须声明 operationId、stepType 与 annotations 字段映射,以及具名 branches。每个 branch 包含 id、config field,以及可选的 optional。未接线的 optional branch 会显示为失败 decision。Trigger factory 必须声明 triggerName 和 bindingName 字段映射。所有 factory 都必须声明 connectionName 映射。

可选映射 resultAttribute、resultStream、progressStream 和 textStream 指向已声明的资源。configurationUI 映射静态覆盖值;manifest 也可以提供默认 configurationUI,其中包含 units、各自的 bindings 和 JSON pointer。定义图会保留这些信息供 Dex Web 使用。可选的 executeFailure 映射指向静态恢复 Step,并生成 Execute 失败恢复边。

Python factory 调用使用 keyword argument、一个内联 dictionary,或一个使用 keyword argument 的内联 config constructor。TypeScript 调用使用一个内联 object。Factory annotations 是包含 groupId、groupLabel 和 explanation 的静态对象;生成的 Step 使用这些值,不需要本地 class 注释。Branch field 引用已注册的本地 Step、静态 Step reference,或直接使用 go_to / goTo 包装。Trigger binding 必须从 get_connector_trigger_bindings / getConnectorTriggerBindings 以一个静态 list 或 array 返回。

如果 Step 注册或 Trigger binding 调用的名称来自没有 dex-connector.json 的 package,会产生 connector_manifest_missing。消息会写明 package 和预期的 manifest 路径。Package 未安装时也会报告同一诊断。格式错误或字段类型错误的 manifest 会产生 connector_manifest,并写明出错的字段。两个 Step 使用同一 Step type 时会产生 duplicate_step_type,包括两个 factory 调用使用同一 Step type,以及 factory 的 Step type 与本地 Step 相同。

Connector 节点在 metadata.connector 内输出 connectorId、releaseTag 和 package,同时包含 operation identity、connection name、configurationUI 和 configurationEnabled。非空的静态 connection name 会启用自动配置;动态名称会产生 warning 并关闭自动配置。Dex Web 解析精确 release tag 并验证 release metadata。现有 Go modulePath / moduleVersion metadata 继续受支持,Go 定义图输出保持不变。该 manifest 只描述分析 metadata,不会给 SDK 增加运行时 Connector API。

Flow 与 Step 名称​

定义图使用 SDK 注册的 Flow type 和 Step type。Dex Web v2 用这些名称搜索 run、把 run history 对应到 Step,并启动 Flow。因此,除非 analyzer 报告下文列出的诊断,Go Flow 不需要 GetFlowType 或 GetStepType override 就能在 Dex Web v2 中使用。

对于 Go,默认名称是去掉 package 和指针后的 Go type 名称。Flow type ThreadApprovalFlow 和 Step type initializeThread 都原样作为名称。对于 Python,默认名称是 class 名称。

请按业务领域命名每个 Flow type,例如 ApprovalFlow 或 ReplyFlow。不要把 Flow type 只命名为 Flow。名称不包含 package,因此同一个 Worker 注册的每个 Flow 都需要不同的 type 名称。Dex Web、run history 和 Flow 搜索都会原样显示这个名称。

对于 Go,泛型 Flow 或 Step type 没有默认名称。请声明返回非空名称的 GetFlowType 或 GetStepType override。

GetFlowType 或 GetStepType override 可以声明在 package 的任意文件中,也可以从嵌入类型提升而来,包括来自其他 package 的类型。Override 中的每条 return 语句都必须返回同一个 compile-time string。空字符串表示保留默认名称。Dex 会拒绝包含 /、$ 或 : 的 Step type。StepRef target 指向注册的 Step type,而不是 Go type。

Analyzer 无法确定注册名称,或无法为每个 Step 生成独立节点时,会报告 blocking diagnostic:

  • dynamic_type_name:override 返回不同的字符串、返回的值不是 compile-time string,或名称来自嵌入的 interface。
  • generic_flow_type_name:泛型 Flow type 没有非空 override。
  • generic_step_type_name:泛型 Step type 没有非空 override。
  • duplicate_step_type:两个注册使用同一个 Step type,或同一个 Go type 注册了多次。Step 节点以 Go type 为键,因此一个泛型 Step 只能用一个类型参数注册。

Go 节点 ID 使用 Go type,例如 step:initializeThread,即使 override 改变了 Step type 也是如此。

Python 和 TypeScript 可以注册同一个 Step class 的多个实例,但每个实例都必须有不同的静态 Step type。get_step_type 或 getStepType 可以返回字符串字面量、用字符串字面量初始化的 class 属性,或从构造参数赋值的实例字段;构造参数的值必须在注册处是字符串字面量。Analyzer 也能读取构造参数的默认值和静态变量别名。每个节点 ID 使用实例的 Step type,例如 step:First 和 step:Second。

Analyzer 只从已注册的 class 本身读取 Step 方法。Python Step class 只能继承 Step 和 Generic。TypeScript Step class 不能继承其他 class。否则 analyzer 会报告 step_inheritance_unsupported。请把共享逻辑移到 helper 函数中,并把每个 Step 方法保留在已注册的 class 上。

只有当 class 恰好为某个实例字段赋值一次时,analyzer 才能读取该字段。这次赋值必须是 class 属性、TypeScript 属性初始化器或参数属性,或者构造函数中的直接语句。在其他方法中赋值、通过 setattr、vars 或 Object.assign 赋值,或者从 class 外部赋值的字段都不可读取。带有装饰器(例如 dataclass)的 Python Step class,其字段同样不可读取。构造期间读取某个字段的代码,必须在该字段赋值之后运行。因此,TypeScript 属性初始化器不能读取由构造函数赋值的字段。请在构造函数中为每个字段赋值,或者让 get_step_type 或 getStepType 返回字符串字面量。

SDK 按对象身份解析实例目标。目标必须是已注册的那个对象本身,而不是另一个具有相同 Step type 的对象。普通跳转、StepMovement、Execute 失败路由和取消操作接受以下目标:

  • 已注册的实例,并且通过的每个名称都只绑定一次。这些名称包括模块变量、Flow class 属性或属性初始化器、在构造函数中只赋值一次的 Flow 字段、在构造函数中赋值的 Step 字段,以及局部变量。
  • 某个 Step class,前提是该 class 只注册了一个实例。
  • 用字符串字面量构造、且指向已注册 Step type 的 StepRef。TypeScript 必须用 new 构造它。

在决策中直接写构造调用会创建一个未注册的新对象。SDK 会在运行时拒绝这个目标,因此 analyzer 会报告 unknown_step_target。请改为指向已注册的实例或使用 StepRef。同一个 Connector 的不同 factory 调用只要使用不同的静态 Step type,也会生成独立节点。

只有当 analyzer 能证明某个名称持有哪个对象时,才会接受该实例。在同一个函数或模块中绑定两次的名称无法证明。重新赋值、增量赋值、循环变量、with 目标、海象运算符目标、解构、global 重新绑定,以及 TypeScript 嵌套代码块中的第二个声明,都算作另一次绑定。被构造函数重新赋值的构造参数同样无法证明。

字段是否可读,还取决于 Flow 或 Step 对象的使用方式。在 Python 中,self 只能用于读取或赋值字段,或者传给同一文件中某个 class 的构造函数。把它传给其他函数、存入变量,或者重新绑定 self,都会让该 class 的字段不可读取,并报告 dynamic_class。在两种语言中,带有装饰器的 Flow class,或者构造函数返回了值,其字段同样不可读取。

TypeScript 通过检查每个被追踪对象的每一次使用来证明字段。被追踪对象是 class 体中的 this、同一文件中的 class 构造的实例,或者 Connector factory 调用返回的 Connector Step。被追踪对象只能出现在以下位置:

  • 读取字段,例如 this.target 或 first.target,在回调中也可以。class 体中的 getter 可以返回被追踪对象,读取这个 getter 视为读取字段。
  • 调用其 class 体中声明的方法,例如 this.retryOptions()。
  • 通过 this 为字段赋值,例如 this.target = target。
  • 用 StepList.startStep、otherSteps 或 withoutStartStep 注册。
  • 作为 Dex 目标:goTo、StepMovement.of、ExecuteFailure.proceedTo、FlowTimeoutHandlerFailure.proceedTo、withCancelingSteps 或 withCancelingSiblingSteps 要取消的 Step,或者 Connector factory 配置中的 Execute 失败字段或分支字段。
  • 作为别名:变量、参数默认值、class 字段,或者同一文件中某个 class 的构造参数。别名的每一次使用都必须遵守同一规则。一个别名可以持有多个对象。

其他任何使用都是逃逸(escape)。以下使用都是逃逸:

  • 把对象传给函数,或者传给文件外 class 的构造函数,包括 Object.assign、Reflect.set 和 new Object。
  • 把它存入数组、Map、Set、对象字面量或展开表达式。
  • 返回它、导出它或解构它。
  • 用 as、satisfies 或类型断言转换它。
  • 用计算键读取它,或者通过它写入,例如 first.target = value 或 this[key] = value。
  • 把它赋给由 class 体中的 setter 处理的字段名。
  • 在其他任何表达式中使用它,例如比较。

逃逸会在逃逸的位置报告 dynamic_class。逃逸对象的字段,以及它通过字段能到达的每个对象的字段,都会变得不可读取。逃逸的 Connector Step 会报告 dynamic_step_target。对其他值进行计算键写入(例如 words[index] = value)不影响图。

在 class 之外创建的 Flow 对象(例如 export const approvalFlow = new ApprovalFlow())可以被导出、返回,或者传给导入的函数或构造函数,例如 new Registry([approvalFlow])。通过它读取的字段(例如 approvalFlow.review)是被追踪对象。对这个 Flow 对象的其他任何使用,例如类型转换、写入、反射调用,或者调用同一文件中的 helper,都是逃逸。同一文件中的 class 如果被当作值使用,而不是用于 new、extends、instanceof、读取静态字段、导出或 Dex 目标,它的静态字段就不可读取。它的实例存入字段的被追踪对象(构造参数除外)也会逃逸。

任何对 prototype、proto、globalThis、window、self 或 global 的访问,都会让所有 Flow 和 Step 字段不可读取。为未声明的全局名称赋值也一样。目标不是对象字面量或数组字面量的反射写入(例如 Object.assign、Object.defineProperty 或 Reflect.set)同样会让字段不可读取。与字段同名的 TypeScript getter 或 setter 会让该字段不可读取。

这些检查依赖三个假设。第一,其他模块中的代码不会修改 Flow 或 Step 对象,包括通过导出、返回值或导入的函数(例如 Registry)收到 Flow 对象的代码。第二,源代码不会运行动态代码,也不会使用 analyzer 看不到的反射。第三,源代码不会以 analyzer 看不到的方式修改原型或全局对象。Analyzer 不会追踪进入其他模块的对象,因此不要把 Flow 或 Step 对象传给源文件之外的代码。

Analyzer 只从 class 本身读取 Flow 和 Step 方法。Python Flow 或 Step class 如果定义了 getattr、getattribute、setattr、delattr、get、set、delete、set_name、new、init_subclass 或 class_getitem,会报告 dynamic_class。在两种语言中,在 class 之外重新赋值 Flow 或 Step 方法都会报告 dynamic_class。Flow 或 Step class 及其成员上的装饰器也会报告该诊断,因为装饰器可以替换 class 或成员。例外情况是:Dex 的 rpc 装饰器、class 上的 Python typing.final 和 dataclasses.dataclass,以及方法上的 Python staticmethod、classmethod 和 typing.override。Python 内置的 property 作为方法唯一的装饰器时也允许使用。它的名称不能是字段,也不能是 Dex 调用的 Step 或 Flow 方法。property 的 setter 或 deleter 仍会报告 dynamic_class。Python 的 dict、vars、setattr 和 class 赋值,以及上文所述的 TypeScript 逃逸和反射写入,也会报告 dynamic_class。

Analyzer 只通过 dex 或 @superdurable/dex 的导入识别 Dex API。在任何作用域中重新绑定 Dex 导入别名或 dex 模块名,都会报告 dex_alias_rebinding。为 Dex 导入的属性赋值也会报告该诊断。Python 中从任何模块进行通配符导入都会报告 wildcard_import,因为它可能重新绑定 Dex 名称。Python 的 exec、eval、globals、locals、getattr、setattr、delattr 和 builtins 模块会报告 dynamic_python。这些名称以及 vars 不是被直接调用、而是被当作值使用时(例如通过别名,或者作为 map 的参数),也会报告该诊断。TypeScript 的 eval 和 Function 会报告 dynamic_typescript。

计算得到的 Step type、无法证明的目标、未注册的 StepRef 名称和存在歧义的 class 目标都会产生阻断诊断。Execute 决策隐藏在回调或 helper 中时,仍然会报告 hidden_dex_decision;实例的 Step type 可解析,并不代表回调中的决策也可解析。

启动输入​

Go StartFlow 会在发送请求前检查输入是否符合已注册起始 Step 的类型。已知输入类型不匹配时,dexcli visualize 会报 start_flow_input_type_mismatch。两个 schema 都将其视为阻断错误,无论是否启用 --lint app。

传入起始 Step 所需类型的值。没有输入时,将起始 Step 的输入声明为 dex.None,调用时传 nil。空 struct 是另一种类型;指针与对应的值类型也不同。允许空值的输入类型仍然可以接收 nil。

检查覆盖 Flow 文件中的调用,以及处理方法调用的同一 package 辅助函数。目标 Flow 的 GetSteps 必须直接返回含一个 DefineStartStep 的 Step slice 字面量。目标声明可以位于另一个文件或导入的 package 中。动态选择的 Flow、泛型 Flow、interface 类型的输入和无法解析的注册不会被检查。

状态加载​

Step 方法、RPC 和 Flow timeout handler 会收到普通 Attribute 和 Channel size,但除非显式选择,否则不会收到 AttributeMap value 或 pending Channel message。见选择性状态加载和选择性 RPC 状态加载。读取未选择的状态会在运行时失败,失败的 Step 方法会一直重试,直到 retry policy 结束。对于 Go 源码,dexcli visualize 在所有 schema version 中都会把这类读取报告为阻断诊断:

  • attribute_map_read_not_loaded:AttributeMap.Get 读取的 map 没有被该方法的整表或实例加载选择。
  • attribute_map_enumeration_not_loaded:AllInstanceKeys 或 MapSize 在没有整表加载时运行。精确实例加载无法枚举,SDK 会 panic。
  • attribute_map_instance_not_loaded:Get 读取一个常量实例 key,而该 map 的所有加载都指定了其他常量 key。
  • channel_messages_not_loaded:PendingMessages 或 FindPendingMessage 读取的 Channel 或 ChannelMap 没有被任何加载选择。

写入永远不需要加载。Set、Delete、Publish、删除 Channel message、Size、ChannelMap 的 key 和 size、WaitFor 条件以及 GetConditionResults 都不会被报告。

Analyzer 从 Flow 所在 package 的声明中读取加载。对于 Step,来源是 GetStepOptions、每个带 WithStepOptions 并移动到该 Step 的 GoTo 或 MovementOf,以及每个指向它的 ProceedToOnExecuteFailure。Execute 使用 ExecuteLoad 字段,WaitFor 使用 WaitForLoad 字段。对于 RPC,来源是传给 DefineRPC 的 RPCOptions,以及 package 中传入该 RPC method value 并带有 Load 或 LoadMessages 的调用(例如 InvokeRPCWithOptions)所选择的精确实例。对于 HandleTimeout,来源是 package 中的 FlowTimeoutHandlerOptions 值。

Options 可以是字面量、package 变量、带字段赋值的局部变量,或参数来自调用点的同一 package 函数的返回值。同一 package 辅助函数中的读取会归到调用它的处理方法上。诊断会指向 Flow 文件中的调用,并给出辅助函数所在的文件和行号。

当另一个 package 在调用时选择 RPC 的 map 实例时,请在 RPC 方法上声明:

// dex:invocation-load attribute-map:customer-profiles

使用 channel-map 可以指定 ChannelMap。该 directive 满足对该 map 的实例读取,但不满足枚举。名称不是该 Flow 的 AttributeMap 或 ChannelMap 时,会产生 invocation_load_directive。

该分析不区分路径:进入某个 Step 的任一 movement 上的加载,对所有 movement 都有效。通过 interface、函数值和反射的调用不会被追踪。加载选项无法求值时(例如由其他 package 返回的 options),默认模式不会为该读取报告任何诊断。

应用 lint​

--lint app 会增加检查,用于发现能够编译并通过默认分析的应用错误。下列应用检查适用于 Go。Python 和 TypeScript 也接受该选项,并执行相同的 FDG 2.0 group 和 explanation 要求。上文的状态加载诊断在所有模式下都会报告。

dexcli visualize ./order_flow.go --schema-version 2.0 --json --lint app
  • state_load_unresolved:某个读取需要加载,但无法求值其处理方法的加载选项。请在 GetStepOptions、RPCOptions 或 Flow 所在 package 的变量中声明加载。
  • connector_branches_merged_unread:某个 Step 接收同一 Connector Step 的两个或更多 branch,其中包括 defect 或 uncertain。它使用了结果,却从不读取 result.Branch 或 result.Failure,因此失败会被当作成功处理。
  • action_state_not_rechecked:Action RPC 从不读取其 WhenAttributeMatches 条件中的 Attribute,也不读取它加锁的 Attribute。条件只用于选出候选 run,并不授权写入。请在锁内读取状态,拒绝过期或重复的决定。
  • start_flow_request_id_missing:Flow 文件中,或处理方法调用的辅助函数中的 StartFlow 调用,其 options 没有 RequestID。重试的启动将无法被识别。请从业务标识派生 RequestID,并设置带 IgnoreError 的 AlreadyStarted。
  • cross_flow_read_not_found_unhandled:Step 调用 InvokeRPC 或 InvokeRPCWithOptions,但其调用路径上没有针对 FlowNotActiveOrNotFoundError 或 FlowNotFoundError 的 errors.As 检查。Flow 不存在时,该 Step 会一直重试,直到 retry policy 结束。

Unknown 节点和诊断​

不支持的动态控制流不会被静默省略。输出会包含 Unknown 节点和错误诊断。默认 renderer 仍会显示部分图。使用 --json 时,命令会写出部分 JSON,并以状态码 1 退出。

warning 不会使图失效。例如,无法确定 value type 的 Attribute 会显示 unknown 并产生 warning。从起始 Step 或 RPC 都无法到达的已注册 Step 也会产生 warning。无效命令参数以状态码 2 退出。

存在任何阻断诊断时,JSON 中的 valid 字段为 false。消费方应阻止基于无效图的编辑或部署,但仍可使用其中已知的节点和边解释问题。

JSON 合约​

Flow Definition Graph v1 schema 定义了源码位置、父子关系、结构化 WaitFor 和 decision 详情、资源类型、分支条件、边方向和诊断。Flow Definition Graph v2 schema 在此基础上增加有序 group 与 Dex Web v2 metadata。

运行中 Flow 的操作命令请参阅 Dex CLI。