Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
143 changes: 143 additions & 0 deletions content/docs/build/automation/approvals.zh-Hans.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
title: 审批流程
description: 把记录路由给人签核 —— 同时不让自动化悄悄绕过行级安全。
---

# 审批流程

**审批就是带审批节点的[流程](/docs/build/automation/flows):流程暂停,直到有人批准或拒绝,然后沿匹配的分支继续。**没有需要另学的独立审批引擎 —— 触发器、分支、错误处理都与任何其他流程完全一致。本页新增的内容是审批节点本身,以及两个与路由同样重要的访问决策。

## 谁做什么

三个角色,刻意分离:

| 角色 | 能做 | 需要 |
|---|---|---|
| **构建者** | 编写和编辑审批流程 | `manage_metadata`(通常是 Studio 用户) |
| **提交人** | 提交进入流程的记录 | 正常的记录访问 —— 不需要任何自动化配置权限 |
| **审批人** | 处理审批请求(批准 / 拒绝) | 由其权限集授予的能力 |

最终用户提交记录、处理请求;他们从不编辑自动化。把自动化配置界面挡在消费者应用之外。

## 以谁的身份运行 —— 安全决策

流程通过 `runAs` 声明运行身份,对审批而言,正是这个决策防止流程悄悄绕过行级安全:

| `runAs` | 数据操作以谁运行 | 何时使用 |
|---|---|---|
| `'user'`(默认) | **提交人**,遵守其 RLS | 流程只触碰提交人本来就能看到的记录 |
| `'system'` | **提权** —— 绕过 RLS | 流程必须读写提交人看不到的记录(写入总账、通知一个拥有提交人不可见行的审批人) |

**显式**声明提权,让它可见而非意外。默认的 `'user'` 意味着审批流程无法悄悄给提交人跨租户或跨所有者的触达 —— 提权是自愿开启且可审计的。

> **提示:**由定时触发的升级流程没有触发用户 —— 它必须 `runAs: 'system'` 才能行动。这是提权的正当场景;"到处 `system` 好让它能跑"才是反模式。

## 审批节点

节点声明**谁来审批**以及多个决策如何聚合:

```ts
{
id: 'manager_approval',
type: 'approval',
label: 'Manager Approval',
config: {
approvers: [{ type: 'field', value: 'owner_manager_id' }],
behavior: 'unanimous',
approvalStatusField: 'approval_status',
lockRecord: true,
},
}
```

| 配置 | 作用 |
|---|---|
| `approvers` | 谁必须决策 —— 具名用户、岗位,或从记录字段解析出的用户(如提交人的经理) |
| `behavior` | 多个审批人如何聚合,如 `'unanimous'`(一致同意) |
| `approvalStatusField` | 可选的记录字段,插件把请求状态镜像进去 |
| `lockRecord` | 请求待定期间锁定记录、禁止编辑 |

> **警告 —— `position` vs `role`。**`{ type: 'position', value: 'finance_manager' }` 路由给某个岗位的持有者(`sys_user_position`)。而 `role` 审批人类型是组织成员层级(`sys_member.role`:`owner`/`admin`/`member`)—— 把岗位名写成 `type: 'role'` 谁也匹配不上,请求就会卡住。`os lint` 会标记这个问题(`approval-role-not-membership-tier`)。

## 一个完整的审批流程

把大额提案路由给负责人的经理,再按决策分支:

