@josephyan/qingflow-app-builder-mcp 1.1.16 → 1.1.18

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.
@@ -49,6 +49,8 @@ from ..builder_facade.models import (
49
49
  ViewUpsertPatch,
50
50
  ViewsPreset,
51
51
  ViewsPlanRequest,
52
+ public_view_partial_payload,
53
+ public_view_upsert_payload,
52
54
  )
53
55
  from ..builder_facade.service import AiBuilderFacade, INTEGRATION_OUTPUT_TARGET_FIELD_TYPES
54
56
  from ..solution.compiler.icon_utils import (
@@ -575,17 +577,18 @@ class AiBuilderTools(ToolBase):
575
577
  visibility: JSONObject | None = None,
576
578
  create_if_missing: bool | None = None,
577
579
  publish: bool = True,
580
+ form: list[JSONObject] | None = None,
578
581
  add_fields: list[JSONObject] | None = None,
579
582
  update_fields: list[JSONObject] | None = None,
580
583
  remove_fields: list[JSONObject] | None = None,
581
584
  apps: list[JSONObject] | None = None,
582
585
  ) -> JSONObject:
583
586
  if apps is not None:
584
- if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
587
+ if app_key or app_name or app_title or form or add_fields or update_fields or remove_fields:
585
588
  return _config_failure(
586
589
  tool_name="app_schema_apply",
587
590
  message="app_schema_apply multi-app mode accepts package_id plus apps only; create_if_missing is optional.",
588
- fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
591
+ fix_hint="Put per-app form in apps[].form when using batch mode.",
589
592
  )
590
593
  if package_id is None:
591
594
  return _config_failure(
@@ -600,6 +603,7 @@ class AiBuilderTools(ToolBase):
600
603
  create_if_missing=True if create_if_missing is None else bool(create_if_missing),
601
604
  publish=publish,
602
605
  apps=apps,
606
+ form=None,
603
607
  add_fields=[],
604
608
  update_fields=[],
605
609
  remove_fields=[],
@@ -636,10 +640,11 @@ class AiBuilderTools(ToolBase):
636
640
  visibility=visibility,
637
641
  create_if_missing=effective_create_if_missing,
638
642
  publish=publish,
643
+ form=form or [],
639
644
  add_fields=add_fields or [],
640
645
  update_fields=update_fields or [],
641
646
  remove_fields=remove_fields or [],
642
- apps=[],
647
+ apps=None,
643
648
  )
644
649
 
645
650
  @mcp.tool()
@@ -2018,6 +2023,7 @@ class AiBuilderTools(ToolBase):
2018
2023
  profile: str,
2019
2024
  app_key: str,
2020
2025
  upsert_views: list[JSONObject] | None = None,
2026
+ patch_views: list[JSONObject] | None = None,
2021
2027
  remove_views: list[str] | None = None,
2022
2028
  preset: str | None = None,
2023
2029
  ) -> JSONObject:
@@ -2036,6 +2042,7 @@ class AiBuilderTools(ToolBase):
2036
2042
  {
2037
2043
  "app_key": app_key,
2038
2044
  "upsert_views": upsert_views or [],
2045
+ "patch_views": patch_views or [],
2039
2046
  "remove_views": remove_views or [],
2040
2047
  "preset": preset,
2041
2048
  }
@@ -2052,6 +2059,7 @@ class AiBuilderTools(ToolBase):
2052
2059
  "app_key": app_key,
2053
2060
  "preset": "default_table",
2054
2061
  "upsert_views": [],
2062
+ "patch_views": [],
2055
2063
  "remove_views": [],
2056
2064
  },
2057
2065
  },
