@josephyan/qingflow-app-builder-mcp 1.1.25 → 1.1.27

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 (100) hide show
  1. package/README.md +6 -6
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +51 -164
  10. package/skills/qingflow-app-builder/references/build-complete-system.md +234 -0
  11. package/skills/qingflow-app-builder/references/build-single-app.md +138 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +24 -31
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +13 -39
  15. package/skills/qingflow-app-builder/references/match-rules.md +18 -29
  16. package/skills/qingflow-app-builder/references/public-surface-sync.md +2 -2
  17. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
  18. package/skills/qingflow-app-builder/references/tool-selection.md +25 -42
  19. package/skills/qingflow-app-builder/references/update-flow.md +38 -22
  20. package/skills/qingflow-app-builder/references/update-schema.md +0 -14
  21. package/skills/qingflow-app-builder/references/update-views.md +14 -83
  22. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
  23. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  24. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  25. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  26. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  27. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  28. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  29. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  30. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  31. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  32. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  33. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  34. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  35. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  36. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  37. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  38. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  39. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  40. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  41. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  42. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  43. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  44. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  45. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  46. package/src/qingflow_mcp/__init__.py +1 -1
  47. package/src/qingflow_mcp/__main__.py +2 -6
  48. package/src/qingflow_mcp/builder_facade/models.py +49 -330
  49. package/src/qingflow_mcp/builder_facade/service.py +1621 -6149
  50. package/src/qingflow_mcp/cli/commands/builder.py +366 -322
  51. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  52. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  53. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  54. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  55. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  56. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  57. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  58. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  59. package/src/qingflow_mcp/cli/context.py +3 -0
  60. package/src/qingflow_mcp/cli/formatters.py +16 -784
  61. package/src/qingflow_mcp/cli/main.py +41 -120
  62. package/src/qingflow_mcp/errors.py +2 -43
  63. package/src/qingflow_mcp/public_surface.py +17 -26
  64. package/src/qingflow_mcp/response_trim.py +17 -81
  65. package/src/qingflow_mcp/server.py +12 -14
  66. package/src/qingflow_mcp/server_app_builder.py +39 -81
  67. package/src/qingflow_mcp/server_app_user.py +16 -22
  68. package/src/qingflow_mcp/session_store.py +7 -11
  69. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  70. package/src/qingflow_mcp/solution/executor.py +18 -245
  71. package/src/qingflow_mcp/tools/ai_builder_tools.py +1483 -3139
  72. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  73. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  74. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  75. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  76. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  77. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  78. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  79. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  80. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  81. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  82. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  83. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  84. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  85. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  86. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  87. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  88. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  89. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  90. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  91. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  92. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  93. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  94. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  95. package/src/qingflow_mcp/version.py +2 -0
  96. package/npm/bin/qingflow-skills.mjs +0 -5
  97. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
  98. package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
  99. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
  100. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -71,36 +71,20 @@ class PublicExternalVisibilityMode(str, Enum):
71
71
 
72
72
 
73
73
  FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
74
+ "textarea": PublicFieldType.long_text,
74
75
  "multiline": PublicFieldType.long_text,
75
- "multiline_text": PublicFieldType.long_text,
76
76
  "multi_line": PublicFieldType.long_text,
77
- "multi_line_text": PublicFieldType.long_text,
78
- "textarea": PublicFieldType.long_text,
79
- "longtext": PublicFieldType.long_text,
80
- "long-text": PublicFieldType.long_text,
77
+ "multiline_text": PublicFieldType.long_text,
81
78
  "amount": PublicFieldType.amount,
82
79
  "currency": PublicFieldType.amount,
83
80
  "mobile": PublicFieldType.phone,
84
81
  "user": PublicFieldType.member,
85
82
  "users": PublicFieldType.member,
86
83
  "select": PublicFieldType.single_select,
87
- "single_choice": PublicFieldType.single_select,
88
- "single-choice": PublicFieldType.single_select,
89
- "single choice": PublicFieldType.single_select,
90
- "choice": PublicFieldType.single_select,
91
- "dropdown": PublicFieldType.single_select,
92
84
  "radio": PublicFieldType.single_select,
