跳转至

Init Flow Orchestrator — PRD

状态:Implemented(Phase 1-2 后端引擎 + Phase 5 清理已完成,Phase 3-4 前端进行中) 创建日期:2026-03-29 替代:postcol 模块 + Postman/ApiFox Collection 方案(已删除)


1. 问题陈述

Robot 部署成功后,需要向 Robot 发起一组 HTTP 请求完成初始化(如登录、配置渠道、注册 webhook 等)。当前方案是让运营在 ApiFox/Postman 中编写 Collection JSON,粘贴到 Admin Portal。

此方案的核心问题:

  1. 生态不兼容:ApiFox CLI 导出的脚本使用 async/await、require() 等 Node.js 特性,与我们的 Goja ES5 引擎完全不兼容
  2. 维护者认知负担:要理解 Postman Collection v2.1 schema、ApiFox CLI 格式差异、三层变量作用域映射关系
  3. 本质错配:我们不是在做 API 测试工具,而是在做 Robot 初始化编排——流程控制、变量注入、断言校验的需求都是明确且有限的,不需要 1:1 复现 Postman/ApiFox

目标:自建一套轻量的 HTTP 请求编排系统,包含 AdminPortal 可视化编辑器 + TFRSManager 执行引擎,完全替代 Postman/ApiFox Collection 方案。


2. 解决方案概览

┌─────────────────────────────────────────────────────────────┐
│                   AdminPortal (React)                        │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐ │
│  │           Init Flow Editor  (可视化编排器)              │ │
│  │                                                         │ │
│  │  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐        │ │
│  │  │ HTTP │ │  IF  │ │LOOP  │ │FOREACH│ │DELAY │  步骤  │ │
│  │  └──────┘ └──────┘ └──────┘ └──────┘ └──────┘  工具箱 │ │
│  │                                                         │ │
│  │  ┌─ 1. POST /api/login ─────────────────────────────┐  │ │
│  │  │  Body: {"user":"admin","pass":"{{Password}}"}     │  │ │
│  │  │  提取: token ← $.data.accessToken                 │  │ │
│  │  │  断言: HTTP 200 + $.code == 0                      │  │ │
│  │  └───────────────────────────────────────────────────┘  │ │
│  │  ┌─ IF {{Feature.enableFeishu}} == true ────────────┐  │ │
│  │  │  ┌─ 2. POST /api/feishu/config ──────────────┐   │  │ │
│  │  │  │  Body: {"token":"{{Feature.feishuToken}}"}  │   │  │ │
│  │  │  │  断言: HTTP 200                              │   │  │ │
│  │  │  └────────────────────────────────────────────┘   │  │ │
│  │  └───────────────────────────────────────────────────┘  │ │
│  │  ┌─ LOOP 最多10次, 间隔3s, 直到 $.status=="ready" ──┐  │ │
│  │  │  ┌─ 3. GET /api/status ──────────────────────┐   │  │ │
│  │  │  └────────────────────────────────────────────┘   │  │ │
│  │  └───────────────────────────────────────────────────┘  │ │
│  └────────────────────────────────────────────────────────┘ │
│                         │                                    │
│                    保存为 JSON                                │
│                         ▼                                    │
└─────────────────────────────────────────────────────────────┘
              Admin API: PUT /templates/:id/init-flow
┌─────────────────────────────────────────────────────────────┐
│                   TFRSManager (Go)                           │
│                                                              │
│  ┌─────────────┐    ┌──────────────┐    ┌────────────────┐ │
│  │ FlowParser  │───▶│ FlowRunner   │───▶│ InitExecutor   │ │
│  │ (校验+解析)  │    │ (执行引擎)    │    │ (任务集成)      │ │
│  └─────────────┘    └──────────────┘    └────────────────┘ │
│                            │                                 │
│                     ┌──────┴──────┐                         │
│                     │             │                          │
│              ┌──────▼───┐  ┌─────▼──────┐                  │
│              │VariableScope│ │ScriptSandbox│  ← 复用现有代码 │
│              │(三层变量)    │ │(Goja ES5)   │                │
│              └────────────┘ └────────────┘                  │
└─────────────────────────────────────────────────────────────┘

