@josephyan/qingflow-app-builder-mcp 1.1.17 → 1.1.19

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.
@@ -2260,6 +2260,22 @@ class ChartApplyRequest(StrictModel):
2260
2260
  return self
2261
2261
 
2262
2262
 
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
+
2263
2279
  class PortalComponentPositionPatch(StrictModel):
2264
2280
  pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
2265
2281
  pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
@@ -2344,6 +2360,7 @@ class PortalSectionPatch(StrictModel):
2344
2360
  dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
2345
2361
  config: dict[str, Any] = Field(default_factory=dict)
2346
2362
  chart_ref: PortalChartRefPatch | None = None
2363
+ chart: PortalInlineChartPatch | None = Field(default=None, validation_alias=AliasChoices("chart", "inline_chart", "inlineChart", "create_chart", "createChart"))
2347
2364
  view_ref: PortalViewRefPatch | None = None
2348
2365
  text: str | None = None
2349
2366
  url: str | None = None
@@ -2362,6 +2379,14 @@ class PortalSectionPatch(StrictModel):
2362
2379
  payload["role"] = raw_role.strip().lower()
2363
2380
  if "chartRef" in payload and "chart_ref" not in payload:
2364
2381
  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")
2365
2390
  if "viewRef" in payload and "view_ref" not in payload:
2366
2391
  payload["view_ref"] = payload.pop("viewRef")
2367
2392
  if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
@@ -2373,8 +2398,8 @@ class PortalSectionPatch(StrictModel):
2373
2398
  supported = {"chart", "view", "grid", "filter", "text", "link"}
2374
2399
  if self.source_type not in supported:
2375
2400
  raise ValueError(f"unsupported portal source_type '{self.source_type}'")
2376
- if self.source_type == "chart" and self.chart_ref is None:
2377
- raise ValueError("chart section requires chart_ref")
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")
2378
2403
  if self.source_type == "view" and self.view_ref is None:
2379
2404
  raise ValueError("view section requires view_ref")
2380
2405
  if self.source_type == "text" and self.text is None:
@@ -2662,6 +2687,14 @@ def _normalize_field_payload(value: Any) -> Any:
2662
2687
  if not isinstance(value, dict):
2663
2688
  return value
2664
2689
  payload = dict(value)
2690
+ if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2691
+ payload["as_data_title"] = payload.pop("data_title")
2692
+ if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2693
+ payload["as_data_title"] = payload.pop("dataTitle")
2694
+ if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2695
+ payload["as_data_cover"] = payload.pop("data_cover")
2696
+ if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2697
+ payload["as_data_cover"] = payload.pop("dataCover")
2665
2698
  if "fields" in payload and "subfields" not in payload:
2666
2699
  payload["subfields"] = payload.pop("fields")
2667
2700
  if "options" in payload:
@@ -3155,7 +3155,7 @@ class AiBuilderFacade:
3155
3155
  return expanded, issues, results
3156
3156
 
3157
3157
  def app_custom_buttons_apply(self, *, profile: str, request: CustomButtonsApplyRequest) -> JSONObject:
3158
- normalized_args = request.model_dump(mode="json")
3158
+ normalized_args = request.model_dump(mode="json", exclude_none=True, exclude_defaults=True)
3159
3159
  app_key = request.app_key
3160
3160
  permission_outcomes: list[PermissionCheckOutcome] = []
3161
3161
  button_write_intent = bool(request.upsert_buttons or request.patch_buttons or request.remove_buttons)
@@ -3239,7 +3239,10 @@ class AiBuilderFacade:
3239
3239
  )
3240
3240
  )
3241
3241
  upsert_buttons.extend(expanded_buttons)
3242
- normalized_args["upsert_buttons"] = [patch.model_dump(mode="json") for patch in upsert_buttons]
3242
+ normalized_args["upsert_buttons"] = [
3243
+ patch.model_dump(mode="json", exclude_none=True, exclude_defaults=True)
3244
+ for patch in upsert_buttons
3245
+ ]
3243
3246
  normalized_args["patch_results"] = patch_results
3244
3247
 