93
85
  "checkbox": PublicFieldType.multi_select,
94
86
  "multi_select": PublicFieldType.multi_select,
95
87
  "multi-select": PublicFieldType.multi_select,
96
- "multi select": PublicFieldType.multi_select,
97
- "multiselect": PublicFieldType.multi_select,
98
- "multi_choice": PublicFieldType.multi_select,
99
- "multi-choice": PublicFieldType.multi_select,
100
- "multi choice": PublicFieldType.multi_select,
101
- "multiple_choice": PublicFieldType.multi_select,
102
- "multiple-choice": PublicFieldType.multi_select,
103
- "multiple choice": PublicFieldType.multi_select,
104
88
  "departments": PublicFieldType.department,
105
89
  "qlinker": PublicFieldType.q_linker,
106
90
  "q_linker": PublicFieldType.q_linker,
@@ -203,10 +187,6 @@ class LayoutPreset(str, Enum):
203
187
  single_section = "single_section"
204
188
 
205
189
 
206
- class FlowPreset(str, Enum):
207
- basic_approval = "basic_approval"
208
- basic_fill_then_approve = "basic_fill_then_approve"
209
-
210
190
 
211
191
  class ViewsPreset(str, Enum):
212
192
  default_table = "default_table"
@@ -1105,91 +1085,6 @@ class ViewAssociatedResourcesPatch(StrictModel):
1105
1085
  )
1106
1086
 
1107
1087
 
1108
- class ViewActionButtonPatch(StrictModel):
1109
- button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
1110
- client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
1111
- text: str = Field(validation_alias=AliasChoices("text", "button_text", "buttonText", "name"))
1112
- action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("action", "trigger_action", "triggerAction"))
1113
- url: str | None = Field(default=None, validation_alias=AliasChoices("url", "link_url", "linkUrl", "trigger_link_url", "triggerLinkUrl"))
1114
- target_app_key: str | None = Field(default=None, validation_alias=AliasChoices("target_app_key", "targetAppKey", "related_app_key", "relatedAppKey"))
1115
- target_app_name: str | None = Field(default=None, validation_alias=AliasChoices("target_app_name", "targetAppName", "related_app_name", "relatedAppName"))
1116
- field_mappings: list[dict[str, Any]] = Field(default_factory=list, validation_alias=AliasChoices("field_mappings", "fieldMappings", "mappings"))
1117
- default_values: dict[str, Any] = Field(default_factory=dict, validation_alias=AliasChoices("default_values", "defaultValues", "defaults"))
1118
- placement: PublicButtonPlacement = Field(default=PublicButtonPlacement.detail, validation_alias=AliasChoices("placement", "position"))
1119
- primary: bool = Field(default=False, validation_alias=AliasChoices("primary", "being_main", "beingMain"))
1120
- visible_when: list[list[ViewFilterRulePatch]] = Field(
1121
- default_factory=list,
1122
- validation_alias=AliasChoices("visible_when", "visibleWhen", "button_limit", "buttonLimit"),
1123
- )
1124
- button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
1125
- button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
1126
- print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
1127
- style_preset: str | None = Field(default=None, validation_alias=AliasChoices("style_preset", "stylePreset"))
1128
- background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
1129
- text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
1130
- button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
1131
- external_qrobot_config: dict[str, Any] | None = Field(
1132
- default=None,
1133
- validation_alias=AliasChoices(
1134
- "external_qrobot_config",
1135
- "externalQrobotConfig",
1136
- "externalQRobotConfig",
1137
- "custom_button_external_qrobot_relation_vo",
1138
- "customButtonExternalQRobotRelationVO",
1139
- ),
1140
- )
1141
- trigger_wings_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"))
1142
-
1143
- @model_validator(mode="before")
1144
- @classmethod
1145
- def normalize_aliases(cls, value: Any) -> Any:
1146
- if not isinstance(value, dict):
1147
- return value
1148
- payload = dict(value)
1149
- raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
1150
- if isinstance(raw_action, str):
1151
- normalized = raw_action.strip()
1152
- aliases = {
1153
- "add_data": PublicButtonTriggerAction.add_data.value,
1154
- "add-data": PublicButtonTriggerAction.add_data.value,
1155
- "adddata": PublicButtonTriggerAction.add_data.value,
1156
- "addData": PublicButtonTriggerAction.add_data.value,
1157
- "link": PublicButtonTriggerAction.link.value,
1158
- "url": PublicButtonTriggerAction.link.value,
1159
- "qrobot": PublicButtonTriggerAction.qrobot.value,
1160
- "qRobot": PublicButtonTriggerAction.qrobot.value,
1161
- "wings": PublicButtonTriggerAction.wings.value,
1162
- }
1163
- payload["action"] = aliases.get(normalized, aliases.get(normalized.lower(), normalized))
1164
- raw_placement = payload.get("placement", payload.get("position", payload.get("config_type", payload.get("configType"))))
1165
- if isinstance(raw_placement, str):
1166
- normalized = raw_placement.strip().lower()
1167
- if normalized in {"top", "header"}:
1168
- payload["placement"] = PublicButtonPlacement.header.value
1169
- elif normalized in {"detail", "data_detail"}:
1170
- payload["placement"] = PublicButtonPlacement.detail.value
1171
- elif normalized in {"list", "row", "row_action", "inside"}:
1172
- payload["placement"] = PublicButtonPlacement.list.value
1173
- raw_limits = payload.get("visible_when", payload.get("visibleWhen", payload.get("button_limit", payload.get("buttonLimit"))))
1174
- if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1175
- payload["visible_when"] = [raw_limits]
1176
- return payload
1177
-
1178
- @model_validator(mode="after")
1179
- def validate_shape(self) -> "ViewActionButtonPatch":
1180
- if not str(self.text or "").strip():
1181
- raise ValueError("action_buttons[].text is required")
1182
- if self.action == PublicButtonTriggerAction.link and not str(self.url or "").strip():
1183
- raise ValueError("link action_buttons require url")
1184
- if self.action == PublicButtonTriggerAction.add_data and not str(self.target_app_key or "").strip():
1185
- raise ValueError("add_data action_buttons require target_app_key")
1186
- if self.action == PublicButtonTriggerAction.qrobot and self.external_qrobot_config is None:
1187
- raise ValueError("qRobot action_buttons require external_qrobot_config")
1188
- if self.action == PublicButtonTriggerAction.wings and self.trigger_wings_config is None:
1189
- raise ValueError("wings action_buttons require trigger_wings_config")
1190
- return self
1191
-
1192
-
1193
1088
  class ViewUpsertPatch(StrictModel):