@@ -2059,8 +2067,24 @@ class AiBuilderTools(ToolBase):
2059
2067
  return _safe_tool_call(
2060
2068
  lambda: self._facade.app_views_plan(profile=profile, request=request),
2061
2069
  error_code="VIEWS_PLAN_FAILED",
2062
- normalized_args=request.model_dump(mode="json"),
2063
- suggested_next_call={"tool_name": "app_views_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
2070
+ normalized_args={
2071
+ "app_key": request.app_key,
2072
+ "upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
2073
+ "patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
2074
+ "remove_views": list(request.remove_views),
2075
+ "preset": request.preset.value if request.preset is not None else None,
2076
+ },
2077
+ suggested_next_call={
2078
+ "tool_name": "app_views_plan",
2079
+ "arguments": {
2080
+ "profile": profile,
2081
+ "app_key": request.app_key,
2082
+ "upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
2083
+ "patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
2084
+ "remove_views": list(request.remove_views),
2085
+ "preset": request.preset.value if request.preset is not None else None,
2086
+ },
2087
+ },
2064
2088
  )
2065
2089
 
2066
2090
  @tool_cn_name("应用结构应用")
@@ -2080,6 +2104,7 @@ class AiBuilderTools(ToolBase):
2080
2104
  visibility: JSONObject | None = None,
2081
2105
  create_if_missing: bool | None = None,
2082
2106
  publish: bool = True,
2107
+ form: list[JSONObject] | None = None,
2083
2108
  add_fields: list[JSONObject],
2084
2109
  update_fields: list[JSONObject],
2085
2110
  remove_fields: list[JSONObject],
@@ -2105,6 +2130,21 @@ class AiBuilderTools(ToolBase):
2105
2130
  package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
2106
2131
  apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
2107
2132
  input_warnings = list(normalized_apps_payload.get("warnings") or [])
2133
+ normalized_apps: list[JSONObject] = []
2134
+ for index, app_item in enumerate(apps or []):
2135
+ if not isinstance(app_item, dict):
2136
+ normalized_apps.append(app_item)
2137
+ continue
2138
+ form_result = _apply_schema_form_to_payload(
2139
+ tool_name="app_schema_apply",
2140
+ payload=app_item,
2141
+ path=f"apps[{index}]",
2142
+ )
2143
+ failure = form_result.get("failure")
2144
+ if isinstance(failure, dict):
2145
+ return _attach_builder_apply_envelope("app_schema_apply", failure)
2146
+ normalized_apps.append(form_result.get("payload") or app_item)
2147
+ apps = normalized_apps
2108
2148
  result = self._app_schema_apply_multi(
2109
2149
  profile=profile,
2110
2150
  package_id=package_id,
@@ -2118,6 +2158,22 @@ class AiBuilderTools(ToolBase):
2118
2158
  result_warnings.extend(deepcopy(input_warnings))
2119
2159
  result["warnings"] = result_warnings
2120
2160
  return _attach_builder_apply_envelope("app_schema_apply", result)
2161
+ inline_form_layout_sections: list[JSONObject] = []
2162
+ if form:
2163
+ form_payload: JSONObject = {"form": form}
2164
+ if add_fields:
2165
+ form_payload["add_fields"] = add_fields
2166
+ form_result = _apply_schema_form_to_payload(
2167
+ tool_name="app_schema_apply",
2168
+ payload=form_payload,
2169
+ path="schema",
2170
+ )
2171
+ failure = form_result.get("failure")
2172
+ if isinstance(failure, dict):
2173
+ return _attach_builder_apply_envelope("app_schema_apply", failure)
2174
+ normalized_payload = form_result.get("payload") if isinstance(form_result.get("payload"), dict) else {}
2175
+ add_fields = list(normalized_payload.get("add_fields") or [])
2176
+ inline_form_layout_sections = list(form_result.get("layout_sections") or [])
2121
2177
  result = self._app_schema_apply_once(
2122
2178
  profile=profile,
2123
2179
  app_key=app_key,
@@ -2132,6 +2188,7 @@ class AiBuilderTools(ToolBase):
2132
2188
  add_fields=add_fields,
2133
2189
  update_fields=update_fields,
2134
2190
  remove_fields=remove_fields,
2191
+ inline_form_layout_sections=inline_form_layout_sections,
2135
2192
  )
2136
2193
  result = self._retry_after_self_lock_release(
2137
2194
  profile=profile,
@@ -2150,6 +2207,7 @@ class AiBuilderTools(ToolBase):
2150
2207
  add_fields=add_fields,
2151
2208
  update_fields=update_fields,
2152
2209
  remove_fields=remove_fields,
2210
+ inline_form_layout_sections=inline_form_layout_sections,
2153
2211
  ),
2154
2212
  )
2155
2213
  return _attach_builder_apply_envelope("app_schema_apply", result)
@@ -2408,6 +2466,19 @@ class AiBuilderTools(ToolBase):
2408
2466
  remove_fields = list(compiled_item.get("remove_fields") or [])
2409
2467
  if bool(existing.get("created")) and not deferred_add_fields and not update_fields and not remove_fields:
2410
2468
  shell_result = existing.get("shell_result") if isinstance(existing.get("shell_result"), dict) else {}
2469
+ layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
2470
+ if layout_sections:
2471
+ shell_result = self._apply_inline_form_layout_after_schema(
2472
+ profile=profile,
2473
+ schema_result=shell_result,
2474
+ fallback_app_key=app_key,
2475
+ publish=publish,
2476
+ sections=layout_sections,
2477
+ )
2478
+ if _schema_apply_result_has_write(shell_result):
2479
+ any_write_executed = True
2480
+ if _schema_apply_result_may_have_write(shell_result):
2481
+ any_write_may_have_succeeded = True
2411
2482
  item_status = shell_result.get("status") if shell_result.get("status") in {"success", "partial_success"} else "failed"
2412
2483
  shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
2413
2484
  shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
@@ -2428,6 +2499,8 @@ class AiBuilderTools(ToolBase):
2428
2499
  "safe_to_retry": False,
2429
2500
  **({"next_action": shell_result.get("next_action")} if shell_result.get("next_action") else {}),
2430
2501
  **({"verification": shell_result.get("verification")} if isinstance(shell_result.get("verification"), dict) else {}),
2502
+ **({"inline_form_layout": shell_result.get("inline_form_layout")} if isinstance(shell_result.get("inline_form_layout"), dict) else {}),
2503
+ **({"inline_form_layout_result": shell_result.get("inline_form_layout_result")} if isinstance(shell_result.get("inline_form_layout_result"), dict) else {}),
2431
2504
  })
2432
2505
  continue
2433
2506
 
@@ -2447,6 +2520,15 @@ class AiBuilderTools(ToolBase):
2447
2520
  remove_fields=remove_fields,
2448
2521
  )
2449
2522
  public_result = _publicize_package_fields(field_result)
2523
+ layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
2524
+ if layout_sections:
2525
+ public_result = self._apply_inline_form_layout_after_schema(
2526
+ profile=profile,
2527
+ schema_result=public_result,
2528
+ fallback_app_key=app_key,
2529
+ publish=publish,
2530
+ sections=layout_sections,
2531
+ )
2450
2532
  if _schema_apply_result_has_write(public_result):
2451
2533
  any_write_executed = True
2452
2534
  if _schema_apply_result_may_have_write(public_result):
@@ -2478,6 +2560,8 @@ class AiBuilderTools(ToolBase):
2478
2560
  "safe_to_retry": False,
2479
2561
  **({"next_action": public_result.get("next_action")} if public_result.get("next_action") else {}),
2480
2562
  **({"verification": public_result.get("verification")} if isinstance(public_result.get("verification"), dict) else {}),
2563
+ **({"inline_form_layout": public_result.get("inline_form_layout")} if isinstance(public_result.get("inline_form_layout"), dict) else {}),
2564
+ **({"inline_form_layout_result": public_result.get("inline_form_layout_result")} if isinstance(public_result.get("inline_form_layout_result"), dict) else {}),
2481
2565
  })
2482
2566
 
2483
2567
  pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
@@ -2541,6 +2625,7 @@ class AiBuilderTools(ToolBase):
2541
2625
  add_fields: list[JSONObject],