3245
3248
  compiled_add_data_configs, add_data_issues = self._compile_custom_button_semantic_add_data_configs(
@@ -10261,8 +10264,14 @@ class AiBuilderFacade:
10261
10264
  return CustomButtonsApplyRequest.model_validate(
10262
10265
  {
10263
10266
  "app_key": app_key,
10264
- "upsert_buttons": [item.model_dump(mode="json") for item in upsert_buttons],
10265
- "view_configs": [item.model_dump(mode="json") for item in view_configs],
10267
+ "upsert_buttons": [
10268
+ item.model_dump(mode="json", exclude_none=True, exclude_defaults=True)
10269
+ for item in upsert_buttons
10270
+ ],
10271
+ "view_configs": [
10272
+ item.model_dump(mode="json", exclude_none=True, exclude_defaults=True)
10273
+ for item in view_configs
10274
+ ],
10266
10275
  }
10267
10276
  ), []
10268
10277
  except Exception as error:
@@ -13018,6 +13027,7 @@ class AiBuilderFacade:
13018
13027
  draft_readback_error: JSONObject | None = None
13019
13028
  live_readback_error: JSONObject | None = None
13020
13029
  update_payload: dict[str, Any] = {}
13030
+ inline_chart_results: list[dict[str, Any]] = []
13021
13031
  try:
13022
13032
  layout_diagnostics: dict[str, Any] = _empty_portal_layout_diagnostics()
13023
13033
  if creating:
@@ -13065,10 +13075,11 @@ class AiBuilderFacade:
13065
13075
  base_payload=base_payload,
13066
13076
  )
13067
13077
  if sections_requested:
13068
- component_payload, layout_metadata = self._build_portal_components_from_sections(
13078
+ component_payload, layout_metadata, inline_chart_results = self._build_portal_components_from_sections(
13069
13079
  profile=profile,
13070
13080
  sections=request.sections,
13071
13081
  layout_preset=request.layout_preset,
13082
+ inline_chart_results=inline_chart_results,
13072
13083
  )
13073
13084
  layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload, layout_metadata=layout_metadata)
13074
13085
  update_payload["components"] = component_payload
@@ -13096,11 +13107,12 @@ class AiBuilderFacade:
13096
13107
  draft_result = {}
13097
13108
  except (QingflowApiError, RuntimeError, ValueError) as error:
13098
13109
  api_error = _coerce_api_error(error) if not isinstance(error, ValueError) else None
13099
- if write_executed:
13110
+ inline_chart_write_executed = any(bool(item.get("write_executed")) for item in inline_chart_results)
13111
+ if write_executed or inline_chart_write_executed:
13100
13112
  transport_error = _transport_error_payload(api_error) if api_error is not None else None
13101
13113
  warning = _warning(
13102
13114
  "PORTAL_WRITE_INCOMPLETE_AFTER_PARTIAL_WRITE",
13103
- "one or more portal write steps executed before a later write step failed",
13115
+ "one or more portal or inline-chart write steps executed before a later write step failed",
13104
13116
  )
13105
13117
  if transport_error is not None:
13106
13118
  for key in ("backend_code", "http_status", "request_id"):
@@ -13121,6 +13133,7 @@ class AiBuilderFacade:
13121
13133
  if api_error is not None
13122
13134
  else {"message": str(error)}
13123
13135
  ),
13136
+ **({"inline_chart_results": deepcopy(inline_chart_results)} if inline_chart_results else {}),
13124
13137
  },
13125
13138
  "request_id": api_error.request_id if api_error else None,
13126
13139
  "backend_code": api_error.backend_code if api_error else None,
@@ -13136,6 +13149,7 @@ class AiBuilderFacade:
13136
13149
  "published": False,
13137
13150
  "publish_failed": False,
13138
13151
  "write_incomplete": True,
13152
+ "inline_charts_verified": False if inline_chart_results else None,
13139
13153
  "readback_unavailable": False,
13140
13154
  "metadata_unverified": True,
13141
13155
  },
@@ -13147,6 +13161,7 @@ class AiBuilderFacade:
13147
13161
  "verified": False,
13148
13162
  "write_executed": True,
