@qingflow-tech/qingflow-cli 1.1.2 → 1.1.34

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.
Files changed (103) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +10 -0
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/skills/qingflow-cli/SKILL.md +61 -47
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +21 -477
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +44 -290
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +11 -102
  11. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +57 -0
  12. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +84 -0
  13. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +317 -0
  14. package/skills/qingflow-cli/reference/builder/40-layout.md +70 -0
  15. package/skills/qingflow-cli/reference/builder/50-views.md +540 -0
  16. package/skills/qingflow-cli/reference/builder/60-charts.md +328 -0
  17. package/skills/qingflow-cli/reference/builder/70-portal.md +372 -0
  18. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +148 -0
  19. package/skills/qingflow-cli/reference/builder/90-workflow.md +35 -0
  20. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +64 -0
  21. package/skills/qingflow-cli/reference/builder/README.md +62 -0
  22. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  23. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  25. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +508 -0
  26. package/skills/qingflow-cli/reference/builder/reference/match-rules.md +229 -0
  27. package/skills/qingflow-cli/reference/builder/reference/workspace-icons.md +89 -0
  28. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +48 -0
  29. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +63 -0
  30. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +59 -0
  31. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  32. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  33. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +211 -0
  34. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +36 -0
  35. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +66 -0
  36. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  37. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  38. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +18 -0
  39. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  40. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  41. package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +81 -0
  42. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
  43. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
  44. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
  45. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
  46. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
  47. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
  48. package/skills/qingflow-cli/reference/examples/README.md +17 -0
  49. package/skills/qingflow-cli/reference/examples/associated/associated_resources_semantic_full.example.json +50 -0
  50. package/skills/qingflow-cli/reference/examples/buttons/custom_buttons_advanced.example.json +56 -0
  51. package/skills/qingflow-cli/reference/examples/charts/charts_remove.example.json +1 -0
  52. package/skills/qingflow-cli/reference/examples/charts/charts_reorder.example.json +1 -0
  53. package/skills/qingflow-cli/reference/examples/charts/charts_upsert_bar.example.json +8 -0
  54. package/skills/qingflow-cli/reference/examples/charts/charts_upsert_dashboard_starter.example.json +118 -0
  55. package/skills/qingflow-cli/reference/examples/charts/charts_upsert_minimal.example.json +13 -0
  56. package/skills/qingflow-cli/reference/examples/layout/layout_sections_full.example.json +22 -0
  57. package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +145 -0
  58. package/skills/qingflow-cli/reference/examples/portal/portal_sections_five_types.example.json +126 -0
  59. package/skills/qingflow-cli/reference/examples/portal/portal_sections_standard_workbench.example.json +178 -0
  60. package/skills/qingflow-cli/reference/examples/record/record_insert_all_field_types.example.json +35 -0
  61. package/skills/qingflow-cli/reference/examples/schema/_batch_schema_complex.json +18 -0
  62. package/skills/qingflow-cli/reference/examples/schema/_batch_schema_scalar.json +17 -0
  63. package/skills/qingflow-cli/reference/examples/schema/schema_add_fields_minimal.example.json +7 -0
  64. package/skills/qingflow-cli/reference/examples/schema/schema_apply_add_fields_all_types.json +78 -0
  65. package/skills/qingflow-cli/reference/examples/schema/schema_multi_app_form_relation_layout.example.json +166 -0
  66. package/skills/qingflow-cli/reference/examples/schema/schema_single_app_form_standard.example.json +95 -0
  67. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +159 -0
  68. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +42 -0
  69. package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +18 -4
  70. package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +3 -3
  71. package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +57 -7
  72. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +22 -0
  73. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  74. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
  75. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +167 -0
  76. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  77. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  78. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  79. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  80. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  81. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  82. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  83. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  84. package/skills/qingflow-cli/reference/record/insert/README.md +125 -0
  85. package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +1 -1
  86. package/skills/qingflow-cli/reference/task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +153 -0
  87. package/skills/qingflow-cli/reference/task/ops/README.md +134 -0
  88. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  89. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +29 -0
  90. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +42 -7
  91. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  92. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  93. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +627 -0
  94. package/src/qingflow_mcp/builder_facade/models.py +413 -119
  95. package/src/qingflow_mcp/builder_facade/service.py +4238 -666
  96. package/src/qingflow_mcp/cli/commands/builder.py +232 -75
  97. package/src/qingflow_mcp/cli/main.py +34 -0
  98. package/src/qingflow_mcp/public_surface.py +10 -0
  99. package/src/qingflow_mcp/server_app_builder.py +62 -34
  100. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  101. package/src/qingflow_mcp/solution/executor.py +2 -0
  102. package/src/qingflow_mcp/tools/ai_builder_tools.py +2649 -457
  103. package/src/qingflow_mcp/tools/record_tools.py +23 -11
@@ -203,6 +203,10 @@ class LayoutPreset(str, Enum):
203
203
  single_section = "single_section"
204
204
 
205
205
 
206
+ class FlowPreset(str, Enum):
207
+ basic_approval = "basic_approval"
208
+ basic_fill_then_approve = "basic_fill_then_approve"
209
+
206
210
 