2542
2626
  update_fields: list[JSONObject],
2543
2627
  remove_fields: list[JSONObject],
2628
+ inline_form_layout_sections: list[JSONObject] | None = None,
2544
2629
  ) -> JSONObject:
2545
2630
  """执行内部辅助逻辑。"""
2546
2631
  effective_app_name = app_name or app_title
@@ -2652,7 +2737,83 @@ class AiBuilderTools(ToolBase):
2652
2737
  normalized_args=normalized_args,
2653
2738
  suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
2654
2739
  )
2655
- return _publicize_package_fields(result)
2740
+ public_result = _publicize_package_fields(result)
2741
+ if inline_form_layout_sections:
2742
+ public_result = self._apply_inline_form_layout_after_schema(
2743
+ profile=profile,
2744
+ schema_result=public_result,
2745
+ fallback_app_key=str(plan_args.get("app_key") or app_key),
2746
+ publish=publish,
2747
+ sections=inline_form_layout_sections,
2748
+ )
2749
+ return public_result
2750
+
2751
+ def _apply_inline_form_layout_after_schema(
2752
+ self,
2753
+ *,
2754
+ profile: str,
2755
+ schema_result: JSONObject,
2756
+ fallback_app_key: str = "",
2757
+ publish: bool,
2758
+ sections: list[JSONObject],
2759
+ ) -> JSONObject:
2760
+ result = deepcopy(schema_result)
2761
+ if not sections:
2762
+ return result
2763
+ app_key = str(result.get("app_key") or result.get("appKey") or fallback_app_key or "").strip()
2764
+ result["inline_form_layout"] = {
2765
+ "requested": True,
2766
+ "section_count": len(sections),
2767
+ "applied": False,
2768
+ }
2769
+ if result.get("status") not in {"success", "partial_success"}:
2770
+ result["inline_form_layout"]["skipped_reason"] = "schema_apply_failed"
2771
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
2772
+ verification = deepcopy(verification)
2773
+ verification["inline_form_layout_verified"] = False
2774
+ result["verification"] = verification
2775
+ return result
2776
+ if not app_key:
2777
+ result["status"] = "partial_success"
2778
+ result["error_code"] = result.get("error_code") or "INLINE_FORM_LAYOUT_APP_KEY_MISSING"
2779
+ result["message"] = "schema applied but inline form layout could not run because app_key was not resolved"
2780
+ result["inline_form_layout"]["error_code"] = "INLINE_FORM_LAYOUT_APP_KEY_MISSING"
2781
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
2782
+ verification = deepcopy(verification)
2783
+ verification["inline_form_layout_verified"] = False
2784
+ result["verification"] = verification
2785
+ result["safe_to_retry"] = False
2786
+ return result
2787
+ layout_result = self._app_layout_apply_once(
2788
+ profile=profile,
2789
+ app_key=app_key,
2790
+ mode="merge",
2791
+ publish=publish,
2792
+ sections=sections,
2793
+ )
2794
+ layout_ok = isinstance(layout_result, dict) and layout_result.get("status") in {"success", "partial_success"}
2795
+ result["inline_form_layout"] = {
2796
+ "requested": True,
2797
+ "section_count": len(sections),
2798
+ "app_key": app_key,
2799
+ "applied": layout_ok,
2800
+ "layout_status": layout_result.get("status") if isinstance(layout_result, dict) else "failed",
2801
+ **({"error_code": layout_result.get("error_code")} if isinstance(layout_result, dict) and layout_result.get("error_code") else {}),
2802
+ **({"message": layout_result.get("message")} if isinstance(layout_result, dict) and layout_result.get("message") else {}),
2803
+ }
2804
+ result["inline_form_layout_result"] = layout_result
2805
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
2806
+ verification = deepcopy(verification)
2807
+ verification["inline_form_layout_verified"] = layout_ok
2808
+ result["verification"] = verification
2809
+ if layout_ok:
2810
+ result["write_executed"] = True
2811
+ return result
2812
+ result["status"] = "partial_success" if result.get("status") == "success" else result.get("status", "partial_success")
2813
+ result["error_code"] = result.get("error_code") or "INLINE_FORM_LAYOUT_APPLY_FAILED"
2814
+ result["message"] = "schema applied but inline form layout failed"
2815
+ result["safe_to_retry"] = False
2816
+ return result
2656
2817
 
2657
2818
  @tool_cn_name("应用布局应用")
2658
2819
  def app_layout_apply(
@@ -2990,7 +3151,7 @@ class AiBuilderTools(ToolBase):
2990
3151
  )
2991
3152
  if reserved_failure is not None:
2992
3153
  return reserved_failure
2993
- if patch_views:
3154
+ if patch_views or _view_payload_has_action_buttons(upsert_views=upsert_views, patch_views=patch_views):
2994
3155
  try:
2995
3156
  parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
2996
3157
  parsed_patch_views = [ViewPartialPatch.model_validate(item) for item in patch_views]
@@ -3016,8 +3177,8 @@ class AiBuilderTools(ToolBase):
3016
3177
  normalized_args = {
3017
3178
  "app_key": app_key,
3018
3179
  "publish": publish,
3019
- "upsert_views": [view.model_dump(mode="json") for view in parsed_views],
3020
- "patch_views": [patch.model_dump(mode="json") for patch in parsed_patch_views],
3180
+ "upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
3181
+ "patch_views": [public_view_partial_payload(patch) for patch in parsed_patch_views],
3021
3182
  "remove_views": list(remove_views or []),
3022
3183
  }