3. 数据模型

3.1 InitFlow JSON Schema

完全自定义的格式,不兼容 Postman/ApiFox,为我们的场景量身设计。

{
  "version": "1.0",
  "name": "飞书客服机器人初始化",
  "description": "部署后配置飞书渠道和管理员账户",
  "steps": [
    {
      "type": "http",
      "name": "登录获取 Token",
      "request": {
        "method": "POST",
        "path": "/api/admin/login",
        "headers": {
          "{{AdminKey}}": "{{AdminSecret}}",
          "Content-Type": "application/json"
        },
        "body": {
          "robotId": "{{RobotID}}",
          "password": "{{Password}}"
        }
      },
      "extract": [
        {
          "var": "token",
          "from": "body",
          "path": "$.data.accessToken"
        }
      ],
      "assert": [
        { "source": "status", "op": "eq", "value": 200 },
        { "source": "body", "path": "$.code", "op": "eq", "value": 0 }
      ]
    },
    {
      "type": "if",
      "condition": {
        "left": "{{Feature.enableFeishu}}",
        "op": "eq",
        "right": "true"
      },
      "steps": [
        {
          "type": "http",
          "name": "配置飞书渠道",
          "request": {
            "method": "POST",
            "path": "/api/channels/feishu",
            "headers": {
              "Authorization": "Bearer {{token}}"
            },
            "body": {
              "appId": "{{Feature.feishuAppId}}",
              "appSecret": "{{Feature.feishuAppSecret}}"
            }
          },
          "assert": [
            { "source": "status", "op": "eq", "value": 200 }
          ]
        }
      ]
    },
    {
      "type": "forEach",
      "array": "{{Feature.channels}}",
      "itemVar": "channel",
      "indexVar": "i",
      "steps": [
        {
          "type": "http",
          "name": "注册渠道 {{channel.name}}",
          "request": {
            "method": "POST",
            "path": "/api/channels",
            "headers": { "Authorization": "Bearer {{token}}" },
            "body": {
              "name": "{{channel.name}}",
              "type": "{{channel.type}}"
            }
          },
          "assert": [
            { "source": "status", "op": "eq", "value": 200 }
          ]
        }
      ]
    },
    {
      "type": "loop",
      "maxIterations": 10,
      "delay": 3000,
      "breakWhen": {
        "source": "body",
        "path": "$.data.status",
        "op": "eq",
        "value": "ready"
      },
      "steps": [
        {
          "type": "http",
          "name": "轮询服务就绪状态",
          "request": {
            "method": "GET",
            "path": "/api/health",
            "headers": { "Authorization": "Bearer {{token}}" }
          }
        }
      ]
    },
    {
      "type": "delay",
      "ms": 2000
    }
  ]
}

3.2 步骤类型定义

type 说明 必填字段 可选字段
http HTTP 请求 name, request extract, assert, script
if 条件分支 condition, steps else(步骤数组)
forEach 遍历数组 array, steps itemVar(默认 _item), indexVar(默认 _index)
loop 固定次数/条件循环 steps maxIterations(默认 1), delay(ms), breakWhen
delay 等待 ms

3.3 HTTP 步骤详解

request 对象

interface Request {
  method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
  path: string                    // 相对路径,基于 ServiceURL
  headers?: Record<string, string>  // 支持 {{变量}} 模板
  body?: object | string           // JSON 对象或 raw 字符串,支持模板
  timeout?: number                 // 单个请求超时(ms),默认 30000
}

extract(变量提取)

从响应中提取值,设为后续步骤可用的变量:

interface ExtractRule {
  var: string          // 变量名
  from: "body" | "header" | "status"  // 提取来源
  path?: string        // JSONPath 表达式(from=body 时必填)
  headerName?: string  // Header 名称(from=header 时必填)
}

示例:

[
  { "var": "token", "from": "body", "path": "$.data.accessToken" },
  { "var": "requestId", "from": "header", "headerName": "X-Request-Id" },
  { "var": "httpCode", "from": "status" }
]

assert(声明式断言)

interface AssertRule {
  source: "status" | "body"       // 断言来源
  path?: string                    // JSONPath(source=body 时必填)
  op: "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "contains" | "exists" | "notExist"
  value?: any                      // 期望值(exists/notExist 不需要)
}