207
211
  class ViewsPreset(str, Enum):
208
212
  default_table = "default_table"
@@ -243,6 +247,116 @@ class ViewFilterOperator(str, Enum):
243
247
  not_empty = "not_empty"
244
248
 
245
249
 
250
+ _PUBLIC_CONDITION_OPERATOR_ALIASES: dict[str, str] = {
251
+ "eq": "eq",
252
+ "equal": "eq",
253
+ "equals": "eq",
254
+ "=": "eq",
255
+ "==": "eq",
256
+ "neq": "neq",
257
+ "ne": "neq",
258
+ "not_eq": "neq",
259
+ "not_equal": "neq",
260
+ "not_equals": "neq",
261
+ "noteq": "neq",
262
+ "notequal": "neq",
263
+ "notequals": "neq",
264
+ "!=": "neq",
265
+ "in": "in",
266
+ "any": "in",
267
+ "any_of": "in",
268
+ "anyof": "in",
269
+ "one_of": "in",
270
+ "oneof": "in",
271
+ "between_any": "in",
272
+ "betweenany": "in",
273
+ "equal_any": "in",
274
+ "equalany": "in",
275
+ "any_match": "in",
276
+ "anymatch": "in",
277
+ "contains": "contains",
278
+ "contain": "contains",
279
+ "include": "contains",
280
+ "includes": "contains",
281
+ "like": "contains",
282
+ "fuzzy": "contains",
283
+ "fuzzy_match": "contains",
284
+ "fuzzymatch": "contains",
285
+ "gte": "gte",
286
+ "ge": "gte",
287
+ ">=": "gte",
288
+ "greater_or_equal": "gte",
289
+ "greaterorequal": "gte",
290
+ "greater_than_or_equal": "gte",
291
+ "greaterthanorequal": "gte",
292
+ "lte": "lte",
293
+ "le": "lte",
294
+ "<=": "lte",
295
+ "less_or_equal": "lte",
296
+ "lessorequal": "lte",
297
+ "less_than_or_equal": "lte",
298
+ "lessthanorequal": "lte",
299
+ "is_empty": "is_empty",
300
+ "isempty": "is_empty",
301
+ "empty": "is_empty",
302
+ "blank": "is_empty",
303
+ "is_blank": "is_empty",
304
+ "isblank": "is_empty",
305
+ "null": "is_empty",
306
+ "is_null": "is_empty",
307
+ "isnull": "is_empty",
308
+ "not_empty": "not_empty",
309
+ "notempty": "not_empty",
310
+ "is_not_empty": "not_empty",
311
+ "isnotempty": "not_empty",
312
+ "not_blank": "not_empty",
313
+ "notblank": "not_empty",
314
+ "not_null": "not_empty",
315
+ "notnull": "not_empty",
316
+ "is_not_null": "not_empty",
317
+ "isnotnull": "not_empty",
318
+ }
319
+
320
+
321
+ def normalize_public_condition_operator(raw_operator: Any, *, extra_aliases: dict[str, str] | None = None) -> Any:
322
+ if not isinstance(raw_operator, str):
323
+ return raw_operator
324
+ text = raw_operator.strip().lower()
325
+ if not text:
326
+ return text
327
+ lookup = dict(_PUBLIC_CONDITION_OPERATOR_ALIASES)
328
+ if extra_aliases:
329
+ lookup.update(extra_aliases)
330
+ candidates = [
331
+ text,
332
+ re.sub(r"[\s-]+", "_", text),
333
+ re.sub(r"[\s_-]+", "", text),
334
+ ]
335
+ for candidate in candidates:
336
+ if candidate in lookup:
337
+ return lookup[candidate]
338
+ return text
339
+
340
+
341
+ def _normalize_public_condition_payload(value: Any) -> Any:
342
+ if not isinstance(value, dict):
343
+ return value
344
+ payload = dict(value)
345
+ if "value" in payload and "values" not in payload:
346
+ raw_value = payload.pop("value")
347
+ payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
348
+ elif "values" in payload and not isinstance(payload.get("values"), list):
349
+ payload["values"] = [payload.get("values")]
350
+ raw_operator = payload.get("operator", payload.get("op"))
351
+ if raw_operator is None:
352
+ payload["operator"] = "eq"
353
+ raw_operator = "eq"
354
+ if isinstance(raw_operator, str):
355
+ payload["operator"] = normalize_public_condition_operator(raw_operator)
356
+ payload.pop("op", None)
357
+ return payload
358
+
359
+
246
360
  class FlowAssigneePatch(StrictModel):
247
361
  role_ids: list[int] = Field(default_factory=list)
248
362
  role_names: list[str] = Field(default_factory=list)
@@ -258,45 +372,13 @@ class FlowNodePermissionsPatch(StrictModel):
258
372
 
259
373
  class FlowConditionRulePatch(StrictModel):
260
374
  field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
261
- operator: FlowConditionOperator = Field(validation_alias=AliasChoices("operator", "op"))
375
+ operator: FlowConditionOperator = Field(default=FlowConditionOperator.eq, validation_alias=AliasChoices("operator", "op"))
262
376
  values: list[Any] = Field(default_factory=list)
