@josephyan/qingflow-app-builder-mcp 1.1.17 → 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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +56 -11
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +54 -5
- package/skills/qingflow-app-builder/references/create-app.md +10 -1
- package/skills/qingflow-app-builder/references/gotchas.md +17 -3
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +33 -4
- package/skills/qingflow-app-builder/references/tool-selection.md +20 -4
- package/skills/qingflow-app-builder/references/update-schema.md +11 -0
- package/skills/qingflow-app-builder/references/update-views.md +49 -0
- package/src/qingflow_mcp/builder_facade/models.py +35 -2
- package/src/qingflow_mcp/builder_facade/service.py +66 -13
- package/src/qingflow_mcp/cli/commands/builder.py +6 -3
- package/src/qingflow_mcp/server_app_builder.py +6 -3
- package/src/qingflow_mcp/tools/ai_builder_tools.py +391 -24
|
@@ -577,17 +577,18 @@ class AiBuilderTools(ToolBase):
|
|
|
577
577
|
visibility: JSONObject | None = None,
|
|
578
578
|
create_if_missing: bool | None = None,
|
|
579
579
|
publish: bool = True,
|
|
580
|
+
form: list[JSONObject] | None = None,
|
|
580
581
|
add_fields: list[JSONObject] | None = None,
|
|
581
582
|
update_fields: list[JSONObject] | None = None,
|
|
582
583
|
remove_fields: list[JSONObject] | None = None,
|
|
583
584
|
apps: list[JSONObject] | None = None,
|
|
584
585
|
) -> JSONObject:
|
|
585
586
|
if apps is not None:
|
|
586
|
-
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:
|
|
587
588
|
return _config_failure(
|
|
588
589
|
tool_name="app_schema_apply",
|
|
589
590
|
message="app_schema_apply multi-app mode accepts package_id plus apps only; create_if_missing is optional.",
|
|
590
|
-
fix_hint="
|
|
591
|
+
fix_hint="Put per-app form in apps[].form when using batch mode.",
|
|
591
592
|
)
|
|
592
593
|
if package_id is None:
|
|
593
594
|
return _config_failure(
|
|
@@ -602,6 +603,7 @@ class AiBuilderTools(ToolBase):
|
|
|
602
603
|
create_if_missing=True if create_if_missing is None else bool(create_if_missing),
|
|
603
604
|
publish=publish,
|
|
604
605
|
apps=apps,
|
|
606
|
+
form=None,
|
|
605
607
|
add_fields=[],
|
|
606
608
|
update_fields=[],
|
|
607
609
|
remove_fields=[],
|
|
@@ -638,10 +640,11 @@ class AiBuilderTools(ToolBase):
|
|
|
638
640
|
visibility=visibility,
|
|
639
641
|
create_if_missing=effective_create_if_missing,
|
|
640
642
|
publish=publish,
|
|
643
|
+
form=form or [],
|
|
641
644
|
add_fields=add_fields or [],
|
|
642
645
|
update_fields=update_fields or [],
|
|
643
646
|
remove_fields=remove_fields or [],
|
|
644
|
-
apps=
|
|
647
|
+
apps=None,
|
|
645
648
|
)
|
|
646
649
|
|
|
647
650
|
@mcp.tool()
|
|
@@ -2101,6 +2104,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2101
2104
|
visibility: JSONObject | None = None,
|
|
2102
2105
|
create_if_missing: bool | None = None,
|
|
2103
2106
|
publish: bool = True,
|
|
2107
|
+
form: list[JSONObject] | None = None,
|
|
2104
2108
|
add_fields: list[JSONObject],
|
|
2105
2109
|
update_fields: list[JSONObject],
|
|
2106
2110
|
remove_fields: list[JSONObject],
|
|
@@ -2126,6 +2130,21 @@ class AiBuilderTools(ToolBase):
|
|
|
2126
2130
|
package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
|
|
2127
2131
|
apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
|
|
2128
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
|
|
2129
2148
|
result = self._app_schema_apply_multi(
|
|
2130
2149
|
profile=profile,
|
|
2131
2150
|
package_id=package_id,
|
|
@@ -2139,6 +2158,22 @@ class AiBuilderTools(ToolBase):
|
|
|
2139
2158
|
result_warnings.extend(deepcopy(input_warnings))
|
|
2140
2159
|
result["warnings"] = result_warnings
|
|
2141
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 [])
|
|
2142
2177
|
result = self._app_schema_apply_once(
|
|
2143
2178
|
profile=profile,
|
|
2144
2179
|
app_key=app_key,
|
|
@@ -2153,6 +2188,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2153
2188
|
add_fields=add_fields,
|
|
2154
2189
|
update_fields=update_fields,
|
|
2155
2190
|
remove_fields=remove_fields,
|
|
2191
|
+
inline_form_layout_sections=inline_form_layout_sections,
|
|
2156
2192
|
)
|
|
2157
2193
|
result = self._retry_after_self_lock_release(
|
|
2158
2194
|
profile=profile,
|
|
@@ -2171,6 +2207,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2171
2207
|
add_fields=add_fields,
|
|
2172
2208
|
update_fields=update_fields,
|
|
2173
2209
|
remove_fields=remove_fields,
|
|
2210
|
+
inline_form_layout_sections=inline_form_layout_sections,
|
|
2174
2211
|
),
|
|
2175
2212
|
)
|
|
2176
2213
|
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
@@ -2429,6 +2466,19 @@ class AiBuilderTools(ToolBase):
|
|
|
2429
2466
|
remove_fields = list(compiled_item.get("remove_fields") or [])
|
|
2430
2467
|
if bool(existing.get("created")) and not deferred_add_fields and not update_fields and not remove_fields:
|
|
2431
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
|
|
2432
2482
|
item_status = shell_result.get("status") if shell_result.get("status") in {"success", "partial_success"} else "failed"
|
|
2433
2483
|
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
2434
2484
|
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
@@ -2449,6 +2499,8 @@ class AiBuilderTools(ToolBase):
|
|
|
2449
2499
|
"safe_to_retry": False,
|
|
2450
2500
|
**({"next_action": shell_result.get("next_action")} if shell_result.get("next_action") else {}),
|
|
2451
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 {}),
|
|
2452
2504
|
})
|
|
2453
2505
|
continue
|
|
2454
2506
|
|
|
@@ -2468,6 +2520,15 @@ class AiBuilderTools(ToolBase):
|
|
|
2468
2520
|
remove_fields=remove_fields,
|
|
2469
2521
|
)
|
|
2470
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
|
+
)
|
|
2471
2532
|
if _schema_apply_result_has_write(public_result):
|
|
2472
2533
|
any_write_executed = True
|
|
2473
2534
|
if _schema_apply_result_may_have_write(public_result):
|
|
@@ -2499,6 +2560,8 @@ class AiBuilderTools(ToolBase):
|
|
|
2499
2560
|
"safe_to_retry": False,
|
|
2500
2561
|
**({"next_action": public_result.get("next_action")} if public_result.get("next_action") else {}),
|
|
2501
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 {}),
|
|
2502
2565
|
})
|
|
2503
2566
|
|
|
2504
2567
|
pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
|
|
@@ -2562,6 +2625,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2562
2625
|
add_fields: list[JSONObject],
|
|
2563
2626
|
update_fields: list[JSONObject],
|
|
2564
2627
|
remove_fields: list[JSONObject],
|
|
2628
|
+
inline_form_layout_sections: list[JSONObject] | None = None,
|
|
2565
2629
|
) -> JSONObject:
|
|
2566
2630
|
"""执行内部辅助逻辑。"""
|
|
2567
2631
|
effective_app_name = app_name or app_title
|
|
@@ -2673,7 +2737,83 @@ class AiBuilderTools(ToolBase):
|
|
|
2673
2737
|
normalized_args=normalized_args,
|
|
2674
2738
|
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2675
2739
|
)
|
|
2676
|
-
|
|
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
|
|
2677
2817
|
|
|
2678
2818
|
@tool_cn_name("应用布局应用")
|
|
2679
2819
|
def app_layout_apply(
|
|
@@ -3556,7 +3696,12 @@ def _schema_apps_expected_shape() -> JSONObject:
|
|
|
3556
3696
|
"app_name": "员工花名册",
|
|
3557
3697
|
"icon": "business-personalcard",
|
|
3558
3698
|
"color": "emerald",
|
|
3559
|
-
"
|
|
3699
|
+
"form": [
|
|
3700
|
+
{
|
|
3701
|
+
"section": "基础信息",
|
|
3702
|
+
"rows": [[{"name": "员工名称", "type": "text", "data_title": True}]],
|
|
3703
|
+
}
|
|
3704
|
+
],
|
|
3560
3705
|
}
|
|
3561
3706
|
],
|
|
3562
3707
|
}
|
|
@@ -3565,7 +3710,7 @@ def _schema_apps_expected_shape() -> JSONObject:
|
|
|
3565
3710
|
def _schema_apps_expected_shape_json() -> str:
|
|
3566
3711
|
return (
|
|
3567
3712
|
'{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
|
|
3568
|
-
'"icon":"business-personalcard","color":"emerald","
|
|
3713
|
+
'"icon":"business-personalcard","color":"emerald","form":[{"section":"基础信息","rows":[[{"name":"员工名称","type":"text","data_title":true}]]}]}]}'
|
|
3569
3714
|
)
|
|
3570
3715
|
|
|
3571
3716
|
|
|
@@ -3650,6 +3795,163 @@ def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, a
|
|
|
3650
3795
|
return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
|
|
3651
3796
|
|
|
3652
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
|
+
|
|
3653
3955
|
def _multi_app_item_app_name(item: JSONObject) -> str:
|
|
3654
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()
|
|
3655
3957
|
|
|
@@ -3671,7 +3973,7 @@ def _field_type_for_static_validation(field: JSONObject) -> str:
|
|
|
3671
3973
|
|
|
3672
3974
|
|
|
3673
3975
|
def _is_data_title_field(field: JSONObject) -> bool:
|
|
3674
|
-
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"))
|
|
3675
3977
|
|
|
3676
3978
|
|
|
3677
3979
|
def _collect_multi_app_target_refs(value: object, *, path: str) -> list[JSONObject]:
|
|
@@ -6004,6 +6306,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6004
6306
|
"field.customBtnTextStatus": "field.custom_button_text_enabled",
|
|
6005
6307
|
"field.customBtnText": "field.custom_button_text",
|
|
6006
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",
|
|
6007
6313
|
"field.asDataTitle": "field.as_data_title",
|
|
6008
6314
|
"field.asDataCover": "field.as_data_cover",
|
|
6009
6315
|
},
|
|
@@ -6031,7 +6337,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6031
6337
|
"color": "emerald",
|
|
6032
6338
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
6033
6339
|
"create_if_missing": True,
|
|
6034
|
-
"add_fields": [{"name": "项目名称", "type": "text", "
|
|
6340
|
+
"add_fields": [{"name": "项目名称", "type": "text", "data_title": True}],
|
|
6035
6341
|
"update_fields": [],
|
|
6036
6342
|
"remove_fields": [],
|
|
6037
6343
|
},
|
|
@@ -6066,6 +6372,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6066
6372
|
"visibility",
|
|
6067
6373
|
"create_if_missing",
|
|
6068
6374
|
"publish",
|
|
6375
|
+
"form",
|
|
6069
6376
|
"add_fields",
|
|
6070
6377
|
"update_fields",
|
|
6071
6378
|
"remove_fields",
|
|
@@ -6079,6 +6386,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6079
6386
|
"apps[].icon_color",
|
|
6080
6387
|
"apps[].icon_config",
|
|
6081
6388
|
"apps[].visibility",
|
|
6389
|
+
"apps[].form",
|
|
6390
|
+
"apps[].form[].section",
|
|
6391
|
+
"apps[].form[].rows",
|
|
6082
6392
|
"apps[].add_fields",
|
|
6083
6393
|
"apps[].update_fields",
|
|
6084
6394
|
"apps[].remove_fields",
|
|
@@ -6123,6 +6433,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6123
6433
|
"field.customBtnTextStatus": "field.custom_button_text_enabled",
|
|
6124
6434
|
"field.customBtnText": "field.custom_button_text",
|
|
6125
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",
|
|
6126
6440
|
"field.asDataTitle": "field.as_data_title",
|
|
6127
6441
|
"field.asDataCover": "field.as_data_cover",
|
|
6128
6442
|
},
|
|
@@ -6143,6 +6457,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6143
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",
|
|
6144
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",
|
|
6145
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",
|
|
6146
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",
|
|
6147
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",
|
|
6148
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",
|
|
@@ -6186,9 +6502,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6186
6502
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
6187
6503
|
"create_if_missing": True,
|
|
6188
6504
|
"publish": True,
|
|
6189
|
-
"
|
|
6190
|
-
{
|
|
6191
|
-
|
|
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
|
+
}
|
|
6192
6515
|
],
|
|
6193
6516
|
"update_fields": [],
|
|
6194
6517
|
"remove_fields": [],
|
|
@@ -6203,9 +6526,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6203
6526
|
"app_name": "员工花名册",
|
|
6204
6527
|
"icon": "business-personalcard",
|
|
6205
6528
|
"color": "emerald",
|
|
6206
|
-
"
|
|
6207
|
-
{
|
|
6208
|
-
|
|
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
|
+
}
|
|
6209
6539
|
],
|
|
6210
6540
|
},
|
|
6211
6541
|
{
|
|
@@ -6213,15 +6543,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6213
6543
|
"app_name": "工时表",
|
|
6214
6544
|
"icon": "clock",
|
|
6215
6545
|
"color": "blue",
|
|
6216
|
-
"
|
|
6217
|
-
{"name": "工时标题", "type": "text", "as_data_title": True},
|
|
6546
|
+
"form": [
|
|
6218
6547
|
{
|
|
6219
|
-
"
|
|
6220
|
-
"
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
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
|
+
}
|
|
6225
6562
|
],
|
|
6226
6563
|
},
|
|
6227
6564
|
],
|
|
@@ -7105,15 +7442,38 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
7105
7442
|
"name": "dash_name",
|
|
7106
7443
|
"sourceType": "source_type",
|
|
7107
7444
|
"chartRef": "chart_ref",
|
|
7445
|
+
"inlineChart": "inline_chart",
|
|
7446
|
+
"createChart": "inline_chart",
|
|
7108
7447
|
"viewRef": "view_ref",
|
|
7109
7448
|
"dashStyleConfigBO": "dash_style_config",
|
|
7110
7449
|
},
|
|
7111
|
-
"section_allowed_keys": [
|
|
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
|
+
],
|
|
7112
7470
|
"section_aliases": {
|
|
7113
7471
|
"sourceType": "source_type",
|
|
7114
7472
|
"zone": "role",
|
|
7115
7473
|
"sectionRole": "role",
|
|
7116
7474
|
"chartRef": "chart_ref",
|
|
7475
|
+
"inlineChart": "inline_chart",
|
|
7476
|
+
"createChart": "inline_chart",
|
|
7117
7477
|
"viewRef": "view_ref",
|
|
7118
7478
|
"dashStyleConfigBO": "dash_style_config",
|
|
7119
7479
|
},
|
|
@@ -7139,6 +7499,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
7139
7499
|
"package_id is required when creating a new portal",
|
|
7140
7500
|
"publish=false only guarantees draft and base-info updates; it does not claim live has changed",
|
|
7141
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",
|
|
7142
7504
|
"view_ref resolves by view_key first, then exact unique view_name",
|
|
7143
7505
|
"pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
|
|
7144
7506
|
"if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
|
|
@@ -7166,7 +7528,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
7166
7528
|
{
|
|
7167
7529
|
"title": "经营概览",
|
|
7168
7530
|
"source_type": "chart",
|
|
7169
|
-
"
|
|
7531
|
+
"inline_chart": {
|
|
7532
|
+
"app_key": "APP_KEY",
|
|
7533
|
+
"chart_name": "数据总量",
|
|
7534
|
+
"chart_type": "summary",
|
|
7535
|
+
"metric": {"field": "数据ID", "agg": "count"},
|
|
7536
|
+
},
|
|
7170
7537
|
"position": {
|
|
7171
7538
|
"pc": {"x": 0, "y": 0, "cols": 12, "rows": 6},
|
|
7172
7539
|
"mobile": {"x": 0, "y": 0, "cols": 6, "rows": 6},
|