script(高级脚本,escape hatch)

当声明式 extract/assert 无法满足时,使用 ES5 脚本:

{
  "type": "http",
  "name": "复杂处理",
  "request": { ... },
  "script": {
    "post": "var items = pm.response.json().data.items;\nvar names = items.map(function(i) { return i.name; });\npm.variables.set('itemNames', JSON.stringify(names));"
  }
}
  • script.pre:请求前执行(可修改变量、跳过请求)
  • script.post:请求后执行(可提取变量、做复杂断言)
  • 脚本与 extract/assert 可共存,执行顺序:pre → 发送请求 → extract → assert → post

3.4 条件表达式

if 和 loop.breakWhen 共用同一个条件结构:

interface Condition {
  // 简单条件(三元素)
  left?: string    // 支持 {{变量}} 模板
  op: string       // eq | neq | gt | gte | lt | lte | contains | exists | notExist
  right?: string   // 支持 {{变量}} 模板

  // 响应条件(用于 loop.breakWhen)
  source?: "body" | "status"
  path?: string    // JSONPath
  value?: any      // 期望值
}

4. 与 Postman/ApiFox 的关键差异

维度 Postman/ApiFox Init Flow Orchestrator
定位 通用 API 测试工具 Robot 初始化专用编排
断言 JS 脚本为主 声明式规则为主,脚本为辅
变量 多层作用域 + 全局 平台注入 + 步骤提取,两层够用
流程控制 setNextRequest / GUI 流程 结构化嵌套(if/forEach/loop/delay)
请求 URL 完整 URL 相对路径(ServiceURL 自动拼接)
数据格式 Postman Collection v2.1 自定义 JSON,简洁直观
运行时 Node.js sandbox Go 原生(声明式)+ Goja ES5(脚本)
前端 外部工具(ApiFox/Postman) 内置 AdminPortal 编排器

5. 平台注入变量

执行引擎在运行 InitFlow 前自动注入以下变量,步骤中通过 {{变量名}} 引用:

变量 说明 示例
ServiceURL Robot 服务地址(自动拼接到 path 前) http://friday.ns.svc:8080
AdminKey 管理认证 Header 名 admin_key
AdminSecret 管理认证密钥 ac4a30ae...
RobotID 机器人标识 friday
Password 用户创建时的密码 user-pwd
Feature 完整 Feature JSON {"enableFeishu":true,...}
Feature.* Feature 子字段 Feature.feishuTokenxxx

6. 后端架构

6.1 模块划分

internal/shared/initflow/           ← 新建包(替代 postcol/)
├── types.go                        ← InitFlow / Step / Condition 等类型定义
├── parser.go                       ← JSON 解析 + 校验(schema validation)
├── runner.go                       ← 执行引擎(遍历 steps,处理流程控制)
├── evaluator.go                    ← 条件求值器 + JSONPath 求值
├── extractor.go                    ← 变量提取器(extract 规则执行)
├── asserter.go                     ← 声明式断言求值器
└── runner_test.go                  ← 单元测试

internal/shared/initflow/ 复用:
├── (import) digital_employee_service.VariableScope   ← 变量作用域
├── (import) digital_employee_service.ScriptSandbox   ← Goja ES5 沙箱
├── (import) digital_employee_service.BuildEnvironmentVars ← 变量构建

6.2 复用 vs 新建

现有模块 决策 原因
VariableScope 复用 三层变量作用域机制完全适用
ScriptSandbox + pmAPI 复用 ES5 脚本 escape hatch 仍需要
BuildEnvironmentVars 复用 平台变量注入逻辑不变
InitProgress + 断点续传 复用 进度追踪和崩溃恢复仍需要
init_executor_v2.go 改造 改为调用 initflow.Runner 而非 CollectionRunner
postcol/ 删除 Postman/ApiFox 解析器不再需要
collection_runner.go 删除 initflow.Runner 替代
EchoServer (go-httpbin) 保留 本地测试仍需要

6.3 执行流程