1194
1089
  name: str
1195
1090
  view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
@@ -1201,8 +1096,6 @@ class ViewUpsertPatch(StrictModel):
1201
1096
  end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
1202
1097
  title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
1203
1098
  buttons: list["ViewButtonBindingPatch"] | None = None
1204
- action_buttons: list[ViewActionButtonPatch] | None = Field(default=None, validation_alias=AliasChoices("action_buttons", "actionButtons"))
1205
- action_buttons_mode: str = Field(default="merge", validation_alias=AliasChoices("action_buttons_mode", "actionButtonsMode", "button_mode", "buttonMode"))
1206
1099
  visibility: VisibilityPatch | None = None
1207
1100
  query_conditions: ViewQueryConditionsPatch | None = Field(default=None, validation_alias=AliasChoices("query_conditions", "queryConditions", "query_condition", "queryCondition"))
1208
1101
  associated_resources: ViewAssociatedResourcesPatch | None = Field(
@@ -1252,12 +1145,6 @@ class ViewUpsertPatch(StrictModel):
1252
1145
  payload["associated_resources"] = payload.pop("associatedReports")
1253
1146
  if "asosChartConfig" in payload and "associated_resources" not in payload:
1254
1147
  payload["associated_resources"] = payload.pop("asosChartConfig")
1255
- if "actionButtons" in payload and "action_buttons" not in payload:
1256
- payload["action_buttons"] = payload.pop("actionButtons")
1257
- if "actionButtonsMode" in payload and "action_buttons_mode" not in payload:
1258
- payload["action_buttons_mode"] = payload.pop("actionButtonsMode")
1259
- if "buttonMode" in payload and "action_buttons_mode" not in payload:
1260
- payload["action_buttons_mode"] = payload.pop("buttonMode")
1261
1148
  raw_type = payload.get("type")
1262
1149
  if isinstance(raw_type, str):
1263
1150
  normalized = raw_type.strip().lower()
@@ -1279,10 +1166,6 @@ class ViewUpsertPatch(StrictModel):
1279
1166
  raise ValueError("board view requires group_by")
1280
1167
  if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
1281
1168
  raise ValueError("gantt view requires start_field and end_field")
1282
- normalized_mode = str(self.action_buttons_mode or "merge").strip().lower()
1283
- if normalized_mode not in {"merge", "replace"}:
1284
- raise ValueError("action_buttons_mode must be merge or replace")
1285
- self.action_buttons_mode = normalized_mode
1286
1169
  return self
1287
1170
 
1288
1171
 
@@ -1308,76 +1191,6 @@ class ViewPartialPatch(StrictModel):
1308
1191
  return self
1309
1192
 
1310
1193
 
1311
- def public_button_trigger_action_value(value: Any) -> str:
1312
- raw = value.value if isinstance(value, PublicButtonTriggerAction) else str(value or "").strip()
1313
- normalized = raw.lower()
1314
- if raw == PublicButtonTriggerAction.add_data.value or normalized in {"add_data", "add-data", "adddata"}:
1315
- return "add_data"
1316
- if normalized in {PublicButtonTriggerAction.link.value, "url"}:
1317
- return "link"
1318
- if raw == PublicButtonTriggerAction.qrobot.value or normalized == "qrobot":
1319
- return "qRobot"
1320
- if normalized == PublicButtonTriggerAction.wings.value:
1321
- return "wings"
1322
- return raw
1323
-
1324
-
1325
- def _drop_empty_public_payload_values(payload: dict[str, Any]) -> dict[str, Any]:
1326
- compact: dict[str, Any] = {}
1327
- for key, item in payload.items():
1328
- if item is None or item == "" or item == [] or item == {}:
1329
- continue
1330
- if key == "primary" and item is False:
1331
- continue
1332
- compact[key] = item
1333
- return compact
1334
-
1335
-
1336
- def public_view_action_button_payload(button: Any, *, compact: bool = False) -> dict[str, Any]:
1337
- if isinstance(button, ViewActionButtonPatch):
1338
- payload = button.model_dump(mode="json")
1339
- elif isinstance(button, dict):
1340
- payload = dict(button)
1341
- else:
1342
- return {}
1343
- raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
1344
- if raw_action is not None:
1345
- payload["action"] = public_button_trigger_action_value(raw_action)
1346
- payload.pop("trigger_action", None)
1347
- payload.pop("triggerAction", None)
1348
- if compact:
1349
- return _drop_empty_public_payload_values(payload)
1350
- return payload
1351
-
1352
-
1353
- def public_view_upsert_payload(view: ViewUpsertPatch) -> dict[str, Any]:
1354
- payload = view.model_dump(mode="json")
1355
- if view.action_buttons is not None:
1356
- payload["action_buttons"] = [
1357
- public_view_action_button_payload(button, compact=True)
1358
- for button in view.action_buttons
1359
- ]
1360
- return payload
1361
-
1362
-
1363
- def public_view_partial_payload(patch: ViewPartialPatch) -> dict[str, Any]:
1364
- payload = patch.model_dump(mode="json")
1365
- patch_set = payload.get("set")
1366
- if isinstance(patch_set, dict):
1367
- raw_buttons = patch_set.get("action_buttons")
1368
- if raw_buttons is None:
1369
- raw_buttons = patch_set.get("actionButtons")
1370
- if isinstance(raw_buttons, list):
1371
- patch_set = dict(patch_set)
1372
- patch_set.pop("actionButtons", None)
1373
- patch_set["action_buttons"] = [
1374
- public_view_action_button_payload(button, compact=True)
1375
- for button in raw_buttons
1376
- ]
1377
- payload["set"] = patch_set
1378
- return payload
1379
-
1380
-
1381
1194
  class CustomButtonJudgeValuePatch(StrictModel):
1382
1195
  id: int | str | None = None
1383
1196
  value: Any | None = None
@@ -1446,30 +1259,11 @@ class CustomButtonFieldMappingPatch(StrictModel):
1446
1259
 
1447
1260
 
1448
1261
  class FieldMatchMappingPatch(StrictModel):
1449
- target_field: Any = Field(
1450
- validation_alias=AliasChoices("target_field", "targetField", "target", "field", "field_name", "fieldName")
1451
- )
1262
+ target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "field"))
1452
1263
  source_field: Any | None = Field(default=None, validation_alias=AliasChoices("source_field", "sourceField", "source"))
