@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.
- 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 +79 -17
- 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 +404 -33
|
@@ -220,9 +220,10 @@ class AiBuilderTools(ToolBase):
|
|
|
220
220
|
wrapped["error"] = error
|
|
221
221
|
app_results.append(wrapped)
|
|
222
222
|
|
|
223
|
+
partial = sum(1 for item in app_results if str(item.get("status") or "") == "partial_success")
|
|
223
224
|
succeeded = sum(1 for item in app_results if str(item.get("status") or "") in {"success", "partial_success"})
|
|
224
225
|
failed = len(app_results) - succeeded
|
|
225
|
-
status = "
|
|
226
|
+
status = "failed" if succeeded == 0 else "partial_success" if failed > 0 or partial > 0 else "success"
|
|
226
227
|
write_executed = any(
|
|
227
228
|
isinstance(item.get("result"), dict) and bool(item["result"].get("write_executed"))
|
|
228
229
|
for item in app_results
|
|
@@ -230,7 +231,7 @@ class AiBuilderTools(ToolBase):
|
|
|
230
231
|
payload: JSONObject = {
|
|
231
232
|
"status": status,
|
|
232
233
|
"error_code": None if status == "success" else f"{tool_name.upper()}_BATCH_PARTIAL" if succeeded else f"{tool_name.upper()}_BATCH_FAILED",
|
|
233
|
-
"recoverable": failed > 0,
|
|
234
|
+
"recoverable": failed > 0 or partial > 0,
|
|
234
235
|
"message": (
|
|
235
236
|
f"applied {tool_name} to {succeeded}/{len(app_results)} apps"
|
|
236
237
|
if status != "success"
|
|
@@ -240,12 +241,13 @@ class AiBuilderTools(ToolBase):
|
|
|
240
241
|
"apps": app_results,
|
|
241
242
|
"errors": errors,
|
|
242
243
|
"verification": {
|
|
243
|
-
"batch_verified": failed == 0,
|
|
244
|
+
"batch_verified": failed == 0 and partial == 0,
|
|
244
245
|
"succeeded": succeeded,
|
|
246
|
+
"partial": partial,
|
|
245
247
|
"failed": failed,
|
|
246
248
|
},
|
|
247
249
|
"write_executed": write_executed,
|
|
248
|
-
"write_succeeded": write_executed and failed == 0,
|
|
250
|
+
"write_succeeded": write_executed and failed == 0 and partial == 0,
|
|
249
251
|
"safe_to_retry": not write_executed,
|
|
250
252
|
}
|
|
251
253
|
return _attach_builder_apply_envelope(tool_name, payload)
|
|
@@ -295,18 +297,20 @@ class AiBuilderTools(ToolBase):
|
|
|
295
297
|
app_item["error"] = error
|
|
296
298
|
errors.append(error)
|
|
297
299
|
apps.append(app_item)
|
|
300
|
+
partial = sum(1 for item in apps if str(item.get("status") or "") == "partial_success")
|
|
298
301
|
succeeded = sum(1 for item in apps if str(item.get("status") or "") in {"success", "partial_success"})
|
|
299
302
|
failed = len(apps) - succeeded
|
|
303
|
+
status = "failed" if succeeded == 0 else "partial_success" if failed > 0 or partial > 0 else "success"
|
|
300
304
|
return {
|
|
301
|
-
"status":
|
|
302
|
-
"error_code": None if
|
|
303
|
-
"recoverable": failed > 0,
|
|
304
|
-
"message": f"read {tool_name} for {succeeded}/{len(apps)} apps" if
|
|
305
|
+
"status": status,
|
|
306
|
+
"error_code": None if status == "success" else f"{tool_name.upper()}_BATCH_PARTIAL" if succeeded else f"{tool_name.upper()}_BATCH_FAILED",
|
|
307
|
+
"recoverable": failed > 0 or partial > 0,
|
|
308
|
+
"message": f"read {tool_name} for {succeeded}/{len(apps)} apps" if status != "success" else f"read {tool_name} for {succeeded} apps",
|
|
305
309
|
"normalized_args": {"app_keys": normalized_keys},
|
|
306
310
|
"app_keys": normalized_keys,
|
|
307
311
|
"apps": apps,
|
|
308
312
|
"errors": errors,
|
|
309
|
-
"verification": {"batch_verified": failed == 0, "succeeded": succeeded, "failed": failed},
|
|
313
|
+
"verification": {"batch_verified": failed == 0 and partial == 0, "succeeded": succeeded, "partial": partial, "failed": failed},
|
|
310
314
|
}
|
|
311
315
|
|
|
312
316
|
def register(self, mcp) -> None:
|
|
@@ -577,17 +581,18 @@ class AiBuilderTools(ToolBase):
|
|
|
577
581
|
visibility: JSONObject | None = None,
|
|
578
582
|
create_if_missing: bool | None = None,
|
|
579
583
|
publish: bool = True,
|
|
584
|
+
form: list[JSONObject] | None = None,
|
|
580
585
|
add_fields: list[JSONObject] | None = None,
|
|
581
586
|
update_fields: list[JSONObject] | None = None,
|
|
582
587
|
remove_fields: list[JSONObject] | None = None,
|
|
583
588
|
apps: list[JSONObject] | None = None,
|
|
584
589
|
) -> JSONObject:
|
|
585
590
|
if apps is not None:
|
|
586
|
-
if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
|
|
591
|
+
if app_key or app_name or app_title or form or add_fields or update_fields or remove_fields:
|
|
587
592
|
return _config_failure(
|
|
588
593
|
tool_name="app_schema_apply",
|
|
589
594
|
message="app_schema_apply multi-app mode accepts package_id plus apps only; create_if_missing is optional.",
|
|
590
|
-
fix_hint="
|
|
595
|
+
fix_hint="Put per-app form in apps[].form when using batch mode.",
|
|
591
596
|
)
|
|
592
597
|
if package_id is None:
|
|
593
598
|
return _config_failure(
|
|
@@ -602,6 +607,7 @@ class AiBuilderTools(ToolBase):
|
|
|
602
607
|
create_if_missing=True if create_if_missing is None else bool(create_if_missing),
|
|
603
608
|
publish=publish,
|
|
604
609
|
apps=apps,
|
|
610
|
+
form=None,
|
|
605
611
|
add_fields=[],
|
|
606
612
|
update_fields=[],
|
|
607
613
|
remove_fields=[],
|
|
@@ -638,10 +644,11 @@ class AiBuilderTools(ToolBase):
|
|
|
638
644
|
visibility=visibility,
|
|
639
645
|
create_if_missing=effective_create_if_missing,
|
|
640
646
|
publish=publish,
|
|
647
|
+
form=form or [],
|
|
641
648
|
add_fields=add_fields or [],
|
|
642
649
|
update_fields=update_fields or [],
|
|
643
650
|
remove_fields=remove_fields or [],
|
|
644
|
-
apps=
|
|
651
|
+
apps=None,
|
|
645
652
|
)
|
|
646
653
|
|
|
647
654
|
@mcp.tool()
|
|
@@ -2101,6 +2108,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2101
2108
|
visibility: JSONObject | None = None,
|
|
2102
2109
|
create_if_missing: bool | None = None,
|
|
2103
2110
|
publish: bool = True,
|
|
2111
|
+
form: list[JSONObject] | None = None,
|
|
2104
2112
|
add_fields: list[JSONObject],
|
|
2105
2113
|
update_fields: list[JSONObject],
|
|
2106
2114
|
remove_fields: list[JSONObject],
|
|
@@ -2126,6 +2134,21 @@ class AiBuilderTools(ToolBase):
|
|
|
2126
2134
|
package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
|
|
2127
2135
|
apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
|
|
2128
2136
|
input_warnings = list(normalized_apps_payload.get("warnings") or [])
|
|
2137
|
+
normalized_apps: list[JSONObject] = []
|
|
2138
|
+
for index, app_item in enumerate(apps or []):
|
|
2139
|
+
if not isinstance(app_item, dict):
|
|
2140
|
+
normalized_apps.append(app_item)
|
|
2141
|
+
continue
|
|
2142
|
+
form_result = _apply_schema_form_to_payload(
|
|
2143
|
+
tool_name="app_schema_apply",
|
|
2144
|
+
payload=app_item,
|
|
2145
|
+
path=f"apps[{index}]",
|
|
2146
|
+
)
|
|
2147
|
+
failure = form_result.get("failure")
|
|
2148
|
+
if isinstance(failure, dict):
|
|
2149
|
+
return _attach_builder_apply_envelope("app_schema_apply", failure)
|
|
2150
|
+
normalized_apps.append(form_result.get("payload") or app_item)
|
|
2151
|
+
apps = normalized_apps
|
|
2129
2152
|
result = self._app_schema_apply_multi(
|
|
2130
2153
|
profile=profile,
|
|
2131
2154
|
package_id=package_id,
|
|
@@ -2139,6 +2162,22 @@ class AiBuilderTools(ToolBase):
|
|
|
2139
2162
|
result_warnings.extend(deepcopy(input_warnings))
|
|
2140
2163
|
result["warnings"] = result_warnings
|
|
2141
2164
|
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
2165
|
+
inline_form_layout_sections: list[JSONObject] = []
|
|
2166
|
+
if form:
|
|
2167
|
+
form_payload: JSONObject = {"form": form}
|
|
2168
|
+
if add_fields:
|
|
2169
|
+
form_payload["add_fields"] = add_fields
|
|
2170
|
+
form_result = _apply_schema_form_to_payload(
|
|
2171
|
+
tool_name="app_schema_apply",
|
|
2172
|
+
payload=form_payload,
|
|
2173
|
+
path="schema",
|
|
2174
|
+
)
|
|
2175
|
+
failure = form_result.get("failure")
|
|
2176
|
+
if isinstance(failure, dict):
|
|
2177
|
+
return _attach_builder_apply_envelope("app_schema_apply", failure)
|
|
2178
|
+
normalized_payload = form_result.get("payload") if isinstance(form_result.get("payload"), dict) else {}
|
|
2179
|
+
add_fields = list(normalized_payload.get("add_fields") or [])
|
|
2180
|
+
inline_form_layout_sections = list(form_result.get("layout_sections") or [])
|
|
2142
2181
|
result = self._app_schema_apply_once(
|
|
2143
2182
|
profile=profile,
|
|
2144
2183
|
app_key=app_key,
|
|
@@ -2153,6 +2192,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2153
2192
|
add_fields=add_fields,
|
|
2154
2193
|
update_fields=update_fields,
|
|
2155
2194
|
remove_fields=remove_fields,
|
|
2195
|
+
inline_form_layout_sections=inline_form_layout_sections,
|
|
2156
2196
|
)
|
|
2157
2197
|
result = self._retry_after_self_lock_release(
|
|
2158
2198
|
profile=profile,
|
|
@@ -2171,6 +2211,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2171
2211
|
add_fields=add_fields,
|
|
2172
2212
|
update_fields=update_fields,
|
|
2173
2213
|
remove_fields=remove_fields,
|
|
2214
|
+
inline_form_layout_sections=inline_form_layout_sections,
|
|
2174
2215
|
),
|
|
2175
2216
|
)
|
|
2176
2217
|
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
@@ -2429,6 +2470,19 @@ class AiBuilderTools(ToolBase):
|
|
|
2429
2470
|
remove_fields = list(compiled_item.get("remove_fields") or [])
|
|
2430
2471
|
if bool(existing.get("created")) and not deferred_add_fields and not update_fields and not remove_fields:
|
|
2431
2472
|
shell_result = existing.get("shell_result") if isinstance(existing.get("shell_result"), dict) else {}
|
|
2473
|
+
layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
|
|
2474
|
+
if layout_sections:
|
|
2475
|
+
shell_result = self._apply_inline_form_layout_after_schema(
|
|
2476
|
+
profile=profile,
|
|
2477
|
+
schema_result=shell_result,
|
|
2478
|
+
fallback_app_key=app_key,
|
|
2479
|
+
publish=publish,
|
|
2480
|
+
sections=layout_sections,
|
|
2481
|
+
)
|
|
2482
|
+
if _schema_apply_result_has_write(shell_result):
|
|
2483
|
+
any_write_executed = True
|
|
2484
|
+
if _schema_apply_result_may_have_write(shell_result):
|
|
2485
|
+
any_write_may_have_succeeded = True
|
|
2432
2486
|
item_status = shell_result.get("status") if shell_result.get("status") in {"success", "partial_success"} else "failed"
|
|
2433
2487
|
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
2434
2488
|
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
@@ -2449,6 +2503,8 @@ class AiBuilderTools(ToolBase):
|
|
|
2449
2503
|
"safe_to_retry": False,
|
|
2450
2504
|
**({"next_action": shell_result.get("next_action")} if shell_result.get("next_action") else {}),
|
|
2451
2505
|
**({"verification": shell_result.get("verification")} if isinstance(shell_result.get("verification"), dict) else {}),
|
|
2506
|
+
**({"inline_form_layout": shell_result.get("inline_form_layout")} if isinstance(shell_result.get("inline_form_layout"), dict) else {}),
|
|
2507
|
+
**({"inline_form_layout_result": shell_result.get("inline_form_layout_result")} if isinstance(shell_result.get("inline_form_layout_result"), dict) else {}),
|
|
2452
2508
|
})
|
|
2453
2509
|
continue
|
|
2454
2510
|
|
|
@@ -2468,6 +2524,15 @@ class AiBuilderTools(ToolBase):
|
|
|
2468
2524
|
remove_fields=remove_fields,
|
|
2469
2525
|
)
|
|
2470
2526
|
public_result = _publicize_package_fields(field_result)
|
|
2527
|
+
layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
|
|
2528
|
+
if layout_sections:
|
|
2529
|
+
public_result = self._apply_inline_form_layout_after_schema(
|
|
2530
|
+
profile=profile,
|
|
2531
|
+
schema_result=public_result,
|
|
2532
|
+
fallback_app_key=app_key,
|
|
2533
|
+
publish=publish,
|
|
2534
|
+
sections=layout_sections,
|
|
2535
|
+
)
|
|
2471
2536
|
if _schema_apply_result_has_write(public_result):
|
|
2472
2537
|
any_write_executed = True
|
|
2473
2538
|
if _schema_apply_result_may_have_write(public_result):
|
|
@@ -2499,6 +2564,8 @@ class AiBuilderTools(ToolBase):
|
|
|
2499
2564
|
"safe_to_retry": False,
|
|
2500
2565
|
**({"next_action": public_result.get("next_action")} if public_result.get("next_action") else {}),
|
|
2501
2566
|
**({"verification": public_result.get("verification")} if isinstance(public_result.get("verification"), dict) else {}),
|
|
2567
|
+
**({"inline_form_layout": public_result.get("inline_form_layout")} if isinstance(public_result.get("inline_form_layout"), dict) else {}),
|
|
2568
|
+
**({"inline_form_layout_result": public_result.get("inline_form_layout_result")} if isinstance(public_result.get("inline_form_layout_result"), dict) else {}),
|
|
2502
2569
|
})
|
|
2503
2570
|
|
|
2504
2571
|
pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
|
|
@@ -2562,6 +2629,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2562
2629
|
add_fields: list[JSONObject],
|
|
2563
2630
|
update_fields: list[JSONObject],
|
|
2564
2631
|
remove_fields: list[JSONObject],
|
|
2632
|
+
inline_form_layout_sections: list[JSONObject] | None = None,
|
|
2565
2633
|
) -> JSONObject:
|
|
2566
2634
|
"""执行内部辅助逻辑。"""
|
|
2567
2635
|
effective_app_name = app_name or app_title
|
|
@@ -2673,7 +2741,83 @@ class AiBuilderTools(ToolBase):
|
|
|
2673
2741
|
normalized_args=normalized_args,
|
|
2674
2742
|
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2675
2743
|
)
|
|
2676
|
-
|
|
2744
|
+
public_result = _publicize_package_fields(result)
|
|
2745
|
+
if inline_form_layout_sections:
|
|
2746
|
+
public_result = self._apply_inline_form_layout_after_schema(
|
|
2747
|
+
profile=profile,
|
|
2748
|
+
schema_result=public_result,
|
|
2749
|
+
fallback_app_key=str(plan_args.get("app_key") or app_key),
|
|
2750
|
+
publish=publish,
|
|
2751
|
+
sections=inline_form_layout_sections,
|
|
2752
|
+
)
|
|
2753
|
+
return public_result
|
|
2754
|
+
|
|
2755
|
+
def _apply_inline_form_layout_after_schema(
|
|
2756
|
+
self,
|
|
2757
|
+
*,
|
|
2758
|
+
profile: str,
|
|
2759
|
+
schema_result: JSONObject,
|
|
2760
|
+
fallback_app_key: str = "",
|
|
2761
|
+
publish: bool,
|
|
2762
|
+
sections: list[JSONObject],
|
|
2763
|
+
) -> JSONObject:
|
|
2764
|
+
result = deepcopy(schema_result)
|
|
2765
|
+
if not sections:
|
|
2766
|
+
return result
|
|
2767
|
+
app_key = str(result.get("app_key") or result.get("appKey") or fallback_app_key or "").strip()
|
|
2768
|
+
result["inline_form_layout"] = {
|
|
2769
|
+
"requested": True,
|
|
2770
|
+
"section_count": len(sections),
|
|
2771
|
+
"applied": False,
|
|
2772
|
+
}
|
|
2773
|
+
if result.get("status") not in {"success", "partial_success"}:
|
|
2774
|
+
result["inline_form_layout"]["skipped_reason"] = "schema_apply_failed"
|
|
2775
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
2776
|
+
verification = deepcopy(verification)
|
|
2777
|
+
verification["inline_form_layout_verified"] = False
|
|
2778
|
+
result["verification"] = verification
|
|
2779
|
+
return result
|
|
2780
|
+
if not app_key:
|
|
2781
|
+
result["status"] = "partial_success"
|
|
2782
|
+
result["error_code"] = result.get("error_code") or "INLINE_FORM_LAYOUT_APP_KEY_MISSING"
|
|
2783
|
+
result["message"] = "schema applied but inline form layout could not run because app_key was not resolved"
|
|
2784
|
+
result["inline_form_layout"]["error_code"] = "INLINE_FORM_LAYOUT_APP_KEY_MISSING"
|
|
2785
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
2786
|
+
verification = deepcopy(verification)
|
|
2787
|
+
verification["inline_form_layout_verified"] = False
|
|
2788
|
+
result["verification"] = verification
|
|
2789
|
+
result["safe_to_retry"] = False
|
|
2790
|
+
return result
|
|
2791
|
+
layout_result = self._app_layout_apply_once(
|
|
2792
|
+
profile=profile,
|
|
2793
|
+
app_key=app_key,
|
|
2794
|
+
mode="merge",
|
|
2795
|
+
publish=publish,
|
|
2796
|
+
sections=sections,
|
|
2797
|
+
)
|
|
2798
|
+
layout_ok = isinstance(layout_result, dict) and layout_result.get("status") in {"success", "partial_success"}
|
|
2799
|
+
result["inline_form_layout"] = {
|
|
2800
|
+
"requested": True,
|
|
2801
|
+
"section_count": len(sections),
|
|
2802
|
+
"app_key": app_key,
|
|
2803
|
+
"applied": layout_ok,
|
|
2804
|
+
"layout_status": layout_result.get("status") if isinstance(layout_result, dict) else "failed",
|
|
2805
|
+
**({"error_code": layout_result.get("error_code")} if isinstance(layout_result, dict) and layout_result.get("error_code") else {}),
|
|
2806
|
+
**({"message": layout_result.get("message")} if isinstance(layout_result, dict) and layout_result.get("message") else {}),
|
|
2807
|
+
}
|
|
2808
|
+
result["inline_form_layout_result"] = layout_result
|
|
2809
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
2810
|
+
verification = deepcopy(verification)
|
|
2811
|
+
verification["inline_form_layout_verified"] = layout_ok
|
|
2812
|
+
result["verification"] = verification
|
|
2813
|
+
if layout_ok:
|
|
2814
|
+
result["write_executed"] = True
|
|
2815
|
+
return result
|
|
2816
|
+
result["status"] = "partial_success" if result.get("status") == "success" else result.get("status", "partial_success")
|
|
2817
|
+
result["error_code"] = result.get("error_code") or "INLINE_FORM_LAYOUT_APPLY_FAILED"
|
|
2818
|
+
result["message"] = "schema applied but inline form layout failed"
|
|
2819
|
+
result["safe_to_retry"] = False
|
|
2820
|
+
return result
|
|
2677
2821
|
|
|
2678
2822
|
@tool_cn_name("应用布局应用")
|
|
2679
2823
|
def app_layout_apply(
|
|
@@ -3556,7 +3700,12 @@ def _schema_apps_expected_shape() -> JSONObject:
|
|
|
3556
3700
|
"app_name": "员工花名册",
|
|
3557
3701
|
"icon": "business-personalcard",
|
|
3558
3702
|
"color": "emerald",
|
|
3559
|
-
"
|
|
3703
|
+
"form": [
|
|
3704
|
+
{
|
|
3705
|
+
"section": "基础信息",
|
|
3706
|
+
"rows": [[{"name": "员工名称", "type": "text", "data_title": True}]],
|
|
3707
|
+
}
|
|
3708
|
+
],
|
|
3560
3709
|
}
|
|
3561
3710
|
],
|
|
3562
3711
|
}
|
|
@@ -3565,7 +3714,7 @@ def _schema_apps_expected_shape() -> JSONObject:
|
|
|
3565
3714
|
def _schema_apps_expected_shape_json() -> str:
|
|
3566
3715
|
return (
|
|
3567
3716
|
'{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
|
|
3568
|
-
'"icon":"business-personalcard","color":"emerald","
|
|
3717
|
+
'"icon":"business-personalcard","color":"emerald","form":[{"section":"基础信息","rows":[[{"name":"员工名称","type":"text","data_title":true}]]}]}]}'
|
|
3569
3718
|
)
|
|
3570
3719
|
|
|
3571
3720
|
|
|
@@ -3650,6 +3799,163 @@ def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, a
|
|
|
3650
3799
|
return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
|
|
3651
3800
|
|
|
3652
3801
|
|
|
3802
|
+
def _normalize_schema_form_field(field: JSONObject) -> JSONObject:
|
|
3803
|
+
payload = deepcopy(field)
|
|
3804
|
+
if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
|
|
3805
|
+
payload["as_data_title"] = payload.pop("data_title")
|
|
3806
|
+
if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
|
|
3807
|
+
payload["as_data_title"] = payload.pop("dataTitle")
|
|
3808
|
+
if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
|
|
3809
|
+
payload["as_data_cover"] = payload.pop("data_cover")
|
|
3810
|
+
if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
|
|
3811
|
+
payload["as_data_cover"] = payload.pop("dataCover")
|
|
3812
|
+
return payload
|
|
3813
|
+
|
|
3814
|
+
|
|
3815
|
+
def _schema_form_entry_name(entry: object) -> str:
|
|
3816
|
+
if isinstance(entry, dict):
|
|
3817
|
+
for key in ("name", "title", "label", "field_name", "fieldName"):
|
|
3818
|
+
if entry.get(key) is not None:
|
|
3819
|
+
return str(entry.get(key) or "").strip()
|
|
3820
|
+
return ""
|
|
3821
|
+
if isinstance(entry, (str, int)):
|
|
3822
|
+
return str(entry).strip()
|
|
3823
|
+
return ""
|
|
3824
|
+
|
|
3825
|
+
|
|
3826
|
+
def _schema_form_failure(*, tool_name: str, path: str, message: str, fix_hint: str) -> JSONObject:
|
|
3827
|
+
return _config_failure(
|
|
3828
|
+
tool_name=tool_name,
|
|
3829
|
+
error_code="FORM_SHAPE_INVALID",
|
|
3830
|
+
message=message,
|
|
3831
|
+
fix_hint=fix_hint,
|
|
3832
|
+
details={
|
|
3833
|
+
"path": path,
|
|
3834
|
+
"expected_shape": {
|
|
3835
|
+
"form": [
|
|
3836
|
+
{
|
|
3837
|
+
"section": "基础信息",
|
|
3838
|
+
"rows": [
|
|
3839
|
+
[
|
|
3840
|
+
{"name": "标题", "type": "text", "data_title": True},
|
|
3841
|
+
{"name": "状态", "type": "single_select", "options": ["待处理", "处理中", "已完成"]},
|
|
3842
|
+
]
|
|
3843
|
+
],
|
|
3844
|
+
}
|
|
3845
|
+
]
|
|
3846
|
+
},
|
|
3847
|
+
},
|
|
3848
|
+
)
|
|
3849
|
+
|
|
3850
|
+
|
|
3851
|
+
def _compile_schema_form_payload(*, tool_name: str, form: object, path: str = "form") -> JSONObject:
|
|
3852
|
+
if form is None:
|
|
3853
|
+
return {"add_fields": [], "layout_sections": []}
|
|
3854
|
+
if not isinstance(form, list) or not form:
|
|
3855
|
+
return {
|
|
3856
|
+
"failure": _schema_form_failure(
|
|
3857
|
+
tool_name=tool_name,
|
|
3858
|
+
path=path,
|
|
3859
|
+
message=f"{path} must be a non-empty list of form sections.",
|
|
3860
|
+
fix_hint='Use form=[{"section":"基础信息","rows":[[{"name":"标题","type":"text","data_title":true}]]}].',
|
|
3861
|
+
)
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
add_fields: list[JSONObject] = []
|
|
3865
|
+
layout_sections: list[JSONObject] = []
|
|
3866
|
+
for section_index, section in enumerate(form):
|
|
3867
|
+
section_path = f"{path}[{section_index}]"
|
|
3868
|
+
if not isinstance(section, dict):
|
|
3869
|
+
return {
|
|
3870
|
+
"failure": _schema_form_failure(
|
|
3871
|
+
tool_name=tool_name,
|
|
3872
|
+
path=section_path,
|
|
3873
|
+
message=f"{section_path} must be an object.",
|
|
3874
|
+
fix_hint="Each form section must include section and rows.",
|
|
3875
|
+
)
|
|
3876
|
+
}
|
|
3877
|
+
section_title = str(section.get("section") or section.get("title") or section.get("name") or "").strip()
|
|
3878
|
+
rows = section.get("rows")
|
|
3879
|
+
if not section_title:
|
|
3880
|
+
return {
|
|
3881
|
+
"failure": _schema_form_failure(
|
|
3882
|
+
tool_name=tool_name,
|
|
3883
|
+
path=f"{section_path}.section",
|
|
3884
|
+
message=f"{section_path}.section is required.",
|
|
3885
|
+
fix_hint='Set a business section title such as "基础信息" or "生产信息".',
|
|
3886
|
+
)
|
|
3887
|
+
}
|
|
3888
|
+
if not isinstance(rows, list) or not rows:
|
|
3889
|
+
return {
|
|
3890
|
+
"failure": _schema_form_failure(
|
|
3891
|
+
tool_name=tool_name,
|
|
3892
|
+
path=f"{section_path}.rows",
|
|
3893
|
+
message=f"{section_path}.rows must be a non-empty list.",
|
|
3894
|
+
fix_hint="Use rows as a list of field rows; each row is a list of field objects.",
|
|
3895
|
+
)
|
|
3896
|
+
}
|
|
3897
|
+
layout_rows: list[list[str]] = []
|
|
3898
|
+
for row_index, row in enumerate(rows):
|
|
3899
|
+
row_path = f"{section_path}.rows[{row_index}]"
|
|
3900
|
+
if isinstance(row, dict):
|
|
3901
|
+
row_fields = row.get("fields")
|
|
3902
|
+
else:
|
|
3903
|
+
row_fields = row
|
|
3904
|
+
if not isinstance(row_fields, list) or not row_fields:
|
|
3905
|
+
return {
|
|
3906
|
+
"failure": _schema_form_failure(
|
|
3907
|
+
tool_name=tool_name,
|
|
3908
|
+
path=row_path,
|
|
3909
|
+
message=f"{row_path} must be a non-empty field list.",
|
|
3910
|
+
fix_hint='Use rows like [[{"name":"字段A","type":"text"},{"name":"字段B","type":"number"}]].',
|
|
3911
|
+
)
|
|
3912
|
+
}
|
|
3913
|
+
layout_row: list[str] = []
|
|
3914
|
+
for field_index, field in enumerate(row_fields):
|
|
3915
|
+
field_path = f"{row_path}[{field_index}]"
|
|
3916
|
+
field_name = _schema_form_entry_name(field)
|
|
3917
|
+
if not field_name:
|
|
3918
|
+
return {
|
|
3919
|
+
"failure": _schema_form_failure(
|
|
3920
|
+
tool_name=tool_name,
|
|
3921
|
+
path=field_path,
|
|
3922
|
+
message=f"{field_path} must include a field name.",
|
|
3923
|
+
fix_hint='Each new field object must include name, for example {"name":"工单编号","type":"text"}.',
|
|
3924
|
+
)
|
|
3925
|
+
}
|
|
3926
|
+
layout_row.append(field_name)
|
|
3927
|
+
if isinstance(field, dict):
|
|
3928
|
+
add_fields.append(_normalize_schema_form_field(field))
|
|
3929
|
+
layout_rows.append(layout_row)
|
|
3930
|
+
layout_sections.append({"title": section_title, "rows": layout_rows})
|
|
3931
|
+
return {"add_fields": add_fields, "layout_sections": layout_sections}
|
|
3932
|
+
|
|
3933
|
+
|
|
3934
|
+
def _apply_schema_form_to_payload(*, tool_name: str, payload: JSONObject, path: str) -> JSONObject:
|
|
3935
|
+
form = payload.get("form")
|
|
3936
|
+
if form is None:
|
|
3937
|
+
return {"payload": payload, "layout_sections": []}
|
|
3938
|
+
existing_add_fields = payload.get("add_fields") or payload.get("addFields")
|
|
3939
|
+
if isinstance(existing_add_fields, list) and existing_add_fields:
|
|
3940
|
+
return {
|
|
3941
|
+
"failure": _config_failure(
|
|
3942
|
+
tool_name=tool_name,
|
|
3943
|
+
error_code="FORM_WITH_ADD_FIELDS_CONFLICT",
|
|
3944
|
+
message=f"{path} cannot use form and add_fields together.",
|
|
3945
|
+
fix_hint="Use form as the single schema creation shape: put fields under form[].rows[][].",
|
|
3946
|
+
details={"path": path, "conflicting_keys": ["form", "add_fields"]},
|
|
3947
|
+
)
|
|
3948
|
+
}
|
|
3949
|
+
compiled = _compile_schema_form_payload(tool_name=tool_name, form=form, path=f"{path}.form")
|
|
3950
|
+
if isinstance(compiled.get("failure"), dict):
|
|
3951
|
+
return {"failure": compiled["failure"]}
|
|
3952
|
+
normalized = deepcopy(payload)
|
|
3953
|
+
normalized.pop("form", None)
|
|
3954
|
+
normalized["add_fields"] = compiled.get("add_fields") or []
|
|
3955
|
+
normalized["_inline_form_layout_sections"] = compiled.get("layout_sections") or []
|
|
3956
|
+
return {"payload": normalized, "layout_sections": compiled.get("layout_sections") or []}
|
|
3957
|
+
|
|
3958
|
+
|
|
3653
3959
|
def _multi_app_item_app_name(item: JSONObject) -> str:
|
|
3654
3960
|
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
3961
|
|
|
@@ -3671,7 +3977,7 @@ def _field_type_for_static_validation(field: JSONObject) -> str:
|
|
|
3671
3977
|
|
|
3672
3978
|
|
|
3673
3979
|
def _is_data_title_field(field: JSONObject) -> bool:
|
|
3674
|
-
return bool(field.get("as_data_title") or field.get("asDataTitle"))
|
|
3980
|
+
return bool(field.get("as_data_title") or field.get("asDataTitle") or field.get("data_title") or field.get("dataTitle"))
|
|
3675
3981
|
|
|
3676
3982
|
|
|
3677
3983
|
def _collect_multi_app_target_refs(value: object, *, path: str) -> list[JSONObject]:
|
|
@@ -6004,6 +6310,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6004
6310
|
"field.customBtnTextStatus": "field.custom_button_text_enabled",
|
|
6005
6311
|
"field.customBtnText": "field.custom_button_text",
|
|
6006
6312
|
"field.subfieldUpdates": "field.subfield_updates",
|
|
6313
|
+
"field.data_title": "field.as_data_title",
|
|
6314
|
+
"field.dataTitle": "field.as_data_title",
|
|
6315
|
+
"field.data_cover": "field.as_data_cover",
|
|
6316
|
+
"field.dataCover": "field.as_data_cover",
|
|
6007
6317
|
"field.asDataTitle": "field.as_data_title",
|
|
6008
6318
|
"field.asDataCover": "field.as_data_cover",
|
|
6009
6319
|
},
|
|
@@ -6031,7 +6341,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6031
6341
|
"color": "emerald",
|
|
6032
6342
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
6033
6343
|
"create_if_missing": True,
|
|
6034
|
-
"add_fields": [{"name": "项目名称", "type": "text", "
|
|
6344
|
+
"add_fields": [{"name": "项目名称", "type": "text", "data_title": True}],
|
|
6035
6345
|
"update_fields": [],
|
|
6036
6346
|
"remove_fields": [],
|
|
6037
6347
|
},
|
|
@@ -6066,6 +6376,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6066
6376
|
"visibility",
|
|
6067
6377
|
"create_if_missing",
|
|
6068
6378
|
"publish",
|
|
6379
|
+
"form",
|
|
6069
6380
|
"add_fields",
|
|
6070
6381
|
"update_fields",
|
|
6071
6382
|
"remove_fields",
|
|
@@ -6079,6 +6390,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6079
6390
|
"apps[].icon_color",
|
|
6080
6391
|
"apps[].icon_config",
|
|
6081
6392
|
"apps[].visibility",
|
|
6393
|
+
"apps[].form",
|
|
6394
|
+
"apps[].form[].section",
|
|
6395
|
+
"apps[].form[].rows",
|
|
6082
6396
|
"apps[].add_fields",
|
|
6083
6397
|
"apps[].update_fields",
|
|
6084
6398
|
"apps[].remove_fields",
|
|
@@ -6123,6 +6437,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6123
6437
|
"field.customBtnTextStatus": "field.custom_button_text_enabled",
|
|
6124
6438
|
"field.customBtnText": "field.custom_button_text",
|
|
6125
6439
|
"field.subfieldUpdates": "field.subfield_updates",
|
|
6440
|
+
"field.data_title": "field.as_data_title",
|
|
6441
|
+
"field.dataTitle": "field.as_data_title",
|
|
6442
|
+
"field.data_cover": "field.as_data_cover",
|
|
6443
|
+
"field.dataCover": "field.as_data_cover",
|
|
6126
6444
|
"field.asDataTitle": "field.as_data_title",
|
|
6127
6445
|
"field.asDataCover": "field.as_data_cover",
|
|
6128
6446
|
},
|
|
@@ -6143,6 +6461,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6143
6461
|
"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
6462
|
"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
6463
|
"CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
|
|
6464
|
+
"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",
|
|
6465
|
+
"single-app CLI primary shape is --form-file; multi-app primary shape is apps[].form inside --apps-file",
|
|
6146
6466
|
"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
6467
|
"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
6468
|
"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 +6506,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6186
6506
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
6187
6507
|
"create_if_missing": True,
|
|
6188
6508
|
"publish": True,
|
|
6189
|
-
"
|
|
6190
|
-
{
|
|
6191
|
-
|
|
6509
|
+
"form": [
|
|
6510
|
+
{
|
|
6511
|
+
"section": "基础信息",
|
|
6512
|
+
"rows": [
|
|
6513
|
+
[
|
|
6514
|
+
{"name": "项目名称", "type": "text", "data_title": True},
|
|
6515
|
+
{"name": "项目封面", "type": "attachment", "data_cover": True},
|
|
6516
|
+
]
|
|
6517
|
+
],
|
|
6518
|
+
}
|
|
6192
6519
|
],
|
|
6193
6520
|
"update_fields": [],
|
|
6194
6521
|
"remove_fields": [],
|
|
@@ -6203,9 +6530,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6203
6530
|
"app_name": "员工花名册",
|
|
6204
6531
|
"icon": "business-personalcard",
|
|
6205
6532
|
"color": "emerald",
|
|
6206
|
-
"
|
|
6207
|
-
{
|
|
6208
|
-
|
|
6533
|
+
"form": [
|
|
6534
|
+
{
|
|
6535
|
+
"section": "基础信息",
|
|
6536
|
+
"rows": [
|
|
6537
|
+
[
|
|
6538
|
+
{"name": "员工名称", "type": "text", "data_title": True},
|
|
6539
|
+
{"name": "员工照片", "type": "attachment", "data_cover": True},
|
|
6540
|
+
]
|
|
6541
|
+
],
|
|
6542
|
+
}
|
|
6209
6543
|
],
|
|
6210
6544
|
},
|
|
6211
6545
|
{
|
|
@@ -6213,15 +6547,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6213
6547
|
"app_name": "工时表",
|
|
6214
6548
|
"icon": "clock",
|
|
6215
6549
|
"color": "blue",
|
|
6216
|
-
"
|
|
6217
|
-
{"name": "工时标题", "type": "text", "as_data_title": True},
|
|
6550
|
+
"form": [
|
|
6218
6551
|
{
|
|
6219
|
-
"
|
|
6220
|
-
"
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6552
|
+
"section": "基础信息",
|
|
6553
|
+
"rows": [
|
|
6554
|
+
[{"name": "工时标题", "type": "text", "data_title": True}],
|
|
6555
|
+
[
|
|
6556
|
+
{
|
|
6557
|
+
"name": "关联员工",
|
|
6558
|
+
"type": "relation",
|
|
6559
|
+
"target_app_ref": "employee",
|
|
6560
|
+
"display_field": {"name": "员工名称"},
|
|
6561
|
+
"visible_fields": [{"name": "员工名称"}],
|
|
6562
|
+
}
|
|
6563
|
+
],
|
|
6564
|
+
],
|
|
6565
|
+
}
|
|
6225
6566
|
],
|
|
6226
6567
|
},
|
|
6227
6568
|
],
|
|
@@ -7105,15 +7446,38 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
7105
7446
|
"name": "dash_name",
|
|
7106
7447
|
"sourceType": "source_type",
|
|
7107
7448
|
"chartRef": "chart_ref",
|
|
7449
|
+
"inlineChart": "inline_chart",
|
|
7450
|
+
"createChart": "inline_chart",
|
|
7108
7451
|
"viewRef": "view_ref",
|
|
7109
7452
|
"dashStyleConfigBO": "dash_style_config",
|
|
7110
7453
|
},
|
|
7111
|
-
"section_allowed_keys": [
|
|
7454
|
+
"section_allowed_keys": [
|
|
7455
|
+
"title",
|
|
7456
|
+
"source_type",
|
|
7457
|
+
"role",
|
|
7458
|
+
"position",
|
|
7459
|
+
"dash_style_config",
|
|
7460
|
+
"config",
|
|
7461
|
+
"chart_ref",
|
|
7462
|
+
"inline_chart",
|
|
7463
|
+
"inline_chart.app_key",
|
|
7464
|
+
"inline_chart.chart_name",
|
|
7465
|
+
"inline_chart.chart_type",
|
|
7466
|
+
"inline_chart.metric",
|
|
7467
|
+
"inline_chart.metrics",
|
|
7468
|
+
"inline_chart.group_by",
|
|
7469
|
+
"inline_chart.where",
|
|
7470
|
+
"view_ref",
|
|
7471
|
+
"text",
|
|
7472
|
+
"url",
|
|
7473
|
+
],
|
|
7112
7474
|
"section_aliases": {
|
|
7113
7475
|
"sourceType": "source_type",
|
|
7114
7476
|
"zone": "role",
|
|
7115
7477
|
"sectionRole": "role",
|
|
7116
7478
|
"chartRef": "chart_ref",
|
|
7479
|
+
"inlineChart": "inline_chart",
|
|
7480
|
+
"createChart": "inline_chart",
|
|
7117
7481
|
"viewRef": "view_ref",
|
|
7118
7482
|
"dashStyleConfigBO": "dash_style_config",
|
|
7119
7483
|
},
|
|
@@ -7139,6 +7503,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
7139
7503
|
"package_id is required when creating a new portal",
|
|
7140
7504
|
"publish=false only guarantees draft and base-info updates; it does not claim live has changed",
|
|
7141
7505
|
"chart_ref resolves by chart_id first, then exact unique chart_name",
|
|
7506
|
+
"inline_chart internally creates or updates an app-source QingBI report, then uses the returned chart_id for this portal section",
|
|
7507
|
+
"use chart_ref when an existing report already matches; use inline_chart when the portal needs a new or adjusted report",
|
|
7142
7508
|
"view_ref resolves by view_key first, then exact unique view_name",
|
|
7143
7509
|
"pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
|
|
7144
7510
|
"if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
|
|
@@ -7166,7 +7532,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
7166
7532
|
{
|
|
7167
7533
|
"title": "经营概览",
|
|
7168
7534
|
"source_type": "chart",
|
|
7169
|
-
"
|
|
7535
|
+
"inline_chart": {
|
|
7536
|
+
"app_key": "APP_KEY",
|
|
7537
|
+
"chart_name": "数据总量",
|
|
7538
|
+
"chart_type": "summary",
|
|
7539
|
+
"metric": {"field": "数据ID", "agg": "count"},
|
|
7540
|
+
},
|
|
7170
7541
|
"position": {
|
|
7171
7542
|
"pc": {"x": 0, "y": 0, "cols": 12, "rows": 6},
|
|
7172
7543
|
"mobile": {"x": 0, "y": 0, "cols": 6, "rows": 6},
|