```ts
export const opportunityApproval = defineFlow({
name: 'opportunity_approval',
label: 'Opportunity Approval',
type: 'record_change',
status: 'active',
runAs: 'user', // 用提交人的 RLS,除非某一步确实需要更多权限
nodes: [
{
id: 'start',
type: 'start',
config: {
triggerType: 'record-after-update',
objectName: 'opportunity',
condition: "record.amount >= 50000 && record.stage == 'proposal'",
},
},
{
id: 'manager_approval',
type: 'approval',
label: 'Manager Approval',
config: {
approvers: [{ type: 'field', value: 'owner_manager_id' }],
behavior: 'unanimous',
approvalStatusField: 'approval_status',
lockRecord: true,
},
},
{ id: 'mark_approved', type: 'update_record', label: 'Mark Approved' },
{ id: 'mark_rejected', type: 'update_record', label: 'Mark Rejected' },
{ id: 'end', type: 'end' },
],
edges: [
{ id: 'e1', source: 'start', target: 'manager_approval' },
{ id: 'approved', source: 'manager_approval', target: 'mark_approved', label: 'approve' },
{ id: 'rejected', source: 'manager_approval', target: 'mark_rejected', label: 'reject' },
{ id: 'e4', source: 'mark_approved', target: 'end' },
{ id: 'e5', source: 'mark_rejected', target: 'end' },
],
});
```

注意带标签的边:`approve` 和 `reject` 命名了决策之后的分支。永远要建模拒绝路径 —— 只有批准分支的流程会让被拒绝的记录搁浅。

**多级审批:**串联多个 `approval` 节点。**并行审批:**见[流程](/docs/build/automation/flows)中的聚合节点模式。

## 审批人体验到什么

`@objectstack/plugin-approvals` 包持有持久化的审批状态。请求待定期间:

1. 插件持久化请求(`sys_approval_request`)以及针对它的每个决策(`sys_approval_action`)—— 你的审批历史是可查询的数据。
2. 设置 `lockRecord: true` 时,记录被锁定、禁止编辑,直到决策落地。
3. 如果设置了 `approvalStatusField`,记录自身的字段会镜像请求状态,视图和报表就能按它筛选。
4. 审批人批准或拒绝;插件沿匹配的边恢复被暂停的流程。

批准本身也是被门控的操作。把"可以批准"建模为一个能力(如 `approve_invoice`),由审批人的权限集授予,并把批准操作的 `requiredPermissions` 建立在它之上 —— 这样门控就在 **UI 和服务端**两侧同时强制,而不只是从屏幕上藏起来。

## 最佳实践

| 应该 | 不应该 |
|---|---|
| 定义清晰的进入条件 | 设置过多的审批环节 |
| 设置合理的超时时间 | 把审批做得过于复杂 |
| 在合适的场景允许撤回 | 忘记拒绝路径 |
| 通知所有相关方 | 把审批人写死 |
| 追踪审批历史 | 只在 UI 层门控"批准" |
| 默认 `runAs: 'user'`,一步一步地提权 | 到处设置 `runAs: 'system'` "好让它能跑" |

## 下一步

| 页面 | 原因 |
|---|---|
| [流程](/docs/build/automation/flows) | 本页所基于的流程参考 —— 触发器、步骤、错误处理 |
| [工作流](/docs/build/automation/workflows) | 约束审批所处的生命周期 |
| [操作](/docs/build/interface/actions) | 把提交和批准做成界面上的按钮 |
| [CEL 表达式](/docs/reference/cel) | 进入条件背后的语言 |
| [Email](/docs/configure/email) | 通知审批人与相关方 |
| [自动化总览](/docs/build/automation) | 选择器:流程 vs 工作流 vs 审批 |
39 changes: 39 additions & 0 deletions content/docs/build/automation/index.zh-Hans.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: 自动化
description: 为任务选对工具 —— 流程管步骤,工作流管状态,审批管人工签核。
---

# 自动化

**自动化以声明的方式把业务逻辑挂到数据模型上 —— 作为由运行时执行的元数据 —— 而不是散落在应用代码里。**三个工具覆盖全部场景,一开始就选对,能省去日后返工。

## 我该用哪一个?