1453
1264
  value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "static_value", "staticValue"))
1454
1265
  operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
1455
1266
 
1456
- @model_validator(mode="before")
1457
- @classmethod
1458
- def normalize_semantic_aliases(cls, value: Any) -> Any:
1459
- if not isinstance(value, dict):
1460
- return value
1461
- payload = dict(value)
1462
- has_static_value = any(key in payload for key in ("value", "static_value", "staticValue"))
1463
- if "values" in payload:
1464
- values = payload.pop("values")
1465
- if has_static_value:
1466
- return payload
1467
- if isinstance(values, list) and len(values) == 1:
1468
- payload["value"] = values[0]
1469
- else:
1470
- payload["value"] = values
1471
- return payload
1472
-
1473
1267
  @model_validator(mode="after")
1474
1268
  def validate_shape(self) -> "FieldMatchMappingPatch":
1475
1269
  has_source = self.source_field is not None and str(self.source_field).strip() != ""
@@ -1665,6 +1459,9 @@ class CustomButtonViewButtonBindingPatch(StrictModel):
1665
1459
  raw_limits = payload.get("button_limit", payload.get("buttonLimit", payload.get("visible_when", payload.get("visibleWhen"))))
1666
1460
  if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1667
1461
  payload["button_limit"] = [raw_limits]