263
377
 
264
378
  @model_validator(mode="before")
265
379
  @classmethod
266
380
  def normalize_aliases(cls, value: Any) -> Any:
267
- if not isinstance(value, dict):
268
- return value
269
- payload = dict(value)
270
- if "value" in payload and "values" not in payload:
271
- raw_value = payload.pop("value")
272
- payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
273
- raw_operator = payload.get("operator", payload.get("op"))
274
- if isinstance(raw_operator, str):
275
- normalized = raw_operator.strip().lower()
276
- operator_aliases = {
277
- "equals": FlowConditionOperator.eq.value,
278
- "equal": FlowConditionOperator.eq.value,
279
- "=": FlowConditionOperator.eq.value,
280
- "not_equals": FlowConditionOperator.neq.value,
281
- "not_equal": FlowConditionOperator.neq.value,
282
- "!=": FlowConditionOperator.neq.value,
283
- ">=": FlowConditionOperator.gte.value,
284
- "<=": FlowConditionOperator.lte.value,
285
- "any_of": FlowConditionOperator.in_.value,
286
- "one_of": FlowConditionOperator.in_.value,
287
- "between_any": FlowConditionOperator.in_.value,
288
- "empty": FlowConditionOperator.is_empty.value,
289
- "is blank": FlowConditionOperator.is_empty.value,
290
- "blank": FlowConditionOperator.is_empty.value,
291
- "not_empty": FlowConditionOperator.not_empty.value,
292
- "not blank": FlowConditionOperator.not_empty.value,
293
- }
294
- if normalized in operator_aliases:
295
- payload["operator"] = operator_aliases[normalized]
296
- elif "operator" not in payload:
297
- payload["operator"] = normalized
298
- payload.pop("op", None)
299
- return payload
381
+ return _normalize_public_condition_payload(value)
300
382
 
301
383
  @model_validator(mode="after")
302
384
  def validate_shape(self) -> "FlowConditionRulePatch":
@@ -310,45 +392,13 @@ class FlowConditionRulePatch(StrictModel):
310
392
 
311
393
  class ViewFilterRulePatch(StrictModel):
312
394
  field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
313
- operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
395
+ operator: ViewFilterOperator = Field(default=ViewFilterOperator.eq, validation_alias=AliasChoices("operator", "op"))
314
396
  values: list[Any] = Field(default_factory=list)
315
397
 
316
398
  @model_validator(mode="before")
317
399
  @classmethod
318
400
  def normalize_aliases(cls, value: Any) -> Any:
319
- if not isinstance(value, dict):
320
- return value
321
- payload = dict(value)
322
- if "value" in payload and "values" not in payload:
323
- raw_value = payload.pop("value")
324
- payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
325
- raw_operator = payload.get("operator", payload.get("op"))
326
- if isinstance(raw_operator, str):
327
- normalized = raw_operator.strip().lower()
328
- operator_aliases = {
329
- "equals": ViewFilterOperator.eq.value,
330
- "equal": ViewFilterOperator.eq.value,
331
- "=": ViewFilterOperator.eq.value,
332
- "not_equals": ViewFilterOperator.neq.value,
333
- "not_equal": ViewFilterOperator.neq.value,
334
- "!=": ViewFilterOperator.neq.value,
335
- ">=": ViewFilterOperator.gte.value,
336
- "<=": ViewFilterOperator.lte.value,
337
- "any_of": ViewFilterOperator.in_.value,
338
- "one_of": ViewFilterOperator.in_.value,
339
- "between_any": ViewFilterOperator.in_.value,
340
- "empty": ViewFilterOperator.is_empty.value,
341
- "is blank": ViewFilterOperator.is_empty.value,
342
- "blank": ViewFilterOperator.is_empty.value,
343
- "not_empty": ViewFilterOperator.not_empty.value,
344
- "not blank": ViewFilterOperator.not_empty.value,
345
- }
346
- if normalized in operator_aliases:
347
- payload["operator"] = operator_aliases[normalized]
348
- elif "operator" not in payload:
349
- payload["operator"] = normalized
350
- payload.pop("op", None)
351
- return payload
401
+ return _normalize_public_condition_payload(value)
352
402
 
353
403
  @model_validator(mode="after")
354
404
  def validate_shape(self) -> "ViewFilterRulePatch":
@@ -1101,6 +1151,91 @@ class ViewAssociatedResourcesPatch(StrictModel):
1101
1151
  )
1102
1152
 
1103
1153
 
