@josephyan/qingflow-app-user-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.
Files changed (47) hide show
  1. package/README.md +4 -6
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/skills/qingflow-app-user/SKILL.md +5 -3
  5. package/skills/qingflow-app-user/references/public-surface-sync.md +4 -2
  6. package/src/qingflow_mcp/builder_facade/models.py +167 -71
  7. package/src/qingflow_mcp/builder_facade/service.py +617 -155
  8. package/src/qingflow_mcp/cli/commands/builder.py +3 -1
  9. package/src/qingflow_mcp/cli/main.py +8 -3
  10. package/src/qingflow_mcp/server_app_builder.py +9 -8
  11. package/src/qingflow_mcp/tools/ai_builder_tools.py +231 -180
  12. package/src/qingflow_mcp/version.py +2 -0
  13. package/skills/qingflow-app-builder/SKILL.md +0 -277
  14. package/skills/qingflow-app-builder/agents/openai.yaml +0 -4
  15. package/skills/qingflow-app-builder/references/build-complete-system.md +0 -428
  16. package/skills/qingflow-app-builder/references/build-single-app.md +0 -530
  17. package/skills/qingflow-app-builder/references/create-app.md +0 -149
  18. package/skills/qingflow-app-builder/references/environments.md +0 -63
  19. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +0 -123
  20. package/skills/qingflow-app-builder/references/gotchas.md +0 -108
  21. package/skills/qingflow-app-builder/references/match-rules.md +0 -114
  22. package/skills/qingflow-app-builder/references/public-surface-sync.md +0 -75
  23. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -52
  24. package/skills/qingflow-app-builder/references/tool-selection.md +0 -107
  25. package/skills/qingflow-app-builder/references/update-flow.md +0 -186
  26. package/skills/qingflow-app-builder/references/update-layout.md +0 -68
  27. package/skills/qingflow-app-builder/references/update-schema.md +0 -72
  28. package/skills/qingflow-app-builder/references/update-views.md +0 -291
  29. package/skills/qingflow-app-builder-code-integrations/SKILL.md +0 -137
  30. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +0 -4
  31. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +0 -66
  32. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +0 -77
  33. package/skills/qingflow-workflow-builder/SKILL.md +0 -96
  34. package/skills/qingflow-workflow-builder/manifest.yaml +0 -8
  35. package/skills/qingflow-workflow-builder/references/01-overview.md +0 -45
  36. package/skills/qingflow-workflow-builder/references/02-update-mode.md +0 -53
  37. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +0 -57
  38. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +0 -131
  39. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +0 -29
  40. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +0 -165
  41. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +0 -33
  42. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +0 -51
  43. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +0 -88
  44. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +0 -93
  45. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +0 -15
  46. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +0 -275
  47. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +0 -605
@@ -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
- client_key_to_app_key: dict[str, str] = {}
1896
- created_app_keys: list[str] = []
1897
- results: list[JSONObject] = []
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
- results.append(_multi_app_item_failure(index, raw_item, "INVALID_APP_ITEM", "apps[] items must be objects"))
1907
+ final_by_index[index] = _multi_app_item_failure(index, raw_item, "INVALID_APP_ITEM", "apps[] items must be objects")
1904
1908
  continue
1905
- item = deepcopy(raw_item)
1906
- client_key = str(item.get("client_key") or item.get("clientKey") or "").strip()
1907
- app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip()
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
- results.append(_multi_app_item_failure(index, item, "DUPLICATE_CLIENT_KEY", f"duplicate client_key '{client_key}'"))
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
- results.append(_multi_app_item_failure(index, item, "APP_SELECTOR_REQUIRED", "apps[] requires app_key or app_name"))
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
- initial_add_fields, deferred_add_fields = _split_multi_app_initial_add_fields(item, is_new_app=not bool(app_key))
1919
- item["_deferred_add_fields"] = deferred_add_fields
1920
- shell = self._app_schema_apply_once(
1921
- profile=profile,
1922
- app_key=app_key,
1923
- package_id=package_id if not app_key else None,
1924
- app_name=app_name,
1925
- app_title="",
1926
- icon=str(item.get("icon") or ""),
1927
- color=str(item.get("color") or ""),
1928
- visibility=item.get("visibility", visibility),
1929
- create_if_missing=create_if_missing and not app_key,
1930
- publish=publish and not deferred_add_fields,
1931
- add_fields=initial_add_fields,
1932
- update_fields=[],
1933
- remove_fields=[],
1934
- )
1935
- public_shell = _publicize_package_fields(shell)
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
- results.append({
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": not any_write_executed,
1949
- })
1950
- continue
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
- "deferred_add_fields": deferred_add_fields,
1969
- })
1972
+ }
1970
1973
 