InitExecutorV3.Execute(task)
  ├─ 1. 加载 Instance + Template.InitFlow (新字段,存 InitFlow JSON)
  ├─ 2. initflow.Parse(json) → 校验 + 解析为 Flow 结构体
  ├─ 3. 加载 Checkpoint(断点续传)
  ├─ 4. BuildEnvironmentVars() → 构建注入变量
  ├─ 5. NewVariableScope(envVars)
  ├─ 6. runner.Execute(flow, scope, checkpoint)
  │     │
  │     ├─ 遍历 steps:
  │     │   ├─ http  → 解析模板 → 发送请求 → extract → assert → script.post
  │     │   ├─ if    → 求值 condition → true 执行 steps / false 执行 else
  │     │   ├─ forEach → 解析 array → 每个元素设 itemVar → 执行 steps
  │     │   ├─ loop  → 循环执行 steps → 检查 breakWhen → delay
  │     │   └─ delay → time.Sleep
  │     │
  │     └─ 每步完成后:保存进度 + 变量快照
  └─ 7. 返回结果 → FSM 事件(init_complete_success / init_complete_failure)

6.4 JSONPath 实现

轻量 JSONPath 子集,支持以下语法即可覆盖所有场景:

$.data.accessToken         → 嵌套字段访问
$.data.items[0].name       → 数组索引
$.data.items.length        → 数组长度
$.code                     → 顶层字段

推荐使用 github.com/PaesslerAG/jsonpath 或自行实现(仅需点路径 + 数组索引)。


7. 前端架构

7.1 AdminPortal 编排组件

交互形态:列表式步骤编辑器(非 DAG 流程图)

理由: - Init 流程本质是顺序执行 + 嵌套控制,不是复杂 DAG - 列表式更直观,学习成本低 - 实现复杂度远低于流程图编辑器 - 通过缩进/嵌套表达控制流程

7.2 组件结构

InitFlowEditor/
├── FlowEditor.tsx              ← 主编辑器容器
├── StepList.tsx                ← 步骤列表(支持拖拽排序、嵌套)
├── StepCard.tsx                ← 单步卡片(显示摘要 + 展开编辑)
├── HttpStepForm.tsx            ← HTTP 步骤编辑表单
│   ├── RequestSection.tsx      ← Method/Path/Headers/Body
│   ├── ExtractSection.tsx      ← 变量提取规则
│   ├── AssertSection.tsx       ← 声明式断言规则
│   └── ScriptSection.tsx       ← 高级脚本(折叠,按需展开)
├── IfStepForm.tsx              ← IF 条件配置
├── ForEachStepForm.tsx         ← ForEach 配置
├── LoopStepForm.tsx            ← Loop 配置
├── DelayStepForm.tsx           ← Delay 配置
├── ConditionEditor.tsx         ← 条件表达式编辑器(复用于 if/loop)
├── VariableChips.tsx           ← 可用变量提示(平台变量 + 已提取变量)
├── FlowPreview.tsx             ← 只读预览/执行进度展示
└── useInitFlow.ts              ← 状态管理 hook