1154
+ class ViewActionButtonPatch(StrictModel):
1155
+ button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
1156
+ client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
1157
+ text: str = Field(validation_alias=AliasChoices("text", "button_text", "buttonText", "name"))
1158
+ action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("action", "trigger_action", "triggerAction"))
1159
+ url: str | None = Field(default=None, validation_alias=AliasChoices("url", "link_url", "linkUrl", "trigger_link_url", "triggerLinkUrl"))
1160
+ target_app_key: str | None = Field(default=None, validation_alias=AliasChoices("target_app_key", "targetAppKey", "related_app_key", "relatedAppKey"))
1161
+ target_app_name: str | None = Field(default=None, validation_alias=AliasChoices("target_app_name", "targetAppName", "related_app_name", "relatedAppName"))
1162
+ field_mappings: list[dict[str, Any]] = Field(default_factory=list, validation_alias=AliasChoices("field_mappings", "fieldMappings", "mappings"))
1163
+ default_values: dict[str, Any] = Field(default_factory=dict, validation_alias=AliasChoices("default_values", "defaultValues", "defaults"))
1164
+ placement: PublicButtonPlacement = Field(default=PublicButtonPlacement.detail, validation_alias=AliasChoices("placement", "position"))
1165
+ primary: bool = Field(default=False, validation_alias=AliasChoices("primary", "being_main", "beingMain"))
1166
+ visible_when: list[list[ViewFilterRulePatch]] = Field(
1167
+ default_factory=list,
1168
+ validation_alias=AliasChoices("visible_when", "visibleWhen", "button_limit", "buttonLimit"),
1169
+ )
1170
+ button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
1171
+ button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
1172
+ print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
1173
+ style_preset: str | None = Field(default=None, validation_alias=AliasChoices("style_preset", "stylePreset"))
1174
+ background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
1175
+ text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
1176
+ button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
1177
+ external_qrobot_config: dict[str, Any] | None = Field(
1178
+ default=None,
1179
+ validation_alias=AliasChoices(
1180
+ "external_qrobot_config",
1181
+ "externalQrobotConfig",
1182
+ "externalQRobotConfig",
1183
+ "custom_button_external_qrobot_relation_vo",
1184
+ "customButtonExternalQRobotRelationVO",
1185
+ ),
1186
+ )
1187
+ trigger_wings_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"))
1188
+
1189
+ @model_validator(mode="before")
1190
+ @classmethod
1191
+ def normalize_aliases(cls, value: Any) -> Any:
1192
+ if not isinstance(value, dict):
1193
+ return value
1194
+ payload = dict(value)
1195
+ raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
1196
+ if isinstance(raw_action, str):
1197
+ normalized = raw_action.strip()
1198
+ aliases = {
1199
+ "add_data": PublicButtonTriggerAction.add_data.value,
1200
+ "add-data": PublicButtonTriggerAction.add_data.value,
1201
+ "adddata": PublicButtonTriggerAction.add_data.value,
1202
+ "addData": PublicButtonTriggerAction.add_data.value,
1203
+ "link": PublicButtonTriggerAction.link.value,
1204
+ "url": PublicButtonTriggerAction.link.value,
1205
+ "qrobot": PublicButtonTriggerAction.qrobot.value,
1206
+ "qRobot": PublicButtonTriggerAction.qrobot.value,
1207
+ "wings": PublicButtonTriggerAction.wings.value,
1208
+ }
1209
+ payload["action"] = aliases.get(normalized, aliases.get(normalized.lower(), normalized))
1210
+ raw_placement = payload.get("placement", payload.get("position", payload.get("config_type", payload.get("configType"))))
1211
+ if isinstance(raw_placement, str):
1212
+ normalized = raw_placement.strip().lower()
1213
+ if normalized in {"top", "header"}:
1214
+ payload["placement"] = PublicButtonPlacement.header.value
1215
+ elif normalized in {"detail", "data_detail"}:
1216
+ payload["placement"] = PublicButtonPlacement.detail.value
1217
+ elif normalized in {"list", "row", "row_action", "inside"}:
1218
+ payload["placement"] = PublicButtonPlacement.list.value
1219
+ raw_limits = payload.get("visible_when", payload.get("visibleWhen", payload.get("button_limit", payload.get("buttonLimit"))))
1220
+ if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1221
+ payload["visible_when"] = [raw_limits]
1222
+ return payload
1223
+
1224
+ @model_validator(mode="after")
1225
+ def validate_shape(self) -> "ViewActionButtonPatch":
1226
+ if not str(self.text or "").strip():
1227
+ raise ValueError("action_buttons[].text is required")
1228
+ if self.action == PublicButtonTriggerAction.link and not str(self.url or "").strip():
1229
+ raise ValueError("link action_buttons require url")
1230
+ if self.action == PublicButtonTriggerAction.add_data and not str(self.target_app_key or "").strip():
1231
+ raise ValueError("add_data action_buttons require target_app_key")
1232
+ if self.action == PublicButtonTriggerAction.qrobot and self.external_qrobot_config is None:
1233
+ raise ValueError("qRobot action_buttons require external_qrobot_config")
1234
+ if self.action == PublicButtonTriggerAction.wings and self.trigger_wings_config is None:
1235
+ raise ValueError("wings action_buttons require trigger_wings_config")
1236
+ return self
1237
+
1238
+
1104
1239
  class ViewUpsertPatch(StrictModel):
1105
1240
  name: str
1106
1241
  view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
@@ -1112,6 +1247,8 @@ class ViewUpsertPatch(StrictModel):
1112
1247
  end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
1113
1248
  title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
1114
1249
  buttons: list["ViewButtonBindingPatch"] | None = None