3023
3184
  return _safe_tool_call(
@@ -3072,7 +3233,8 @@ class AiBuilderTools(ToolBase):
3072
3233
  normalized_args = {
3073
3234
  "app_key": str(plan_args.get("app_key") or app_key),
3074
3235
  "publish": publish,
3075
- "upsert_views": [view.model_dump(mode="json") for view in parsed_views],
3236
+ "upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
3237
+ "patch_views": [],
3076
3238
  "remove_views": list(plan_args.get("remove_views") or remove_views),
3077
3239
  }
3078
3240
  return _safe_tool_call(
@@ -3534,7 +3696,12 @@ def _schema_apps_expected_shape() -> JSONObject:
3534
3696
  "app_name": "员工花名册",
3535
3697
  "icon": "business-personalcard",
3536
3698
  "color": "emerald",
3537
- "add_fields": [{"name": "员工名称", "type": "text", "as_data_title": True}],
3699
+ "form": [
3700
+ {
3701
+ "section": "基础信息",
3702
+ "rows": [[{"name": "员工名称", "type": "text", "data_title": True}]],
3703
+ }
3704
+ ],
3538
3705
  }
3539
3706
  ],
3540
3707
  }
@@ -3543,7 +3710,7 @@ def _schema_apps_expected_shape() -> JSONObject:
3543
3710
  def _schema_apps_expected_shape_json() -> str:
3544
3711
  return (
3545
3712
  '{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
3546
- '"icon":"business-personalcard","color":"emerald","add_fields":[{"name":"员工名称","type":"text","as_data_title":true}]}]}'
3713
+ '"icon":"business-personalcard","color":"emerald","form":[{"section":"基础信息","rows":[[{"name":"员工名称","type":"text","data_title":true}]]}]}]}'
3547
3714
  )
3548
3715
 
3549
3716
 
@@ -3628,6 +3795,163 @@ def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, a
3628
3795
  return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
3629
3796
 
3630
3797
 
3798
+ def _normalize_schema_form_field(field: JSONObject) -> JSONObject:
3799
+ payload = deepcopy(field)
3800
+ if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
3801
+ payload["as_data_title"] = payload.pop("data_title")
3802
+ if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
3803
+ payload["as_data_title"] = payload.pop("dataTitle")
3804
+ if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
3805
+ payload["as_data_cover"] = payload.pop("data_cover")
3806
+ if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
3807
+ payload["as_data_cover"] = payload.pop("dataCover")
3808
+ return payload
3809
+
3810
+
3811
+ def _schema_form_entry_name(entry: object) -> str:
3812
+ if isinstance(entry, dict):
3813
+ for key in ("name", "title", "label", "field_name", "fieldName"):
3814
+ if entry.get(key) is not None:
3815
+ return str(entry.get(key) or "").strip()
3816
+ return ""
3817
+ if isinstance(entry, (str, int)):
3818
+ return str(entry).strip()
3819
+ return ""
3820
+
3821
+
3822
+ def _schema_form_failure(*, tool_name: str, path: str, message: str, fix_hint: str) -> JSONObject:
3823
+ return _config_failure(
3824
+ tool_name=tool_name,
3825
+ error_code="FORM_SHAPE_INVALID",
3826
+ message=message,
3827
+ fix_hint=fix_hint,
3828
+ details={
3829
+ "path": path,
3830
+ "expected_shape": {
3831
+ "form": [
3832
+ {
3833
+ "section": "基础信息",
3834
+ "rows": [
3835
+ [
3836
+ {"name": "标题", "type": "text", "data_title": True},
3837
+ {"name": "状态", "type": "single_select", "options": ["待处理", "处理中", "已完成"]},
3838
+ ]
3839
+ ],
3840
+ }
3841
+ ]
3842
+ },
3843
+ },
3844
+ )
3845
+
3846
+
3847
+ def _compile_schema_form_payload(*, tool_name: str, form: object, path: str = "form") -> JSONObject:
3848
+ if form is None:
3849
+ return {"add_fields": [], "layout_sections": []}
3850
+ if not isinstance(form, list) or not form:
3851
+ return {
3852
+ "failure": _schema_form_failure(
3853
+ tool_name=tool_name,
3854
+ path=path,
3855
+ message=f"{path} must be a non-empty list of form sections.",
3856
+ fix_hint='Use form=[{"section":"基础信息","rows":[[{"name":"标题","type":"text","data_title":true}]]}].',
3857
+ )
3858
+ }
3859
+
3860
+ add_fields: list[JSONObject] = []
3861
+ layout_sections: list[JSONObject] = []
3862
+ for section_index, section in enumerate(form):
3863
+ section_path = f"{path}[{section_index}]"
3864
+ if not isinstance(section, dict):
3865
+ return {
3866
+ "failure": _schema_form_failure(
3867
+ tool_name=tool_name,
3868
+ path=section_path,
3869
+ message=f"{section_path} must be an object.",
3870
+ fix_hint="Each form section must include section and rows.",
3871
+ )
3872
+ }
3873
+ section_title = str(section.get("section") or section.get("title") or section.get("name") or "").strip()
3874
+ rows = section.get("rows")
3875
+ if not section_title:
3876
+ return {
3877
+ "failure": _schema_form_failure(
3878
+ tool_name=tool_name,
3879
+ path=f"{section_path}.section",
3880
+ message=f"{section_path}.section is required.",
3881
+ fix_hint='Set a business section title such as "基础信息" or "生产信息".',
3882
+ )
3883
+ }
3884
+ if not isinstance(rows, list) or not rows:
3885
+ return {
3886
+ "failure": _schema_form_failure(
3887
+ tool_name=tool_name,
3888
+ path=f"{section_path}.rows",
3889
+ message=f"{section_path}.rows must be a non-empty list.",
3890
+ fix_hint="Use rows as a list of field rows; each row is a list of field objects.",
3891
+ )
3892
+ }
3893
+ layout_rows: list[list[str]] = []
3894
+ for row_index, row in enumerate(rows):
3895
+ row_path = f"{section_path}.rows[{row_index}]"
3896
+ if isinstance(row, dict):
3897
+ row_fields = row.get("fields")
3898
+ else:
3899
+ row_fields = row
3900
+ if not isinstance(row_fields, list) or not row_fields:
3901
+ return {
3902
+ "failure": _schema_form_failure(
3903
+ tool_name=tool_name,
3904
+ path=row_path,
3905
+ message=f"{row_path} must be a non-empty field list.",
3906
+ fix_hint='Use rows like [[{"name":"字段A","type":"text"},{"name":"字段B","type":"number"}]].',
3907
+ )
3908
+ }
3909
+ layout_row: list[str] = []
3910
+ for field_index, field in enumerate(row_fields):
3911
+ field_path = f"{row_path}[{field_index}]"
3912
+ field_name = _schema_form_entry_name(field)
3913
+ if not field_name:
3914
+ return {
3915
+ "failure": _schema_form_failure(
3916
+ tool_name=tool_name,
3917
+ path=field_path,
3918
+ message=f"{field_path} must include a field name.",
3919
+ fix_hint='Each new field object must include name, for example {"name":"工单编号","type":"text"}.',
3920
+ )
3921
+ }
3922
+ layout_row.append(field_name)
3923
+ if isinstance(field, dict):
3924
+ add_fields.append(_normalize_schema_form_field(field))
3925
+ layout_rows.append(layout_row)
3926
+ layout_sections.append({"title": section_title, "rows": layout_rows})
3927
+ return {"add_fields": add_fields, "layout_sections": layout_sections}
3928
+
3929
+
3930
+ def _apply_schema_form_to_payload(*, tool_name: str, payload: JSONObject, path: str) -> JSONObject:
3931
+ form = payload.get("form")
3932
+ if form is None:
3933
+ return {"payload": payload, "layout_sections": []}
3934
+ existing_add_fields = payload.get("add_fields") or payload.get("addFields")
3935
+ if isinstance(existing_add_fields, list) and existing_add_fields:
3936
+ return {
3937
+ "failure": _config_failure(
3938
+ tool_name=tool_name,
3939
+ error_code="FORM_WITH_ADD_FIELDS_CONFLICT",
3940
+ message=f"{path} cannot use form and add_fields together.",
3941
+ fix_hint="Use form as the single schema creation shape: put fields under form[].rows[][].",
3942
+ details={"path": path, "conflicting_keys": ["form", "add_fields"]},
3943
+ )
3944
+ }
3945
+ compiled = _compile_schema_form_payload(tool_name=tool_name, form=form, path=f"{path}.form")
3946
+ if isinstance(compiled.get("failure"), dict):
3947
+ return {"failure": compiled["failure"]}
3948
+ normalized = deepcopy(payload)
3949
+ normalized.pop("form", None)
3950
+ normalized["add_fields"] = compiled.get("add_fields") or []
3951
+ normalized["_inline_form_layout_sections"] = compiled.get("layout_sections") or []
3952
+ return {"payload": normalized, "layout_sections": compiled.get("layout_sections") or []}
3953
+
3954
+
3631
3955
  def _multi_app_item_app_name(item: JSONObject) -> str:
3632
3956
  return str(item.get("app_name") or item.get("appName") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip()
3633
3957
 
@@ -3649,7 +3973,7 @@ def _field_type_for_static_validation(field: JSONObject) -> str:
3649
3973
 
3650
3974
 
3651
3975
  def _is_data_title_field(field: JSONObject) -> bool:
3652
- return bool(field.get("as_data_title") or field.get("asDataTitle"))
3976
+ return bool(field.get("as_data_title") or field.get("asDataTitle") or field.get("data_title") or field.get("dataTitle"))
3653
3977
 
3654
3978
 
3655
3979
  def _collect_multi_app_target_refs(value: object, *, path: str) -> list[JSONObject]:
@@ -4278,6 +4602,23 @@ def _reserved_system_view_name_failure(
4278
4602
  return None
4279
4603
 
4280
4604
 
4605
+ def _view_payload_has_action_buttons(
4606
+ *,
4607
+ upsert_views: list[JSONObject] | None = None,
4608
+ patch_views: list[JSONObject] | None = None,
4609
+ ) -> bool:
4610
+ for item in upsert_views or []:
4611
+ if isinstance(item, dict) and any(key in item for key in ("action_buttons", "actionButtons")):
4612
+ return True
4613
+ for item in patch_views or []:
4614
+ if not isinstance(item, dict):
4615
+ continue
4616
+ raw_set = item.get("set")
4617
+ if isinstance(raw_set, dict) and any(key in raw_set for key in ("action_buttons", "actionButtons")):
4618
+ return True
4619
+ return False
4620
+
4621
+
4281
4622
  def _config_failure(
4282
4623
  *,
4283
4624
  tool_name: str,
@@ -4986,6 +5327,15 @@ def _builder_view_resources(payload: JSONObject) -> list[JSONObject]:
4986
5327
  message=message,
4987
5328
  )
4988
5329
  )
5330
+ details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
5331
+ action_buttons_result = details.get("action_buttons_result") if isinstance(details, dict) else None
5332
+ if isinstance(action_buttons_result, dict):
5333
+ nested_payload = {
5334
+ **action_buttons_result,
5335
+ "app_key": payload.get("app_key"),
5336
+ "app_name": payload.get("app_name"),
5337
+ }
5338
+ resources.extend(_builder_button_resources(nested_payload))
4989
5339
  return resources
4990
5340
 
4991
5341
 
@@ -5956,6 +6306,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5956
6306
  "field.customBtnTextStatus": "field.custom_button_text_enabled",
5957
6307
  "field.customBtnText": "field.custom_button_text",
5958
6308
  "field.subfieldUpdates": "field.subfield_updates",
6309
+ "field.data_title": "field.as_data_title",
6310
+ "field.dataTitle": "field.as_data_title",
6311
+ "field.data_cover": "field.as_data_cover",
6312
+ "field.dataCover": "field.as_data_cover",
5959
6313
  "field.asDataTitle": "field.as_data_title",
5960
6314
  "field.asDataCover": "field.as_data_cover",
5961
6315
  },