13149
13163
  "safe_to_retry": False,
13164
+ "inline_chart_results": deepcopy(inline_chart_results),
13150
13165
  })
13151
13166
  return _failed(
13152
13167
  "PORTAL_APPLY_FAILED",
@@ -13258,6 +13273,7 @@ class AiBuilderFacade:
13258
13273
  "draft": draft_meta_mismatches,
13259
13274
  "live": live_meta_mismatches,
13260
13275
  },
13276
+ **({"inline_chart_results": deepcopy(inline_chart_results)} if inline_chart_results else {}),
13261
13277
  **({"publish_error": publish_error} if publish_error is not None else {}),
13262
13278
  **({"draft_readback_error": draft_readback_error} if draft_readback_error is not None else {}),
13263
13279
  **({"live_readback_error": live_readback_error} if live_readback_error is not None else {}),
@@ -13295,6 +13311,7 @@ class AiBuilderFacade:
13295
13311
  "noop": False,
13296
13312
  "warnings": warnings,
13297
13313
  "layout_diagnostics": layout_diagnostics,
13314
+ "inline_chart_results": deepcopy(inline_chart_results),
13298
13315
  "verification": {
13299
13316
  "draft_verified": draft_verified,
13300
13317
  "draft_metadata_verified": draft_meta_verified,
@@ -13302,6 +13319,7 @@ class AiBuilderFacade:
13302
13319
  "live_metadata_verified": live_meta_verified,
13303
13320
  "published": published,
13304
13321
  "publish_failed": publish_failed,
13322
+ "inline_charts_verified": all(str(item.get("status") or "") in {"created", "updated"} and str(item.get("chart_id") or "").strip() for item in inline_chart_results),
13305
13323
  "readback_unavailable": draft_readback_error is not None or live_readback_error is not None,
13306
13324
  "metadata_unverified": draft_readback_error is not None or live_readback_error is not None,
13307
13325
  },
@@ -14055,9 +14073,11 @@ class AiBuilderFacade:
14055
14073
  profile: str,
14056
14074
  sections: list[PortalSectionPatch],
14057
14075
  layout_preset: str | None = None,
14058
- ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
14076
+ inline_chart_results: list[dict[str, Any]] | None = None,
14077
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
14059
14078
  resolved_components: list[dict[str, Any]] = []
14060
14079
  layout_metadata: list[dict[str, Any]] = []
14080
+ inline_chart_results = inline_chart_results if inline_chart_results is not None else []
14061
14081
  pc_x = 0
14062
14082
  pc_y = 0
14063
14083
  pc_row_height = 0
@@ -14084,11 +14104,53 @@ class AiBuilderFacade:
14084
14104
  dash_style = deepcopy(section.dash_style_config) if isinstance(section.dash_style_config, dict) else None
14085
14105
  component: dict[str, Any]
14086
14106
  if section.source_type == "chart":