1250
+ action_buttons: list[ViewActionButtonPatch] | None = Field(default=None, validation_alias=AliasChoices("action_buttons", "actionButtons"))
1251
+ action_buttons_mode: str = Field(default="merge", validation_alias=AliasChoices("action_buttons_mode", "actionButtonsMode", "button_mode", "buttonMode"))
1115
1252
  visibility: VisibilityPatch | None = None
1116
1253
  query_conditions: ViewQueryConditionsPatch | None = Field(default=None, validation_alias=AliasChoices("query_conditions", "queryConditions", "query_condition", "queryCondition"))
1117
1254
  associated_resources: ViewAssociatedResourcesPatch | None = Field(
@@ -1161,6 +1298,12 @@ class ViewUpsertPatch(StrictModel):
1161
1298
  payload["associated_resources"] = payload.pop("associatedReports")
1162
1299
  if "asosChartConfig" in payload and "associated_resources" not in payload:
1163
1300
  payload["associated_resources"] = payload.pop("asosChartConfig")
1301
+ if "actionButtons" in payload and "action_buttons" not in payload:
1302
+ payload["action_buttons"] = payload.pop("actionButtons")
1303
+ if "actionButtonsMode" in payload and "action_buttons_mode" not in payload:
1304
+ payload["action_buttons_mode"] = payload.pop("actionButtonsMode")
1305
+ if "buttonMode" in payload and "action_buttons_mode" not in payload:
1306
+ payload["action_buttons_mode"] = payload.pop("buttonMode")
1164
1307
  raw_type = payload.get("type")
1165
1308
  if isinstance(raw_type, str):
1166
1309
  normalized = raw_type.strip().lower()
@@ -1182,6 +1325,10 @@ class ViewUpsertPatch(StrictModel):
1182
1325
  raise ValueError("board view requires group_by")
1183
1326
  if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
1184
1327
  raise ValueError("gantt view requires start_field and end_field")
1328
+ normalized_mode = str(self.action_buttons_mode or "merge").strip().lower()
1329
+ if normalized_mode not in {"merge", "replace"}:
1330
+ raise ValueError("action_buttons_mode must be merge or replace")
1331
+ self.action_buttons_mode = normalized_mode
1185
1332
  return self
1186
1333
 
1187
1334
 
@@ -1207,6 +1354,76 @@ class ViewPartialPatch(StrictModel):
1207
1354
  return self
1208
1355
 
1209
1356
 
1357
+ def public_button_trigger_action_value(value: Any) -> str:
1358
+ raw = value.value if isinstance(value, PublicButtonTriggerAction) else str(value or "").strip()
1359
+ normalized = raw.lower()
1360
+ if raw == PublicButtonTriggerAction.add_data.value or normalized in {"add_data", "add-data", "adddata"}:
1361
+ return "add_data"
1362
+ if normalized in {PublicButtonTriggerAction.link.value, "url"}:
1363
+ return "link"
1364
+ if raw == PublicButtonTriggerAction.qrobot.value or normalized == "qrobot":
1365
+ return "qRobot"
1366
+ if normalized == PublicButtonTriggerAction.wings.value:
1367
+ return "wings"
1368
+ return raw
1369
+
1370
+
1371
+ def _drop_empty_public_payload_values(payload: dict[str, Any]) -> dict[str, Any]:
1372
+ compact: dict[str, Any] = {}
1373
+ for key, item in payload.items():
1374
+ if item is None or item == "" or item == [] or item == {}:
1375
+ continue
1376
+ if key == "primary" and item is False:
1377
+ continue
1378
+ compact[key] = item
1379
+ return compact
1380
+
1381
+
1382
+ def public_view_action_button_payload(button: Any, *, compact: bool = False) -> dict[str, Any]:
1383
+ if isinstance(button, ViewActionButtonPatch):
1384
+ payload = button.model_dump(mode="json")
1385
+ elif isinstance(button, dict):
1386
+ payload = dict(button)
1387
+ else:
1388
+ return {}
1389
+ raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
1390
+ if raw_action is not None:
1391
+ payload["action"] = public_button_trigger_action_value(raw_action)
1392
+ payload.pop("trigger_action", None)
1393
+ payload.pop("triggerAction", None)
1394
+ if compact:
1395
+ return _drop_empty_public_payload_values(payload)
1396
+ return payload
1397
+
1398
+
1399
+ def public_view_upsert_payload(view: ViewUpsertPatch) -> dict[str, Any]:
1400
+ payload = view.model_dump(mode="json")
1401
+ if view.action_buttons is not None:
1402
+ payload["action_buttons"] = [
1403
+ public_view_action_button_payload(button, compact=True)
1404
+ for button in view.action_buttons
1405
+ ]
1406
+ return payload
1407
+
1408
+
1409
+ def public_view_partial_payload(patch: ViewPartialPatch) -> dict[str, Any]:
1410
+ payload = patch.model_dump(mode="json")
1411
+ patch_set = payload.get("set")
1412
+ if isinstance(patch_set, dict):
1413
+ raw_buttons = patch_set.get("action_buttons")
1414
+ if raw_buttons is None:
1415
+ raw_buttons = patch_set.get("actionButtons")
1416
+ if isinstance(raw_buttons, list):
1417
+ patch_set = dict(patch_set)
1418
+ patch_set.pop("actionButtons", None)
1419
+ patch_set["action_buttons"] = [
1420
+ public_view_action_button_payload(button, compact=True)
1421
+ for button in raw_buttons
1422
+ ]
1423
+ payload["set"] = patch_set
1424
+ return payload
1425
+
1426
+
1210
1427
  class CustomButtonJudgeValuePatch(StrictModel):
1211
1428
  id: int | str | None = None
1212
1429
  value: Any | None = None
@@ -1275,32 +1492,39 @@ class CustomButtonFieldMappingPatch(StrictModel):
1275
1492
 
1276
1493
 
1277
1494
  class FieldMatchMappingPatch(StrictModel):
1278
- target_field: Any = Field(
1279
- validation_alias=AliasChoices("target_field", "targetField", "target", "field", "field_name", "fieldName")
1280
- )
1495
+ target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "field", "field_name", "fieldName", "name"))
1281
1496
  source_field: Any | None = Field(default=None, validation_alias=AliasChoices("source_field", "sourceField", "source"))