1462
+ for alias in ("buttonLimit", "visible_when", "visibleWhen"):
1463
+ if alias in payload and alias != "button_limit":
1464
+ payload.pop(alias, None)
1668
1465
  return payload
1669
1466
 
1670
1467
 
@@ -1816,7 +1613,6 @@ class AssociatedResourcesApplyRequest(StrictModel):
1816
1613
  if not isinstance(value, dict):
1817
1614
  return value
1818
1615
  payload = dict(value)
1819
- default_target_app_key = str(payload.get("app_key", payload.get("appKey", "")) or "").strip()
1820
1616
  if "upsertResources" in payload and "upsert_resources" not in payload:
1821
1617
  payload["upsert_resources"] = payload.pop("upsertResources")
1822
1618
  if "patchResources" in payload and "patch_resources" not in payload:
@@ -1829,16 +1625,6 @@ class AssociatedResourcesApplyRequest(StrictModel):
1829
1625
  payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
1830
1626
  if "viewConfigs" in payload and "view_configs" not in payload:
1831
1627
  payload["view_configs"] = payload.pop("viewConfigs")
1832
- if default_target_app_key and isinstance(payload.get("upsert_resources"), list):
1833
- normalized_resources = []
1834
- for item in payload["upsert_resources"]:
1835
- if isinstance(item, dict) and not any(
1836
- str(item.get(key) or "").strip()
1837
- for key in ("target_app_key", "targetAppKey", "app_key", "appKey")
1838
- ):
1839
- item = {**item, "target_app_key": default_target_app_key}
1840
- normalized_resources.append(item)
1841
- payload["upsert_resources"] = normalized_resources
1842
1628
  return payload
1843
1629
 
1844
1630
  @model_validator(mode="after")
@@ -1862,7 +1648,7 @@ class ViewButtonBindingPatch(StrictModel):
1862
1648
  being_main: bool = Field(default=False, validation_alias=AliasChoices("being_main", "beingMain"))
1863
1649
  button_limit: list[list[ViewFilterRulePatch]] = Field(
1864
1650
  default_factory=list,
1865
- validation_alias=AliasChoices("button_limit", "buttonLimit"),
1651
+ validation_alias=AliasChoices("button_limit", "buttonLimit", "visible_when", "visibleWhen"),
1866
1652
  )
1867
1653
  button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