| 你需要 | 用 | 阅读 |
|---|---|---|
| "X 发生时,做 Y" —— 由记录变更、定时或按钮点击驱动的步骤 | **流程** | [流程](/docs/build/automation/flows) |
| "这条记录只能按这些事件在这些状态间移动" —— 受控的生命周期 | **工作流**(状态机) | [工作流](/docs/build/automation/workflows) |
| "必须有人签核后才能继续" —— 路由给人的决策 | **审批** | [审批流程](/docs/build/automation/approvals) |

经验法则:**状态**用工作流建模,**步骤**用流程建模。审批不是独立引擎 —— 审批就是在审批节点暂停、等人决策的流程。

## 它们如何组合

三者是互补而非竞争:

- **工作流**约束哪些生命周期迁移是合法的 —— 状态、迁移、守卫条件,仅此而已。
- **流程**执行这些迁移周边的副作用:发邮件、更新记录、调用外部服务、等待、分支。
- 流程内的**审批节点**阻塞执行直到审批人行动,然后沿批准或拒绝分支继续。

所以"工单从 new → assigned → resolved"是工作流;"升级时通知经理"是流程;"超过 5 万美元的折扣需要财务经理签核"是带审批节点的流程。

> **提示:**如果需求读起来是"X 发生时,做 Y",那就是流程。如果读起来是"这条记录绝不能跳过某个状态",那就是工作流。从这里出发,你几乎不会选错。

## 下一步

| 页面 | 原因 |
|---|---|
| [流程](/docs/build/automation/flows) | 完整的流程参考 —— 触发器、步骤类型、错误处理、CEL |
| [工作流](/docs/build/automation/workflows) | 严格生命周期的状态、迁移与守卫条件 |
| [审批流程](/docs/build/automation/approvals) | 审批节点、运行身份与审批人体验 |
| [CEL 表达式](/docs/reference/cel) | 条件与守卫条件所用的表达式语言 |
139 changes: 139 additions & 0 deletions content/docs/build/automation/workflows.zh-Hans.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
title: 工作流
description: 把记录的生命周期建模成状态机 —— 合法状态、带守卫条件的迁移,其余的交给流程。
---

# 工作流

**工作流把记录的生命周期建模为有限状态机:记录可以处于的状态、在状态间移动它的事件,以及移动成立所必须满足的守卫条件。**当核心需求是"这个对象只能按这些事件在这些状态间移动"时,就用它。

不存在独立的 Salesforce 式 Workflow Rule 编写类型。旧的"工作流"概念被干净地一分为二:

- **状态机元数据**负责严格的生命周期迁移 —— 即本页。
- **[流程](/docs/build/automation/flows)**负责事件触发或定时的自动化,包括[审批](/docs/build/automation/approvals)暂停。

## 工作流 vs 流程

| | 工作流(状态机) | 流程 |
|---|---|---|
| 建模 | *状态* —— 记录处于生命周期的哪里 | *步骤* —— 某事发生时做什么 |
| 回答 | "此刻允许这个迁移吗?" | "我们要对它做什么?" |
| 形态 | 状态、迁移、守卫条件 | 节点与边:触发器、动作、分支 |
| 副作用 | 无 —— 它只做约束 | 全都有 —— 邮件、更新、HTTP、等待 |

两者组合使用:状态机约束迁移;当你需要通知、更新记录或调用外部系统时,由流程执行迁移周边的副作用。

## 定义状态机

一个必须按 `new → assigned → resolved` 移动、且带升级路径的支持工单:

```ts
import type { StateMachineConfig } from '@objectstack/spec/automation';

export const caseLifecycle: StateMachineConfig = {
id: 'case_lifecycle',
initial: 'new',
states: {
new: {
on: {
ASSIGN: { target: 'assigned' },
},
},
assigned: {
on: {
RESOLVE: { target: 'resolved', cond: 'has_resolution' },
ESCALATE: { target: 'escalated' },
},
},
escalated: {
on: {
RESOLVE: { target: 'resolved', cond: 'has_resolution' },
},
},
resolved: {
type: 'final',
},
},
};
```