1282
- value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "static_value", "staticValue"))
1497
+ value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "values", "static_value", "staticValue"))
1283
1498
  operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
1284
1499
 
1285
1500
  @model_validator(mode="before")
1286
1501
  @classmethod
1287
- def normalize_semantic_aliases(cls, value: Any) -> Any:
1502
+ def normalize_aliases(cls, value: Any) -> Any:
1288
1503
  if not isinstance(value, dict):
1289
1504
  return value
1290
1505
  payload = dict(value)
1291
- has_static_value = any(key in payload for key in ("value", "static_value", "staticValue"))
1292
- if "values" in payload:
1293
- values = payload.pop("values")
1294
- if has_static_value:
1295
- return payload
1296
- if isinstance(values, list) and len(values) == 1:
1297
- payload["value"] = values[0]
1506
+ if "values" in payload and "value" not in payload:
1507
+ raw_values = payload.pop("values")
1508
+ if isinstance(raw_values, list) and len(raw_values) == 1:
1509
+ payload["value"] = raw_values[0]
1298
1510
  else:
1299
- payload["value"] = values
1511
+ payload["value"] = raw_values
1512
+ raw_operator = payload.get("operator", payload.get("op", payload.get("judge_type", payload.get("judgeType"))))
1513
+ if isinstance(raw_operator, str):
1514
+ payload["operator"] = normalize_public_condition_operator(
1515
+ raw_operator,
1516
+ extra_aliases={"include_any": "include_any", "includes_any": "include_any", "includeany": "include_any"},
1517
+ )
1518
+ for alias in ("op", "judge_type", "judgeType"):
1519
+ payload.pop(alias, None)
1300
1520
  return payload
1301
1521
 
1302
1522
  @model_validator(mode="after")
1303
1523
  def validate_shape(self) -> "FieldMatchMappingPatch":
1524
+ if str(self.operator or "").strip().lower() in {"is_empty", "not_empty"}:
1525
+ self.source_field = None
1526
+ self.value = None
1527
+ return self
1304
1528
  has_source = self.source_field is not None and str(self.source_field).strip() != ""
1305
1529
  has_value = self.value is not None
1306
1530
  if has_source == has_value:
@@ -1762,45 +1986,13 @@ class ViewButtonBindingPatch(StrictModel):
1762
1986
 
1763
1987
  class ChartFilterRulePatch(StrictModel):
1764
1988
  field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
1765
- operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
1989
+ operator: ViewFilterOperator = Field(default=ViewFilterOperator.eq, validation_alias=AliasChoices("operator", "op"))
1766
1990
  values: list[Any] = Field(default_factory=list)
1767
1991
 
1768
1992
  @model_validator(mode="before")
1769
1993
  @classmethod
1770
1994
  def normalize_aliases(cls, value: Any) -> Any:
1771
- if not isinstance(value, dict):
1772
- return value
1773
- payload = dict(value)
1774
- if "value" in payload and "values" not in payload:
1775
- raw_value = payload.pop("value")
1776
- payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
1777
- raw_operator = payload.get("operator", payload.get("op"))
1778
- if isinstance(raw_operator, str):
1779
- normalized = raw_operator.strip().lower()
1780
- operator_aliases = {
1781
- "equals": ViewFilterOperator.eq.value,
1782
- "equal": ViewFilterOperator.eq.value,
1783
- "=": ViewFilterOperator.eq.value,
1784
- "not_equals": ViewFilterOperator.neq.value,
1785
- "not_equal": ViewFilterOperator.neq.value,
1786
- "!=": ViewFilterOperator.neq.value,
1787
- ">=": ViewFilterOperator.gte.value,
1788
- "<=": ViewFilterOperator.lte.value,
1789
- "any_of": ViewFilterOperator.in_.value,
1790
- "one_of": ViewFilterOperator.in_.value,
1791
- "between_any": ViewFilterOperator.in_.value,
1792
- "empty": ViewFilterOperator.is_empty.value,
1793
- "is blank": ViewFilterOperator.is_empty.value,
1794
- "blank": ViewFilterOperator.is_empty.value,
1795
- "not_empty": ViewFilterOperator.not_empty.value,
1796
- "not blank": ViewFilterOperator.not_empty.value,
1797
- }
1798
- if normalized in operator_aliases:
1799
- payload["operator"] = operator_aliases[normalized]
1800
- elif "operator" not in payload:
1801
- payload["operator"] = normalized
1802
- payload.pop("op", None)
1803
- return payload
1995
+ return _normalize_public_condition_payload(value)
1804
1996
 