7.3 关键交互

  1. 步骤工具箱:顶部工具栏,点击添加不同类型的步骤
  2. 拖拽排序:步骤卡片可拖拽调整顺序
  3. 嵌套:if/forEach/loop 的子步骤缩进展示,折叠/展开
  4. 变量自动补全:输入 {{ 时弹出可用变量列表(平台变量 + 前序步骤 extract 的变量)
  5. 断言配置器:下拉选择 source → 填 JSONPath → 选操作符 → 填期望值
  6. 实时校验:编辑时即时校验 JSON 合法性、变量引用合法性
  7. 预览模式:展示执行时的步骤流程(带变量标记)

7.4 推荐依赖

用途 推荐库
拖拽排序 @dnd-kit/core + @dnd-kit/sortable
代码编辑 @monaco-editor/react(Body/Script 编辑)
表单 Ant Design Form(AdminPortal 已有)
JSON Schema 校验 ajv(前端校验 InitFlow JSON)

8. 数据库变更

8.1 模板表

digital_employee_templates 表新增字段:

ALTER TABLE digital_employee_templates
ADD COLUMN init_flow JSONB;        -- InitFlow JSON(新格式)
-- init_steps 字段保留但标记废弃,迁移完成后删除

8.2 迁移策略

  • 新创建的模板使用 init_flow 字段
  • 旧模板的 init_steps(Postman JSON)通过一次性迁移脚本转换为 init_flow 格式
  • 过渡期 InitExecutor 同时支持两种格式(先查 init_flow,为空则 fallback 到 init_steps)
  • 所有模板迁移完成后删除 init_steps 字段和 postcol 包

9. API 变更

9.1 Admin API

# 更新模板的初始化流程
PUT /api/admin/templates/:id/init-flow
Body: { "initFlow": { ... InitFlow JSON ... } }

# 获取模板的初始化流程
GET /api/admin/templates/:id/init-flow
Response: { "initFlow": { ... }, "preview": { "stepCount": 5, "hasConditions": true, ... } }

# 校验初始化流程(不保存)
POST /api/admin/templates/validate-init-flow
Body: { "initFlow": { ... } }
Response: { "valid": true, "warnings": [...] }

# 预览执行计划(干运行,展示变量解析后的步骤)
POST /api/admin/templates/:id/preview-init-flow
Body: { "featureValues": { "enableFeishu": true, ... } }
Response: { "steps": [ ... 解析后的步骤列表 ... ] }

10. 实现阶段

Phase 1:后端执行引擎(MVP)

目标:能跑通 InitFlow JSON,替代 CollectionRunner

  • 定义 initflow 包类型(types.go
  • 实现解析器 + JSON Schema 校验(parser.go
  • 实现执行引擎 — 仅 httpdelay 步骤(runner.go
  • 实现声明式断言和变量提取(asserter.go, extractor.go
  • 改造 InitExecutorV2 支持 InitFlow 格式
  • 编写 seed testdata(用 InitFlow JSON 替代 Postman Collection)
  • 单元测试 + EchoServer 端到端验证

验证make run-echo + 手动触发 Init 任务,InitFlow 正确执行

Phase 2:流程控制

目标:支持 if/forEach/loop 控制节点

  • 实现条件求值器(evaluator.go
  • 实现 if/else 分支执行
  • 实现 forEach 数组遍历(含 itemVar/indexVar 注入)
  • 实现 loop 循环 + breakWhen + delay
  • 嵌套深度限制(max 5 层)+ 循环安全阀门(maxIterations 上限 100)
  • 单元测试覆盖所有控制流组合

Phase 3:AdminPortal 编排器(基础版)

目标:Admin 可视化编辑 InitFlow

  • InitFlowEditor 主容器 + StepList 拖拽
  • HttpStepForm(Method/Path/Headers/Body 编辑)
  • ExtractSection + AssertSection(声明式规则配置)
  • 变量自动补全(VariableChips)
  • JSON 导入/导出(从编辑器导出 InitFlow JSON)
  • Admin API(save/load/validate)

Phase 4:AdminPortal 编排器(完整版)

目标:完整的流程控制编辑 + 脚本支持

  • IfStepForm + ForEachStepForm + LoopStepForm
  • ConditionEditor(条件表达式可视化编辑)
  • ScriptSection(Monaco 编辑器,ES5 脚本编写)
  • FlowPreview(只读预览 + 执行进度展示)
  • 预览执行计划 API(干运行)

Phase 5:迁移 + 清理

目标:完全切换到新系统

  • 迁移脚本:将所有 init_steps(Postman JSON)转为 init_flow
  • 删除 postcol/
  • 删除 collection_runner.go
  • 删除 init_steps 数据库字段
  • 更新文档

11. 验证方案

开发阶段

# 1. 启动 EchoServer
make run-echo    # go-httpbin :9999

# 2. 运行单元测试
go test ./internal/shared/initflow/...

# 3. 端到端测试(Mock 模式)
make seed-local  # 写入带 InitFlow 的 seed 模板
make local-run   # 启动服务
# → 创建数字员工 → 触发部署 → Init 执行 → 验证进度

UAT 阶段

# 1. AdminPortal 编排器中创建 InitFlow
# 2. 对着 EchoServer 预览执行
# 3. 部署到 K8s 对着真实 Robot 执行
# 4. 验证断点续传(中途 kill executor → 恢复后从断点继续)