@@ -5983,7 +6337,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5983
6337
  "color": "emerald",
5984
6338
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
5985
6339
  "create_if_missing": True,
5986
- "add_fields": [{"name": "项目名称", "type": "text", "as_data_title": True}],
6340
+ "add_fields": [{"name": "项目名称", "type": "text", "data_title": True}],
5987
6341
  "update_fields": [],
5988
6342
  "remove_fields": [],
5989
6343
  },
@@ -6018,6 +6372,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6018
6372
  "visibility",
6019
6373
  "create_if_missing",
6020
6374
  "publish",
6375
+ "form",
6021
6376
  "add_fields",
6022
6377
  "update_fields",
6023
6378
  "remove_fields",
@@ -6031,6 +6386,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6031
6386
  "apps[].icon_color",
6032
6387
  "apps[].icon_config",
6033
6388
  "apps[].visibility",
6389
+ "apps[].form",
6390
+ "apps[].form[].section",
6391
+ "apps[].form[].rows",
6034
6392
  "apps[].add_fields",
6035
6393
  "apps[].update_fields",
6036
6394
  "apps[].remove_fields",
@@ -6075,6 +6433,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6075
6433
  "field.customBtnTextStatus": "field.custom_button_text_enabled",
6076
6434
  "field.customBtnText": "field.custom_button_text",
6077
6435
  "field.subfieldUpdates": "field.subfield_updates",
6436
+ "field.data_title": "field.as_data_title",
6437
+ "field.dataTitle": "field.as_data_title",
6438
+ "field.data_cover": "field.as_data_cover",
6439
+ "field.dataCover": "field.as_data_cover",
6078
6440
  "field.asDataTitle": "field.as_data_title",
6079
6441
  "field.asDataCover": "field.as_data_cover",
6080
6442
  },
@@ -6095,6 +6457,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6095
6457
  "create mode follows backend CreateAppBean: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
6096
6458
  "multi-app mode: pass package_id + apps[]; create_if_missing defaults to true for app_name items and may be set false only when every item uses app_key",
6097
6459
  "CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
6460
+ "create app schemas with form: form[].section names form sections; form[].rows[][] contains field objects; builder splits form into field creation plus form layout apply",
6461
+ "single-app CLI primary shape is --form-file; multi-app primary shape is apps[].form inside --apps-file",
6098
6462
  "multi-app mode preflights static errors before writing: duplicate client_key/app_name, missing data title on new apps, explicit create_if_missing=false with app_name items, and unresolved target_app_ref/target_app",
6099
6463
  "multi-app relation fields may use target_app_ref to point at another apps[].client_key; the tool creates/resolves app shells first and compiles it to target_app_key",
6100
6464
  "multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
@@ -6138,9 +6502,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6138
6502
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
6139
6503
  "create_if_missing": True,
6140
6504
  "publish": True,
6141
- "add_fields": [
6142
- {"name": "项目名称", "type": "text", "as_data_title": True},
6143
- {"name": "项目封面", "type": "attachment", "as_data_cover": True},
6505
+ "form": [
6506
+ {
6507
+ "section": "基础信息",
6508
+ "rows": [
6509
+ [
6510
+ {"name": "项目名称", "type": "text", "data_title": True},
6511
+ {"name": "项目封面", "type": "attachment", "data_cover": True},
6512
+ ]
6513
+ ],
6514
+ }
6144
6515
  ],
6145
6516
  "update_fields": [],
6146
6517
  "remove_fields": [],
@@ -6155,9 +6526,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6155
6526
  "app_name": "员工花名册",
6156
6527
  "icon": "business-personalcard",
6157
6528
  "color": "emerald",
6158
- "add_fields": [
6159
- {"name": "员工名称", "type": "text", "as_data_title": True},
6160
- {"name": "员工照片", "type": "attachment", "as_data_cover": True},
6529
+ "form": [
6530
+ {
6531
+ "section": "基础信息",
6532
+ "rows": [
6533
+ [
6534
+ {"name": "员工名称", "type": "text", "data_title": True},
6535
+ {"name": "员工照片", "type": "attachment", "data_cover": True},
6536
+ ]
6537
+ ],
6538
+ }
6161
6539
  ],
6162
6540
  },
6163
6541
  {
@@ -6165,15 +6543,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6165
6543
  "app_name": "工时表",
6166
6544
  "icon": "clock",
6167
6545
  "color": "blue",
6168
- "add_fields": [
6169
- {"name": "工时标题", "type": "text", "as_data_title": True},
6546
+ "form": [
6170
6547
  {
6171
- "name": "关联员工",
6172
- "type": "relation",
6173
- "target_app_ref": "employee",
6174
- "display_field": {"name": "员工名称"},
6175
- "visible_fields": [{"name": "员工名称"}],
6176
- },
6548
+ "section": "基础信息",
6549
+ "rows": [
6550
+ [{"name": "工时标题", "type": "text", "data_title": True}],
6551
+ [
6552
+ {
6553
+ "name": "关联员工",
6554
+ "type": "relation",
6555
+ "target_app_ref": "employee",
6556
+ "display_field": {"name": "员工名称"},
6557
+ "visible_fields": [{"name": "员工名称"}],
6558
+ }
6559
+ ],
6560
+ ],
6561
+ }
6177
6562
  ],
6178
6563
  },
6179
6564
  ],
@@ -6460,11 +6845,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6460
6845
  "upsert_views[].columns",
6461
6846
  "upsert_views[].filters",
6462
6847
  "upsert_views[].buttons",
6848
+ "upsert_views[].action_buttons",
6849
+ "upsert_views[].action_buttons_mode",
6463
6850
  "upsert_views[].visibility",
6464
6851
  "upsert_views[].query_conditions",
6465
6852
  "patch_views[].view_key",
6466
6853
  "patch_views[].name",
6467
6854
  "patch_views[].set",