1868
1654
  button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
@@ -1891,9 +1677,12 @@ class ViewButtonBindingPatch(StrictModel):
1891
1677
  payload["config_type"] = "DETAIL"
1892
1678
  elif normalized_config in {"inside", "list", "row", "row_action"}:
1893
1679
  payload["config_type"] = "INSIDE"
1894
- raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
1680
+ raw_limits = payload.get("button_limit", payload.get("buttonLimit", payload.get("visible_when", payload.get("visibleWhen"))))
1895
1681
  if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1896
1682
  payload["button_limit"] = [raw_limits]
1683
+ for alias in ("buttonLimit", "visible_when", "visibleWhen"):
1684
+ if alias in payload and alias != "button_limit":
1685
+ payload.pop(alias, None)
1897
1686
  return payload
1898
1687
 
1899
1688
  @model_validator(mode="after")
@@ -2260,22 +2049,6 @@ class ChartApplyRequest(StrictModel):
2260
2049
  return self
2261
2050
 
2262
2051
 
2263
- class PortalInlineChartPatch(ChartUpsertPatch):
2264
- app_key: str = Field(validation_alias=AliasChoices("app_key", "appKey"))
2265
-
2266
- @model_validator(mode="before")
2267
- @classmethod
2268
- def normalize_inline_aliases(cls, value: Any) -> Any:
2269
- if not isinstance(value, dict):
2270
- return value
2271
- payload = dict(value)
2272
- if "chart_name" in payload and "name" not in payload:
2273
- payload["name"] = payload.pop("chart_name")
2274
- if "chartName" in payload and "name" not in payload:
2275
- payload["name"] = payload.pop("chartName")
2276
- return payload
2277
-
2278
-
2279
2052
  class PortalComponentPositionPatch(StrictModel):
2280
2053
  pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
2281
2054
  pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
@@ -2322,14 +2095,33 @@ class PortalComponentPositionPatch(StrictModel):
2322
2095
  class PortalChartRefPatch(StrictModel):
2323
2096
  app_key: str | None = None
2324
2097
  chart_id: str | None = None
2098
+ chart_key: str | None = None
2325
2099
  chart_name: str | None = None
2326
2100
 
2101
+ @model_validator(mode="before")
2102
+ @classmethod
2103
+ def normalize_aliases(cls, value: Any) -> Any:
2104
+ if not isinstance(value, dict):
2105
+ return value
2106
+ payload = dict(value)
2107
+ if "chartKey" in payload and "chart_key" not in payload:
2108
+ payload["chart_key"] = payload.pop("chartKey")
2109
+ if "chartId" in payload and "chart_id" not in payload:
2110
+ payload["chart_id"] = payload.pop("chartId")
2111
+ if "biChartId" in payload and "chart_id" not in payload:
2112
+ payload["chart_id"] = payload.pop("biChartId")
2113
+ if "chartName" in payload and "chart_name" not in payload:
2114
+ payload["chart_name"] = payload.pop("chartName")
2115
+ if "appKey" in payload and "app_key" not in payload:
2116
+ payload["app_key"] = payload.pop("appKey")
2117
+ return payload
2118
+
2327
2119
  @model_validator(mode="after")
2328
2120
  def validate_target(self) -> "PortalChartRefPatch":
2329
- if not (self.chart_id or self.chart_name):
2330
- raise ValueError("chart_ref requires chart_id or chart_name")
2331
- if self.chart_name and not self.chart_id and not self.app_key:
2332
- raise ValueError("chart_ref.app_key is required when resolving by chart_name")
2121
+ if not (self.chart_id or self.chart_key or self.chart_name):
2122
+ raise ValueError("chart_ref requires chart_id, chart_key, or chart_name")
2123
+ if self.chart_name and not self.app_key and not (self.chart_id or self.chart_key):
2124
+ raise ValueError("chart_ref with chart_name requires app_key unless chart_id/chart_key is also provided")
2333
2125
  return self
2334
2126
 
2335
2127
 
@@ -2355,12 +2147,12 @@ class PortalViewRefPatch(StrictModel):
2355
2147
  class PortalSectionPatch(StrictModel):
2356
2148
  title: str
2357
2149
  source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