14087
- resolved_chart = _resolve_chart_reference(
14088
- charts=self.charts,
14089
- profile=profile,
14090
- ref=section.chart_ref,
14091
- )
14107
+ if section.chart is not None:
14108
+ inline_chart_payload = section.chart.model_dump(mode="json", exclude={"app_key"})
14109
+ chart_request = ChartApplyRequest(
14110
+ app_key=section.chart.app_key,
14111
+ upsert_charts=[ChartUpsertPatch.model_validate(inline_chart_payload)],
14112
+ patch_charts=[],
14113
+ remove_chart_ids=[],
14114
+ reorder_chart_ids=[],
14115
+ )
14116
+ chart_apply = self.chart_apply(profile=profile, request=chart_request)
14117
+ chart_items = chart_apply.get("chart_results") if isinstance(chart_apply.get("chart_results"), list) else []
14118
+ successful_chart = next(
14119
+ (
14120
+ item
14121
+ for item in chart_items
14122
+ if isinstance(item, dict)
14123
+ and str(item.get("status") or "") in {"created", "updated"}
14124
+ and str(item.get("chart_id") or "").strip()
14125
+ ),
14126
+ None,
14127
+ )
14128
+ inline_chart_results.append({
14129
+ "title": section.title,
14130
+ "app_key": section.chart.app_key,
14131
+ "chart_name": section.chart.name,
14132
+ "chart_type": section.chart.chart_type.value,
14133
+ "status": str(successful_chart.get("status") or "") if isinstance(successful_chart, dict) else "failed",
14134
+ "chart_id": str(successful_chart.get("chart_id") or "") if isinstance(successful_chart, dict) else "",
14135
+ "chart_apply_status": chart_apply.get("status"),
14136
+ "write_executed": bool(chart_apply.get("write_executed")),
14137
+ **({"error_code": chart_apply.get("error_code")} if chart_apply.get("error_code") else {}),
14138
+ **({"message": chart_apply.get("message")} if chart_apply.get("message") else {}),
14139
+ })
14140
+ if not isinstance(successful_chart, dict):
14141
+ raise ValueError(f"inline chart '{section.chart.name}' could not be created or updated for portal section '{section.title}'")
14142
+ resolved_chart = {
14143
+ "chart_id": str(successful_chart.get("chart_id") or "").strip(),
14144
+ "chart_name": str(successful_chart.get("name") or section.chart.name).strip(),
14145
+ "app_key": section.chart.app_key,
14146
+ "chart_type": section.chart.chart_type.value,
14147
+ }
14148
+ else:
14149
+ resolved_chart = _resolve_chart_reference(
14150
+ charts=self.charts,
14151
+ profile=profile,
14152
+ ref=section.chart_ref,
14153
+ )
14092
14154
  chart_type = str(
14093
14155
  resolved_chart.get("chart_type")
14094
14156
  or _public_chart_type_from_backend(section.config.get("chartType") or section.config.get("chart_type"))
@@ -14148,7 +14210,7 @@ class AiBuilderFacade:
14148
14210
  if dash_style is not None:
14149
14211
  component["dashStyleConfigBO"] = dash_style
14150
14212
  resolved_components.append(component)
14151
- return resolved_components, layout_metadata
14213
+ return resolved_components, layout_metadata, inline_chart_results
14152
14214
 
14153
14215
  def _resolve_current_user_identity(self, *, profile: str) -> JSONObject:
14154
14216
  session_profile = self.apps.sessions.get_profile(profile)
@@ -17794,12 +17856,12 @@ def _portal_layout_diagnostics(
17794
17856
  if source_type == "chart" and role in {"metric", "metrics", "indicator", "kpi"} and not is_metric_chart:
17795
17857
  warnings.append(_warning(
17796
17858
  "PORTAL_METRIC_SECTION_CHART_TYPE_MISMATCH",
17797
- "metric portal section must reference a target/indicator chart; create the missing metric chart before assembling the portal",
17859
+ "metric portal section must reference a target/indicator chart; use inline_chart to create the missing metric chart while assembling the portal",
17798
17860
  section_index=index,
17799
17861
  title=title,
17800
17862
  role=role,
17801
17863
  chart_type=chart_type or None,
17802
- fix_hint="Use app_charts_apply with chart_type=target and metric='count(*)' or another metric expression, then reference that chart.",
17864
+ fix_hint="Use section.inline_chart with chart_type=target or indicator and a metric expression, or change role away from metric.",
17803
17865
  ))
17804
17866
  if section is not None and section.position is not None and not bool(getattr(section.position, "mobile_provided", False)):
17805
17867
  warnings.append(_warning(
@@ -17859,7 +17921,7 @@ def _append_portal_standard_count_warnings(
17859
17921
  "standard portal metric area should contain 4-6 metric cards",
17860
17922
  actual_count=metric_count,
17861
17923
  expected_count="4-6",
17862
- fix_hint="Create missing target/indicator charts first, or keep 4 metric cards in one row with pc.cols=6, pc.rows=5.",
17924
+ fix_hint="Use inline_chart for missing target/indicator metric cards, or keep 4 metric cards in one row with pc.cols=6, pc.rows=5.",
17863
17925
  ))
17864
17926
  if (bi_count or require_complete_standard) and not 2 <= bi_count <= 3:
17865
17927
  warnings.append(_warning(
@@ -214,7 +214,8 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
214
214
  schema_apply_apply.add_argument("--visibility-file")
215
215
  schema_apply_apply.add_argument("--create-if-missing", action="store_true")
216
216
  schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=None)
217
- schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 数组;每项可带 client_key/app_name/add_fields,支持 relation target_app_ref")
217
+ schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 对象;每项可带 client_key/app_name/form,支持 relation target_app_ref")
218
+ schema_apply_apply.add_argument("--form-file", help="单应用表单结构 JSON 数组;form[].rows[][] 同时声明字段和表单分组布局")
218
219
  schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
219
220
  schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
220
221
  schema_apply_apply.add_argument("--remove-fields-file")
@@ -578,10 +579,10 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
578
579
  fix_hint="Pass a JSON array, or a JSON object like {\"package_id\":1001,\"apps\":[...]} with at least one app item.",
579
580
  error_code="APPS_FILE_EMPTY",
580
581
  )
581
- if args.app_key or args.app_name or args.app_title or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
582
+ if args.app_key or args.app_name or args.app_title or args.form_file or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
582
583
  raise_config_error(
583
584
  "schema apply multi-app mode accepts --package-id plus --apps-file only; --create-if-missing is optional.",
584
- fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
585
+ fix_hint="Use apps[].form inside --apps-file for batch mode, or remove --apps-file and use --form-file for a single app.",
585
586
  )
586
587
  if package_id is None:
587
588
  raise_config_error(
@@ -595,6 +596,7 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
595
596
  create_if_missing=effective_create_if_missing,
596
597
  publish=effective_publish,
597
598
  apps=apps,
599
+ form=None,
598
600
  add_fields=[],
599
601
  update_fields=[],
600
602
  remove_fields=[],
@@ -631,6 +633,7 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
631
633
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
632
634
  create_if_missing=bool(args.create_if_missing),
633
635
  publish=True if args.publish is None else bool(args.publish),
636
+ form=load_list_arg(args.form_file, option_name="--form-file") if args.form_file else [],
634
637
  add_fields=load_list_arg(args.add_fields_file, option_name="--add-fields-file"),
635
638
  update_fields=load_list_arg(args.update_fields_file, option_name="--update-fields-file"),
636
639
  remove_fields=load_list_arg(args.remove_fields_file, option_name="--remove-fields-file"),
@@ -428,16 +428,17 @@ def build_builder_server() -> FastMCP:
428
428
  visibility: dict | None = None,
429
429
  create_if_missing: bool | None = None,
430
430
  publish: bool = True,
431
+ form: list[dict] | None = None,
431
432
  add_fields: list[dict] | None = None,
432
433
  update_fields: list[dict] | None = None,
433
434
  remove_fields: list[dict] | None = None,
434
435
  apps: list[dict] | None = None,
435
436
  ) -> dict:
436
437
  if apps:
437
- if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
438
+ if app_key or app_name or app_title or form or add_fields or update_fields or remove_fields:
438
439
  return _config_failure(
439
440
  "app_schema_apply multi-app mode accepts package_id plus apps only; create_if_missing is optional.",
440
- fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
441
+ fix_hint="Use apps[].form for batch mode, or use the single-app arguments without apps.",
441
442
  )
442
443
  if package_id is None:
443
444
  return _config_failure(
@@ -450,6 +451,7 @@ def build_builder_server() -> FastMCP:
450
451
  visibility=visibility,
451
452
  create_if_missing=True if create_if_missing is None else bool(create_if_missing),
452
453
  publish=publish,
454
+ form=None,
453
455
  add_fields=[],
454
456
  update_fields=[],
455
457
  remove_fields=[],
@@ -482,10 +484,11 @@ def build_builder_server() -> FastMCP:
482
484
  visibility=visibility,
483
485
  create_if_missing=effective_create_if_missing,
484
486
  publish=publish,
487
+ form=form or [],
485
488
  add_fields=add_fields or [],
486
489
  update_fields=update_fields or [],
487
490
  remove_fields=remove_fields or [],
488
- apps=[],
491
+ apps=None,
489
492
  )
490
493
 
491
494
  @server.tool()