@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkingai/ae-cli",
3
- "version": "6.0.17",
3
+ "version": "6.0.18-1",
4
4
  "description": "CLI tool for ThinkingAI (AE) analytics platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 final `--req` for `+save_flow` must be the complete canvas request payload, not an intent description. It must contain at least:
149
- - `flowName`
150
- - `flowDesc`
151
- - `nodeList`
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 `nodeList` nodes
167
- - Do not put `config` into `--req` as an object
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 nodeList / edgeList
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 update a flow canvas draft.
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
- This document is not only meant to explain the `save_flow` interface itself. It provides a complete path **from user requirements to CLI arguments**:
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
- 1. Identify intent first
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 the **final flow canvas request payload that is ready to submit**.
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
- Therefore, you must first organize user requirements into a unified intermediate intent structure, and then generate the payload according to the mapping rules:
20
-
21
- - `flowName`
22
- - `flowDesc`
23
- - `nodeList`
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
- Then call the CLI:
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
- ```bash
30
- ae-cli engage +save_flow --project_id <projectId> --req '<req-json>'
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 in the project and match real `channelId` values for touchpoint nodes.
42
- 4. Map the intent JSON to the final `req`: `flowName`, `flowDesc`, `nodeList`, and `edgeList`.
43
- 5. Run `ae-cli engage +save_flow --project_id <projectId> --req '<req-json>'` to submit.
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. Step One: Intent Identification
42
+ ## 3. `req` Schema (authoritative)
48
43
 
49
- ### 3.1 Information That Must Be Confirmed First
44
+ `req` accepts these fields (only `operation` is hard-required):
50
45
 
51
- Before generating any `req`, confirm at least these four categories of information:
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 | What kind of flow this is, for example new-user activation, churn win-back, or paid conversion |
56
- | target users | Who can enter the flow, for example users inactive for the last 14 days or users registered today |
57
- | touchpoint method | Which channel will be used for outreach, for example Push, WeChat subscription, or Webhook |
58
- | branching conditions | Whether grouped handling is needed; if so, which conditions define the groups |
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 of these four categories is missing, do not build `req` directly.
107
+ If any is missing, do not build.
61
108
 
62
- ### 3.2 Intent Output Format
109
+ ### 4.2 Intent Output Format
63
110
 
64
- First organize the user requirement into the following intent JSON. This JSON is an intermediate representation, not the final `save_flow.req`.
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
- ### 3.3 Field Meaning
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
- ## 4. Step Two: Required CLI Queries
150
+ ## 5. Step Two: Required CLI Queries
136
151
 
137
- ### 4.1 Query the Cluster Definition Schema
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
- Purpose:
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
- ### 4.2 Query Project Channels
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
- Purpose:
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
- 1. exact name match
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
- ## 5. Step Three: Map the Intent to `req`
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
- The entry node must always be the only entry node in `nodeList`.
182
+ There must be exactly one entry node.
222
183
 
223
184
  ### 6.2 Business Node Mapping
224
185
 
225
- | Intent-node semantic meaning | Canvas node `type` |
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 / other external touchpoint | `webhook_push` |
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 condition | `targetClusterQp` |
245
- | Feature judgment condition | `targetClusterQp` |
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
- These fields inside action-semantic nodes:
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 Five: How to Build `nodeList`
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. `config` must be a string, not an object.
286
- 2. `node.id` must be unique.
287
- 3. Any `branchId` later referenced by `edge.sourceBranchId` in split or judgment nodes must be defined in `config` first.
288
- 4. Every path must eventually end at `exit_flow`.
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
- - `single_trigger`
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
- ```json
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
- Rules:
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
- Rules:
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
- Rules:
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
- Rules:
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
- Rules:
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
- Rules:
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
- Common parsing examples:
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 Six: How to Build `edgeList`
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 Edges
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 Edges
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 Most Important Rules
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
- ### 8.4 Standard Outgoing-Edge Rules
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 | use the corresponding `branchList[].branchId` |
660
- | `event_judge` / `feature_judge` | 2 | use `meetBranchId` and `notMeetBranchId` respectively |
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. Step Seven: Graph Constraint Checks
339
+ ## 9. QP Validation Rules (high-frequency pitfalls)
667
340
 
668
- Before submitting, you must self-check:
341
+ When a node config carries a QP (`targetClusterQp`, `triggerRule[].events[]`, etc.), the v2 backend validates it strictly:
669
342
 