2358
- role: str | None = Field(default=None, validation_alias=AliasChoices("role", "zone", "section_role", "sectionRole"))
2150
+ role: str | None = None
2359
2151
  position: PortalComponentPositionPatch | None = None
2360
2152
  dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
2361
2153
  config: dict[str, Any] = Field(default_factory=dict)
2154
+ chart: dict[str, Any] | None = None
2362
2155
  chart_ref: PortalChartRefPatch | None = None
2363
- chart: PortalInlineChartPatch | None = Field(default=None, validation_alias=AliasChoices("chart", "inline_chart", "inlineChart", "create_chart", "createChart"))
2364
2156
  view_ref: PortalViewRefPatch | None = None
2365
2157
  text: str | None = None
2366
2158
  url: str | None = None
@@ -2374,19 +2166,8 @@ class PortalSectionPatch(StrictModel):
2374
2166
  raw_type = payload.get("source_type", payload.get("sourceType"))
2375
2167
  if isinstance(raw_type, str):
2376
2168
  payload["source_type"] = raw_type.strip().lower()
2377
- raw_role = payload.get("role", payload.get("zone", payload.get("section_role", payload.get("sectionRole"))))
2378
- if isinstance(raw_role, str):
2379
- payload["role"] = raw_role.strip().lower()
2380
2169
  if "chartRef" in payload and "chart_ref" not in payload:
2381
2170
  payload["chart_ref"] = payload.pop("chartRef")
2382
- if "inlineChart" in payload and "chart" not in payload:
2383
- payload["chart"] = payload.pop("inlineChart")
2384
- if "inline_chart" in payload and "chart" not in payload:
2385
- payload["chart"] = payload.pop("inline_chart")
2386
- if "createChart" in payload and "chart" not in payload:
2387
- payload["chart"] = payload.pop("createChart")
2388
- if "create_chart" in payload and "chart" not in payload:
2389
- payload["chart"] = payload.pop("create_chart")
2390
2171
  if "viewRef" in payload and "view_ref" not in payload:
2391
2172
  payload["view_ref"] = payload.pop("viewRef")
2392
2173
  if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
@@ -2398,8 +2179,18 @@ class PortalSectionPatch(StrictModel):
2398
2179
  supported = {"chart", "view", "grid", "filter", "text", "link"}
2399
2180
  if self.source_type not in supported:
2400
2181
  raise ValueError(f"unsupported portal source_type '{self.source_type}'")
2401
- if self.source_type == "chart" and self.chart_ref is None and self.chart is None:
2402
- raise ValueError("chart section requires chart_ref or chart")
2182
+ if self.role is not None:
2183
+ normalized_role = str(self.role or "").strip().lower()
2184
+ if normalized_role not in {"metric"}:
2185
+ raise ValueError(f"unsupported portal section role '{self.role}'")
2186
+ self.role = normalized_role
2187
+ if self.source_type != "chart":
2188
+ raise ValueError("portal section role is only supported for chart sections")
2189
+ if self.source_type == "chart":
2190
+ if self.chart is not None and self.chart_ref is not None:
2191
+ raise ValueError("chart section accepts either chart or chart_ref, not both")
2192
+ if self.chart is None and self.chart_ref is None:
2193
+ raise ValueError("chart section requires chart or chart_ref")
2403
2194
  if self.source_type == "view" and self.view_ref is None:
2404
2195
  raise ValueError("view section requires view_ref")
2405
2196
  if self.source_type == "text" and self.text is None:
@@ -2536,7 +2327,6 @@ AppReadSummaryResponse = AppGetResponse
2536
2327
  AppFieldsReadResponse = AppGetFieldsResponse
2537
2328
  AppLayoutReadResponse = AppGetLayoutResponse
2538
2329
  AppViewsReadResponse = AppGetViewsResponse
2539
- AppFlowReadResponse = AppGetFlowResponse
2540
2330
  AppChartsReadResponse = AppGetChartsResponse
2541
2331
 
2542
2332
 
@@ -2603,6 +2393,7 @@ class SchemaPlanRequest(StrictModel):
2603
2393
  icon: str | None = None
2604
2394
  color: str | None = None
2605
2395
  visibility: VisibilityPatch | None = None
2396
+ create_if_missing: bool = False
2606
2397
  add_fields: list[FieldPatch] = Field(default_factory=list)
2607
2398
  update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
2608
2399
  remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
@@ -2625,40 +2416,6 @@ class LayoutPlanRequest(StrictModel):
2625
2416
  return payload
2626
2417
 
2627
2418
 
2628
- class FlowPlanRequest(StrictModel):
2629
- app_key: str
2630
- mode: str = "replace"
2631
- nodes: list[FlowNodePatch] = Field(default_factory=list)
2632
- transitions: list[FlowTransitionPatch] = Field(default_factory=list)
2633
- preset: FlowPreset | None = None
2634
-
2635
- @model_validator(mode="before")
2636
- @classmethod
2637
- def normalize_mode_alias(cls, value: Any) -> Any:
2638
- if not isinstance(value, dict):
2639
- return value
2640
- payload = dict(value)
2641
- if str(payload.get("mode") or "").strip().lower() == "overwrite":
2642
- payload["mode"] = "replace"
2643
- raw_preset = payload.get("preset")
2644
- if raw_preset is None and isinstance(payload.get("base_preset"), str):
2645
- raw_preset = payload["base_preset"]
2646
- payload["preset"] = raw_preset
2647
- if isinstance(raw_preset, str):
2648
- normalized_preset = raw_preset.strip().lower()
2649
- preset_aliases = {
2650
- "default_approval": FlowPreset.basic_approval.value,
2651
- "approval": FlowPreset.basic_approval.value,
2652
- "basic approval": FlowPreset.basic_approval.value,
2653
- "default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
2654
- "default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
2655
- "fill_then_approve": FlowPreset.basic_fill_then_approve.value,
2656
- }
2657
- if normalized_preset in preset_aliases:
2658
- payload["preset"] = preset_aliases[normalized_preset]
2659
- return payload
2660
-
2661
-
2662
2419
  class ViewsPlanRequest(StrictModel):
2663
2420
  app_key: str
2664
2421
  upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
@@ -2686,18 +2443,8 @@ def _normalize_field_payload(value: Any) -> Any:
2686
2443
  if not isinstance(value, dict):
2687
2444
  return value
2688
2445
  payload = dict(value)
2689
- if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2690
- payload["as_data_title"] = payload.pop("data_title")
2691
- if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2692
- payload["as_data_title"] = payload.pop("dataTitle")
2693
- if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2694
- payload["as_data_cover"] = payload.pop("data_cover")
2695
- if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2696
- payload["as_data_cover"] = payload.pop("dataCover")
2697
2446
  if "fields" in payload and "subfields" not in payload:
2698
2447
  payload["subfields"] = payload.pop("fields")
2699
- if "options" in payload:
2700
- payload["options"] = _normalize_field_options(payload.get("options"))
2701
2448
  raw_type = payload.get("type")
2702
2449
  if isinstance(raw_type, int):
2703
2450
  normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
@@ -2730,34 +2477,6 @@ def _normalize_field_payload(value: Any) -> Any:
2730
2477
  return payload
2731
2478
 
2732
2479
 
2733
- def _normalize_field_options(value: Any) -> Any:
2734
- if not isinstance(value, list):
2735
- return value
2736
- normalized: list[str] = []
2737
- for item in value:
2738
- if isinstance(item, str):
2739
- normalized.append(item)
2740
- continue
2741
- if isinstance(item, (int, float, bool)):
2742
- normalized.append(str(item))
2743
- continue
2744
- if isinstance(item, dict):
2745
- label = (
2746
- item.get("label")
2747
- or item.get("name")
2748
- or item.get("title")
2749
- or item.get("value")
2750
- or item.get("text")
2751
- or item.get("optValue")
2752
- or item.get("optName")
2753
- )
2754
- if label is not None:
2755
- normalized.append(str(label))
2756
- continue
2757
- normalized.append(str(item))
2758
- return normalized
2759
-
2760
-
2761
2480
  def _slugify_title(title: str) -> str:
2762
2481
  normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
2763
2482
  collapsed = "_".join(part for part in normalized.split("_") if part)