@thinkingai/ae-cli 6.0.17 → 6.0.18-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -145,14 +145,10 @@ ae-cli analysis_audience +get_cluster_definition_schema --cluster_type condition
|
|
|
145
145
|
ae-cli engage +channel_list --project_id <projectId>
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
5. The
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
- `edgeList`
|
|
153
|
-
6. `nodeList[].config` and `edgeList[].config` must be JSON strings, not JSON objects.
|
|
154
|
-
7. If `targetClusterQp` appears inside a node `config`, its value should usually also be a string produced by `JSON.stringify`, not a raw object.
|
|
155
|
-
8. You must self-check before submitting:
|
|
148
|
+
5. `+save_flow` is **operation-based** (protocol v2). The `--req` object must carry an `operation` of `build`, `preview`, or `commit`. Do **not** use the old `nodeList` / `edgeList` field names — use `nodes` / `edges` with `operation=build`. A legacy `nodeList`/`edgeList` payload (or a missing `operation`) is rejected with `Unsupported save_flow operation: null`.
|
|
149
|
+
6. Run the lifecycle: `build` (returns `ready_to_preview` or `need_input`) → resolve any `need_input` slot → `preview` (re-issues `draftVersion` + `confirmToken`) → `commit` (uses the **preview** `draftVersion` + `confirmToken`) → returns `flowUuid`.
|
|
150
|
+
7. `nodes[].config` / `edges[].config` may be a JSON object or a JSON string. If `targetClusterQp` appears inside a node `config`, its value is usually a `JSON.stringify`'d string, not a raw object.
|
|
151
|
+
8. You must self-check before previewing/committing:
|
|
156
152
|
- There is exactly one entry node
|
|
157
153
|
- There is at least one `exit_flow`
|
|
158
154
|
- `edge.source` and `edge.target` both reference valid nodes
|
|
@@ -163,8 +159,8 @@ ae-cli engage +channel_list --project_id <projectId>
|
|
|
163
159
|
|
|
164
160
|
- Do not invent a `channelId`
|
|
165
161
|
- Do not fill in branching logic when the user has not provided enough information
|
|
166
|
-
- Do not submit business-semantic nodes directly as final `
|
|
167
|
-
- Do not
|
|
162
|
+
- Do not submit business-semantic nodes directly as final `nodes`
|
|
163
|
+
- Do not use the legacy `nodeList` / `edgeList`, and do not omit `operation`
|
|
168
164
|
|
|
169
165
|
### Recommended Order
|
|
170
166
|
|
|
@@ -173,9 +169,10 @@ User request
|
|
|
173
169
|
-> Organize intent
|
|
174
170
|
-> analysis_audience +get_cluster_definition_schema --cluster_type condition
|
|
175
171
|
-> +channel_list --project_id <projectId>
|
|
176
|
-
-> Build
|
|
172
|
+
-> Build nodes / edges
|
|
177
173
|
-> Self-check
|
|
178
|
-
-> +save_flow
|
|
174
|
+
-> +save_flow operation=build -> (need_input?) -> preview -> commit
|
|
175
|
+
-> +flow_detail (verify)
|
|
179
176
|
```
|
|
180
177
|
|
|
181
178
|
For more detailed generation rules, consult these references first:
|
|
@@ -1,67 +1,114 @@
|
|
|
1
1
|
# ae-engage `+save_flow`
|
|
2
2
|
|
|
3
|
-
Create or
|
|
3
|
+
Create, edit, preview, or commit a flow canvas draft.
|
|
4
4
|
|
|
5
|
-
Mapped command: `ae-cli engage +save_flow`
|
|
5
|
+
Mapped command: `ae-cli engage +save_flow --project_id <projectId> --req '<req-json>'`
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
> **Protocol v2 (current).** `save_flow` is now **operation-based**. The `--req` object must carry an `operation` of `build`, `preview`, or `commit`. Do **not** use the old `nodeList` / `edgeList` field names — send `nodes` / `edges` instead. Posting a legacy `nodeList`/`edgeList` payload (or omitting `operation`) makes the backend reject the call with `Unsupported save_flow operation: null`.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
2. Then map the intent to the flow canvas `req`
|
|
11
|
-
3. Finally submit through the CLI
|
|
9
|
+
The CLI passes your `--req` straight through to the backend (it only injects `projectId`). So everything in this document is about building the correct `req` object.
|
|
12
10
|
|
|
13
11
|
---
|
|
14
12
|
|
|
15
13
|
## 1. General Principles
|
|
16
14
|
|
|
17
|
-
`+save_flow` does not accept natural language or vague business descriptions directly. It accepts
|
|
15
|
+
`+save_flow` does not accept natural language or vague business descriptions directly. It accepts a **flow canvas request whose `req.operation` drives a three-step lifecycle**:
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- `edgeList`
|
|
25
|
-
- and optional `groupId`、`tzOffset`、`flowUuid`、`parentFlowUuid`、`versionType`
|
|
17
|
+
```text
|
|
18
|
+
build ──► status = ready_to_preview ──► preview ──► commit ──► flowUuid
|
|
19
|
+
│ │
|
|
20
|
+
└──► status = need_input ──► (answer slot, build again) └──► +flow_detail (verify)
|
|
21
|
+
```
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
You still organize user requirements into an intermediate intent first, then map the intent to `nodes` / `edges`, then run the lifecycle.
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
- `build`: assemble/edit a draft from `nodes` + `edges` (or `dsl`, or clone). Returns `ready_to_preview` (with `draftId`, `draftVersion`, `confirmToken`, `preview.mainPath`) or `need_input` (one more slot required).
|
|
26
|
+
- `preview`: validate the draft; **re-issues** a fresh `draftVersion` + `confirmToken`.
|
|
27
|
+
- `commit`: finalize the draft into a flow version using the **preview** `draftVersion` + `confirmToken`; returns the final `flowUuid`.
|
|
28
|
+
- Always verify with `ae-cli engage +flow_detail --project_id <projectId> --flow_uuid <flowUuid>`.
|
|
32
29
|
|
|
33
30
|
---
|
|
34
31
|
|
|
35
32
|
## 2. Workflow
|
|
36
33
|
|
|
37
|
-
Use the following five-step workflow:
|
|
38
|
-
|
|
39
34
|
1. Identify the flow intent from the user input and produce a unified intent JSON.
|
|
40
35
|
2. Run `ae-cli analysis_audience +get_cluster_definition_schema --cluster_type condition` to obtain the condition cluster definition schema for assembling condition-related fields later.
|
|
41
|
-
3. Run `ae-cli engage +channel_list --project_id <projectId>` to get the available channels
|
|
42
|
-
4. Map the intent JSON to
|
|
43
|
-
5.
|
|
36
|
+
3. Run `ae-cli engage +channel_list --project_id <projectId>` to get the available channels and match real `channelId` values for touchpoint nodes. For `webhook_push`, also run `ae-cli engage +channel_detail` and use `data.config.paramsList` to build `contentList`.
|
|
37
|
+
4. Map the intent JSON to `nodes` and `edges` (compact form, see §7 / §8).
|
|
38
|
+
5. `build` → resolve any `need_input` slot → `preview` → `commit`, then verify with `+flow_detail`.
|
|
44
39
|
|
|
45
40
|
---
|
|
46
41
|
|
|
47
|
-
## 3.
|
|
42
|
+
## 3. `req` Schema (authoritative)
|
|
48
43
|
|
|
49
|
-
|
|
44
|
+
`req` accepts these fields (only `operation` is hard-required):
|
|
50
45
|
|
|
51
|
-
|
|
46
|
+
| Field | Type | Used in | Notes |
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
| `operation` | string | all | **Required.** `build` \| `preview` \| `commit` |
|
|
49
|
+
| `flowName` | string | build | Flow name. If it is the only content, returns a `need_input` draft with no default nodes |
|
|
50
|
+
| `flowDesc` | string | build | Flow description, **max 200 characters** |
|
|
51
|
+
| `groupId` | int | build | Flow group ID, default `0` |
|
|
52
|
+
| `tzOffset` | number | build | Timezone offset in hours; defaults to server setting |
|
|
53
|
+
| `versionType` | int | build | `1=current, 2=update, 3=new, 4=test` |
|
|
54
|
+
| `nodes` | array | build | Compact nodes for structured build (see §7) |
|
|
55
|
+
| `edges` | array | build | Compact edges for structured build (see §8) |
|
|
56
|
+
| `dsl` | string | build | Topology-only: lines `node <id> <type>` and `edge <source> -> <target>` |
|
|
57
|
+
| `sourceFlowUuid` | string | build | Clone an existing flow into a new draft |
|
|
58
|
+
| `flowUuid` | string | build | Draft flow UUID for update-draft mode |
|
|
59
|
+
| `parentFlowUuid` | string | build | Base version flow UUID for new-version mode |
|
|
60
|
+
| `userIntent` | string | build | Natural-language intent (optional) |
|
|
61
|
+
| `nodeConfigs` | array | build (edit) | Declarative node config replacements; each item needs `nodeId` + `config` |
|
|
62
|
+
| `deleteNodeIds` | array | build (edit) | Node IDs to delete from the draft |
|
|
63
|
+
| `deleteEdges` | array | build (edit) | Edges to delete (by `edgeId` or `source`+`target`) |
|
|
64
|
+
| `slotAnswer` | object | build (continue) | Answer for the current server-requested slot when status is `need_input` |
|
|
65
|
+
| `draftId` | string | preview, commit, build(continue) | Server draft ID returned by `build` |
|
|
66
|
+
| `expectedVersion` | int | preview, build(edit) | Expected draft version for editing or preview |
|
|
67
|
+
| `draftVersion` | int | commit | Draft version returned by `preview` |
|
|
68
|
+
| `confirmToken` | string | commit | Opaque token returned by **preview** (not build) |
|
|
69
|
+
|
|
70
|
+
`flowUuid` and `parentFlowUuid` are mutually exclusive; when creating a brand-new draft, provide neither.
|
|
71
|
+
|
|
72
|
+
### Compact node (`nodes[]`)
|
|
73
|
+
|
|
74
|
+
| Field | Required | Description |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `id` | Yes | Request-local node ID, unique within the request |
|
|
77
|
+
| `type` | Yes | Flow node type (see §7.2) |
|
|
78
|
+
| `config` | No | Full node business config — JSON object or JSON string |
|
|
79
|
+
| `name` | No | Display name |
|
|
80
|
+
| `desc` | No | Node description |
|
|
81
|
+
|
|
82
|
+
### Compact edge (`edges[]`)
|
|
83
|
+
|
|
84
|
+
| Field | Required | Description |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `source` | Yes | Upstream node ID |
|
|
87
|
+
| `target` | Yes | Downstream node ID |
|
|
88
|
+
| `edgeId` | No | Edge ID, unique within the canvas |
|
|
89
|
+
| `sourceBranchId` | No | Branch ID for edges leaving a split/judge node |
|
|
90
|
+
| `config` | No | Edge business config as JSON string |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 4. Step One: Intent Identification
|
|
95
|
+
|
|
96
|
+
### 4.1 Information That Must Be Confirmed First
|
|
97
|
+
|
|
98
|
+
Before building, confirm at least these four categories:
|
|
52
99
|
|
|
53
100
|
| Item | Description |
|
|
54
101
|
|---|---|
|
|
55
|
-
| business scenario |
|
|
56
|
-
| target users |
|
|
57
|
-
| touchpoint method |
|
|
58
|
-
| branching conditions |
|
|
102
|
+
| business scenario | new-user activation, churn win-back, paid conversion, etc. |
|
|
103
|
+
| target users | who can enter the flow |
|
|
104
|
+
| touchpoint method | Push, WeChat subscription, Webhook, etc. |
|
|
105
|
+
| branching conditions | whether grouped handling is needed, and the conditions |
|
|
59
106
|
|
|
60
|
-
If any
|
|
107
|
+
If any is missing, do not build.
|
|
61
108
|
|
|
62
|
-
###
|
|
109
|
+
### 4.2 Intent Output Format
|
|
63
110
|
|
|
64
|
-
|
|
111
|
+
Organize the user requirement into an intermediate intent JSON (not the final `req`):
|
|
65
112
|
|
|
66
113
|
```json
|
|
67
114
|
{
|
|
@@ -74,13 +121,7 @@ First organize the user requirement into the following intent JSON. This JSON is
|
|
|
74
121
|
"schedule": "<string|null>",
|
|
75
122
|
"start_date": "<YYYY-MM-DD|YYYY-MM-DD HH:mm|null>",
|
|
76
123
|
"end_date": "<YYYY-MM-DD|YYYY-MM-DD HH:mm|null>",
|
|
77
|
-
"trigger_event": {
|
|
78
|
-
"event": "<string|null>",
|
|
79
|
-
"op": "<string|null>",
|
|
80
|
-
"count": "<number|null>",
|
|
81
|
-
"property_filter": "<object|null>",
|
|
82
|
-
"time_window": "<string|null>"
|
|
83
|
-
}
|
|
124
|
+
"trigger_event": { "event": "<string|null>", "op": "<string|null>", "count": "<number|null>", "property_filter": "<object|null>", "time_window": "<string|null>" }
|
|
84
125
|
},
|
|
85
126
|
"nodes": [
|
|
86
127
|
{
|
|
@@ -94,121 +135,41 @@ First organize the user requirement into the following intent JSON. This JSON is
|
|
|
94
135
|
"condition": "<object|null>",
|
|
95
136
|
"event": "<object|null>",
|
|
96
137
|
"wait_time": "<string|null>",
|
|
97
|
-
"duration": "<string|null>",
|
|
98
138
|
"split_flow_type": "<1|2|null>",
|
|
99
|
-
"branches": [
|
|
100
|
-
{
|
|
101
|
-
"bid": "b1",
|
|
102
|
-
"label": "<string>",
|
|
103
|
-
"condition": "<object|null>",
|
|
104
|
-
"time_limit": "<string|null>",
|
|
105
|
-
"percentage": "<number|null>"
|
|
106
|
-
}
|
|
107
|
-
]
|
|
139
|
+
"branches": [ { "bid": "b1", "label": "<string>", "condition": "<object|null>", "time_limit": "<string|null>", "percentage": "<number|null>" } ]
|
|
108
140
|
}
|
|
109
141
|
],
|
|
110
|
-
"edges": [
|
|
111
|
-
{
|
|
112
|
-
"source": "n1",
|
|
113
|
-
"target": "n2",
|
|
114
|
-
"branch": "<branch label|null>"
|
|
115
|
-
}
|
|
116
|
-
]
|
|
142
|
+
"edges": [ { "source": "n1", "target": "n2", "branch": "<branch label|null>" } ]
|
|
117
143
|
}
|
|
118
144
|
```
|
|
119
145
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
- `entry`
|
|
123
|
-
Describes how users enter the flow.
|
|
124
|
-
- `nodes`
|
|
125
|
-
Describes business-semantic nodes; they are not yet final canvas nodes.
|
|
126
|
-
- `edges`
|
|
127
|
-
Describes the connection relationships at the business-semantics level.
|
|
128
|
-
- `channel_name`
|
|
129
|
-
Keep it as a semantic field first, then match a real `channelId` from the project channel list later.
|
|
130
|
-
- `branches`
|
|
131
|
-
Only describes branch semantics; later it will be materialized into `node.config.branchList` and `edge.sourceBranchId`.
|
|
146
|
+
`channel_name` stays semantic here; match it to a real `channelId` later. `branches` later materialize into `node.config.branchList` and `edge.sourceBranchId`.
|
|
132
147
|
|
|
133
148
|
---
|
|
134
149
|
|
|
135
|
-
##
|
|
150
|
+
## 5. Step Two: Required CLI Queries
|
|
136
151
|
|
|
137
|
-
###
|
|
138
|
-
|
|
139
|
-
Run:
|
|
152
|
+
### 5.1 Cluster Definition Schema
|
|
140
153
|
|
|
141
154
|
```bash
|
|
142
155
|
ae-cli analysis_audience +get_cluster_definition_schema --cluster_type condition
|
|
143
156
|
```
|
|
144
157
|
|
|
145
|
-
|
|
146
|
-
- Prepare the basis for building QP for condition nodes, audience nodes, and entry nodes
|
|
147
|
-
- Help generate `targetClusterQp`
|
|
148
|
-
- Help generate `triggerRule.events`
|
|
149
|
-
|
|
150
|
-
This step does not return final nodes directly. It provides the rule foundation for expressing conditions as QP or event conditions.
|
|
158
|
+
Provides the basis for `targetClusterQp` and `triggerRule.events`.
|
|
151
159
|
|
|
152
|
-
###
|
|
153
|
-
|
|
154
|
-
Run:
|
|
160
|
+
### 5.2 Project Channels
|
|
155
161
|
|
|
156
162
|
```bash
|
|
157
163
|
ae-cli engage +channel_list --project_id <projectId>
|
|
158
164
|
```
|
|
159
165
|
|
|
160
|
-
|
|
161
|
-
- Get the available channels in the project
|
|
162
|
-
- Match a real `channelId` for touchpoint nodes based on `channel_name` in the intent
|
|
163
|
-
- Determine whether the node should be `message_push`, `wechat_push`, or `webhook_push` based on the channel type
|
|
164
|
-
|
|
165
|
-
If `channel_name` does not have an exact match, use this priority:
|
|
166
|
+
Match a real `channelId` for touchpoint nodes (priority: exact name match → keyword match → fallback by node/channel type). For `webhook_push`, also call `ae-cli engage +channel_detail` and drive `contentList` from `data.config.paramsList`.
|
|
166
167
|
|
|
167
|
-
|
|
168
|
-
2. name keyword match
|
|
169
|
-
3. fallback match by node type and channel type
|
|
168
|
+
**The channel must be enabled.** A node referencing a disabled channel (`channelStatus = 2`) fails with `disabled_channel: channelId must reference an enabled channel`, and `build` stalls on a `need_input` channel slot. Enable it first via `ae-cli engage +update_channel_status --status 1` (write op — requires explicit user intent).
|
|
170
169
|
|
|
171
170
|
---
|
|
172
171
|
|
|
173
|
-
##
|
|
174
|
-
|
|
175
|
-
### 5.1 Top-Level `req` Structure
|
|
176
|
-
|
|
177
|
-
The object passed to `--req` should have the following structure:
|
|
178
|
-
|
|
179
|
-
```json
|
|
180
|
-
{
|
|
181
|
-
"flowName": "<string>",
|
|
182
|
-
"flowDesc": "<string>",
|
|
183
|
-
"groupId": 0,
|
|
184
|
-
"tzOffset": 8,
|
|
185
|
-
"flowUuid": "<string, optional>",
|
|
186
|
-
"parentFlowUuid": "<string, optional>",
|
|
187
|
-
"versionType": 1,
|
|
188
|
-
"nodeList": [],
|
|
189
|
-
"edgeList": []
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
Notes:
|
|
194
|
-
- `projectId` does not need to be written into `--req` manually; the CLI injects it from `--project_id`
|
|
195
|
-
- `flowUuid` and `parentFlowUuid` are mutually exclusive
|
|
196
|
-
- When creating a new draft, neither of these fields should be provided
|
|
197
|
-
|
|
198
|
-
### 5.2 Sources of Top-Level Fields
|
|
199
|
-
|
|
200
|
-
| `req` Field | Source |
|
|
201
|
-
|---|---|
|
|
202
|
-
| `flowName` | Intent field `flow_name` |
|
|
203
|
-
| `flowDesc` | Intent field `flow_desc`; use an empty string or a short description if absent |
|
|
204
|
-
| `groupId` | Default `0`, unless the business requirement specifies a group |
|
|
205
|
-
| `tzOffset` | User timezone or project default timezone; a common value is `8` |
|
|
206
|
-
| `nodeList` | Generated from intent `entry` plus `nodes` |
|
|
207
|
-
| `edgeList` | Generated from intent `edges` and branch structure |
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## 6. Step Four: Map Intent Nodes to Canvas Nodes
|
|
172
|
+
## 6. Step Three: Map Intent Nodes to Canvas Nodes
|
|
212
173
|
|
|
213
174
|
### 6.1 Entry Node Mapping
|
|
214
175
|
|
|
@@ -218,11 +179,11 @@ Notes:
|
|
|
218
179
|
| `repeat_trigger` | `repeat_trigger` |
|
|
219
180
|
| `event_trigger` | `event_trigger` |
|
|
220
181
|
|
|
221
|
-
|
|
182
|
+
There must be exactly one entry node.
|
|
222
183
|
|
|
223
184
|
### 6.2 Business Node Mapping
|
|
224
185
|
|
|
225
|
-
| Intent
|
|
186
|
+
| Intent semantics | Canvas node `type` |
|
|
226
187
|
|---|---|
|
|
227
188
|
| Behavioral split | `event_split_flow` |
|
|
228
189
|
| Feature split | `feature_split_flow` |
|
|
@@ -231,641 +192,323 @@ The entry node must always be the only entry node in `nodeList`.
|
|
|
231
192
|
| Feature judgment | `feature_judge` |
|
|
232
193
|
| Push touchpoint | `message_push` |
|
|
233
194
|
| WeChat touchpoint | `wechat_push` |
|
|
234
|
-
| Webhook /
|
|
195
|
+
| Webhook / external touchpoint | `webhook_push` |
|
|
235
196
|
| Wait | `time_control` |
|
|
236
197
|
| End | `exit_flow` |
|
|
237
198
|
|
|
238
199
|
### 6.3 Condition Field Mapping
|
|
239
200
|
|
|
240
|
-
Condition-related semantics cannot be copied into `req` as-is. They must be materialized in the following way:
|
|
241
|
-
|
|
242
201
|
| Semantic type | Target field |
|
|
243
202
|
|---|---|
|
|
244
|
-
| Audience segmentation
|
|
245
|
-
|
|
|
246
|
-
| Feature split branch condition | `targetClusterQp` |
|
|
247
|
-
| Event trigger condition | `triggerRule[].events[]` |
|
|
248
|
-
| Behavioral judgment condition | `triggerRule[].events[]` |
|
|
249
|
-
| Behavioral split branch condition | `triggerRule[].events[]` |
|
|
250
|
-
|
|
251
|
-
In short:
|
|
252
|
-
- Conditions that judge by audience or property usually map to `targetClusterQp`
|
|
253
|
-
- Conditions that judge whether an event happened, or how many times it happened, usually map to `triggerRule.events`
|
|
203
|
+
| Audience segmentation / feature judgment / feature split branch | `targetClusterQp` |
|
|
204
|
+
| Event trigger / behavioral judgment / behavioral split branch | `triggerRule[].events[]` |
|
|
254
205
|
|
|
255
206
|
### 6.4 Touchpoint Node Mapping
|
|
256
207
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
- `channel_name`
|
|
260
|
-
- `content`
|
|
261
|
-
- `languages`
|
|
262
|
-
|
|
263
|
-
need to be materialized into the push-node `config`:
|
|
264
|
-
|
|
265
|
-
- `channel_name` -> match to a real `channelId`
|
|
266
|
-
- `content` -> place into `contentList`
|
|
267
|
-
- `languages` -> determine whether multilingual `contentList` entries should be generated
|
|
208
|
+
Inside action nodes: `channel_name` → real `channelId`; `content` → `contentList`; `languages` → whether to generate multilingual `contentList` entries.
|
|
268
209
|
|
|
269
210
|
---
|
|
270
211
|
|
|
271
|
-
## 7. Step
|
|
272
|
-
|
|
273
|
-
Each item in `nodeList` has the following structure:
|
|
274
|
-
|
|
275
|
-
| Field | Type | Required | Description |
|
|
276
|
-
|---|---|---|---|
|
|
277
|
-
| `id` | string | Yes | Unique node ID within the request |
|
|
278
|
-
| `name` | string | Yes | Display name of the node |
|
|
279
|
-
| `type` | string | Yes | Node type |
|
|
280
|
-
| `config` | string | Yes | **JSON string** whose top level must be a JSON object |
|
|
281
|
-
| `desc` | string | No | Node description |
|
|
212
|
+
## 7. Step Four: Build `nodes`
|
|
282
213
|
|
|
283
214
|
### 7.1 Most Important Rules
|
|
284
215
|
|
|
285
|
-
1. `
|
|
286
|
-
2. `
|
|
287
|
-
3.
|
|
288
|
-
4.
|
|
216
|
+
1. `node.id` must be unique within the request.
|
|
217
|
+
2. Any `branchId` later referenced by `edge.sourceBranchId` must be declared in that node's `config` first.
|
|
218
|
+
3. Every path must eventually end at `exit_flow`.
|
|
219
|
+
4. `config` may be a JSON object or a JSON string. (`targetClusterQp` inside it is usually a `JSON.stringify`'d string — see §9.)
|
|
289
220
|
|
|
290
221
|
### 7.2 Common Node Types
|
|
291
222
|
|
|
292
|
-
|
|
293
|
-
- `repeat_trigger`
|
|
294
|
-
- `event_trigger`
|
|
295
|
-
- `event_split_flow`
|
|
296
|
-
- `feature_split_flow`
|
|
297
|
-
- `ab_split_flow`
|
|
298
|
-
- `event_judge`
|
|
299
|
-
- `feature_judge`
|
|
300
|
-
- `message_push`
|
|
301
|
-
- `wechat_push`
|
|
302
|
-
- `webhook_push`
|
|
303
|
-
- `time_control`
|
|
304
|
-
- `exit_flow`
|
|
305
|
-
|
|
306
|
-
### 7.3 Example: Simplest Node
|
|
223
|
+
`single_trigger`, `repeat_trigger`, `event_trigger`, `event_split_flow`, `feature_split_flow`, `ab_split_flow`, `event_judge`, `feature_judge`, `message_push`, `wechat_push`, `webhook_push`, `time_control`, `exit_flow`.
|
|
307
224
|
|
|
308
|
-
|
|
309
|
-
{
|
|
310
|
-
"id": "node_1",
|
|
311
|
-
"name": "One-Time Scheduled Entry",
|
|
312
|
-
"type": "single_trigger",
|
|
313
|
-
"config": {
|
|
314
|
-
"triggerTime": "2026-04-10 06:35",
|
|
315
|
-
"flowEndDate": "2026-04-11 06:35"
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### 7.4 Common `config` Templates
|
|
321
|
-
|
|
322
|
-
The templates below are the most important references when you assemble `req` directly. Build them as objects first, then wrap them with `JSON.stringify` before placing them into `nodeList[].config`.
|
|
225
|
+
### 7.3 Common `config` Templates
|
|
323
226
|
|
|
324
227
|
#### `repeat_trigger`
|
|
325
228
|
|
|
326
229
|
```json
|
|
327
|
-
{
|
|
328
|
-
"targetUserType": 1,
|
|
329
|
-
"startDate": "<YYYY-MM-DD>",
|
|
330
|
-
"endDate": "<YYYY-MM-DD>",
|
|
331
|
-
"flowEndDate": "<YYYY-MM-DD HH:mm>",
|
|
332
|
-
"crontab": "0 00 09 * * ?",
|
|
333
|
-
"entryControlLimits": {
|
|
334
|
-
"enableMultEntry": false,
|
|
335
|
-
"disableConcurrentEntry": false
|
|
336
|
-
},
|
|
337
|
-
"targetClusterName": null,
|
|
338
|
-
"clusterPredictCount": null,
|
|
339
|
-
"clusterPredictTime": "<YYYY-MM-DD HH:mm:ss>",
|
|
340
|
-
"targetClusterQp": "<JSON.stringify(qp)>"
|
|
341
|
-
}
|
|
230
|
+
{ "targetUserType": 1, "startDate": "<YYYY-MM-DD>", "endDate": "<YYYY-MM-DD>", "flowEndDate": "<YYYY-MM-DD HH:mm>", "crontab": "0 00 09 * * ?", "entryControlLimits": { "enableMultEntry": false, "disableConcurrentEntry": false }, "targetClusterName": null, "clusterPredictCount": null, "clusterPredictTime": "<YYYY-MM-DD HH:mm:ss>", "targetClusterQp": "<JSON.stringify(qp)>" }
|
|
342
231
|
```
|
|
343
232
|
|
|
344
|
-
|
|
345
|
-
- `entry.segment` -> `targetClusterQp`
|
|
346
|
-
- `entry.schedule` -> `crontab`
|
|
347
|
-
- Common default value: `0 00 09 * * ?`
|
|
233
|
+
`entry.segment` → `targetClusterQp`; `entry.schedule` → `crontab` (common default `0 00 09 * * ?`).
|
|
348
234
|
|
|
349
235
|
#### `event_trigger`
|
|
350
236
|
|
|
351
237
|
```json
|
|
352
|
-
{
|
|
353
|
-
"triggerType": 3,
|
|
354
|
-
"targetUserType": 1,
|
|
355
|
-
"realtime": 0,
|
|
356
|
-
"clusterRefresh": 12,
|
|
357
|
-
"clusterRefreshTime": null,
|
|
358
|
-
"startDate": "<YYYY-MM-DD HH:mm>",
|
|
359
|
-
"endDate": "<YYYY-MM-DD HH:mm>",
|
|
360
|
-
"flowEndDate": "<YYYY-MM-DD HH:mm>",
|
|
361
|
-
"clusterPredictCount": null,
|
|
362
|
-
"clusterPredictTime": "<YYYY-MM-DD HH:mm:ss>",
|
|
363
|
-
"triggerRule": [
|
|
364
|
-
{
|
|
365
|
-
"periodStart": "<startDate>",
|
|
366
|
-
"periodEnd": "<endDate>",
|
|
367
|
-
"periodTimeSymbol": "TS02",
|
|
368
|
-
"dayStartTime": null,
|
|
369
|
-
"startDay": null,
|
|
370
|
-
"eventTriggerType": 0,
|
|
371
|
-
"zoneoffset": 8,
|
|
372
|
-
"events": []
|
|
373
|
-
}
|
|
374
|
-
],
|
|
375
|
-
"entryControlLimits": {
|
|
376
|
-
"enableMultEntry": false,
|
|
377
|
-
"disableConcurrentEntry": false
|
|
378
|
-
},
|
|
379
|
-
"targetClusterQp": "<JSON.stringify(qp) or null>"
|
|
380
|
-
}
|
|
238
|
+
{ "triggerType": 3, "targetUserType": 1, "realtime": 0, "clusterRefresh": 12, "clusterRefreshTime": null, "startDate": "<YYYY-MM-DD HH:mm>", "endDate": "<YYYY-MM-DD HH:mm>", "flowEndDate": "<YYYY-MM-DD HH:mm>", "clusterPredictCount": null, "clusterPredictTime": "<YYYY-MM-DD HH:mm:ss>", "triggerRule": [ { "periodStart": "<startDate>", "periodEnd": "<endDate>", "periodTimeSymbol": "TS02", "dayStartTime": null, "startDay": null, "eventTriggerType": 0, "zoneoffset": 8, "events": [] } ], "entryControlLimits": { "enableMultEntry": false, "disableConcurrentEntry": false }, "targetClusterQp": "<JSON.stringify(qp) or null>" }
|
|
381
239
|
```
|
|
382
240
|
|
|
383
|
-
|
|
384
|
-
- `entry.trigger_event` -> `triggerRule[0].events`
|
|
385
|
-
- Generate `targetClusterQp` only when `entry.segment` exists
|
|
386
|
-
- When `segment` is absent, `targetClusterQp` may be `null`
|
|
241
|
+
`entry.trigger_event` → `triggerRule[0].events`; generate `targetClusterQp` only when `entry.segment` exists.
|
|
387
242
|
|
|
388
243
|
#### `event_split_flow`
|
|
389
244
|
|
|
390
245
|
```json
|
|
391
|
-
{
|
|
392
|
-
"splitFlowType": 1,
|
|
393
|
-
"branchList": [
|
|
394
|
-
{
|
|
395
|
-
"branchId": "<branchId>",
|
|
396
|
-
"branchName": "<label>",
|
|
397
|
-
"branchType": 1,
|
|
398
|
-
"triggerRule": [
|
|
399
|
-
{
|
|
400
|
-
"delayTimeSymbol": "<minute|hour|day>",
|
|
401
|
-
"delayTime": "<number>",
|
|
402
|
-
"eventTriggerType": "<0 or -1>",
|
|
403
|
-
"zoneoffset": 8,
|
|
404
|
-
"events": []
|
|
405
|
-
}
|
|
406
|
-
]
|
|
407
|
-
}
|
|
408
|
-
]
|
|
409
|
-
}
|
|
246
|
+
{ "splitFlowType": 1, "branchList": [ { "branchId": "<branchId>", "branchName": "<label>", "branchType": 1, "triggerRule": [ { "delayTimeSymbol": "<minute|hour|day>", "delayTime": "<number>", "eventTriggerType": "<0 or -1>", "zoneoffset": 8, "events": [] } ] } ] }
|
|
410
247
|
```
|
|
411
248
|
|
|
412
|
-
|
|
413
|
-
- When `branch.condition` is an event condition, map it to `triggerRule[].events[]`
|
|
414
|
-
- `time_limit` -> `delayTimeSymbol` + `delayTime`
|
|
415
|
-
- Use `0` for happened and `-1` for not happened
|
|
416
|
-
- For the fallback branch, keep only:
|
|
417
|
-
|
|
418
|
-
```json
|
|
419
|
-
{
|
|
420
|
-
"branchId": "<branchId>",
|
|
421
|
-
"branchType": 2
|
|
422
|
-
}
|
|
423
|
-
```
|
|
249
|
+
`time_limit` → `delayTimeSymbol` + `delayTime`; `0` = happened, `-1` = not happened. Fallback branch keeps only `{ "branchId": "<branchId>", "branchType": 2 }`.
|
|
424
250
|
|
|
425
251
|
#### `feature_split_flow`
|
|
426
252
|
|
|
427
253
|
```json
|
|
428
|
-
{
|
|
429
|
-
"splitFlowType": 1,
|
|
430
|
-
"branchList": [
|
|
431
|
-
{
|
|
432
|
-
"branchId": "<branchId>",
|
|
433
|
-
"branchName": "<label>",
|
|
434
|
-
"branchType": 1,
|
|
435
|
-
"realtime": 0,
|
|
436
|
-
"clusterRefresh": 12,
|
|
437
|
-
"clusterPredictCount": null,
|
|
438
|
-
"clusterPredictTime": "<YYYY-MM-DD HH:mm:ss>",
|
|
439
|
-
"targetClusterQp": "<JSON.stringify(qp)>"
|
|
440
|
-
}
|
|
441
|
-
]
|
|
442
|
-
}
|
|
254
|
+
{ "splitFlowType": 1, "branchList": [ { "branchId": "<branchId>", "branchName": "<label>", "branchType": 1, "realtime": 0, "clusterRefresh": 12, "clusterPredictCount": null, "clusterPredictTime": "<YYYY-MM-DD HH:mm:ss>", "targetClusterQp": "<JSON.stringify(qp)>" } ] }
|
|
443
255
|
```
|
|
444
256
|
|
|
445
|
-
|
|
446
|
-
- Property or tag condition -> `targetClusterQp`
|
|
447
|
-
- For the fallback branch, also keep only `branchId` plus `branchType: 2`
|
|
257
|
+
Fallback branch keeps only `branchId` + `branchType: 2`.
|
|
448
258
|
|
|
449
259
|
#### `ab_split_flow`
|
|
450
260
|
|
|
451
261
|
```json
|
|
452
|
-
{
|
|
453
|
-
"branchList": [
|
|
454
|
-
{
|
|
455
|
-
"branchId": "<branchId>",
|
|
456
|
-
"branchName": "Control Group",
|
|
457
|
-
"branchType": 1,
|
|
458
|
-
"order": 1,
|
|
459
|
-
"percentageInExperiment": 34
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
"branchId": "<branchId>",
|
|
463
|
-
"branchName": "Experiment Group A",
|
|
464
|
-
"branchType": 2,
|
|
465
|
-
"order": 2,
|
|
466
|
-
"percentageInExperiment": 33
|
|
467
|
-
}
|
|
468
|
-
],
|
|
469
|
-
"indicatorsDef": [],
|
|
470
|
-
"activateIndicatorsDef": null
|
|
471
|
-
}
|
|
262
|
+
{ "branchList": [ { "branchId": "<branchId>", "branchName": "Control Group", "branchType": 1, "order": 1, "percentageInExperiment": 34 }, { "branchId": "<branchId>", "branchName": "Experiment Group A", "branchType": 2, "order": 2, "percentageInExperiment": 33 } ], "indicatorsDef": [], "activateIndicatorsDef": null }
|
|
472
263
|
```
|
|
473
264
|
|
|
474
|
-
Rules:
|
|
475
|
-
- If the user does not provide percentages, you may split them evenly
|
|
476
|
-
- For three groups, you can use `34/33/33`
|
|
477
|
-
|
|
478
265
|
#### `event_judge`
|
|
479
266
|
|
|
480
267
|
```json
|
|
481
|
-
{
|
|
482
|
-
"transferType": 1,
|
|
483
|
-
"meetBranchId": "<meetBranchId>",
|
|
484
|
-
"notMeetBranchId": "<notMeetBranchId>",
|
|
485
|
-
"triggerRule": [
|
|
486
|
-
{
|
|
487
|
-
"delayTimeSymbol": "<minute|hour|day>",
|
|
488
|
-
"delayTime": "<number>",
|
|
489
|
-
"eventTriggerType": 0,
|
|
490
|
-
"zoneoffset": 8,
|
|
491
|
-
"events": []
|
|
492
|
-
}
|
|
493
|
-
]
|
|
494
|
-
}
|
|
268
|
+
{ "transferType": 1, "meetBranchId": "<meetBranchId>", "notMeetBranchId": "<notMeetBranchId>", "triggerRule": [ { "delayTimeSymbol": "<minute|hour|day>", "delayTime": "<number>", "eventTriggerType": 0, "zoneoffset": 8, "events": [] } ] }
|
|
495
269
|
```
|
|
496
270
|
|
|
497
|
-
|
|
498
|
-
- `node.event` -> `triggerRule[].events[]`
|
|
499
|
-
- `wait_time` -> `delayTimeSymbol` + `delayTime`
|
|
500
|
-
- If no wait duration is specified, you may default to `30 minute`
|
|
271
|
+
`wait_time` → `delayTimeSymbol` + `delayTime` (default `30 minute` if unspecified).
|
|
501
272
|
|
|
502
273
|
#### `feature_judge`
|
|
503
274
|
|
|
504
275
|
```json
|
|
505
|
-
{
|
|
506
|
-
"transferType": 1,
|
|
507
|
-
"meetBranchId": "<meetBranchId>",
|
|
508
|
-
"notMeetBranchId": "<notMeetBranchId>",
|
|
509
|
-
"clusterPredictCount": null,
|
|
510
|
-
"clusterPredictTime": "",
|
|
511
|
-
"targetClusterQp": "<JSON.stringify(qp)>"
|
|
512
|
-
}
|
|
276
|
+
{ "transferType": 1, "meetBranchId": "<meetBranchId>", "notMeetBranchId": "<notMeetBranchId>", "clusterPredictCount": null, "clusterPredictTime": "", "targetClusterQp": "<JSON.stringify(qp)>" }
|
|
513
277
|
```
|
|
514
278
|
|
|
515
279
|
#### `message_push` / `webhook_push`
|
|
516
280
|
|
|
517
281
|
```json
|
|
518
|
-
{
|
|
519
|
-
"channelId": "<matched channelId>",
|
|
520
|
-
"channelType": "<matched channelType>",
|
|
521
|
-
"enableChannelTouchLimits": false,
|
|
522
|
-
"isOccasionUp": false,
|
|
523
|
-
"contentList": [
|
|
524
|
-
{
|
|
525
|
-
"pushLanguageCode": "default",
|
|
526
|
-
"content": []
|
|
527
|
-
}
|
|
528
|
-
],
|
|
529
|
-
"processType": 1
|
|
530
|
-
}
|
|
282
|
+
{ "channelId": "<matched channelId>", "channelType": "<matched channelType>", "enableChannelTouchLimits": false, "isOccasionUp": false, "contentList": [ { "pushLanguageCode": "default", "content": [] } ], "processType": 1 }
|
|
531
283
|
```
|
|
532
284
|
|
|
533
|
-
|
|
534
|
-
- `channel_name` -> match a real `channelId`
|
|
535
|
-
- `content` -> fill the parameter that best matches body text, content, or message
|
|
536
|
-
- When the parameter `type = TEXT`, also add:
|
|
537
|
-
|
|
538
|
-
```json
|
|
539
|
-
{
|
|
540
|
-
"config": "[{\"type\":\"paragraph\",\"children\":[{\"text\":\"<same as value>\"}]}]"
|
|
541
|
-
}
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
Multilingual Rules:
|
|
545
|
-
- The first entry must always use `"pushLanguageCode": "default"`
|
|
546
|
-
- Generate additional language versions according to `languages`
|
|
547
|
-
- Each language uses the same `content[]` structure; only `value` changes
|
|
285
|
+
`channel_name` → real `channelId`; `content` → the param that best matches body text. When the param `type = TEXT`, also add `{ "config": "[{\"type\":\"paragraph\",\"children\":[{\"text\":\"<same as value>\"}]}]" }`. First `contentList` entry must use `"pushLanguageCode": "default"`; generate extra languages per `languages`.
|
|
548
286
|
|
|
549
287
|
#### `wechat_push`
|
|
550
288
|
|
|
551
289
|
```json
|
|
552
|
-
{
|
|
553
|
-
"channelId": "<matched channelId>",
|
|
554
|
-
"enableChannelTouchLimits": false,
|
|
555
|
-
"isOccasionUp": false,
|
|
556
|
-
"contentList": [
|
|
557
|
-
{
|
|
558
|
-
"pushLanguageCode": "default",
|
|
559
|
-
"content": [
|
|
560
|
-
{
|
|
561
|
-
"key": "lang",
|
|
562
|
-
"type": "STRING",
|
|
563
|
-
"required": true,
|
|
564
|
-
"paramType": 2,
|
|
565
|
-
"name": "Language",
|
|
566
|
-
"value": "default"
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
"key": "page",
|
|
570
|
-
"type": "STRING",
|
|
571
|
-
"required": true,
|
|
572
|
-
"paramType": 2,
|
|
573
|
-
"name": "Destination Page",
|
|
574
|
-
"value": ""
|
|
575
|
-
},
|
|
576
|
-
{
|
|
577
|
-
"key": "miniprogramState",
|
|
578
|
-
"type": "STRING",
|
|
579
|
-
"required": true,
|
|
580
|
-
"paramType": 2,
|
|
581
|
-
"name": "Version",
|
|
582
|
-
"value": ""
|
|
583
|
-
}
|
|
584
|
-
]
|
|
585
|
-
}
|
|
586
|
-
],
|
|
587
|
-
"processType": 1
|
|
588
|
-
}
|
|
290
|
+
{ "channelId": "<matched channelId>", "enableChannelTouchLimits": false, "isOccasionUp": false, "contentList": [ { "pushLanguageCode": "default", "content": [ { "key": "lang", "type": "STRING", "required": true, "paramType": 2, "name": "Language", "value": "default" }, { "key": "page", "type": "STRING", "required": true, "paramType": 2, "name": "Destination Page", "value": "" }, { "key": "miniprogramState", "type": "STRING", "required": true, "paramType": 2, "name": "Version", "value": "" } ] } ], "processType": 1 }
|
|
589
291
|
```
|
|
590
292
|
|
|
591
293
|
#### `time_control`
|
|
592
294
|
|
|
593
295
|
```json
|
|
594
|
-
{
|
|
595
|
-
"controlType": 1,
|
|
596
|
-
"timeUnit": "<minute|hour|day>",
|
|
597
|
-
"timeUnitNum": "<number>"
|
|
598
|
-
}
|
|
296
|
+
{ "controlType": 1, "timeUnit": "<minute|hour|day>", "timeUnitNum": "<number>" }
|
|
599
297
|
```
|
|
600
298
|
|
|
601
|
-
|
|
602
|
-
- `30 minutes` -> `minute` + `30`
|
|
603
|
-
- `2 hours` -> `hour` + `2`
|
|
604
|
-
- `1 day` -> `day` + `1`
|
|
299
|
+
`30 minutes` → `minute`+`30`; `2 hours` → `hour`+`2`; `1 day` → `day`+`1`.
|
|
605
300
|
|
|
606
301
|
#### `exit_flow`
|
|
607
302
|
|
|
608
|
-
Minimum usable `config`:
|
|
609
|
-
|
|
610
|
-
```json
|
|
611
|
-
{}
|
|
612
|
-
```
|
|
303
|
+
Minimum usable `config`: `{}`.
|
|
613
304
|
|
|
614
305
|
---
|
|
615
306
|
|
|
616
|
-
## 8. Step
|
|
617
|
-
|
|
618
|
-
Each item in `edgeList` has the following structure:
|
|
619
|
-
|
|
620
|
-
| Field | Type | Required | Description |
|
|
621
|
-
|---|---|---|---|
|
|
622
|
-
| `source` | string | Yes | upstream node ID |
|
|
623
|
-
| `target` | string | Yes | downstream node ID |
|
|
624
|
-
| `edgeId` | string | No | edge ID |
|
|
625
|
-
| `sourceBranchId` | string | No | used for edges leaving split or judgment nodes |
|
|
626
|
-
| `config` | string | No | JSON string |
|
|
307
|
+
## 8. Step Five: Build `edges`
|
|
627
308
|
|
|
628
|
-
### 8.1 Regular
|
|
309
|
+
### 8.1 Regular Edge
|
|
629
310
|
|
|
630
311
|
```json
|
|
631
|
-
{
|
|
632
|
-
"source": "node_1",
|
|
633
|
-
"target": "node_2"
|
|
634
|
-
}
|
|
312
|
+
{ "source": "node_1", "target": "node_2" }
|
|
635
313
|
```
|
|
636
314
|
|
|
637
|
-
### 8.2 Branch
|
|
315
|
+
### 8.2 Branch Edge
|
|
638
316
|
|
|
639
317
|
```json
|
|
640
|
-
{
|
|
641
|
-
"source": "node_split",
|
|
642
|
-
"target": "node_a",
|
|
643
|
-
"sourceBranchId": "branch_a"
|
|
644
|
-
}
|
|
318
|
+
{ "source": "node_split", "target": "node_a", "sourceBranchId": "branch_a" }
|
|
645
319
|
```
|
|
646
320
|
|
|
647
|
-
### 8.3
|
|
648
|
-
|
|
649
|
-
1. `source` and `target` must reference existing `node.id` values
|
|
650
|
-
2. Only edges leaving split or judgment nodes need `sourceBranchId`
|
|
651
|
-
3. The graph must be a DAG and cannot contain cycles
|
|
652
|
-
4. For split nodes, `sourceBranchId` must come from a branchId already declared in the corresponding node `config`
|
|
321
|
+
### 8.3 Standard Outgoing-Edge Rules
|
|
653
322
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
| node type | Number of outgoing edges | `sourceBranchId` rule |
|
|
323
|
+
| node type | Outgoing edges | `sourceBranchId` |
|
|
657
324
|
|---|---|---|
|
|
658
325
|
| `single_trigger` / `repeat_trigger` / `event_trigger` | 1 | do not provide |
|
|
659
|
-
| `event_split_flow` / `feature_split_flow` / `ab_split_flow` | one per branch |
|
|
660
|
-
| `event_judge` / `feature_judge` | 2 |
|
|
326
|
+
| `event_split_flow` / `feature_split_flow` / `ab_split_flow` | one per branch | the corresponding `branchList[].branchId` |
|
|
327
|
+
| `event_judge` / `feature_judge` | 2 | `meetBranchId` and `notMeetBranchId` |
|
|
661
328
|
| `message_push` / `wechat_push` / `webhook_push` / `time_control` | 1 | do not provide |
|
|
662
329
|
| `exit_flow` | 0 | do not provide |
|
|
663
330
|
|
|
331
|
+
### 8.4 Rules
|
|
332
|
+
|
|
333
|
+
1. `source` / `target` must reference existing `node.id` values.
|
|
334
|
+
2. Only edges leaving split/judge nodes carry `sourceBranchId`, and it must be a `branchId` already declared in the upstream node `config`.
|
|
335
|
+
3. The graph must be a DAG (no cycles).
|
|
336
|
+
|
|
664
337
|
---
|
|
665
338
|
|
|
666
|
-
## 9.
|
|
339
|
+
## 9. QP Validation Rules (high-frequency pitfalls)
|
|
667
340
|
|
|
668
|
-
|
|
341
|
+
When a node config carries a QP (`targetClusterQp`, `triggerRule[].events[]`, etc.), the v2 backend validates it strictly:
|
|
669
342
|
|
|
670
|
-
1. `
|
|
671
|
-
2.
|
|
672
|
-
3.
|
|
673
|
-
4. Each `exit_flow` must have exactly one incoming edge and no outgoing edges
|
|
674
|
-
5. Each `node.id` must be unique
|
|
675
|
-
6. Every edge must reference existing nodes
|
|
676
|
-
7. The entire graph must be acyclic
|
|
343
|
+
1. **`relation` must be integer `0` or `1`** — not the string `"0"`/`"1"`. Applies to `totalCFilter.relation`, `eventCondition.relation`, `filts[].relation`. Error: `invalid_qp_relation: QP relation must be number 0 or 1`.
|
|
344
|
+
2. **`userCondition` leaves must include `columnType` and `columnDesc`** — `columnName`/`selectType`/`tableType`/`calcuSymbol`/`ftv` alone is not enough. Error: `invalid_qp_leaf: QP property leaf must contain columnType/columnDesc`. (Even legacy stored QPs that omit these now fail.)
|
|
345
|
+
3. **`taPropQuota` must include `quotaDesc`, `quota`, `analysisParams`** — giving only `analysis`/`analysisDesc` fails with `required_by_minimal_valid`. Minimal passing shape: `{ "analysis": ..., "analysisDesc": ..., "quota": "", "quotaDesc": "", "analysisParams": "" }`. Applies to both `triggerRule.events[].taPropQuota` and the entry node's `eventCondition.taPropQuota`.
|
|
677
346
|
|
|
678
|
-
|
|
679
|
-
- Paths from different branches must not converge again into the same node
|
|
680
|
-
- Each branch should independently lead to its own `exit_flow`
|
|
347
|
+
`targetClusterQp` is usually a `JSON.stringify`'d string, e.g. `{ "targetClusterQp": "{\"totalCFilter\":{\"relation\":1,\"filts\":[]}}" }`.
|
|
681
348
|
|
|
682
|
-
|
|
349
|
+
### 9.1 Operator codes (`uceCalcuSymbol` / `calcuSymbol`)
|
|
350
|
+
|
|
351
|
+
Condition leaves use string operator codes, not literal operators:
|
|
683
352
|
|
|
684
|
-
|
|
353
|
+
| Code | Meaning | Value type |
|
|
354
|
+
|---|---|---|
|
|
355
|
+
| `C00` | equals | all |
|
|
356
|
+
| `C01` | not equals | all |
|
|
357
|
+
| `C02` / `C020` | less than / less than or equal | number |
|
|
358
|
+
| `C03` / `C030` | greater than / greater than or equal | number |
|
|
359
|
+
| `C04` / `C05` | has value / no value | all |
|
|
360
|
+
| `C06` / `C060` | range / date range | number / date |
|
|
361
|
+
| `C07` / `C08` | contains / does not contain | string |
|
|
362
|
+
| `C09` / `C10` | true / false | bool |
|
|
363
|
+
| `C11` / `C12` | regex match / not match | string |
|
|
685
364
|
|
|
686
|
-
###
|
|
365
|
+
### 9.2 Event-count condition (`A200`) — `invalid_preset_count_expression`
|
|
687
366
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
367
|
+
For an event condition that counts occurrences with an **empty `taPropQuota`** (analysis `A200`, the default "count" case), the backend treats it as a *preset count* and is strict:
|
|
368
|
+
|
|
369
|
+
- `uceCalcuSymbol` **must be `C030`** (greater-than-or-equal), and
|
|
370
|
+
- `num` **must be `"1"`** (string).
|
|
692
371
|
|
|
693
|
-
|
|
372
|
+
Any other operator/number with an empty quota fails with `invalid_preset_count_expression: A200 empty quota means preset count and must use C030 with num=1`.
|
|
694
373
|
|
|
695
|
-
|
|
374
|
+
A working event-condition leaf (used inside `triggerRule[].events[]` for `event_judge` / `event_trigger` / `event_split_flow`):
|
|
696
375
|
|
|
697
376
|
```json
|
|
698
377
|
{
|
|
699
|
-
"
|
|
700
|
-
"
|
|
701
|
-
"
|
|
702
|
-
"
|
|
378
|
+
"conditionType": "event",
|
|
379
|
+
"eventCondition": {
|
|
380
|
+
"eventName": "ta@active_user",
|
|
381
|
+
"eventDesc": "用户活跃",
|
|
382
|
+
"eventType": "event",
|
|
383
|
+
"uceCalcuSymbol": "C030",
|
|
384
|
+
"num": "1",
|
|
385
|
+
"taPropQuota": { "analysis": "A200", "analysisDesc": "次数", "quota": "", "quotaDesc": "", "analysisParams": "" },
|
|
386
|
+
"recentDay": "0-30",
|
|
387
|
+
"startTime": "",
|
|
388
|
+
"endTime": "",
|
|
389
|
+
"filts": [],
|
|
390
|
+
"relation": 1
|
|
703
391
|
}
|
|
704
392
|
}
|
|
705
393
|
```
|
|
706
394
|
|
|
707
|
-
|
|
708
|
-
- The top-level `projectId` comes from `--project_id`
|
|
709
|
-
- `req.projectId` is also injected automatically by the CLI
|
|
395
|
+
Wrap the leaves with `{ "totalCFilter": { "filts": [ ... ], "relation": 1 } }`, then `JSON.stringify` for `targetClusterQp`.
|
|
710
396
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
```bash
|
|
714
|
-
ae-cli engage +save_flow \
|
|
715
|
-
--project_id 1 \
|
|
716
|
-
--req '{
|
|
717
|
-
"flowName": "Welcome Flow",
|
|
718
|
-
"flowDesc": "New user welcome flow",
|
|
719
|
-
"groupId": 0,
|
|
720
|
-
"tzOffset": 8,
|
|
721
|
-
"nodeList": [
|
|
722
|
-
{
|
|
723
|
-
"id": "node_1",
|
|
724
|
-
"name": "Enter Flow",
|
|
725
|
-
"type": "single_trigger",
|
|
726
|
-
"config": "{}"
|
|
727
|
-
},
|
|
728
|
-
{
|
|
729
|
-
"id": "node_2",
|
|
730
|
-
"name": "End",
|
|
731
|
-
"type": "exit_flow",
|
|
732
|
-
"config": "{}"
|
|
733
|
-
}
|
|
734
|
-
],
|
|
735
|
-
"edgeList": [
|
|
736
|
-
{
|
|
737
|
-
"source": "node_1",
|
|
738
|
-
"target": "node_2"
|
|
739
|
-
}
|
|
740
|
-
]
|
|
741
|
-
}'
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
### 10.4 Output Requirements After Successful Creation
|
|
745
|
-
|
|
746
|
-
Prerequisite:
|
|
747
|
-
- The flow canvas was created successfully and returned the new canvas `flowUuid`
|
|
748
|
-
|
|
749
|
-
On success, you must:
|
|
750
|
-
- Show the creation result to the user, including at least key information such as the canvas name
|
|
751
|
-
- Output a **clickable Markdown link**
|
|
752
|
-
|
|
753
|
-
Link-generation Rules:
|
|
754
|
-
- Use standard Markdown link syntax and do not place it inside a code block
|
|
755
|
-
- The URL must start with `/#/`
|
|
756
|
-
- Do not add a domain name or any domain placeholder
|
|
757
|
-
- Replace the `flowUuid` returned by `save_flow` and the `projectId` used for this creation into the URL
|
|
397
|
+
---
|
|
758
398
|
|
|
759
|
-
|
|
399
|
+
## 10. Step Six: Graph Constraint Checks
|
|
760
400
|
|
|
761
|
-
|
|
401
|
+
Before `preview`/`commit`, self-check:
|
|
762
402
|
|
|
763
|
-
|
|
403
|
+
1. `nodes` is not empty; exactly one entry node; at least one `exit_flow`.
|
|
404
|
+
2. Each `exit_flow` has exactly one incoming edge and no outgoing edges.
|
|
405
|
+
3. Each `node.id` is unique; every edge references existing nodes; the graph is acyclic.
|
|
406
|
+
4. **Every terminal branch must use its own dedicated `exit_flow` node** — do not let multiple upstream nodes point at one shared `exit_flow`. A flow with N terminal paths needs N separate `exit_flow` nodes (otherwise the backend rejects the shared exit). Error: `... branch must use its own dedicated exit ...`.
|
|
407
|
+
5. If a split node uses `splitFlowType = 2`, different branches must not converge into the same node; each branch leads to its own `exit_flow`.
|
|
764
408
|
|
|
765
|
-
|
|
409
|
+
---
|
|
766
410
|
|
|
767
|
-
|
|
768
|
-
- `❌ {domain}/#/hermes/flow/...`: do not add a domain placeholder
|
|
769
|
-
- `❌` putting the link inside a code block ````` : links inside code blocks are not clickable
|
|
770
|
-
- `❌ /#/hermes/flow/detail?flowUuid=...`: do not output a plain-text URL; you must use the `[text](URL)` format
|
|
411
|
+
## 11. Step Seven: Run the Lifecycle (CLI)
|
|
771
412
|
|
|
772
|
-
###
|
|
413
|
+
### 11.1 Flags
|
|
773
414
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
415
|
+
| Flag | Type | Required | Description |
|
|
416
|
+
|---|---|---|---|
|
|
417
|
+
| `--project_id` / `-p` | number | Yes | Project ID |
|
|
418
|
+
| `--req` | json | Yes | operation-based request object |
|
|
777
419
|
|
|
778
|
-
|
|
420
|
+
The CLI injects `projectId` into both the top level and `req`; you do not write `req.projectId` yourself.
|
|
779
421
|
|
|
780
|
-
|
|
781
|
-
{
|
|
782
|
-
"projectId": "<actual projectId>",
|
|
783
|
-
"req": {
|
|
784
|
-
"...": "complete save_flow request body"
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
```
|
|
422
|
+
### 11.2 `need_input` Continuation
|
|
788
423
|
|
|
789
|
-
|
|
424
|
+
`need_input` is a **soft prompt**, not a hard error:
|
|
790
425
|
|
|
791
|
-
|
|
426
|
+
- **`errors` empty + `nextSlot` present** → server needs one more node config (trigger / channel / targetCluster). Answer with `operation=build` + `draftId` + `expectedVersion` + `slotAnswer`. If `nextSlot.targetNodeId` is present, `slotAnswer.nodeConfig` may contain only `config`; otherwise include `nodeId` or `id`.
|
|
427
|
+
- **`errors` non-empty** → hard validation failure. Fix `nodes`/`edges` and `build` again (a new `draftId` is issued; the stale draft is cleaned by TTL).
|
|
792
428
|
|
|
793
|
-
### 11.
|
|
429
|
+
### 11.3 Minimal Working Example
|
|
794
430
|
|
|
795
|
-
|
|
431
|
+
```bash
|
|
432
|
+
# 1) build
|
|
433
|
+
ae-cli engage +save_flow --project_id 1 --req '{
|
|
434
|
+
"operation": "build",
|
|
435
|
+
"flowName": "Welcome Flow",
|
|
436
|
+
"flowDesc": "New user welcome flow",
|
|
437
|
+
"groupId": 0, "tzOffset": 8, "versionType": 1,
|
|
438
|
+
"nodes": [
|
|
439
|
+
{ "id": "n1", "type": "single_trigger", "name": "Enter", "config": {} },
|
|
440
|
+
{ "id": "n2", "type": "exit_flow", "name": "End", "config": {} }
|
|
441
|
+
],
|
|
442
|
+
"edges": [ { "source": "n1", "target": "n2" } ]
|
|
443
|
+
}'
|
|
444
|
+
# → status = ready_to_preview, draftId = D, draftVersion = V0, confirmToken = T0
|
|
796
445
|
|
|
797
|
-
|
|
798
|
-
{
|
|
799
|
-
"id": "node_1",
|
|
800
|
-
"name": "entry",
|
|
801
|
-
"type": "single_trigger",
|
|
802
|
-
"config": "{}"
|
|
803
|
-
}
|
|
804
|
-
```
|
|
446
|
+
# 2) preview (re-issues draftVersion V1 + confirmToken T1)
|
|
447
|
+
ae-cli engage +save_flow --project_id 1 --req '{ "operation": "preview", "draftId": "D", "expectedVersion": 0 }'
|
|
805
448
|
|
|
806
|
-
|
|
449
|
+
# 3) commit (use the preview draftVersion + confirmToken)
|
|
450
|
+
ae-cli engage +save_flow --project_id 1 --req '{ "operation": "commit", "draftId": "D", "draftVersion": 1, "confirmToken": "T1" }'
|
|
451
|
+
# → status = committed, result = flowUuid
|
|
807
452
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
"id": "node_1",
|
|
811
|
-
"name": "entry",
|
|
812
|
-
"type": "single_trigger",
|
|
813
|
-
"config": {}
|
|
814
|
-
}
|
|
453
|
+
# 4) verify
|
|
454
|
+
ae-cli engage +flow_detail --project_id 1 --flow_uuid <flowUuid>
|
|
815
455
|
```
|
|
816
456
|
|
|
817
|
-
|
|
457
|
+
> ⚠️ `commit` must use the `draftVersion` **and** `confirmToken` returned by **preview**, not by build. Build's values cause a token/version mismatch.
|
|
818
458
|
|
|
819
|
-
|
|
459
|
+
### 11.4 Output After Successful Commit
|
|
820
460
|
|
|
821
|
-
- `
|
|
822
|
-
- `hour`
|
|
823
|
-
- `minute`
|
|
824
|
-
- `week`
|
|
825
|
-
- `month`
|
|
461
|
+
The committed flow has `status = 0` (draft) — `commit` only freezes the draft into a flow version, it does **not** start delivery. To actually run it, enable it separately via `ae-cli engage +manage_flow` (with explicit user intent).
|
|
826
462
|
|
|
827
|
-
|
|
463
|
+
Show the user the canvas name and a **clickable Markdown link**:
|
|
828
464
|
|
|
829
|
-
|
|
465
|
+
- Standard Markdown link syntax, not inside a code block.
|
|
466
|
+
- URL starts with `/#/`, no domain prefix.
|
|
467
|
+
- Substitute the real `flowUuid` and `projectId`.
|
|
830
468
|
|
|
831
|
-
|
|
469
|
+
Template: `[Open Canvas](/#/hermes/flow/detail?flowUuid=<flowUuid>¤tProjectId=<projectId>)`
|
|
832
470
|
|
|
833
|
-
|
|
834
|
-
ae-cli engage +channel_list --project_id <projectId>
|
|
835
|
-
```
|
|
471
|
+
Correct example: `[Open Canvas](/#/hermes/flow/detail?flowUuid=0006_831135755¤tProjectId=1)`
|
|
836
472
|
|
|
837
|
-
|
|
473
|
+
Common mistakes: adding a domain placeholder; putting the link inside a code block; outputting a plain-text URL instead of `[text](URL)`.
|
|
838
474
|
|
|
839
|
-
|
|
475
|
+
### 11.5 Output After Failure
|
|
840
476
|
|
|
841
|
-
|
|
842
|
-
{ "sourceBranchId": "branch_a" }
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
then `"branch_a"` must already exist in the corresponding upstream node `config`.
|
|
846
|
-
|
|
847
|
-
### 11.5 `targetClusterQp` Is Usually Also a String
|
|
477
|
+
Output the complete `req` JSON for debugging plus a clear failure reason.
|
|
848
478
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
Illustration:
|
|
479
|
+
---
|
|
852
480
|
|
|
853
|
-
|
|
854
|
-
{ "targetClusterQp": "{\"totalCFilter\":{\"relation\":\"1\",\"filts\":[]}}" }
|
|
855
|
-
```
|
|
481
|
+
## 12. Common Validation Errors
|
|
856
482
|
|
|
857
|
-
|
|
483
|
+
| Symptom | Cause | Fix |
|
|
484
|
+
|---|---|---|
|
|
485
|
+
| `Unsupported save_flow operation: null` | Legacy `nodeList`/`edgeList` payload, or `operation` missing | Put `operation` = `build`/`preview`/`commit` in `req`, use `nodes`/`edges` |
|
|
486
|
+
| operation rejected (`SAVE`/`DRAFT`/`SUBMIT`/`mode`/`action`…) | Wrong field or wrong enum | `operation` is at `req.operation`; enum is only `build`/`preview`/`commit` |
|
|
487
|
+
| `invalid_qp_relation: QP relation must be number 0 or 1` | `relation` sent as string | Use integer `0`/`1` |
|
|
488
|
+
| `invalid_qp_leaf: QP property leaf must contain columnType/columnDesc` | userCondition leaf missing fields | Add `columnType` + `columnDesc` |
|
|
489
|
+
| `required_by_minimal_valid` | `taPropQuota` missing fields | Add `quotaDesc` + `quota` + `analysisParams` |
|
|
490
|
+
| `invalid_preset_count_expression` | Event count condition (`A200`) with empty quota uses wrong operator/num | Use `uceCalcuSymbol = C030` + `num = "1"` (see §9.2) |
|
|
491
|
+
| `... branch must use its own dedicated exit ...` | Multiple paths share one `exit_flow` | Give every terminal branch its own `exit_flow` node (see §10.4) |
|
|
492
|
+
| `disabled_channel: channelId must reference an enabled channel` | Channel status = 2 | `+update_channel_status --status 1` first |
|
|
493
|
+
| commit token/version mismatch | Used build's `confirmToken`/`draftVersion` | Use the values returned by **preview** |
|
|
494
|
+
| `flowDesc` rejected | Over 200 chars | Trim to ≤ 200 |
|
|
495
|
+
| `config` rejected as object where string expected | Wrong shape for `targetClusterQp` / TEXT rich-text | `JSON.stringify` those inner values |
|
|
858
496
|
|
|
859
|
-
|
|
497
|
+
---
|
|
860
498
|
|
|
861
|
-
|
|
499
|
+
## 13. Most Common Mistakes
|
|
862
500
|
|
|
863
|
-
|
|
501
|
+
1. **Legacy `nodeList`/`edgeList`** — use `nodes`/`edges` with `operation=build`.
|
|
502
|
+
2. **Time units must be lowercase** — `day`, `hour`, `minute`, `week`, `month` (not `DAY`/`HOUR`).
|
|
503
|
+
3. **Do not invent `channelId`** — get it from `ae-cli engage +channel_list`.
|
|
504
|
+
4. **Define branch IDs before referencing them** — `edge.sourceBranchId` must already exist in the upstream node `config`.
|
|
505
|
+
5. **`targetClusterQp` is usually a string** — `JSON.stringify` the QP object.
|
|
506
|
+
6. **TEXT rich-text `config` must also be a string** — not an object.
|
|
507
|
+
7. **`commit` uses preview's token/version** — not build's.
|
|
508
|
+
8. **Do not merge branches again when `splitFlowType = 2`**.
|
|
864
509
|
|
|
865
510
|
---
|
|
866
511
|
|
|
867
|
-
##
|
|
868
|
-
|
|
869
|
-
To build `+save_flow` input parameters reliably, do not jump directly from natural language to `req`. Follow this path instead:
|
|
512
|
+
## 14. One-Sentence Summary
|
|
870
513
|
|
|
871
|
-
|
|
514
|
+
Drive `+save_flow` as a state machine — `build` (`nodes`/`edges`, not `nodeList`/`edgeList`) → resolve any `need_input` slots → `preview` (take its fresh `draftVersion` + `confirmToken`) → `commit` → verify with `+flow_detail`; keep QP `relation` integer, fill `columnType`/`columnDesc` on userCondition leaves and `quotaDesc`/`quota`/`analysisParams` on `taPropQuota`, and ensure touchpoint channels are enabled before referencing them.
|