1971
- final_items: list[JSONObject] = []
1972
- for index, raw_item in enumerate(apps):
1973
- existing = next((item for item in results if item.get("index") == index), None)
1974
- if not existing or existing.get("status") != "shell_ready":
1975
- if existing:
1976
- final_items.append(existing)
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 = deepcopy(raw_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(item, client_key_to_app_key)
2010
+ compiled_item = _compile_multi_app_schema_item_refs(deepcopy(raw_item), client_key_to_app_key)
1982
2011
  except ValueError as error:
1983
- final_items.append({
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=deferred_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
- final_items.append({
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
- # Inline layout: apply layout for each successful app that included a layout field.
2061
- for index, raw_item in enumerate(apps):
2062
- if not isinstance(raw_item, dict):
2063
- continue
2064
- layout_spec = raw_item.get("layout")
2065
- if not isinstance(layout_spec, dict) or not layout_spec:
2066
- continue
2067
- final_item = next((it for it in final_items if it.get("index") == index), None)
2068
- if not final_item or final_item.get("status") not in {"success", "partial_success"}:
2069
- continue
2070
- resolved_app_key = str(final_item.get("app_key") or "").strip()
2071
- if not resolved_app_key:
2072
- continue
2073
- layout_mode = str(layout_spec.get("mode") or "merge").strip() or "merge"
2074
- layout_sections = layout_spec.get("sections") or []
2075
- try:
2076
- layout_result = self.app_layout_apply(
2077
- profile=profile,
2078
- app_key=resolved_app_key,
2079
- mode=layout_mode,
2080
- publish=publish,
2081
- sections=list(layout_sections),
2082
- )
2083
- layout_ok = layout_result.get("status") in {"success", "partial_success"}
2084
- except Exception as layout_error:
2085
- layout_ok = False
2086
- layout_result = {"status": "failed", "message": str(layout_error)}
2087
- if layout_ok:
2088
- final_item["layout_applied"] = True
2089
- final_item["layout_status"] = layout_result.get("status")
2090
- else:
2091
- final_item["layout_warning"] = layout_result.get("error_code") or "LAYOUT_APPLY_FAILED"
2092
- final_item["layout_message"] = layout_result.get("message")
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": VIEW_BATCH_MAX_WORKERS, "same_app_parallel": True},
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 _split_multi_app_initial_add_fields(item: JSONObject, *, is_new_app: bool) -> tuple[list[JSONObject], list[JSONObject]]:
3565
- add_fields = _multi_app_list_value(item, "add_fields", "addFields")
3566
- if not is_new_app:
3567
- return [], add_fields
3568
- initial: list[JSONObject] = []
3569
- deferred: list[JSONObject] = []
3570
- for field in add_fields:
3571
- if _contains_multi_app_target_ref(field):
3572
- deferred.append(field)
3573
- else:
3574
- initial.append(field)
3575
- return initial, deferred
3576
-
3577
-
3578
- def _compiled_multi_app_deferred_add_fields(compiled_item: JSONObject, existing_result: JSONObject) -> list[JSONObject]:
3579
- deferred = existing_result.get("deferred_add_fields")
3580
- if not isinstance(deferred, list):
3581
- return list(compiled_item.get("add_fields") or [])
3582
- deferred_names = {str(item.get("name") or item.get("title") or item.get("label") or "").strip() for item in deferred if isinstance(item, dict)}
3583
- if not deferred_names:
3584
- return []
3585
- return [
3586
- deepcopy(field)
3587
- for field in list(compiled_item.get("add_fields") or [])
3588
- if isinstance(field, dict)
3589
- and str(field.get("name") or field.get("title") or field.get("label") or "").strip() in deferred_names
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": ["app_key", "apps", "upsert_charts", "patch_charts", "remove_chart_ids", "reorder_chart_ids", "upsert_charts[].visibility", "patch_charts[].chart_id", "patch_charts[].name", "patch_charts[].set", "patch_charts[].unset"],
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", "indicator_field_ids": [], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
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": [],