6855
+ "patch_views[].set.action_buttons",
6856
+ "patch_views[].set.action_buttons_mode",
6468
6857
  "patch_views[].unset",
6469
6858
  ],
6470
6859
  "aliases": {
@@ -6492,6 +6881,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6492
6881
  "buttons[].buttonFormula": "buttons[].button_formula",
6493
6882
  "buttons[].buttonFormulaType": "buttons[].button_formula_type",
6494
6883
  "buttons[].printTpls": "buttons[].print_tpls",
6884
+ "actionButtons": "action_buttons",
6885
+ "actionButtonsMode": "action_buttons_mode",
6886
+ "action_buttons[].button_text": "action_buttons[].text",
6887
+ "action_buttons[].trigger_action": "action_buttons[].action",
6888
+ "action_buttons[].trigger_link_url": "action_buttons[].url",
6889
+ "action_buttons[].visibleWhen": "action_buttons[].visible_when",
6890
+ "action_buttons[].fieldMappings": "action_buttons[].field_mappings",
6891
+ "action_buttons[].defaultValues": "action_buttons[].default_values",
6495
6892
  },
6496
6893
  "allowed_values": {
6497
6894
  "preset": [member.value for member in ViewsPreset],
@@ -6499,6 +6896,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6499
6896
  "view.filter.operator": [member.value for member in ViewFilterOperator],
6500
6897
  "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
6501
6898
  "view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
6899
+ "view.action_buttons.action": ["add_data", "link", "qRobot", "wings"],
6900
+ "view.action_buttons.placement": [member.value for member in PublicButtonPlacement],
6901
+ "view.action_buttons_mode": ["merge", "replace"],
6502
6902
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
6503
6903
  },
6504
6904
  "execution_notes": [
@@ -6508,6 +6908,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6508
6908
  "upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
6509
6909
  "do not put relation/attachment/subtable/code_block/q_linker/address fields in query_conditions; use filters for fixed filters or app_associated_resources_apply.match_mappings for current-record relation/report matching",
6510
6910
  "use patch_views for partial parameter replacement on existing views; the tool reads current config, merges patch_views[].set/unset, then submits the backend full-save payload internally",
6911
+ "business buttons should be declared with action_buttons on the target view; app_views_apply writes the view first, then compiles action_buttons through the custom button writer and binds them to the resulting view_key",
6912
+ "action_buttons_mode defaults to merge; replace rewrites this view's custom button bindings, and replace plus action_buttons=[] clears this view's custom button bindings without deleting button bodies",
6913
+ "legacy upsert_views[].buttons is a compatibility path for raw full replacement of existing view button config; do not use it as the normal agent path",
6511
6914
  "remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
6512
6915
  "deleted views return verification.by_view[].delete_executed, readback_status, and safe_to_retry_delete=false; if readback is pending, do not blindly repeat the delete",
6513
6916
  "do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送; these are built-in system/default views. Use business-specific names for new views, and pass the existing raw view_key or patch_views when changing a built-in view",
@@ -6538,6 +6941,27 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6538
6941
  ],
6539
6942
  "remove_views": [],
6540
6943
  },
6944
+ "action_buttons_example": {
6945
+ "profile": "default",
6946
+ "app_key": "APP_KEY",
6947
+ "upsert_views": [
6948
+ {
6949
+ "name": "生产工单执行视图",
6950
+ "type": "table",
6951
+ "columns": ["工单编号", "产品", "状态", "负责人"],
6952
+ "action_buttons": [
6953
+ {
6954
+ "text": "开始生产",
6955
+ "action": "link",
6956
+ "url": "https://example.com/start",
6957
+ "placement": "list",
6958
+ "visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
6959
+ }
6960
+ ],
6961
+ }
6962
+ ],
6963
+ "remove_views": [],
6964
+ },
6541
6965
  "full_upsert_query_conditions_example": {
6542
6966
  "profile": "default",
6543
6967
  "app_key": "APP_KEY",
@@ -6595,11 +7019,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6595
7019
  "upsert_views[].columns",
6596
7020
  "upsert_views[].filters",
6597
7021
  "upsert_views[].buttons",
7022
+ "upsert_views[].action_buttons",
7023
+ "upsert_views[].action_buttons_mode",
6598
7024
  "upsert_views[].visibility",
6599
7025
  "upsert_views[].query_conditions",
6600
7026
  "patch_views[].view_key",
6601
7027
  "patch_views[].name",
6602
7028
  "patch_views[].set",
7029
+ "patch_views[].set.action_buttons",
7030
+ "patch_views[].set.action_buttons_mode",
6603
7031
  "patch_views[].unset",
6604
7032
  ],
6605
7033
  "aliases": {
@@ -6627,12 +7055,23 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6627
7055
  "buttons[].buttonFormula": "buttons[].button_formula",
6628
7056
  "buttons[].buttonFormulaType": "buttons[].button_formula_type",
6629
7057
  "buttons[].printTpls": "buttons[].print_tpls",
7058
+ "actionButtons": "action_buttons",
7059
+ "actionButtonsMode": "action_buttons_mode",
7060
+ "action_buttons[].button_text": "action_buttons[].text",
7061
+ "action_buttons[].trigger_action": "action_buttons[].action",
7062
+ "action_buttons[].trigger_link_url": "action_buttons[].url",
7063
+ "action_buttons[].visibleWhen": "action_buttons[].visible_when",
7064
+ "action_buttons[].fieldMappings": "action_buttons[].field_mappings",
7065
+ "action_buttons[].defaultValues": "action_buttons[].default_values",
6630
7066
  },
6631
7067
  "allowed_values": {
6632
7068
  "view.type": [member.value for member in PublicViewType],
6633
7069
  "view.filter.operator": [member.value for member in ViewFilterOperator],
6634
7070
  "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
6635
7071
  "view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
7072
+ "view.action_buttons.action": ["add_data", "link", "qRobot", "wings"],
7073
+ "view.action_buttons.placement": [member.value for member in PublicButtonPlacement],
7074
+ "view.action_buttons_mode": ["merge", "replace"],
6636
7075
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
6637
7076
  },