1805
1997
  @model_validator(mode="after")
1806
1998
  def validate_shape(self) -> "ChartFilterRulePatch":
@@ -2089,6 +2281,22 @@ class ChartApplyRequest(StrictModel):
2089
2281
  return self
2090
2282
 
2091
2283
 
2284
+ class PortalInlineChartPatch(ChartUpsertPatch):
2285
+ app_key: str = Field(validation_alias=AliasChoices("app_key", "appKey"))
2286
+
2287
+ @model_validator(mode="before")
2288
+ @classmethod
2289
+ def normalize_inline_aliases(cls, value: Any) -> Any:
2290
+ if not isinstance(value, dict):
2291
+ return value
2292
+ payload = dict(value)
2293
+ if "chart_name" in payload and "name" not in payload:
2294
+ payload["name"] = payload.pop("chart_name")
2295
+ if "chartName" in payload and "name" not in payload:
2296
+ payload["name"] = payload.pop("chartName")
2297
+ return payload
2298
+
2299
+
2092
2300
  class PortalComponentPositionPatch(StrictModel):
2093
2301
  pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
2094
2302
  pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
@@ -2133,14 +2341,36 @@ class PortalComponentPositionPatch(StrictModel):
2133
2341
 
2134
2342
 
2135
2343
  class PortalChartRefPatch(StrictModel):
2136
- app_key: str
2344
+ app_key: str | None = None
2137
2345
  chart_id: str | None = None
2138
2346
  chart_name: str | None = None
2139
2347
 
2348
+ @model_validator(mode="before")
2349
+ @classmethod
2350
+ def normalize_aliases(cls, value: Any) -> Any:
2351
+ if not isinstance(value, dict):
2352
+ return value
2353
+ payload = dict(value)
2354
+ if "chartId" in payload and "chart_id" not in payload:
2355
+ payload["chart_id"] = payload.pop("chartId")
2356
+ if "chart_key" in payload and "chart_id" not in payload:
2357
+ payload["chart_id"] = payload.pop("chart_key")
2358
+ if "chartKey" in payload and "chart_id" not in payload:
2359
+ payload["chart_id"] = payload.pop("chartKey")
2360
+ if "chartName" in payload and "chart_name" not in payload:
2361
+ payload["chart_name"] = payload.pop("chartName")
2362
+ if "name" in payload and "chart_name" not in payload:
2363
+ payload["chart_name"] = payload.pop("name")
2364
+ if "appKey" in payload and "app_key" not in payload:
2365
+ payload["app_key"] = payload.pop("appKey")
2366
+ return payload
2367
+
2140
2368
  @model_validator(mode="after")
2141
2369
  def validate_target(self) -> "PortalChartRefPatch":
2142
2370
  if not (self.chart_id or self.chart_name):
2143
2371
  raise ValueError("chart_ref requires chart_id or chart_name")
2372
+ if self.chart_name and not self.chart_id and not self.app_key:
2373
+ raise ValueError("chart_ref.app_key is required when resolving by chart_name")
2144
2374
  return self
2145
2375
 
2146
2376
 
@@ -2171,6 +2401,7 @@ class PortalSectionPatch(StrictModel):
2171
2401
  dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
2172
2402
  config: dict[str, Any] = Field(default_factory=dict)
2173
2403
  chart_ref: PortalChartRefPatch | None = None
2404
+ chart: PortalInlineChartPatch | None = Field(default=None, validation_alias=AliasChoices("chart", "inline_chart", "inlineChart", "create_chart", "createChart"))
2174
2405
  view_ref: PortalViewRefPatch | None = None
2175
2406
  text: str | None = None
2176
2407
  url: str | None = None
@@ -2189,6 +2420,27 @@ class PortalSectionPatch(StrictModel):
2189
2420
  payload["role"] = raw_role.strip().lower()
2190
2421
  if "chartRef" in payload and "chart_ref" not in payload:
2191
2422
  payload["chart_ref"] = payload.pop("chartRef")
2423
+ if "inlineChart" in payload and "chart" not in payload:
2424
+ payload["chart"] = payload.pop("inlineChart")
2425
+ if "inline_chart" in payload and "chart" not in payload:
2426
+ payload["chart"] = payload.pop("inline_chart")
2427
+ if "createChart" in payload and "chart" not in payload:
2428
+ payload["chart"] = payload.pop("createChart")
2429
+ if "create_chart" in payload and "chart" not in payload:
2430
+ payload["chart"] = payload.pop("create_chart")
2431
+ raw_chart = payload.get("chart")
2432
+ if isinstance(raw_chart, dict):
2433
+ chart_payload = dict(raw_chart)
2434
+ has_ref_id = any(key in chart_payload for key in ("chart_id", "chartId", "chart_key", "chartKey"))
2435
+ has_chart_type = any(key in chart_payload for key in ("chart_type", "chartType"))
2436
+ has_create_config = has_chart_type or any(
2437
+ key in chart_payload
2438
+ for key in ("metric", "metrics", "group_by", "groupBy", "where", "filters", "dimensions")
2439
+ )
2440
+ has_ref_name = any(key in chart_payload for key in ("chart_name", "chartName", "name"))
2441
+ if has_ref_id or (has_ref_name and not has_create_config):
2442
+ payload["chart_ref"] = chart_payload
2443
+ payload.pop("chart", None)
2192
2444
  if "viewRef" in payload and "view_ref" not in payload:
2193
2445
  payload["view_ref"] = payload.pop("viewRef")
2194
2446
  if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
@@ -2200,8 +2452,8 @@ class PortalSectionPatch(StrictModel):
2200
2452
  supported = {"chart", "view", "grid", "filter", "text", "link"}
2201
2453
  if self.source_type not in supported:
2202
2454
  raise ValueError(f"unsupported portal source_type '{self.source_type}'")
2203
- if self.source_type == "chart" and self.chart_ref is None:
2204
- raise ValueError("chart section requires chart_ref")
2455
+ if self.source_type == "chart" and self.chart_ref is None and self.chart is None:
2456
+ raise ValueError("chart section requires chart_ref or chart")
2205
2457
  if self.source_type == "view" and self.view_ref is None:
2206
2458
  raise ValueError("view section requires view_ref")
2207
2459
  if self.source_type == "text" and self.text is None:
@@ -2338,6 +2590,7 @@ AppReadSummaryResponse = AppGetResponse
2338
2590
  AppFieldsReadResponse = AppGetFieldsResponse
2339
2591
  AppLayoutReadResponse = AppGetLayoutResponse
2340
2592
  AppViewsReadResponse = AppGetViewsResponse
2593
+ AppFlowReadResponse = AppGetFlowResponse
2341
2594
  AppChartsReadResponse = AppGetChartsResponse
2342
2595
 
2343
2596
 
@@ -2404,7 +2657,6 @@ class SchemaPlanRequest(StrictModel):
2404
2657
  icon: str | None = None
2405
2658
  color: str | None = None
2406
2659
  visibility: VisibilityPatch | None = None
2407
- create_if_missing: bool = False
2408
2660
  add_fields: list[FieldPatch] = Field(default_factory=list)
2409
2661
  update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
2410
2662
  remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
@@ -2427,6 +2679,40 @@ class LayoutPlanRequest(StrictModel):
2427
2679
  return payload
2428
2680
 
2429
2681
 
2682
+ class FlowPlanRequest(StrictModel):
2683
+ app_key: str
2684
+ mode: str = "replace"
2685
+ nodes: list[FlowNodePatch] = Field(default_factory=list)
2686
+ transitions: list[FlowTransitionPatch] = Field(default_factory=list)
2687
+ preset: FlowPreset | None = None
2688
+
2689
+ @model_validator(mode="before")
2690
+ @classmethod
2691
+ def normalize_mode_alias(cls, value: Any) -> Any:
2692
+ if not isinstance(value, dict):
2693
+ return value
2694
+ payload = dict(value)
2695
+ if str(payload.get("mode") or "").strip().lower() == "overwrite":
2696
+ payload["mode"] = "replace"
2697
+ raw_preset = payload.get("preset")
2698
+ if raw_preset is None and isinstance(payload.get("base_preset"), str):
2699
+ raw_preset = payload["base_preset"]
2700
+ payload["preset"] = raw_preset
2701
+ if isinstance(raw_preset, str):
2702
+ normalized_preset = raw_preset.strip().lower()
2703
+ preset_aliases = {
2704
+ "default_approval": FlowPreset.basic_approval.value,
2705
+ "approval": FlowPreset.basic_approval.value,
2706
+ "basic approval": FlowPreset.basic_approval.value,
2707
+ "default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
2708
+ "default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
2709
+ "fill_then_approve": FlowPreset.basic_fill_then_approve.value,
2710
+ }
2711
+ if normalized_preset in preset_aliases:
2712
+ payload["preset"] = preset_aliases[normalized_preset]
2713
+ return payload
2714
+
2715
+
2430
2716
  class ViewsPlanRequest(StrictModel):
2431
2717
  app_key: str
2432
2718
  upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
@@ -2454,6 +2740,14 @@ def _normalize_field_payload(value: Any) -> Any:
2454
2740
  if not isinstance(value, dict):
2455
2741
  return value
2456
2742
  payload = dict(value)
2743
+ if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2744
+ payload["as_data_title"] = payload.pop("data_title")
2745
+ if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2746
+ payload["as_data_title"] = payload.pop("dataTitle")
2747
+ if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2748
+ payload["as_data_cover"] = payload.pop("data_cover")
2749
+ if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2750
+ payload["as_data_cover"] = payload.pop("dataCover")
2457
2751
  if "fields" in payload and "subfields" not in payload:
2458
2752
  payload["subfields"] = payload.pop("fields")
2459
2753
  if "options" in payload: