@josephyan/qingflow-app-builder-mcp 1.1.26 → 1.1.28
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 +4 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +24 -75
- package/skills/qingflow-app-builder/references/build-complete-system.md +185 -379
- package/skills/qingflow-app-builder/references/build-single-app.md +103 -495
- package/skills/qingflow-app-builder/references/create-app.md +15 -3
- package/skills/qingflow-app-builder/references/gotchas.md +6 -10
- package/skills/qingflow-app-builder/references/match-rules.md +18 -14
- package/skills/qingflow-app-builder/references/public-surface-sync.md +2 -2
- package/skills/qingflow-app-builder/references/tool-selection.md +9 -11
- package/src/qingflow_mcp/builder_facade/models.py +167 -71
- package/src/qingflow_mcp/builder_facade/service.py +617 -155
- package/src/qingflow_mcp/cli/commands/builder.py +3 -1
- package/src/qingflow_mcp/cli/main.py +8 -3
- package/src/qingflow_mcp/server_app_builder.py +9 -8
- package/src/qingflow_mcp/tools/ai_builder_tools.py +231 -180
- package/src/qingflow_mcp/version.py +2 -0
|
@@ -7,7 +7,7 @@ import json
|
|
|
7
7
|
import re
|
|
8
8
|
from threading import Lock
|
|
9
9
|
import time
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any, cast
|
|
11
11
|
|
|
12
12
|
from pydantic import ValidationError
|
|
13
13
|
|
|
@@ -92,6 +92,7 @@ BUILDER_APPLY_TOOL_NAMES = {
|
|
|
92
92
|
"app_publish_verify",
|
|
93
93
|
}
|
|
94
94
|
VIEW_BATCH_MAX_WORKERS = 20
|
|
95
|
+
SCHEMA_MULTI_APP_MAX_WORKERS = 10
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
def _view_batch_operation(value: Any) -> str:
|
|
@@ -429,14 +430,15 @@ class AiBuilderTools(ToolBase):
|
|
|
429
430
|
add_fields: list[JSONObject] | None = None,
|
|
430
431
|
update_fields: list[JSONObject] | None = None,
|
|
431
432
|
remove_fields: list[JSONObject] | None = None,
|
|
433
|
+
form: JSONObject | None = None,
|
|
432
434
|
apps: list[JSONObject] | None = None,
|
|
433
435
|
) -> JSONObject:
|
|
434
436
|
if apps:
|
|
435
|
-
if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
|
|
437
|
+
if app_key or app_name or app_title or add_fields or update_fields or remove_fields or form:
|
|
436
438
|
return _config_failure(
|
|
437
439
|
tool_name="app_schema_apply",
|
|
438
440
|
message="app_schema_apply multi-app mode accepts package_id/create_if_missing plus apps only.",
|
|
439
|
-
fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
|
|
441
|
+
fix_hint="Use `apps[].form` for batch mode, or use the single-app arguments including `form` without `apps`.",
|
|
440
442
|
)
|
|
441
443
|
if package_id is None:
|
|
442
444
|
return _config_failure(
|
|
@@ -486,6 +488,7 @@ class AiBuilderTools(ToolBase):
|
|
|
486
488
|
add_fields=add_fields or [],
|
|
487
489
|
update_fields=update_fields or [],
|
|
488
490
|
remove_fields=remove_fields or [],
|
|
491
|
+
form=form,
|
|
489
492
|
apps=[],
|
|
490
493
|
)
|
|
491
494
|
|
|
@@ -1750,6 +1753,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1750
1753
|
add_fields: list[JSONObject],
|
|
1751
1754
|
update_fields: list[JSONObject],
|
|
1752
1755
|
remove_fields: list[JSONObject],
|
|
1756
|
+
form: JSONObject | None = None,
|
|
1753
1757
|
apps: list[JSONObject] | None = None,
|
|
1754
1758
|
) -> JSONObject:
|
|
1755
1759
|
"""执行应用相关逻辑。"""
|
|
@@ -1777,6 +1781,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1777
1781
|
add_fields=add_fields,
|
|
1778
1782
|
update_fields=update_fields,
|
|
1779
1783
|
remove_fields=remove_fields,
|
|
1784
|
+
form=form,
|
|
1780
1785
|
)
|
|
1781
1786
|
result = self._retry_after_self_lock_release(
|
|
1782
1787
|
profile=profile,
|
|
@@ -1795,6 +1800,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1795
1800
|
add_fields=add_fields,
|
|
1796
1801
|
update_fields=update_fields,
|
|
1797
1802
|
remove_fields=remove_fields,
|
|
1803
|
+
form=form,
|
|
1798
1804
|
),
|
|
1799
1805
|
)
|
|
1800
1806
|
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
@@ -1892,50 +1898,55 @@ class AiBuilderTools(ToolBase):
|
|
|
1892
1898
|
},
|
|
1893
1899
|
)
|
|
1894
1900
|
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
any_write_executed = False
|
|
1901
|
+
started = time.perf_counter()
|
|
1902
|
+
final_by_index: list[JSONObject | None] = [None for _ in apps]
|
|
1903
|
+
valid_indices: list[int] = []
|
|
1899
1904
|
client_keys: set[str] = set()
|
|
1900
|
-
|
|
1901
1905
|
for index, raw_item in enumerate(apps):
|
|
1902
1906
|
if not isinstance(raw_item, dict):
|
|
1903
|
-
|
|
1907
|
+
final_by_index[index] = _multi_app_item_failure(index, raw_item, "INVALID_APP_ITEM", "apps[] items must be objects")
|
|
1904
1908
|
continue
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
1909
|
+
client_key = str(raw_item.get("client_key") or raw_item.get("clientKey") or "").strip()
|
|
1910
|
+
app_name = str(raw_item.get("app_name") or raw_item.get("appTitle") or raw_item.get("app_title") or raw_item.get("title") or "").strip()
|
|
1911
|
+
app_key = str(raw_item.get("app_key") or raw_item.get("appKey") or "").strip()
|
|
1909
1912
|
if client_key:
|
|
1910
1913
|
if client_key in client_keys:
|
|
1911
|
-
|
|
1914
|
+
final_by_index[index] = _multi_app_item_failure(index, raw_item, "DUPLICATE_CLIENT_KEY", f"duplicate client_key '{client_key}'")
|
|
1912
1915
|
continue
|
|
1913
1916
|
client_keys.add(client_key)
|
|
1914
1917
|
if not app_key and not app_name:
|
|
1915
|
-
|
|
1918
|
+
final_by_index[index] = _multi_app_item_failure(index, raw_item, "APP_SELECTOR_REQUIRED", "apps[] requires app_key or app_name")
|
|
1916
1919
|
continue
|
|
1920
|
+
valid_indices.append(index)
|
|
1917
1921
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1922
|
+
def run_shell(index: int) -> JSONObject:
|
|
1923
|
+
raw_item = cast(JSONObject, apps[index])
|
|
1924
|
+
app_name = str(raw_item.get("app_name") or raw_item.get("appTitle") or raw_item.get("app_title") or raw_item.get("title") or "").strip()
|
|
1925
|
+
app_key = str(raw_item.get("app_key") or raw_item.get("appKey") or "").strip()
|
|
1926
|
+
client_key = str(raw_item.get("client_key") or raw_item.get("clientKey") or "").strip()
|
|
1927
|
+
try:
|
|
1928
|
+
shell = self._app_schema_apply_once(
|
|
1929
|
+
profile=profile,
|
|
1930
|
+
app_key=app_key,
|
|
1931
|
+
package_id=package_id if not app_key else None,
|
|
1932
|
+
app_name=app_name,
|
|
1933
|
+
app_title="",
|
|
1934
|
+
icon=str(raw_item.get("icon") or ""),
|
|
1935
|
+
color=str(raw_item.get("color") or ""),
|
|
1936
|
+
visibility=raw_item.get("visibility", visibility),
|
|
1937
|
+
create_if_missing=create_if_missing and not app_key,
|
|
1938
|
+
publish=False,
|
|
1939
|
+
add_fields=[],
|
|
1940
|
+
update_fields=[],
|
|
1941
|
+
remove_fields=[],
|
|
1942
|
+
form=None,
|
|
1943
|
+
)
|
|
1944
|
+
public_shell = _publicize_package_fields(shell)
|
|
1945
|
+
except Exception as error:
|
|
1946
|
+
public_shell = {"status": "failed", "error_code": "APP_SHELL_APPLY_FAILED", "message": str(error)}
|
|
1936
1947
|
resolved_key = str(public_shell.get("app_key") or "").strip()
|
|
1937
1948
|
if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
|
|
1938
|
-
|
|
1949
|
+
return {
|
|
1939
1950
|
"index": index,
|
|
1940
1951
|
"row_number": index + 1,
|
|
1941
1952
|
"client_key": client_key or None,
|
|
@@ -1945,16 +1956,9 @@ class AiBuilderTools(ToolBase):
|
|
|
1945
1956
|
"stage": "resolve_or_create_shell",
|
|
1946
1957
|
"error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
|
|
1947
1958
|
"message": public_shell.get("message") or "app shell resolve/create failed",
|
|
1948
|
-
"safe_to_retry":
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
if bool(public_shell.get("created")):
|
|
1952
|
-
created_app_keys.append(resolved_key)
|
|
1953
|
-
if _schema_apply_result_has_write(public_shell):
|
|
1954
|
-
any_write_executed = True
|
|
1955
|
-
if client_key:
|
|
1956
|
-
client_key_to_app_key[client_key] = resolved_key
|
|
1957
|
-
results.append({
|
|
1959
|
+
"safe_to_retry": True,
|
|
1960
|
+
}
|
|
1961
|
+
return {
|
|
1958
1962
|
"index": index,
|
|
1959
1963
|
"row_number": index + 1,
|
|
1960
1964
|
"client_key": client_key or None,
|
|
@@ -1965,60 +1969,54 @@ class AiBuilderTools(ToolBase):
|
|
|
1965
1969
|
"shell_result": public_shell,
|
|
1966
1970
|
"shell_field_diff": public_shell.get("field_diff") or {},
|
|
1967
1971
|
"shell_field_diff_details": public_shell.get("field_diff_details") or {},
|
|
1968
|
-
|
|
1969
|
-
})
|
|
1972
|
+
}
|
|
1970
1973
|
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1974
|
+
shell_started = time.perf_counter()
|
|
1975
|
+
shell_workers = min(SCHEMA_MULTI_APP_MAX_WORKERS, len(valid_indices)) if valid_indices else 0
|
|
1976
|
+
if shell_workers:
|
|
1977
|
+
with ThreadPoolExecutor(max_workers=shell_workers) as executor:
|
|
1978
|
+
future_map = {executor.submit(run_shell, index): index for index in valid_indices}
|
|
1979
|
+
for future in as_completed(future_map):
|
|
1980
|
+
index = future_map[future]
|
|
1981
|
+
try:
|
|
1982
|
+
final_by_index[index] = future.result()
|
|
1983
|
+
except Exception as error:
|
|
1984
|
+
final_by_index[index] = _multi_app_item_failure(index, apps[index], "APP_SHELL_APPLY_FAILED", str(error))
|
|
1985
|
+
shell_wall_ms = int((time.perf_counter() - shell_started) * 1000)
|
|
1986
|
+
|
|
1987
|
+
client_key_to_app_key: dict[str, str] = {}
|
|
1988
|
+
created_app_keys: list[str] = []
|
|
1989
|
+
for item in final_by_index:
|
|
1990
|
+
if not isinstance(item, dict) or item.get("status") != "shell_ready":
|
|
1977
1991
|
continue
|
|
1978
|
-
item
|
|
1992
|
+
if bool(item.get("created")) and str(item.get("app_key") or "").strip():
|
|
1993
|
+
created_app_keys.append(str(item.get("app_key")))
|
|
1994
|
+
client_key = str(item.get("client_key") or "").strip()
|
|
1995
|
+
app_key = str(item.get("app_key") or "").strip()
|
|
1996
|
+
if client_key and app_key:
|
|
1997
|
+
client_key_to_app_key[client_key] = app_key
|
|
1998
|
+
|
|
1999
|
+
ready_indices = [
|
|
2000
|
+
index
|
|
2001
|
+
for index, item in enumerate(final_by_index)
|
|
2002
|
+
if isinstance(item, dict) and item.get("status") == "shell_ready"
|
|
2003
|
+
]
|
|
2004
|
+
|
|
2005
|
+
def run_schema(index: int) -> JSONObject:
|
|
2006
|
+
existing = cast(JSONObject, final_by_index[index])
|
|
2007
|
+
raw_item = cast(JSONObject, apps[index])
|
|
1979
2008
|
app_key = str(existing.get("app_key") or "").strip()
|
|
1980
2009
|
try:
|
|
1981
|
-
compiled_item = _compile_multi_app_schema_item_refs(
|
|
2010
|
+
compiled_item = _compile_multi_app_schema_item_refs(deepcopy(raw_item), client_key_to_app_key)
|
|
1982
2011
|
except ValueError as error:
|
|
1983
|
-
|
|
2012
|
+
return {
|
|
1984
2013
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
1985
2014
|
"status": "failed",
|
|
1986
2015
|
"stage": "compile_relation_refs",
|
|
1987
2016
|
"error_code": "TARGET_APP_REF_NOT_FOUND",
|
|
1988
2017
|
"message": str(error),
|
|
1989
2018
|
"safe_to_retry": False,
|
|
1990
|
-
}
|
|
1991
|
-
any_write_executed = True
|
|
1992
|
-
continue
|
|
1993
|
-
|
|
1994
|
-
deferred_add_fields = (
|
|
1995
|
-
_compiled_multi_app_deferred_add_fields(compiled_item, existing)
|
|
1996
|
-
if bool(existing.get("created"))
|
|
1997
|
-
else list(compiled_item.get("add_fields") or [])
|
|
1998
|
-
)
|
|
1999
|
-
update_fields = list(compiled_item.get("update_fields") or [])
|
|
2000
|
-
remove_fields = list(compiled_item.get("remove_fields") or [])
|
|
2001
|
-
if bool(existing.get("created")) and not deferred_add_fields and not update_fields and not remove_fields:
|
|
2002
|
-
shell_result = existing.get("shell_result") if isinstance(existing.get("shell_result"), dict) else {}
|
|
2003
|
-
item_status = shell_result.get("status") if shell_result.get("status") in {"success", "partial_success"} else "failed"
|
|
2004
|
-
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
2005
|
-
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
2006
|
-
final_items.append({
|
|
2007
|
-
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2008
|
-
"status": item_status,
|
|
2009
|
-
"stage": "schema_apply",
|
|
2010
|
-
"field_diff": shell_field_diff,
|
|
2011
|
-
"field_diff_details": shell_field_diff_details,
|
|
2012
|
-
"shell_field_diff": shell_field_diff,
|
|
2013
|
-
"shell_field_diff_details": shell_field_diff_details,
|
|
2014
|
-
"published": bool(shell_result.get("published")),
|
|
2015
|
-
"verified": bool(shell_result.get("verified")),
|
|
2016
|
-
"error_code": shell_result.get("error_code"),
|
|
2017
|
-
"message": shell_result.get("message"),
|
|
2018
|
-
"safe_to_retry": False,
|
|
2019
|
-
})
|
|
2020
|
-
continue
|
|
2021
|
-
|
|
2019
|
+
}
|
|
2022
2020
|
field_result = self._app_schema_apply_once(
|
|
2023
2021
|
profile=profile,
|
|
2024
2022
|
app_key=app_key,
|
|
@@ -2030,19 +2028,19 @@ class AiBuilderTools(ToolBase):
|
|
|
2030
2028
|
visibility=compiled_item.get("visibility"),
|
|
2031
2029
|
create_if_missing=False,
|
|
2032
2030
|
publish=publish,
|
|
2033
|
-
add_fields=
|
|
2034
|
-
update_fields=update_fields,
|
|
2035
|
-
remove_fields=remove_fields,
|
|
2031
|
+
add_fields=list(compiled_item.get("add_fields") or []),
|
|
2032
|
+
update_fields=list(compiled_item.get("update_fields") or []),
|
|
2033
|
+
remove_fields=list(compiled_item.get("remove_fields") or []),
|
|
2034
|
+
form=_multi_app_schema_form(compiled_item),
|
|
2036
2035
|
)
|
|
2037
2036
|
public_result = _publicize_package_fields(field_result)
|
|
2038
|
-
if _schema_apply_result_has_write(public_result):
|
|
2039
|
-
any_write_executed = True
|
|
2040
2037
|
item_status = public_result.get("status") if public_result.get("status") in {"success", "partial_success"} else "failed"
|
|
2041
2038
|
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
2042
2039
|
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
2043
2040
|
field_diff = _merge_schema_field_diffs(shell_field_diff, public_result.get("field_diff") or {})
|
|
2044
2041
|
field_diff_details = _merge_schema_field_diffs(shell_field_diff_details, public_result.get("field_diff_details") or {})
|
|
2045
|
-
|
|
2042
|
+
verification = public_result.get("verification") if isinstance(public_result.get("verification"), dict) else {}
|
|
2043
|
+
return {
|
|
2046
2044
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2047
2045
|
"status": item_status,
|
|
2048
2046
|
"stage": "schema_apply",
|
|
@@ -2052,44 +2050,48 @@ class AiBuilderTools(ToolBase):
|
|
|
2052
2050
|
"shell_field_diff_details": shell_field_diff_details,
|
|
2053
2051
|
"published": bool(public_result.get("published")),
|
|
2054
2052
|
"verified": bool(public_result.get("verified")),
|
|
2053
|
+
"form_layout_verified": verification.get("form_layout_verified"),
|
|
2055
2054
|
"error_code": public_result.get("error_code"),
|
|
2056
2055
|
"message": public_result.get("message"),
|
|
2057
2056
|
"safe_to_retry": False,
|
|
2058
|
-
|
|
2057
|
+
"schema_result": public_result,
|
|
2058
|
+
}
|
|
2059
2059
|
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2060
|
+
schema_started = time.perf_counter()
|
|
2061
|
+
schema_workers = min(SCHEMA_MULTI_APP_MAX_WORKERS, len(ready_indices)) if ready_indices else 0
|
|
2062
|
+
if schema_workers:
|
|
2063
|
+
with ThreadPoolExecutor(max_workers=schema_workers) as executor:
|
|
2064
|
+
future_map = {executor.submit(run_schema, index): index for index in ready_indices}
|
|
2065
|
+
for future in as_completed(future_map):
|
|
2066
|
+
index = future_map[future]
|
|
2067
|
+
try:
|
|
2068
|
+
final_by_index[index] = future.result()
|
|
2069
|
+
except Exception as error:
|
|
2070
|
+
existing = final_by_index[index] if isinstance(final_by_index[index], dict) else {}
|
|
2071
|
+
final_by_index[index] = {
|
|
2072
|
+
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2073
|
+
"status": "failed",
|
|
2074
|
+
"stage": "schema_apply",
|
|
2075
|
+
"error_code": "SCHEMA_APPLY_FAILED",
|
|
2076
|
+
"message": str(error),
|
|
2077
|
+
"safe_to_retry": False,
|
|
2078
|
+
}
|
|
2079
|
+
schema_wall_ms = int((time.perf_counter() - schema_started) * 1000)
|
|
2080
|
+
|
|
2081
|
+
final_items = [item for item in final_by_index if isinstance(item, dict)]
|
|
2082
|
+
any_write_executed = any(
|
|
2083
|
+
isinstance(item, dict)
|
|
2084
|
+
and (
|
|
2085
|
+
_schema_apply_result_has_write(cast(JSONObject, item.get("shell_result") or {}))
|
|
2086
|
+
or _schema_apply_result_has_write(cast(JSONObject, item.get("schema_result") or {}))
|
|
2087
|
+
or bool(item.get("created"))
|
|
2088
|
+
)
|
|
2089
|
+
for item in final_items
|
|
2090
|
+
)
|
|
2091
|
+
if any_write_executed:
|
|
2092
|
+
for item in final_items:
|
|
2093
|
+
if isinstance(item, dict) and item.get("status") == "failed":
|
|
2094
|
+
item["safe_to_retry"] = False
|
|
2093
2095
|
|
|
2094
2096
|
succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
|
|
2095
2097
|
failed = len(final_items) - succeeded
|
|
@@ -2107,6 +2109,18 @@ class AiBuilderTools(ToolBase):
|
|
|
2107
2109
|
"publish_requested": publish,
|
|
2108
2110
|
"apps": final_items,
|
|
2109
2111
|
"normalized_args": normalized_args,
|
|
2112
|
+
"parallelism": {
|
|
2113
|
+
"shell_workers": shell_workers,
|
|
2114
|
+
"schema_workers": schema_workers,
|
|
2115
|
+
},
|
|
2116
|
+
"duration_ms": {
|
|
2117
|
+
"app_shell_parallel_wall": shell_wall_ms,
|
|
2118
|
+
"relation_compile": 0,
|
|
2119
|
+
"app_schema_parallel_wall": schema_wall_ms,
|
|
2120
|
+
"native_inline_layout": schema_wall_ms if any(bool(item.get("form_layout_verified")) for item in final_items if isinstance(item, dict)) else 0,
|
|
2121
|
+
"inline_layout_fallback": 0,
|
|
2122
|
+
"total": int((time.perf_counter() - started) * 1000),
|
|
2123
|
+
},
|
|
2110
2124
|
"verification": {
|
|
2111
2125
|
"all_apps_succeeded": failed == 0,
|
|
2112
2126
|
"created_app_count": len(created_app_keys),
|
|
@@ -2133,9 +2147,25 @@ class AiBuilderTools(ToolBase):
|
|
|
2133
2147
|
add_fields: list[JSONObject],
|
|
2134
2148
|
update_fields: list[JSONObject],
|
|
2135
2149
|
remove_fields: list[JSONObject],
|
|
2150
|
+
form: JSONObject | None = None,
|
|
2136
2151
|
) -> JSONObject:
|
|
2137
2152
|
"""执行内部辅助逻辑。"""
|
|
2138
2153
|
effective_app_name = app_name or app_title
|
|
2154
|
+
try:
|
|
2155
|
+
form_layout_mode, form_layout_sections, form_layout_requested = _extract_schema_form_layout(form)
|
|
2156
|
+
parsed_form_layout_sections = (
|
|
2157
|
+
[LayoutSectionPatch.model_validate(item) for item in form_layout_sections]
|
|
2158
|
+
if form_layout_requested
|
|
2159
|
+
else None
|
|
2160
|
+
)
|
|
2161
|
+
except (TypeError, ValueError, ValidationError) as exc:
|
|
2162
|
+
return _config_failure(
|
|
2163
|
+
tool_name="app_schema_apply",
|
|
2164
|
+
message="invalid form layout payload",
|
|
2165
|
+
fix_hint='Use "form": {"sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}]}.',
|
|
2166
|
+
error_code="FORM_LAYOUT_INVALID",
|
|
2167
|
+
details={"validation_detail": str(exc)},
|
|
2168
|
+
)
|
|
2139
2169
|
icon_failure = _validate_workspace_icon_for_builder(
|
|
2140
2170
|
tool_name="app_schema_apply",
|
|
2141
2171
|
icon=icon,
|
|
@@ -2192,6 +2222,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2192
2222
|
"add_fields": plan_args.get("add_fields") or [{"name": "字段名称", "type": "text", "required": False}],
|
|
2193
2223
|
"update_fields": plan_args.get("update_fields") or [],
|
|
2194
2224
|
"remove_fields": plan_args.get("remove_fields") or [],
|
|
2225
|
+
**({"form": {"mode": form_layout_mode.value, "sections": form_layout_sections}} if form_layout_requested else {}),
|
|
2195
2226
|
},
|
|
2196
2227
|
},
|
|
2197
2228
|
)
|
|
@@ -2208,6 +2239,8 @@ class AiBuilderTools(ToolBase):
|
|
|
2208
2239
|
"update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
|
|
2209
2240
|
"remove_fields": [patch.model_dump(mode="json") for patch in parsed_remove],
|
|
2210
2241
|
}
|
|
2242
|
+
if form_layout_requested:
|
|
2243
|
+
normalized_args["form"] = {"mode": form_layout_mode.value, "sections": form_layout_sections}
|
|
2211
2244
|
result = _safe_tool_call(
|
|
2212
2245
|
lambda: self._facade.app_schema_apply(
|
|
2213
2246
|
profile=profile,
|
|
@@ -2222,6 +2255,8 @@ class AiBuilderTools(ToolBase):
|
|
|
2222
2255
|
add_fields=parsed_add,
|
|
2223
2256
|
update_fields=parsed_update,
|
|
2224
2257
|
remove_fields=parsed_remove,
|
|
2258
|
+
layout_mode=form_layout_mode,
|
|
2259
|
+
layout_sections=parsed_form_layout_sections,
|
|
2225
2260
|
),
|
|
2226
2261
|
error_code="SCHEMA_APPLY_FAILED",
|
|
2227
2262
|
normalized_args=normalized_args,
|
|
@@ -2626,7 +2661,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2626
2661
|
"verified": verified,
|
|
2627
2662
|
"write_executed": write_executed,
|
|
2628
2663
|
"safe_to_retry": not write_executed,
|
|
2629
|
-
"parallelism": {"view_workers":
|
|
2664
|
+
"parallelism": {"view_workers": workers, "same_app_parallel": True},
|
|
2630
2665
|
"duration_ms": {
|
|
2631
2666
|
"normalize": normalized_ms,
|
|
2632
2667
|
"view_write_parallel_wall": write_ms,
|
|
@@ -3561,54 +3596,34 @@ def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key:
|
|
|
3561
3596
|
return visit(compiled)
|
|
3562
3597
|
|
|
3563
3598
|
|
|
3564
|
-
def
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
def _multi_app_list_value(item: JSONObject, *keys: str) -> list[JSONObject]:
|
|
3594
|
-
for key in keys:
|
|
3595
|
-
value = item.get(key)
|
|
3596
|
-
if isinstance(value, list):
|
|
3597
|
-
return [deepcopy(entry) for entry in value if isinstance(entry, dict)]
|
|
3598
|
-
return []
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
def _contains_multi_app_target_ref(value: object) -> bool:
|
|
3602
|
-
if isinstance(value, list):
|
|
3603
|
-
return any(_contains_multi_app_target_ref(item) for item in value)
|
|
3604
|
-
if not isinstance(value, dict):
|
|
3605
|
-
return False
|
|
3606
|
-
for key, entry in value.items():
|
|
3607
|
-
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
|
|
3608
|
-
return True
|
|
3609
|
-
if _contains_multi_app_target_ref(entry):
|
|
3610
|
-
return True
|
|
3611
|
-
return False
|
|
3599
|
+
def _extract_schema_form_layout(form: object) -> tuple[LayoutApplyMode, list[JSONObject], bool]:
|
|
3600
|
+
if form is None:
|
|
3601
|
+
return LayoutApplyMode.merge, [], False
|
|
3602
|
+
if not isinstance(form, dict):
|
|
3603
|
+
raise TypeError("form must be an object")
|
|
3604
|
+
raw_mode = str(form.get("mode") or "merge").strip().lower() or "merge"
|
|
3605
|
+
if raw_mode == "overwrite":
|
|
3606
|
+
raw_mode = "replace"
|
|
3607
|
+
try:
|
|
3608
|
+
mode = LayoutApplyMode(raw_mode)
|
|
3609
|
+
except ValueError as exc:
|
|
3610
|
+
raise ValueError("form.mode must be merge or replace") from exc
|
|
3611
|
+
sections = form.get("sections")
|
|
3612
|
+
if sections is None:
|
|
3613
|
+
return mode, [], False
|
|
3614
|
+
if not isinstance(sections, list):
|
|
3615
|
+
raise TypeError("form.sections must be a JSON array")
|
|
3616
|
+
return mode, [deepcopy(item) for item in sections if isinstance(item, dict)], True
|
|
3617
|
+
|
|
3618
|
+
|
|
3619
|
+
def _multi_app_schema_form(item: JSONObject) -> JSONObject | None:
|
|
3620
|
+
form = item.get("form")
|
|
3621
|
+
if isinstance(form, dict):
|
|
3622
|
+
return deepcopy(form)
|
|
3623
|
+
legacy_layout = item.get("layout")
|
|
3624
|
+
if isinstance(legacy_layout, dict):
|
|
3625
|
+
return deepcopy(legacy_layout)
|
|
3626
|
+
return None
|
|
3612
3627
|
|
|
3613
3628
|
|
|
3614
3629
|
def _merge_schema_field_diffs(*diffs: object) -> JSONObject:
|
|
@@ -3632,6 +3647,11 @@ def _schema_apply_result_has_write(result: JSONObject) -> bool:
|
|
|
3632
3647
|
field_diff = result.get("field_diff")
|
|
3633
3648
|
if isinstance(field_diff, dict):
|
|
3634
3649
|
return any(bool(field_diff.get(key)) for key in ("added", "updated", "removed"))
|
|
3650
|
+
details = result.get("details")
|
|
3651
|
+
if isinstance(details, dict):
|
|
3652
|
+
form_layout_diff = details.get("form_layout_diff")
|
|
3653
|
+
if isinstance(form_layout_diff, dict) and bool(form_layout_diff.get("changed")):
|
|
3654
|
+
return True
|
|
3635
3655
|
return False
|
|
3636
3656
|
|
|
3637
3657
|
|
|
@@ -5340,6 +5360,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5340
5360
|
"add_fields",
|
|
5341
5361
|
"update_fields",
|
|
5342
5362
|
"remove_fields",
|
|
5363
|
+
"form",
|
|
5364
|
+
"form.mode",
|
|
5365
|
+
"form.sections",
|
|
5343
5366
|
"apps",
|
|
5344
5367
|
"apps[].client_key",
|
|
5345
5368
|
"apps[].app_key",
|
|
@@ -5350,6 +5373,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5350
5373
|
"apps[].add_fields",
|
|
5351
5374
|
"apps[].update_fields",
|
|
5352
5375
|
"apps[].remove_fields",
|
|
5376
|
+
"apps[].form",
|
|
5377
|
+
"apps[].form.mode",
|
|
5378
|
+
"apps[].form.sections",
|
|
5353
5379
|
"apps[].add_fields[].target_app_ref",
|
|
5354
5380
|
],
|
|
5355
5381
|
"aliases": {
|
|
@@ -5360,6 +5386,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5360
5386
|
"apps[].appKey": "apps[].app_key",
|
|
5361
5387
|
"apps[].appName": "apps[].app_name",
|
|
5362
5388
|
"apps[].appTitle": "apps[].app_name",
|
|
5389
|
+
"apps[].layout": "apps[].form",
|
|
5363
5390
|
"field.targetAppRef": "field.target_app_ref",
|
|
5364
5391
|
"field.targetAppClientKey": "field.target_app_ref",
|
|
5365
5392
|
"field.title": "field.name",
|
|
@@ -5400,6 +5427,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5400
5427
|
"create mode: package_id + app_name + create_if_missing=true",
|
|
5401
5428
|
"multi-app mode: pass package_id + create_if_missing + apps[]; do not mix apps with top-level app_key/app_name/add_fields/update_fields/remove_fields",
|
|
5402
5429
|
"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",
|
|
5430
|
+
"multi-app form layout belongs in apps[].form.sections and is written in the same schema save as fields and relation fields",
|
|
5431
|
+
"multi-app shell and schema stages run in parallel with an internal max worker count of 10; response order still follows apps[] order",
|
|
5403
5432
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
5404
5433
|
"create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
|
|
5405
5434
|
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
@@ -5408,8 +5437,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5408
5437
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
5409
5438
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
5410
5439
|
"update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
|
|
5411
|
-
"multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
|
|
5412
|
-
"backend 49614 is normalized to MULTIPLE_RELATION_FIELDS_UNSUPPORTED with a workaround message",
|
|
5413
5440
|
"relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
|
|
5414
5441
|
"relation fields now require both display_field and visible_fields in MCP/CLI payloads",
|
|
5415
5442
|
"if relation target metadata lookup is blocked by 40161/40002/40027, explicit display_field.name and visible_fields[].name let builder degrade verification and still continue schema write",
|
|
@@ -6204,7 +6231,23 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6204
6231
|
},
|
|
6205
6232
|
},
|
|
6206
6233
|
"app_charts_apply": {
|
|
6207
|
-
"allowed_keys": [
|
|
6234
|
+
"allowed_keys": [
|
|
6235
|
+
"app_key",
|
|
6236
|
+
"apps",
|
|
6237
|
+
"upsert_charts",
|
|
6238
|
+
"patch_charts",
|
|
6239
|
+
"remove_chart_ids",
|
|
6240
|
+
"reorder_chart_ids",
|
|
6241
|
+
"upsert_charts[].metric",
|
|
6242
|
+
"upsert_charts[].metrics",
|
|
6243
|
+
"upsert_charts[].group_by",
|
|
6244
|
+
"upsert_charts[].where",
|
|
6245
|
+
"upsert_charts[].visibility",
|
|
6246
|
+
"patch_charts[].chart_id",
|
|
6247
|
+
"patch_charts[].name",
|
|
6248
|
+
"patch_charts[].set",
|
|
6249
|
+
"patch_charts[].unset",
|
|
6250
|
+
],
|
|
6208
6251
|
"aliases": {
|
|
6209
6252
|
"patchCharts": "patch_charts",
|
|
6210
6253
|
"chart.id": "chart.chart_id",
|
|
@@ -6212,6 +6255,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6212
6255
|
"chart.dimension_fields": "chart.dimension_field_ids",
|
|
6213
6256
|
"chart.indicator_fields": "chart.indicator_field_ids",
|
|
6214
6257
|
"chart.metric_field_ids": "chart.indicator_field_ids",
|
|
6258
|
+
"chart.dimensions": "chart.group_by",
|
|
6259
|
+
"chart.groupBy": "chart.group_by",
|
|
6260
|
+
"chart.where": "chart.filters",
|
|
6261
|
+
"chart.metric.operation": "chart.metric.op",
|
|
6262
|
+
"chart.metric.aggregation": "chart.metric.op",
|
|
6215
6263
|
"chart.filter.op": "chart.filter.operator",
|
|
6216
6264
|
},
|
|
6217
6265
|
"allowed_values": {
|
|
@@ -6232,6 +6280,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6232
6280
|
"upsert_charts[].visibility compiles to QingBI base visibleAuth only",
|
|
6233
6281
|
"visibility-only updates keep the existing chart config and do not rewrite rawDataConfigDTO.authInfo",
|
|
6234
6282
|
"chart dimension/metric/filter/query fields are resolved from app_get_fields.chart_fields (QingBI datasource fields), not record schema or form-only fields",
|
|
6283
|
+
"preferred chart metric DSL is SQL-like: metric='count(*)', metric='sum(金额)', metrics=['sum(金额)'], group_by=['状态'], where=[{field, op, value}]",
|
|
6284
|
+
"legacy dimension_field_ids/indicator_field_ids/config.aggregate remain supported as advanced compatibility input, but should not be the first choice for agents",
|
|
6285
|
+
"chart_get returns semantic group_by and metrics; raw QingBI config is diagnostic detail",
|
|
6235
6286
|
"system fields such as 申请人/申请时间/编号 are usable only when they appear in chart_fields; otherwise app_charts_apply returns CHART_FIELD_NOT_IN_QINGBI_SCHEMA",
|
|
6236
6287
|
"low-frequency chart types have local prevalidation: gauge requires 0 dimensions and 2 non-duplicated metrics; histogram requires at most 1 dimension and exactly 1 plain numeric metric",
|
|
6237
6288
|
"chart rule failures return chart_results[].diagnostics with rule_code, expected, actual, offending_fields, and next_action; backend 81002/81005 are translated when possible",
|
|
@@ -6243,7 +6294,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6243
6294
|
"minimal_example": {
|
|
6244
6295
|
"profile": "default",
|
|
6245
6296
|
"app_key": "APP_KEY",
|
|
6246
|
-
"upsert_charts": [{"name": "数据总量", "chart_type": "target", "
|
|
6297
|
+
"upsert_charts": [{"name": "数据总量", "chart_type": "target", "metric": "count(*)", "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
|
|
6247
6298
|
"patch_charts": [{"chart_id": "CHART_ID", "set": {"name": "数据总量-新版"}}],
|
|
6248
6299
|
"remove_chart_ids": [],
|
|
6249
6300
|
"reorder_chart_ids": [],
|