670
- 1. `nodeList` is not empty
671
- 2. There must be exactly one entry node
672
- 3. There must be at least one `exit_flow`
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
- If a split node uses `splitFlowType = 2`, also ensure:
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
- ## 10. Step Eight: CLI Submission
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
- ### 10.1 Top-Level Flags
365
+ ### 9.2 Event-count condition (`A200`) — `invalid_preset_count_expression`
687
366
 
688
- | Flag | Type | Required | Description |
689
- |---|---|---|---|
690
- | `--project_id` / `-p` | number | Yes | Project ID |
691
- | `--req` | json | Yes | final request-body object |
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
- ### 10.2 Actual CLI Submission Structure
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
- The CLI will organize the input as:
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
- "projectId": 1,
700
- "req": {
701
- "projectId": 1,
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
- In other words:
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
- ### 10.3 Minimum Working Example
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
- Output template:
399
+ ## 10. Step Six: Graph Constraint Checks
760
400
 
761
- [Open Canvas](/#/hermes/flow/detail?flowUuid=<replace-with-actual-flowUuid>&currentProjectId=<replace-with-actual-projectId>)
401
+ Before `preview`/`commit`, self-check:
762
402
 
763
- Correct example:
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
- [Open Canvas](/#/hermes/flow/detail?flowUuid=0006_831135755&currentProjectId=1)
409
+ ---
766
410
 
767
- Common mistakes:
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
- ### 10.5 Output Requirements After Failed Creation
413
+ ### 11.1 Flags
773
414
 
774
- On failure, you must:
775
- - Output the complete request-body JSON for debugging
776
- - Clearly describe the failure reason
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
- Suggested output structure:
420
+ The CLI injects `projectId` into both the top level and `req`; you do not write `req.projectId` yourself.
779
421
 
780
- ```json
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
- ## 11. Most Common Mistakes
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.1 `--req` Is an Object, but `node.config` / `edge.config` Are Strings
429
+ ### 11.3 Minimal Working Example
794
430
 
795
- Correct:
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
- ```json
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
- Incorrect:
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
- ```json
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
- ### 11.2 Time Units Must Be Lowercase
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
- Inside `time_control`, use:
459
+ ### 11.4 Output After Successful Commit
820
460
 
821
- - `day`
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
- Do not write `DAY`, `HOUR`, or `MINUTE`.
463
+ Show the user the canvas name and a **clickable Markdown link**:
828
464
 
829
- ### 11.3 Do Not Invent `channelId`
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
- For node types such as `message_push`, `wechat_push`, and `webhook_push`, `channelId` must come from:
469
+ Template: `[Open Canvas](/#/hermes/flow/detail?flowUuid=<flowUuid>&currentProjectId=<projectId>)`
832
470
 
833
- ```bash
834
- ae-cli engage +channel_list --project_id <projectId>
835
- ```
471
+ Correct example: `[Open Canvas](/#/hermes/flow/detail?flowUuid=0006_831135755&currentProjectId=1)`
836
472
 
837
- ### 11.4 Define branch IDs Before Referencing Them
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
- If an edge uses:
475
+ ### 11.5 Output After Failure
840
476
 
841
- ```json
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
- Although `targetClusterQp` appears inside the JSON object of node `config`, its value is usually not a raw object. It is typically the QP object converted into a string with `JSON.stringify`.
850
-
851
- Illustration:
479
+ ---
852
480
 
853
- ```json
854
- { "targetClusterQp": "{\"totalCFilter\":{\"relation\":\"1\",\"filts\":[]}}" }
855
- ```
481
+ ## 12. Common Validation Errors
856
482
 
857
- ### 11.6 Rich-Text `config` for `TEXT` Parameters Must Also Be a String
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
- If a push parameter uses `TEXT`, the inner rich-text `config` must not be an object. It must be stringified JSON. This is easy to miss.
497
+ ---
860
498
 
861
- ### 11.7 Do Not Merge Branches Again When `splitFlowType = 2`
499
+ ## 13. Most Common Mistakes
862
500
 
863
- "Enter whenever conditions are met" means users may enter multiple branches at the same time. In that case, later paths should not share the same downstream node, otherwise the semantics easily conflict.
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
- ## 12. One-Sentence Summary
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
- **User requirement -> intent JSON -> schema and channel completion -> `nodeList` / `edgeList` -> `ae-cli engage +save_flow`.**
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.