把它当契约来读:`new` 状态的工单只能被分派。`assigned` 状态的工单可以被解决 —— 但只有 `has_resolution` 守卫条件通过时 —— 或者被升级。`resolved` 是终态;再没有什么能移动它。

## 解剖

| 键 | 声明什么 |
|---|---|
| `id` | 状态机的标识符 |
| `initial` | 每条新记录的初始状态 |
| `states` | 状态名 → 其出向迁移的映射 |
| `on` | 该状态响应的事件(`ASSIGN`、`RESOLVE`……) |
| `target` | 事件把记录移动到的状态 |
| `cond` | 迁移触发前必须满足的守卫条件 |
| `type: 'final'` | 终态 —— 没有出向迁移 |

### 守卫条件

守卫条件(`cond`)让迁移带上前提:上例中,只有当 `has_resolution` 成立时 `RESOLVE` 才能到达 `resolved`。守卫条件把"没有解决方案就不能关单"编码成结构性规则,而不是散落在 UI 代码里的验证。

### 事件,而非字段写入

迁移由具名**事件**(`ASSIGN`、`ESCALATE`)触发,而不是对状态字段的任意编辑。这正是重点:状态机定义了合法移动的完整集合,未声明的一律不可能发生。

> **提示:**保持状态机最小 —— 状态、迁移、守卫条件。一旦你想"然后再发封邮件",你就已经离开了工作流的领地:把副作用放进一个响应该迁移的[流程](/docs/build/automation/flows)。

## 副作用属于流程

状态机只描述合法迁移与守卫条件 —— 别无其他。当某次迁移应该*做*点什么(通知销售、写入关闭日期、调用外部系统)时,给状态机配上一个由记录变更触发的流程:

```ts
export const dealClosedWon = defineFlow({
name: 'deal_closed_won',
type: 'record_change',
nodes: [
{
id: 'start',
type: 'start',
config: {
triggerType: 'record-after-update',
objectName: 'opportunity',
condition: "record.stage == 'closed_won' && previous.stage != 'closed_won'",
},
},
{ id: 'set_closed_date', type: 'update_record', label: 'Set Closed Date' },
{ id: 'notify_sales', type: 'notify', label: 'Notify Sales' },
{ id: 'end', type: 'end' },
],
edges: [
{ id: 'e1', source: 'start', target: 'set_closed_date' },
{ id: 'e2', source: 'set_closed_date', target: 'notify_sales' },
{ id: 'e3', source: 'notify_sales', target: 'end' },
],
});
```

状态机保证这笔交易是合法地*到达* `closed_won` 的;流程处理接下来发生的事。上面这样的条件是 CEL 表达式 —— 见 [CEL](/docs/reference/cel)。

## 从 Workflow Rules 迁移

如果你来自有 Workflow Rules 的平台,对照表如下:

| 旧概念 | 当前对应 |
|---|---|
| Workflow Rule | 流程 |
| 时间触发器 | 定时流程 |
| 字段更新动作 | `update_record` 节点 |
| 邮件提醒 | `notify` 节点 |
| HTTP 调用 | `http` 节点 |
| Approval Process | 带一个或多个 `approval` 节点的流程 |

判断标准:如果旧规则是"X 发生时,做 Y",就建成一个小流程。如果它是"这条记录必须在受控状态间移动",就把生命周期建成状态机,副作用交给流程。

## 下一步

| 页面 | 原因 |
|---|---|
| [流程](/docs/build/automation/flows) | 迁移周边的副作用 —— 触发器、步骤、错误处理 |
| [审批流程](/docs/build/automation/approvals) | 作为流程内暂停的人工签核 |
| [CEL 表达式](/docs/reference/cel) | 守卫条件与流程条件背后的语言 |
| [数据建模](/docs/build/data) | 你正在约束其生命周期的对象 |
| [自动化总览](/docs/build/automation) | 选择器:流程 vs 工作流 vs 审批 |
Loading
Loading