6638
7077
  "execution_notes": [
@@ -6642,6 +7081,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6642
7081
  "read back app_get after any failed or partial view apply",
6643
7082
  "view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
6644
7083
  "buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
7084
+ "business buttons should be declared with action_buttons on the target view; app_views_apply writes the view first, then compiles action_buttons through the custom button writer and binds them to the resulting view_key",
7085
+ "patch_views[].set.action_buttons can add buttons to an existing view without rewriting columns/filters; action_buttons_mode defaults to merge, replace rewrites this view's custom button bindings, and replace plus action_buttons=[] clears bindings without deleting button bodies",
7086
+ "publish=false with action_buttons is blocked as VIEW_ACTION_BUTTONS_REQUIRE_PUBLISH because the underlying custom button writer may publish after successful button writes",
7087
+ "legacy upsert_views[].buttons is a compatibility path for raw full replacement of existing view button config; do not use it as the normal agent path",
6645
7088
  "upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
6646
7089
  "filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
6647
7090
  "upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
@@ -6679,6 +7122,51 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6679
7122
  ],
6680
7123
  "remove_views": [],
6681
7124
  },
7125
+ "action_buttons_example": {
7126
+ "profile": "default",
7127
+ "app_key": "APP_KEY",
7128
+ "publish": True,
7129
+ "upsert_views": [
7130
+ {
7131
+ "name": "生产工单执行视图",
7132
+ "type": "table",
7133
+ "columns": ["工单编号", "产品", "状态", "负责人"],
7134
+ "action_buttons": [
7135
+ {
7136
+ "text": "开始生产",
7137
+ "action": "link",
7138
+ "url": "https://example.com/start",
7139
+ "placement": "list",
7140
+ "visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
7141
+ }
7142
+ ],
7143
+ }
7144
+ ],
7145
+ "remove_views": [],
7146
+ },
7147
+ "action_buttons_patch_example": {
7148
+ "profile": "default",
7149
+ "app_key": "APP_KEY",
7150
+ "publish": True,
7151
+ "patch_views": [
7152
+ {
7153
+ "view_key": "RAW_VIEW_KEY",
7154
+ "set": {
7155
+ "action_buttons": [
7156
+ {
7157
+ "text": "创建验收单",
7158
+ "action": "add_data",
7159
+ "target_app_key": "ARRIVAL_APP",
7160
+ "field_mappings": [{"source_field": "数据ID", "target_field": "关联工单"}],
7161
+ "default_values": {"状态": "待验收"},
7162
+ "placement": "detail",
7163
+ }
7164
+ ]
7165
+ },
7166
+ }
7167
+ ],
7168
+ "remove_views": [],
7169
+ },
6682
7170
  "full_upsert_query_conditions_example": {
6683
7171
  "profile": "default",
6684
7172
  "app_key": "APP_KEY",
@@ -6954,15 +7442,38 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6954
7442
  "name": "dash_name",
6955
7443
  "sourceType": "source_type",
6956
7444
  "chartRef": "chart_ref",
7445
+ "inlineChart": "inline_chart",
7446
+ "createChart": "inline_chart",
6957
7447
  "viewRef": "view_ref",
6958
7448
  "dashStyleConfigBO": "dash_style_config",
6959
7449
  },
6960
- "section_allowed_keys": ["title", "source_type", "role", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
7450
+ "section_allowed_keys": [
7451
+ "title",
7452
+ "source_type",
7453
+ "role",
7454
+ "position",
7455
+ "dash_style_config",
7456
+ "config",
7457
+ "chart_ref",
7458
+ "inline_chart",
7459
+ "inline_chart.app_key",
7460
+ "inline_chart.chart_name",
7461
+ "inline_chart.chart_type",
7462
+ "inline_chart.metric",
7463
+ "inline_chart.metrics",
7464
+ "inline_chart.group_by",
7465
+ "inline_chart.where",
7466
+ "view_ref",
7467
+ "text",
7468
+ "url",
7469
+ ],
6961
7470
  "section_aliases": {
6962
7471
  "sourceType": "source_type",
6963
7472
  "zone": "role",
6964
7473
  "sectionRole": "role",
6965
7474
  "chartRef": "chart_ref",
7475
+ "inlineChart": "inline_chart",
7476
+ "createChart": "inline_chart",
6966
7477
  "viewRef": "view_ref",
6967
7478
  "dashStyleConfigBO": "dash_style_config",
6968
7479
  },
@@ -6988,6 +7499,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6988
7499
  "package_id is required when creating a new portal",
6989
7500
  "publish=false only guarantees draft and base-info updates; it does not claim live has changed",
6990
7501
  "chart_ref resolves by chart_id first, then exact unique chart_name",
7502
+ "inline_chart internally creates or updates an app-source QingBI report, then uses the returned chart_id for this portal section",
7503
+ "use chart_ref when an existing report already matches; use inline_chart when the portal needs a new or adjusted report",
6991
7504
  "view_ref resolves by view_key first, then exact unique view_name",
6992
7505
  "pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
6993
7506
  "if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
@@ -7015,7 +7528,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
7015
7528
  {
7016
7529
  "title": "经营概览",
7017
7530
  "source_type": "chart",
7018
- "chart_ref": {"app_key": "APP_KEY", "chart_name": "数据总量"},
7531
+ "inline_chart": {
7532
+ "app_key": "APP_KEY",
7533
+ "chart_name": "数据总量",
7534
+ "chart_type": "summary",
7535
+ "metric": {"field": "数据ID", "agg": "count"},
7536
+ },
7019
7537
  "position": {
7020
7538
  "pc": {"x": 0, "y": 0, "cols": 12, "rows": 6},
7021
7539
  "mobile": {"x": 0, "y": 0, "cols": 6, "rows": 6},