@qingflow-tech/qingflow-app-builder-mcp 1.0.9 → 1.0.11
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 +8 -0
- package/skills/qingflow-app-builder/references/create-app.md +39 -2
- package/skills/qingflow-app-builder/references/tool-selection.md +2 -2
- package/skills/qingflow-app-builder/references/update-views.md +2 -1
- package/src/qingflow_mcp/builder_facade/models.py +43 -2
- package/src/qingflow_mcp/builder_facade/service.py +1476 -172
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +68 -13
- package/src/qingflow_mcp/cli/commands/record.py +16 -1
- package/src/qingflow_mcp/cli/formatters.py +32 -1
- package/src/qingflow_mcp/cli/main.py +204 -3
- package/src/qingflow_mcp/public_surface.py +3 -1
- package/src/qingflow_mcp/response_trim.py +70 -13
- package/src/qingflow_mcp/server.py +10 -9
- package/src/qingflow_mcp/server_app_builder.py +53 -7
- package/src/qingflow_mcp/server_app_user.py +12 -15
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1642 -75
- package/src/qingflow_mcp/tools/app_tools.py +53 -8
- package/src/qingflow_mcp/tools/package_tools.py +16 -2
- package/src/qingflow_mcp/tools/record_tools.py +1423 -70
- package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from copy import deepcopy
|
|
4
4
|
import json
|
|
5
5
|
import time
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
from pydantic import ValidationError
|
|
8
9
|
|
|
@@ -49,6 +50,15 @@ from ..builder_facade.models import (
|
|
|
49
50
|
ViewsPlanRequest,
|
|
50
51
|
)
|
|
51
52
|
from ..builder_facade.service import AiBuilderFacade, INTEGRATION_OUTPUT_TARGET_FIELD_TYPES
|
|
53
|
+
from ..solution.compiler.icon_utils import (
|
|
54
|
+
GENERIC_WORKSPACE_ICON_NAMES,
|
|
55
|
+
WORKSPACE_ICON_COLORS,
|
|
56
|
+
WORKSPACE_ICON_NAMES,
|
|
57
|
+
normalize_workspace_icon_name,
|
|
58
|
+
validate_workspace_icon_choice,
|
|
59
|
+
workspace_icon_catalog_payload,
|
|
60
|
+
workspace_icon_config,
|
|
61
|
+
)
|
|
52
62
|
from .app_tools import AppTools
|
|
53
63
|
from .base import ToolBase, tool_cn_name
|
|
54
64
|
from .custom_button_tools import CustomButtonTools
|
|
@@ -70,6 +80,19 @@ def _normalize_builder_view_key(value: str) -> str:
|
|
|
70
80
|
|
|
71
81
|
|
|
72
82
|
PUBLIC_STABLE_FLOW_NODE_TYPES = ["start", "approve", "fill", "copy", "webhook", "end"]
|
|
83
|
+
BUILDER_APPLY_SCHEMA_VERSION = "builder.apply.v1"
|
|
84
|
+
BUILDER_APPLY_TOOL_NAMES = {
|
|
85
|
+
"package_apply",
|
|
86
|
+
"app_schema_apply",
|
|
87
|
+
"app_layout_apply",
|
|
88
|
+
"app_flow_apply",
|
|
89
|
+
"app_views_apply",
|
|
90
|
+
"app_custom_buttons_apply",
|
|
91
|
+
"app_associated_resources_apply",
|
|
92
|
+
"app_charts_apply",
|
|
93
|
+
"portal_apply",
|
|
94
|
+
"app_publish_verify",
|
|
95
|
+
}
|
|
73
96
|
|
|
74
97
|
|
|
75
98
|
class AiBuilderTools(ToolBase):
|
|
@@ -104,6 +127,14 @@ class AiBuilderTools(ToolBase):
|
|
|
104
127
|
def builder_tool_contract(tool_name: str = "") -> JSONObject:
|
|
105
128
|
return self.builder_tool_contract(tool_name=tool_name)
|
|
106
129
|
|
|
130
|
+
@mcp.tool()
|
|
131
|
+
def workspace_icon_catalog_get(profile: str = DEFAULT_PROFILE) -> JSONObject:
|
|
132
|
+
return self.workspace_icon_catalog_get(profile=profile)
|
|
133
|
+
|
|
134
|
+
@mcp.tool()
|
|
135
|
+
def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all", query: str = "") -> JSONObject:
|
|
136
|
+
return self.package_list(profile=profile, trial_status=trial_status, query=query)
|
|
137
|
+
|
|
107
138
|
@mcp.tool()
|
|
108
139
|
def package_get(profile: str = DEFAULT_PROFILE, package_id: int = 0) -> JSONObject:
|
|
109
140
|
return self.package_get(profile=profile, package_id=package_id)
|
|
@@ -340,7 +371,32 @@ class AiBuilderTools(ToolBase):
|
|
|
340
371
|
add_fields: list[JSONObject] | None = None,
|
|
341
372
|
update_fields: list[JSONObject] | None = None,
|
|
342
373
|
remove_fields: list[JSONObject] | None = None,
|
|
374
|
+
apps: list[JSONObject] | None = None,
|
|
343
375
|
) -> JSONObject:
|
|
376
|
+
if apps:
|
|
377
|
+
if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
|
|
378
|
+
return _config_failure(
|
|
379
|
+
tool_name="app_schema_apply",
|
|
380
|
+
message="app_schema_apply multi-app mode accepts package_id/create_if_missing plus apps only.",
|
|
381
|
+
fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
|
|
382
|
+
)
|
|
383
|
+
if package_id is None:
|
|
384
|
+
return _config_failure(
|
|
385
|
+
tool_name="app_schema_apply",
|
|
386
|
+
message="app_schema_apply multi-app mode requires package_id.",
|
|
387
|
+
fix_hint="Pass package_id and apps[].app_name for new apps, or apps[].app_key for existing apps.",
|
|
388
|
+
)
|
|
389
|
+
return self.app_schema_apply(
|
|
390
|
+
profile=profile,
|
|
391
|
+
package_id=package_id,
|
|
392
|
+
visibility=visibility,
|
|
393
|
+
create_if_missing=create_if_missing,
|
|
394
|
+
publish=publish,
|
|
395
|
+
apps=apps,
|
|
396
|
+
add_fields=[],
|
|
397
|
+
update_fields=[],
|
|
398
|
+
remove_fields=[],
|
|
399
|
+
)
|
|
344
400
|
has_app_key = bool((app_key or "").strip())
|
|
345
401
|
has_app_name = bool((app_name or "").strip())
|
|
346
402
|
has_app_title = bool((app_title or "").strip())
|
|
@@ -372,6 +428,7 @@ class AiBuilderTools(ToolBase):
|
|
|
372
428
|
add_fields=add_fields or [],
|
|
373
429
|
update_fields=update_fields or [],
|
|
374
430
|
remove_fields=remove_fields or [],
|
|
431
|
+
apps=[],
|
|
375
432
|
)
|
|
376
433
|
|
|
377
434
|
@mcp.tool()
|
|
@@ -443,9 +500,12 @@ class AiBuilderTools(ToolBase):
|
|
|
443
500
|
profile: str = DEFAULT_PROFILE,
|
|
444
501
|
dash_key: str = "",
|
|
445
502
|
dash_name: str = "",
|
|
503
|
+
name: str = "",
|
|
446
504
|
package_id: int | None = None,
|
|
447
505
|
publish: bool = True,
|
|
448
506
|
sections: list[JSONObject] | None = None,
|
|
507
|
+
pages: list[JSONObject] | None = None,
|
|
508
|
+
layout_preset: str = "",
|
|
449
509
|
visibility: JSONObject | None = None,
|
|
450
510
|
auth: JSONObject | None = None,
|
|
451
511
|
icon: str | None = None,
|
|
@@ -453,10 +513,14 @@ class AiBuilderTools(ToolBase):
|
|
|
453
513
|
hide_copyright: bool | None = None,
|
|
454
514
|
dash_global_config: JSONObject | None = None,
|
|
455
515
|
config: JSONObject | None = None,
|
|
516
|
+
payload: JSONObject | None = None,
|
|
456
517
|
) -> JSONObject:
|
|
518
|
+
payload = payload if isinstance(payload, dict) else {}
|
|
457
519
|
has_dash_key = bool((dash_key or "").strip())
|
|
458
|
-
|
|
459
|
-
|
|
520
|
+
effective_dash_name = (dash_name or name or str(payload.get("dash_name") or payload.get("dashName") or payload.get("name") or "")).strip()
|
|
521
|
+
has_dash_name = bool(effective_dash_name)
|
|
522
|
+
effective_package_id = package_id if package_id is not None else payload.get("package_id") or payload.get("packageId") or payload.get("package_tag_id")
|
|
523
|
+
has_package_id = effective_package_id is not None
|
|
460
524
|
if has_dash_key and has_package_id:
|
|
461
525
|
return _config_failure(
|
|
462
526
|
tool_name="portal_apply",
|
|
@@ -473,9 +537,12 @@ class AiBuilderTools(ToolBase):
|
|
|
473
537
|
profile=profile,
|
|
474
538
|
dash_key=dash_key,
|
|
475
539
|
dash_name=dash_name,
|
|
540
|
+
name=name,
|
|
476
541
|
package_id=package_id,
|
|
477
542
|
publish=publish,
|
|
478
543
|
sections=sections or [],
|
|
544
|
+
pages=pages or [],
|
|
545
|
+
layout_preset=layout_preset,
|
|
479
546
|
visibility=visibility,
|
|
480
547
|
auth=auth,
|
|
481
548
|
icon=icon,
|
|
@@ -483,6 +550,7 @@ class AiBuilderTools(ToolBase):
|
|
|
483
550
|
hide_copyright=hide_copyright,
|
|
484
551
|
dash_global_config=dash_global_config,
|
|
485
552
|
config=config or {},
|
|
553
|
+
payload=payload,
|
|
486
554
|
)
|
|
487
555
|
|
|
488
556
|
@mcp.tool()
|
|
@@ -498,14 +566,14 @@ class AiBuilderTools(ToolBase):
|
|
|
498
566
|
)
|
|
499
567
|
|
|
500
568
|
@tool_cn_name("分组列表查询")
|
|
501
|
-
def package_list(self, *, profile: str, trial_status: str = "all") -> JSONObject:
|
|
569
|
+
def package_list(self, *, profile: str, trial_status: str = "all", query: str = "") -> JSONObject:
|
|
502
570
|
"""执行分组与包相关逻辑。"""
|
|
503
|
-
normalized_args = {"trial_status": trial_status}
|
|
571
|
+
normalized_args = {"trial_status": trial_status, "query": query}
|
|
504
572
|
return _safe_tool_call(
|
|
505
|
-
lambda: self._facade.package_list(profile=profile, trial_status=trial_status),
|
|
573
|
+
lambda: self._facade.package_list(profile=profile, trial_status=trial_status, query=query),
|
|
506
574
|
error_code="PACKAGE_LIST_FAILED",
|
|
507
575
|
normalized_args=normalized_args,
|
|
508
|
-
suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": trial_status}},
|
|
576
|
+
suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": trial_status, "query": query}},
|
|
509
577
|
)
|
|
510
578
|
|
|
511
579
|
@tool_cn_name("分组解析")
|
|
@@ -550,6 +618,7 @@ class AiBuilderTools(ToolBase):
|
|
|
550
618
|
"verification": {},
|
|
551
619
|
"verified": False,
|
|
552
620
|
}
|
|
621
|
+
contract = _builder_contract_with_apply_output(lookup_name, contract)
|
|
553
622
|
return {
|
|
554
623
|
"status": "success",
|
|
555
624
|
"error_code": None,
|
|
@@ -571,6 +640,27 @@ class AiBuilderTools(ToolBase):
|
|
|
571
640
|
"contract": contract,
|
|
572
641
|
}
|
|
573
642
|
|
|
643
|
+
@tool_cn_name("工作区图标目录")
|
|
644
|
+
def workspace_icon_catalog_get(self, *, profile: str = DEFAULT_PROFILE) -> JSONObject:
|
|
645
|
+
"""读取应用、应用包、门户可用的工作区图标候选。"""
|
|
646
|
+
catalog = workspace_icon_catalog_payload()
|
|
647
|
+
return {
|
|
648
|
+
"status": "success",
|
|
649
|
+
"error_code": None,
|
|
650
|
+
"recoverable": False,
|
|
651
|
+
"message": "loaded workspace icon catalog",
|
|
652
|
+
"profile": profile,
|
|
653
|
+
"icon_names": catalog["icon_names"],
|
|
654
|
+
"icon_colors": catalog["icon_colors"],
|
|
655
|
+
"generic_icon_names": catalog["generic_icon_names"],
|
|
656
|
+
"common_examples": catalog["common_examples"],
|
|
657
|
+
"notes": catalog["notes"],
|
|
658
|
+
"count": len(catalog["icon_names"]),
|
|
659
|
+
"color_count": len(catalog["icon_colors"]),
|
|
660
|
+
"warnings": [],
|
|
661
|
+
"verification": {"source": "backend AiBuildConstant ICON_NAMES/ICON_COLORS"},
|
|
662
|
+
}
|
|
663
|
+
|
|
574
664
|
@tool_cn_name("分组创建")
|
|
575
665
|
def package_create(
|
|
576
666
|
self,
|
|
@@ -647,7 +737,18 @@ class AiBuilderTools(ToolBase):
|
|
|
647
737
|
try:
|
|
648
738
|
visibility_patch = VisibilityPatch.model_validate(visibility)
|
|
649
739
|
except ValidationError as exc:
|
|
650
|
-
return
|
|
740
|
+
return _attach_builder_apply_envelope(
|
|
741
|
+
"package_apply",
|
|
742
|
+
_visibility_validation_failure(str(exc), tool_name="package_apply", exc=exc),
|
|
743
|
+
)
|
|
744
|
+
icon_failure = _validate_workspace_icon_for_builder(
|
|
745
|
+
tool_name="package_apply",
|
|
746
|
+
icon=icon,
|
|
747
|
+
color=color,
|
|
748
|
+
creating=package_id is None and bool(create_if_missing),
|
|
749
|
+
)
|
|
750
|
+
if icon_failure is not None:
|
|
751
|
+
return _attach_builder_apply_envelope("package_apply", icon_failure)
|
|
651
752
|
normalized_args = {
|
|
652
753
|
"package_id": package_id,
|
|
653
754
|
**({"package_name": package_name} if str(package_name or "").strip() else {}),
|
|
@@ -658,7 +759,7 @@ class AiBuilderTools(ToolBase):
|
|
|
658
759
|
**({"items": deepcopy(items)} if items is not None else {}),
|
|
659
760
|
"allow_detach": bool(allow_detach),
|
|
660
761
|
}
|
|
661
|
-
|
|
762
|
+
result = _publicize_package_fields(_safe_tool_call(
|
|
662
763
|
lambda: self._facade.package_apply(
|
|
663
764
|
profile=profile,
|
|
664
765
|
package_id=package_id,
|
|
@@ -674,6 +775,7 @@ class AiBuilderTools(ToolBase):
|
|
|
674
775
|
normalized_args=normalized_args,
|
|
675
776
|
suggested_next_call={"tool_name": "package_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
676
777
|
))
|
|
778
|
+
return _attach_builder_apply_envelope("package_apply", result)
|
|
677
779
|
|
|
678
780
|
@tool_cn_name("分组更新")
|
|
679
781
|
def package_update(
|
|
@@ -914,7 +1016,7 @@ class AiBuilderTools(ToolBase):
|
|
|
914
1016
|
try:
|
|
915
1017
|
request = CustomButtonsApplyRequest.model_validate(raw_request)
|
|
916
1018
|
except ValidationError as exc:
|
|
917
|
-
return _validation_failure(
|
|
1019
|
+
return _attach_builder_apply_envelope("app_custom_buttons_apply", _validation_failure(
|
|
918
1020
|
str(exc),
|
|
919
1021
|
tool_name="app_custom_buttons_apply",
|
|
920
1022
|
exc=exc,
|
|
@@ -936,14 +1038,14 @@ class AiBuilderTools(ToolBase):
|
|
|
936
1038
|
"view_configs": [],
|
|
937
1039
|
},
|
|
938
1040
|
},
|
|
939
|
-
)
|
|
1041
|
+
))
|
|
940
1042
|
normalized_args = request.model_dump(mode="json")
|
|
941
|
-
return _safe_tool_call(
|
|
1043
|
+
return _attach_builder_apply_envelope("app_custom_buttons_apply", _safe_tool_call(
|
|
942
1044
|
lambda: self._facade.app_custom_buttons_apply(profile=profile, request=request),
|
|
943
1045
|
error_code="CUSTOM_BUTTONS_APPLY_FAILED",
|
|
944
1046
|
normalized_args=normalized_args,
|
|
945
1047
|
suggested_next_call={"tool_name": "app_custom_buttons_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
946
|
-
)
|
|
1048
|
+
))
|
|
947
1049
|
|
|
948
1050
|
@tool_cn_name("应用关联资源声明式应用")
|
|
949
1051
|
def app_associated_resources_apply(
|
|
@@ -969,7 +1071,7 @@ class AiBuilderTools(ToolBase):
|
|
|
969
1071
|
try:
|
|
970
1072
|
request = AssociatedResourcesApplyRequest.model_validate(raw_request)
|
|
971
1073
|
except ValidationError as exc:
|
|
972
|
-
return _validation_failure(
|
|
1074
|
+
return _attach_builder_apply_envelope("app_associated_resources_apply", _validation_failure(
|
|
973
1075
|
str(exc),
|
|
974
1076
|
tool_name="app_associated_resources_apply",
|
|
975
1077
|
exc=exc,
|
|
@@ -996,14 +1098,14 @@ class AiBuilderTools(ToolBase):
|
|
|
996
1098
|
],
|
|
997
1099
|
},
|
|
998
1100
|
},
|
|
999
|
-
)
|
|
1101
|
+
))
|
|
1000
1102
|
normalized_args = request.model_dump(mode="json", exclude_none=True)
|
|
1001
|
-
return _safe_tool_call(
|
|
1103
|
+
return _attach_builder_apply_envelope("app_associated_resources_apply", _safe_tool_call(
|
|
1002
1104
|
lambda: self._facade.app_associated_resources_apply(profile=profile, request=request),
|
|
1003
1105
|
error_code="ASSOCIATED_RESOURCES_APPLY_FAILED",
|
|
1004
1106
|
normalized_args=normalized_args,
|
|
1005
1107
|
suggested_next_call={"tool_name": "app_associated_resources_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
1006
|
-
)
|
|
1108
|
+
))
|
|
1007
1109
|
|
|
1008
1110
|
@tool_cn_name("应用按钮列表")
|
|
1009
1111
|
def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
|
|
@@ -1534,8 +1636,19 @@ class AiBuilderTools(ToolBase):
|
|
|
1534
1636
|
add_fields: list[JSONObject],
|
|
1535
1637
|
update_fields: list[JSONObject],
|
|
1536
1638
|
remove_fields: list[JSONObject],
|
|
1639
|
+
apps: list[JSONObject] | None = None,
|
|
1537
1640
|
) -> JSONObject:
|
|
1538
1641
|
"""执行应用相关逻辑。"""
|
|
1642
|
+
if apps:
|
|
1643
|
+
result = self._app_schema_apply_multi(
|
|
1644
|
+
profile=profile,
|
|
1645
|
+
package_id=package_id,
|
|
1646
|
+
visibility=visibility,
|
|
1647
|
+
create_if_missing=create_if_missing,
|
|
1648
|
+
publish=publish,
|
|
1649
|
+
apps=apps,
|
|
1650
|
+
)
|
|
1651
|
+
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
1539
1652
|
result = self._app_schema_apply_once(
|
|
1540
1653
|
profile=profile,
|
|
1541
1654
|
app_key=app_key,
|
|
@@ -1551,7 +1664,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1551
1664
|
update_fields=update_fields,
|
|
1552
1665
|
remove_fields=remove_fields,
|
|
1553
1666
|
)
|
|
1554
|
-
|
|
1667
|
+
result = self._retry_after_self_lock_release(
|
|
1555
1668
|
profile=profile,
|
|
1556
1669
|
result=result,
|
|
1557
1670
|
retry_call=lambda: self._app_schema_apply_once(
|
|
@@ -1570,6 +1683,291 @@ class AiBuilderTools(ToolBase):
|
|
|
1570
1683
|
remove_fields=remove_fields,
|
|
1571
1684
|
),
|
|
1572
1685
|
)
|
|
1686
|
+
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
1687
|
+
|
|
1688
|
+
def _app_schema_apply_multi(
|
|
1689
|
+
self,
|
|
1690
|
+
*,
|
|
1691
|
+
profile: str,
|
|
1692
|
+
package_id: int | None,
|
|
1693
|
+
visibility: JSONObject | None,
|
|
1694
|
+
create_if_missing: bool,
|
|
1695
|
+
publish: bool,
|
|
1696
|
+
apps: list[JSONObject],
|
|
1697
|
+
) -> JSONObject:
|
|
1698
|
+
normalized_args: JSONObject = {
|
|
1699
|
+
"package_id": package_id,
|
|
1700
|
+
"create_if_missing": create_if_missing,
|
|
1701
|
+
"publish": publish,
|
|
1702
|
+
"apps": deepcopy(apps),
|
|
1703
|
+
}
|
|
1704
|
+
if visibility is not None:
|
|
1705
|
+
normalized_args["visibility"] = deepcopy(visibility)
|
|
1706
|
+
if package_id is None:
|
|
1707
|
+
return _config_failure(
|
|
1708
|
+
tool_name="app_schema_apply",
|
|
1709
|
+
message="app_schema_apply multi-app mode requires package_id.",
|
|
1710
|
+
fix_hint="Pass package_id and apps[].app_name for new apps, or apps[].app_key for existing apps.",
|
|
1711
|
+
)
|
|
1712
|
+
if not apps:
|
|
1713
|
+
return _config_failure(
|
|
1714
|
+
tool_name="app_schema_apply",
|
|
1715
|
+
message="app_schema_apply multi-app mode requires non-empty apps.",
|
|
1716
|
+
fix_hint="Pass apps as a non-empty list of app schema items.",
|
|
1717
|
+
)
|
|
1718
|
+
icon_errors: list[JSONObject] = []
|
|
1719
|
+
seen_new_app_icons: dict[str, int] = {}
|
|
1720
|
+
for index, raw_item in enumerate(apps):
|
|
1721
|
+
if not isinstance(raw_item, dict):
|
|
1722
|
+
continue
|
|
1723
|
+
app_key = str(raw_item.get("app_key") or raw_item.get("appKey") or "").strip()
|
|
1724
|
+
creating_item = not app_key
|
|
1725
|
+
icon_failure = _validate_workspace_icon_for_builder(
|
|
1726
|
+
tool_name="app_schema_apply",
|
|
1727
|
+
icon=str(raw_item.get("icon") or ""),
|
|
1728
|
+
color=str(raw_item.get("color") or ""),
|
|
1729
|
+
creating=creating_item,
|
|
1730
|
+
)
|
|
1731
|
+
if icon_failure is not None:
|
|
1732
|
+
icon_errors.append(
|
|
1733
|
+
{
|
|
1734
|
+
"index": index,
|
|
1735
|
+
"row_number": index + 1,
|
|
1736
|
+
"error_code": icon_failure.get("error_code"),
|
|
1737
|
+
"message": icon_failure.get("message"),
|
|
1738
|
+
"details": icon_failure.get("details"),
|
|
1739
|
+
}
|
|
1740
|
+
)
|
|
1741
|
+
continue
|
|
1742
|
+
_ok, _error_code, _message, icon_details = validate_workspace_icon_choice(
|
|
1743
|
+
icon=str(raw_item.get("icon") or ""),
|
|
1744
|
+
color=str(raw_item.get("color") or ""),
|
|
1745
|
+
require_explicit=creating_item,
|
|
1746
|
+
disallow_generic=creating_item,
|
|
1747
|
+
)
|
|
1748
|
+
normalized_icon = str(icon_details.get("normalized_icon") or "").strip()
|
|
1749
|
+
if creating_item and normalized_icon:
|
|
1750
|
+
if normalized_icon in seen_new_app_icons:
|
|
1751
|
+
icon_errors.append(
|
|
1752
|
+
{
|
|
1753
|
+
"index": index,
|
|
1754
|
+
"row_number": index + 1,
|
|
1755
|
+
"error_code": "DUPLICATE_WORKSPACE_ICON_IN_BATCH",
|
|
1756
|
+
"message": f"apps[{index}] reuses icon '{normalized_icon}' from apps[{seen_new_app_icons[normalized_icon]}]",
|
|
1757
|
+
"details": {
|
|
1758
|
+
"icon": normalized_icon,
|
|
1759
|
+
"first_index": seen_new_app_icons[normalized_icon],
|
|
1760
|
+
"duplicate_index": index,
|
|
1761
|
+
"icon_catalog_command": "qingflow --json builder icon catalog",
|
|
1762
|
+
},
|
|
1763
|
+
}
|
|
1764
|
+
)
|
|
1765
|
+
else:
|
|
1766
|
+
seen_new_app_icons[normalized_icon] = index
|
|
1767
|
+
if icon_errors:
|
|
1768
|
+
return _config_failure(
|
|
1769
|
+
tool_name="app_schema_apply",
|
|
1770
|
+
error_code="WORKSPACE_ICON_BATCH_INVALID",
|
|
1771
|
+
message="one or more apps have invalid workspace icon configuration",
|
|
1772
|
+
fix_hint="Call `qingflow --json builder icon catalog`, choose a distinct non-template icon and color for each new app, then retry.",
|
|
1773
|
+
details={"icon_errors": icon_errors},
|
|
1774
|
+
allowed_values={
|
|
1775
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
1776
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
1777
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
1778
|
+
},
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
client_key_to_app_key: dict[str, str] = {}
|
|
1782
|
+
created_app_keys: list[str] = []
|
|
1783
|
+
results: list[JSONObject] = []
|
|
1784
|
+
any_write_executed = False
|
|
1785
|
+
client_keys: set[str] = set()
|
|
1786
|
+
|
|
1787
|
+
for index, raw_item in enumerate(apps):
|
|
1788
|
+
if not isinstance(raw_item, dict):
|
|
1789
|
+
results.append(_multi_app_item_failure(index, raw_item, "INVALID_APP_ITEM", "apps[] items must be objects"))
|
|
1790
|
+
continue
|
|
1791
|
+
item = deepcopy(raw_item)
|
|
1792
|
+
client_key = str(item.get("client_key") or item.get("clientKey") or "").strip()
|
|
1793
|
+
app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip()
|
|
1794
|
+
app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
1795
|
+
if client_key:
|
|
1796
|
+
if client_key in client_keys:
|
|
1797
|
+
results.append(_multi_app_item_failure(index, item, "DUPLICATE_CLIENT_KEY", f"duplicate client_key '{client_key}'"))
|
|
1798
|
+
continue
|
|
1799
|
+
client_keys.add(client_key)
|
|
1800
|
+
if not app_key and not app_name:
|
|
1801
|
+
results.append(_multi_app_item_failure(index, item, "APP_SELECTOR_REQUIRED", "apps[] requires app_key or app_name"))
|
|
1802
|
+
continue
|
|
1803
|
+
|
|
1804
|
+
initial_add_fields, deferred_add_fields = _split_multi_app_initial_add_fields(item, is_new_app=not bool(app_key))
|
|
1805
|
+
item["_deferred_add_fields"] = deferred_add_fields
|
|
1806
|
+
shell = self._app_schema_apply_once(
|
|
1807
|
+
profile=profile,
|
|
1808
|
+
app_key=app_key,
|
|
1809
|
+
package_id=package_id if not app_key else None,
|
|
1810
|
+
app_name=app_name,
|
|
1811
|
+
app_title="",
|
|
1812
|
+
icon=str(item.get("icon") or ""),
|
|
1813
|
+
color=str(item.get("color") or ""),
|
|
1814
|
+
visibility=item.get("visibility", visibility),
|
|
1815
|
+
create_if_missing=create_if_missing and not app_key,
|
|
1816
|
+
publish=publish and not deferred_add_fields,
|
|
1817
|
+
add_fields=initial_add_fields,
|
|
1818
|
+
update_fields=[],
|
|
1819
|
+
remove_fields=[],
|
|
1820
|
+
)
|
|
1821
|
+
public_shell = _publicize_package_fields(shell)
|
|
1822
|
+
resolved_key = str(public_shell.get("app_key") or "").strip()
|
|
1823
|
+
if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
|
|
1824
|
+
results.append({
|
|
1825
|
+
"index": index,
|
|
1826
|
+
"row_number": index + 1,
|
|
1827
|
+
"client_key": client_key or None,
|
|
1828
|
+
"app_name": app_name or None,
|
|
1829
|
+
"app_key": resolved_key or app_key or None,
|
|
1830
|
+
"status": "failed",
|
|
1831
|
+
"stage": "resolve_or_create_shell",
|
|
1832
|
+
"error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
|
|
1833
|
+
"message": public_shell.get("message") or "app shell resolve/create failed",
|
|
1834
|
+
"safe_to_retry": not any_write_executed,
|
|
1835
|
+
})
|
|
1836
|
+
continue
|
|
1837
|
+
if bool(public_shell.get("created")):
|
|
1838
|
+
created_app_keys.append(resolved_key)
|
|
1839
|
+
if _schema_apply_result_has_write(public_shell):
|
|
1840
|
+
any_write_executed = True
|
|
1841
|
+
if client_key:
|
|
1842
|
+
client_key_to_app_key[client_key] = resolved_key
|
|
1843
|
+
results.append({
|
|
1844
|
+
"index": index,
|
|
1845
|
+
"row_number": index + 1,
|
|
1846
|
+
"client_key": client_key or None,
|
|
1847
|
+
"app_name": str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip() or None,
|
|
1848
|
+
"app_key": resolved_key,
|
|
1849
|
+
"status": "shell_ready",
|
|
1850
|
+
"created": bool(public_shell.get("created")),
|
|
1851
|
+
"shell_result": public_shell,
|
|
1852
|
+
"shell_field_diff": public_shell.get("field_diff") or {},
|
|
1853
|
+
"shell_field_diff_details": public_shell.get("field_diff_details") or {},
|
|
1854
|
+
"deferred_add_fields": deferred_add_fields,
|
|
1855
|
+
})
|
|
1856
|
+
|
|
1857
|
+
final_items: list[JSONObject] = []
|
|
1858
|
+
for index, raw_item in enumerate(apps):
|
|
1859
|
+
existing = next((item for item in results if item.get("index") == index), None)
|
|
1860
|
+
if not existing or existing.get("status") != "shell_ready":
|
|
1861
|
+
if existing:
|
|
1862
|
+
final_items.append(existing)
|
|
1863
|
+
continue
|
|
1864
|
+
item = deepcopy(raw_item)
|
|
1865
|
+
app_key = str(existing.get("app_key") or "").strip()
|
|
1866
|
+
try:
|
|
1867
|
+
compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key)
|
|
1868
|
+
except ValueError as error:
|
|
1869
|
+
final_items.append({
|
|
1870
|
+
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
1871
|
+
"status": "failed",
|
|
1872
|
+
"stage": "compile_relation_refs",
|
|
1873
|
+
"error_code": "TARGET_APP_REF_NOT_FOUND",
|
|
1874
|
+
"message": str(error),
|
|
1875
|
+
"safe_to_retry": False,
|
|
1876
|
+
})
|
|
1877
|
+
any_write_executed = True
|
|
1878
|
+
continue
|
|
1879
|
+
|
|
1880
|
+
deferred_add_fields = (
|
|
1881
|
+
_compiled_multi_app_deferred_add_fields(compiled_item, existing)
|
|
1882
|
+
if bool(existing.get("created"))
|
|
1883
|
+
else list(compiled_item.get("add_fields") or [])
|
|
1884
|
+
)
|
|
1885
|
+
update_fields = list(compiled_item.get("update_fields") or [])
|
|
1886
|
+
remove_fields = list(compiled_item.get("remove_fields") or [])
|
|
1887
|
+
if bool(existing.get("created")) and not deferred_add_fields and not update_fields and not remove_fields:
|
|
1888
|
+
shell_result = existing.get("shell_result") if isinstance(existing.get("shell_result"), dict) else {}
|
|
1889
|
+
item_status = shell_result.get("status") if shell_result.get("status") in {"success", "partial_success"} else "failed"
|
|
1890
|
+
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
1891
|
+
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
1892
|
+
final_items.append({
|
|
1893
|
+
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
1894
|
+
"status": item_status,
|
|
1895
|
+
"stage": "schema_apply",
|
|
1896
|
+
"field_diff": shell_field_diff,
|
|
1897
|
+
"field_diff_details": shell_field_diff_details,
|
|
1898
|
+
"shell_field_diff": shell_field_diff,
|
|
1899
|
+
"shell_field_diff_details": shell_field_diff_details,
|
|
1900
|
+
"published": bool(shell_result.get("published")),
|
|
1901
|
+
"verified": bool(shell_result.get("verified")),
|
|
1902
|
+
"error_code": shell_result.get("error_code"),
|
|
1903
|
+
"message": shell_result.get("message"),
|
|
1904
|
+
"safe_to_retry": False,
|
|
1905
|
+
})
|
|
1906
|
+
continue
|
|
1907
|
+
|
|
1908
|
+
field_result = self._app_schema_apply_once(
|
|
1909
|
+
profile=profile,
|
|
1910
|
+
app_key=app_key,
|
|
1911
|
+
package_id=None,
|
|
1912
|
+
app_name=str(compiled_item.get("app_name") or compiled_item.get("appTitle") or compiled_item.get("app_title") or ""),
|
|
1913
|
+
app_title="",
|
|
1914
|
+
icon=str(compiled_item.get("icon") or ""),
|
|
1915
|
+
color=str(compiled_item.get("color") or ""),
|
|
1916
|
+
visibility=compiled_item.get("visibility"),
|
|
1917
|
+
create_if_missing=False,
|
|
1918
|
+
publish=publish,
|
|
1919
|
+
add_fields=deferred_add_fields,
|
|
1920
|
+
update_fields=update_fields,
|
|
1921
|
+
remove_fields=remove_fields,
|
|
1922
|
+
)
|
|
1923
|
+
public_result = _publicize_package_fields(field_result)
|
|
1924
|
+
if _schema_apply_result_has_write(public_result):
|
|
1925
|
+
any_write_executed = True
|
|
1926
|
+
item_status = public_result.get("status") if public_result.get("status") in {"success", "partial_success"} else "failed"
|
|
1927
|
+
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
1928
|
+
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
1929
|
+
field_diff = _merge_schema_field_diffs(shell_field_diff, public_result.get("field_diff") or {})
|
|
1930
|
+
field_diff_details = _merge_schema_field_diffs(shell_field_diff_details, public_result.get("field_diff_details") or {})
|
|
1931
|
+
final_items.append({
|
|
1932
|
+
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
1933
|
+
"status": item_status,
|
|
1934
|
+
"stage": "schema_apply",
|
|
1935
|
+
"field_diff": field_diff,
|
|
1936
|
+
"field_diff_details": field_diff_details,
|
|
1937
|
+
"shell_field_diff": shell_field_diff,
|
|
1938
|
+
"shell_field_diff_details": shell_field_diff_details,
|
|
1939
|
+
"published": bool(public_result.get("published")),
|
|
1940
|
+
"verified": bool(public_result.get("verified")),
|
|
1941
|
+
"error_code": public_result.get("error_code"),
|
|
1942
|
+
"message": public_result.get("message"),
|
|
1943
|
+
"safe_to_retry": False,
|
|
1944
|
+
})
|
|
1945
|
+
|
|
1946
|
+
succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
|
|
1947
|
+
failed = len(final_items) - succeeded
|
|
1948
|
+
overall_status = "success" if failed == 0 else ("partial_success" if succeeded > 0 or any_write_executed else "failed")
|
|
1949
|
+
return {
|
|
1950
|
+
"status": overall_status,
|
|
1951
|
+
"mode": "multi_app",
|
|
1952
|
+
"total": len(apps),
|
|
1953
|
+
"succeeded": succeeded,
|
|
1954
|
+
"failed": failed,
|
|
1955
|
+
"created_app_keys": created_app_keys,
|
|
1956
|
+
"write_executed": any_write_executed,
|
|
1957
|
+
"safe_to_retry": not any_write_executed,
|
|
1958
|
+
"package_id": package_id,
|
|
1959
|
+
"publish_requested": publish,
|
|
1960
|
+
"apps": final_items,
|
|
1961
|
+
"normalized_args": normalized_args,
|
|
1962
|
+
"verification": {
|
|
1963
|
+
"all_apps_succeeded": failed == 0,
|
|
1964
|
+
"created_app_count": len(created_app_keys),
|
|
1965
|
+
},
|
|
1966
|
+
"request_id": None,
|
|
1967
|
+
"error_code": None if overall_status != "failed" else "MULTI_APP_SCHEMA_APPLY_FAILED",
|
|
1968
|
+
"recoverable": overall_status != "success",
|
|
1969
|
+
"message": "multi-app schema apply completed" if overall_status != "failed" else "multi-app schema apply failed",
|
|
1970
|
+
}
|
|
1573
1971
|
|
|
1574
1972
|
def _app_schema_apply_once(
|
|
1575
1973
|
self,
|
|
@@ -1590,6 +1988,14 @@ class AiBuilderTools(ToolBase):
|
|
|
1590
1988
|
) -> JSONObject:
|
|
1591
1989
|
"""执行内部辅助逻辑。"""
|
|
1592
1990
|
effective_app_name = app_name or app_title
|
|
1991
|
+
icon_failure = _validate_workspace_icon_for_builder(
|
|
1992
|
+
tool_name="app_schema_apply",
|
|
1993
|
+
icon=icon,
|
|
1994
|
+
color=color,
|
|
1995
|
+
creating=not bool(str(app_key or "").strip()) and bool(create_if_missing),
|
|
1996
|
+
)
|
|
1997
|
+
if icon_failure is not None:
|
|
1998
|
+
return icon_failure
|
|
1593
1999
|
plan_result = self._rewrite_plan_result_for_apply(
|
|
1594
2000
|
result=self.app_schema_plan(
|
|
1595
2001
|
profile=profile,
|
|
@@ -1685,7 +2091,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1685
2091
|
publish=publish,
|
|
1686
2092
|
sections=sections,
|
|
1687
2093
|
)
|
|
1688
|
-
|
|
2094
|
+
result = self._retry_after_self_lock_release(
|
|
1689
2095
|
profile=profile,
|
|
1690
2096
|
result=result,
|
|
1691
2097
|
retry_call=lambda: self._app_layout_apply_once(
|
|
@@ -1696,6 +2102,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1696
2102
|
sections=sections,
|
|
1697
2103
|
),
|
|
1698
2104
|
)
|
|
2105
|
+
return _attach_builder_apply_envelope("app_layout_apply", result)
|
|
1699
2106
|
|
|
1700
2107
|
def _app_layout_apply_once(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
|
|
1701
2108
|
"""执行内部辅助逻辑。"""
|
|
@@ -1775,7 +2182,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1775
2182
|
nodes=nodes,
|
|
1776
2183
|
transitions=transitions,
|
|
1777
2184
|
)
|
|
1778
|
-
|
|
2185
|
+
result = self._retry_after_self_lock_release(
|
|
1779
2186
|
profile=profile,
|
|
1780
2187
|
result=result,
|
|
1781
2188
|
retry_call=lambda: self._app_flow_apply_once(
|
|
@@ -1787,6 +2194,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1787
2194
|
transitions=transitions,
|
|
1788
2195
|
),
|
|
1789
2196
|
)
|
|
2197
|
+
return _attach_builder_apply_envelope("app_flow_apply", result)
|
|
1790
2198
|
|
|
1791
2199
|
def _app_flow_apply_once(
|
|
1792
2200
|
self,
|
|
@@ -1886,7 +2294,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1886
2294
|
patch_views=patch_views or [],
|
|
1887
2295
|
remove_views=remove_views,
|
|
1888
2296
|
)
|
|
1889
|
-
|
|
2297
|
+
result = self._retry_after_self_lock_release(
|
|
1890
2298
|
profile=profile,
|
|
1891
2299
|
result=result,
|
|
1892
2300
|
retry_call=lambda: self._app_views_apply_once(
|
|
@@ -1898,6 +2306,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1898
2306
|
patch_views=patch_views or [],
|
|
1899
2307
|
),
|
|
1900
2308
|
)
|
|
2309
|
+
return _attach_builder_apply_envelope("app_views_apply", result)
|
|
1901
2310
|
|
|
1902
2311
|
def _app_views_apply_once(
|
|
1903
2312
|
self,
|
|
@@ -2053,7 +2462,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2053
2462
|
}
|
|
2054
2463
|
)
|
|
2055
2464
|
except ValidationError as exc:
|
|
2056
|
-
return _visibility_validation_failure(
|
|
2465
|
+
return _attach_builder_apply_envelope("app_charts_apply", _visibility_validation_failure(
|
|
2057
2466
|
str(exc),
|
|
2058
2467
|
tool_name="app_charts_apply",
|
|
2059
2468
|
exc=exc,
|
|
@@ -2067,14 +2476,14 @@ class AiBuilderTools(ToolBase):
|
|
|
2067
2476
|
"reorder_chart_ids": [],
|
|
2068
2477
|
},
|
|
2069
2478
|
},
|
|
2070
|
-
)
|
|
2479
|
+
))
|
|
2071
2480
|
normalized_args = request.model_dump(mode="json")
|
|
2072
|
-
return _safe_tool_call(
|
|
2481
|
+
return _attach_builder_apply_envelope("app_charts_apply", _safe_tool_call(
|
|
2073
2482
|
lambda: self._facade.chart_apply(profile=profile, request=request),
|
|
2074
2483
|
error_code="CHART_APPLY_FAILED",
|
|
2075
2484
|
normalized_args=normalized_args,
|
|
2076
2485
|
suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2077
|
-
)
|
|
2486
|
+
))
|
|
2078
2487
|
|
|
2079
2488
|
@tool_cn_name("门户配置应用")
|
|
2080
2489
|
def portal_apply(
|
|
@@ -2083,9 +2492,12 @@ class AiBuilderTools(ToolBase):
|
|
|
2083
2492
|
profile: str,
|
|
2084
2493
|
dash_key: str = "",
|
|
2085
2494
|
dash_name: str = "",
|
|
2495
|
+
name: str = "",
|
|
2086
2496
|
package_id: int | None = None,
|
|
2087
2497
|
publish: bool = True,
|
|
2088
2498
|
sections: list[JSONObject] | None = None,
|
|
2499
|
+
pages: list[JSONObject] | None = None,
|
|
2500
|
+
layout_preset: str = "",
|
|
2089
2501
|
visibility: JSONObject | None = None,
|
|
2090
2502
|
auth: JSONObject | None = None,
|
|
2091
2503
|
icon: str | None = None,
|
|
@@ -2093,27 +2505,46 @@ class AiBuilderTools(ToolBase):
|
|
|
2093
2505
|
hide_copyright: bool | None = None,
|
|
2094
2506
|
dash_global_config: JSONObject | None = None,
|
|
2095
2507
|
config: JSONObject | None = None,
|
|
2508
|
+
payload: JSONObject | None = None,
|
|
2096
2509
|
) -> JSONObject:
|
|
2097
2510
|
"""执行门户相关逻辑。"""
|
|
2511
|
+
request_payload: dict[str, Any] = dict(payload) if isinstance(payload, dict) else {}
|
|
2512
|
+
if dash_key:
|
|
2513
|
+
request_payload["dash_key"] = dash_key
|
|
2514
|
+
if dash_name:
|
|
2515
|
+
request_payload["dash_name"] = dash_name
|
|
2516
|
+
elif name:
|
|
2517
|
+
request_payload["name"] = name
|
|
2518
|
+
if package_id is not None:
|
|
2519
|
+
request_payload["package_id"] = package_id
|
|
2520
|
+
if "publish" not in request_payload or publish is False:
|
|
2521
|
+
request_payload["publish"] = publish
|
|
2522
|
+
if sections:
|
|
2523
|
+
request_payload["sections"] = sections
|
|
2524
|
+
if pages:
|
|
2525
|
+
request_payload["pages"] = pages
|
|
2526
|
+
if layout_preset:
|
|
2527
|
+
request_payload["layout_preset"] = layout_preset
|
|
2528
|
+
if visibility is not None:
|
|
2529
|
+
request_payload["visibility"] = visibility
|
|
2530
|
+
if auth is not None:
|
|
2531
|
+
request_payload["auth"] = auth
|
|
2532
|
+
if icon is not None:
|
|
2533
|
+
request_payload["icon"] = icon
|
|
2534
|
+
if color is not None:
|
|
2535
|
+
request_payload["color"] = color
|
|
2536
|
+
if hide_copyright is not None:
|
|
2537
|
+
request_payload["hide_copyright"] = hide_copyright
|
|
2538
|
+
if dash_global_config is not None:
|
|
2539
|
+
request_payload["dash_global_config"] = dash_global_config
|
|
2540
|
+
if config:
|
|
2541
|
+
merged_config = dict(request_payload.get("config") or {}) if isinstance(request_payload.get("config"), dict) else {}
|
|
2542
|
+
merged_config.update(config)
|
|
2543
|
+
request_payload["config"] = merged_config
|
|
2098
2544
|
try:
|
|
2099
|
-
request = PortalApplyRequest.model_validate(
|
|
2100
|
-
{
|
|
2101
|
-
"dash_key": dash_key or None,
|
|
2102
|
-
"dash_name": dash_name or None,
|
|
2103
|
-
"package_tag_id": package_id,
|
|
2104
|
-
"publish": publish,
|
|
2105
|
-
"sections": sections or [],
|
|
2106
|
-
"visibility": visibility,
|
|
2107
|
-
"auth": auth,
|
|
2108
|
-
"icon": icon,
|
|
2109
|
-
"color": color,
|
|
2110
|
-
"hide_copyright": hide_copyright,
|
|
2111
|
-
"dash_global_config": dash_global_config,
|
|
2112
|
-
"config": config or {},
|
|
2113
|
-
}
|
|
2114
|
-
)
|
|
2545
|
+
request = PortalApplyRequest.model_validate(request_payload)
|
|
2115
2546
|
except ValidationError as exc:
|
|
2116
|
-
return _visibility_validation_failure(
|
|
2547
|
+
return _attach_builder_apply_envelope("portal_apply", _visibility_validation_failure(
|
|
2117
2548
|
str(exc),
|
|
2118
2549
|
tool_name="portal_apply",
|
|
2119
2550
|
exc=exc,
|
|
@@ -2124,6 +2555,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2124
2555
|
"dash_name": dash_name or "业务门户",
|
|
2125
2556
|
"package_id": package_id or 1001,
|
|
2126
2557
|
"publish": True,
|
|
2558
|
+
"layout_preset": "dashboard_2col",
|
|
2127
2559
|
"sections": [
|
|
2128
2560
|
{
|
|
2129
2561
|
"title": "经营概览",
|
|
@@ -2133,15 +2565,24 @@ class AiBuilderTools(ToolBase):
|
|
|
2133
2565
|
],
|
|
2134
2566
|
},
|
|
2135
2567
|
},
|
|
2136
|
-
)
|
|
2568
|
+
))
|
|
2137
2569
|
normalized_args = request.model_dump(mode="json")
|
|
2138
2570
|
normalized_args["package_id"] = normalized_args.pop("package_tag_id", package_id)
|
|
2139
|
-
|
|
2571
|
+
icon_failure = _validate_workspace_icon_for_builder(
|
|
2572
|
+
tool_name="portal_apply",
|
|
2573
|
+
icon=str(request.icon or ""),
|
|
2574
|
+
color=str(request.color or ""),
|
|
2575
|
+
creating=not bool(str(request.dash_key or "").strip()),
|
|
2576
|
+
)
|
|
2577
|
+
if icon_failure is not None:
|
|
2578
|
+
return _attach_builder_apply_envelope("portal_apply", icon_failure)
|
|
2579
|
+
result = _publicize_package_fields(_safe_tool_call(
|
|
2140
2580
|
lambda: self._facade.portal_apply(profile=profile, request=request),
|
|
2141
2581
|
error_code="PORTAL_APPLY_FAILED",
|
|
2142
2582
|
normalized_args=normalized_args,
|
|
2143
2583
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2144
2584
|
))
|
|
2585
|
+
return _attach_builder_apply_envelope("portal_apply", result)
|
|
2145
2586
|
|
|
2146
2587
|
@tool_cn_name("应用发布校验")
|
|
2147
2588
|
def app_publish_verify(
|
|
@@ -2159,7 +2600,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2159
2600
|
normalized_args=normalized_args,
|
|
2160
2601
|
suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, **normalized_args}},
|
|
2161
2602
|
))
|
|
2162
|
-
|
|
2603
|
+
result = _publicize_package_fields(self._retry_after_self_lock_release(
|
|
2163
2604
|
profile=profile,
|
|
2164
2605
|
result=result,
|
|
2165
2606
|
retry_call=lambda: self._facade.app_publish_verify(
|
|
@@ -2168,6 +2609,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2168
2609
|
expected_package_tag_id=expected_package_id,
|
|
2169
2610
|
),
|
|
2170
2611
|
))
|
|
2612
|
+
return _attach_builder_apply_envelope("app_publish_verify", result)
|
|
2171
2613
|
|
|
2172
2614
|
def _retry_after_self_lock_release(self, *, profile: str, result: JSONObject, retry_call) -> JSONObject:
|
|
2173
2615
|
"""执行内部辅助逻辑。"""
|
|
@@ -2286,6 +2728,128 @@ class AiBuilderTools(ToolBase):
|
|
|
2286
2728
|
return rewritten
|
|
2287
2729
|
|
|
2288
2730
|
|
|
2731
|
+
def _multi_app_item_failure(index: int, item: object, error_code: str, message: str) -> JSONObject:
|
|
2732
|
+
app_name = None
|
|
2733
|
+
client_key = None
|
|
2734
|
+
app_key = None
|
|
2735
|
+
if isinstance(item, dict):
|
|
2736
|
+
app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip() or None
|
|
2737
|
+
client_key = str(item.get("client_key") or item.get("clientKey") or "").strip() or None
|
|
2738
|
+
app_key = str(item.get("app_key") or item.get("appKey") or "").strip() or None
|
|
2739
|
+
return {
|
|
2740
|
+
"index": index,
|
|
2741
|
+
"row_number": index + 1,
|
|
2742
|
+
"client_key": client_key,
|
|
2743
|
+
"app_name": app_name,
|
|
2744
|
+
"app_key": app_key,
|
|
2745
|
+
"status": "failed",
|
|
2746
|
+
"stage": "validate_item",
|
|
2747
|
+
"error_code": error_code,
|
|
2748
|
+
"message": message,
|
|
2749
|
+
"safe_to_retry": True,
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
|
|
2753
|
+
def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key: dict[str, str]) -> JSONObject:
|
|
2754
|
+
compiled = deepcopy(item)
|
|
2755
|
+
|
|
2756
|
+
def visit(value):
|
|
2757
|
+
if isinstance(value, list):
|
|
2758
|
+
return [visit(entry) for entry in value]
|
|
2759
|
+
if not isinstance(value, dict):
|
|
2760
|
+
return value
|
|
2761
|
+
payload = {key: visit(entry) for key, entry in value.items()}
|
|
2762
|
+
ref = (
|
|
2763
|
+
payload.pop("target_app_ref", None)
|
|
2764
|
+
or payload.pop("targetAppRef", None)
|
|
2765
|
+
or payload.pop("target_app_client_key", None)
|
|
2766
|
+
or payload.pop("targetAppClientKey", None)
|
|
2767
|
+
)
|
|
2768
|
+
if ref is not None:
|
|
2769
|
+
ref_key = str(ref or "").strip()
|
|
2770
|
+
target_app_key = client_key_to_app_key.get(ref_key)
|
|
2771
|
+
if not target_app_key:
|
|
2772
|
+
raise ValueError(f"target_app_ref '{ref_key}' did not match any apps[].client_key")
|
|
2773
|
+
payload["target_app_key"] = target_app_key
|
|
2774
|
+
return payload
|
|
2775
|
+
|
|
2776
|
+
return visit(compiled)
|
|
2777
|
+
|
|
2778
|
+
|
|
2779
|
+
def _split_multi_app_initial_add_fields(item: JSONObject, *, is_new_app: bool) -> tuple[list[JSONObject], list[JSONObject]]:
|
|
2780
|
+
add_fields = _multi_app_list_value(item, "add_fields", "addFields")
|
|
2781
|
+
if not is_new_app:
|
|
2782
|
+
return [], add_fields
|
|
2783
|
+
initial: list[JSONObject] = []
|
|
2784
|
+
deferred: list[JSONObject] = []
|
|
2785
|
+
for field in add_fields:
|
|
2786
|
+
if _contains_multi_app_target_ref(field):
|
|
2787
|
+
deferred.append(field)
|
|
2788
|
+
else:
|
|
2789
|
+
initial.append(field)
|
|
2790
|
+
return initial, deferred
|
|
2791
|
+
|
|
2792
|
+
|
|
2793
|
+
def _compiled_multi_app_deferred_add_fields(compiled_item: JSONObject, existing_result: JSONObject) -> list[JSONObject]:
|
|
2794
|
+
deferred = existing_result.get("deferred_add_fields")
|
|
2795
|
+
if not isinstance(deferred, list):
|
|
2796
|
+
return list(compiled_item.get("add_fields") or [])
|
|
2797
|
+
deferred_names = {str(item.get("name") or item.get("title") or item.get("label") or "").strip() for item in deferred if isinstance(item, dict)}
|
|
2798
|
+
if not deferred_names:
|
|
2799
|
+
return []
|
|
2800
|
+
return [
|
|
2801
|
+
deepcopy(field)
|
|
2802
|
+
for field in list(compiled_item.get("add_fields") or [])
|
|
2803
|
+
if isinstance(field, dict)
|
|
2804
|
+
and str(field.get("name") or field.get("title") or field.get("label") or "").strip() in deferred_names
|
|
2805
|
+
]
|
|
2806
|
+
|
|
2807
|
+
|
|
2808
|
+
def _multi_app_list_value(item: JSONObject, *keys: str) -> list[JSONObject]:
|
|
2809
|
+
for key in keys:
|
|
2810
|
+
value = item.get(key)
|
|
2811
|
+
if isinstance(value, list):
|
|
2812
|
+
return [deepcopy(entry) for entry in value if isinstance(entry, dict)]
|
|
2813
|
+
return []
|
|
2814
|
+
|
|
2815
|
+
|
|
2816
|
+
def _contains_multi_app_target_ref(value: object) -> bool:
|
|
2817
|
+
if isinstance(value, list):
|
|
2818
|
+
return any(_contains_multi_app_target_ref(item) for item in value)
|
|
2819
|
+
if not isinstance(value, dict):
|
|
2820
|
+
return False
|
|
2821
|
+
for key, entry in value.items():
|
|
2822
|
+
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
|
|
2823
|
+
return True
|
|
2824
|
+
if _contains_multi_app_target_ref(entry):
|
|
2825
|
+
return True
|
|
2826
|
+
return False
|
|
2827
|
+
|
|
2828
|
+
|
|
2829
|
+
def _merge_schema_field_diffs(*diffs: object) -> JSONObject:
|
|
2830
|
+
merged: JSONObject = {"added": [], "updated": [], "removed": []}
|
|
2831
|
+
for diff in diffs:
|
|
2832
|
+
if not isinstance(diff, dict):
|
|
2833
|
+
continue
|
|
2834
|
+
for key in ("added", "updated", "removed"):
|
|
2835
|
+
values = diff.get(key)
|
|
2836
|
+
if not isinstance(values, list):
|
|
2837
|
+
continue
|
|
2838
|
+
for value in values:
|
|
2839
|
+
if value not in merged[key]:
|
|
2840
|
+
merged[key].append(value)
|
|
2841
|
+
return merged
|
|
2842
|
+
|
|
2843
|
+
|
|
2844
|
+
def _schema_apply_result_has_write(result: JSONObject) -> bool:
|
|
2845
|
+
if bool(result.get("created")) or bool(result.get("published")) or bool(result.get("app_base_updated")):
|
|
2846
|
+
return True
|
|
2847
|
+
field_diff = result.get("field_diff")
|
|
2848
|
+
if isinstance(field_diff, dict):
|
|
2849
|
+
return any(bool(field_diff.get(key)) for key in ("added", "updated", "removed"))
|
|
2850
|
+
return False
|
|
2851
|
+
|
|
2852
|
+
|
|
2289
2853
|
def _validation_failure(
|
|
2290
2854
|
detail: str,
|
|
2291
2855
|
*,
|
|
@@ -2366,20 +2930,34 @@ def _visibility_validation_failure(
|
|
|
2366
2930
|
return result
|
|
2367
2931
|
|
|
2368
2932
|
|
|
2369
|
-
def _config_failure(
|
|
2933
|
+
def _config_failure(
|
|
2934
|
+
*,
|
|
2935
|
+
tool_name: str,
|
|
2936
|
+
message: str,
|
|
2937
|
+
fix_hint: str,
|
|
2938
|
+
error_code: str = "CONFIG_ERROR",
|
|
2939
|
+
details: JSONObject | None = None,
|
|
2940
|
+
allowed_values: JSONObject | None = None,
|
|
2941
|
+
) -> JSONObject:
|
|
2370
2942
|
contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
|
|
2943
|
+
public_allowed_values = deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {}
|
|
2944
|
+
if allowed_values:
|
|
2945
|
+
public_allowed_values.update(deepcopy(allowed_values))
|
|
2946
|
+
public_details: JSONObject = {
|
|
2947
|
+
"fix_hint": fix_hint,
|
|
2948
|
+
"allowed_keys": deepcopy(contract.get("allowed_keys", [])) if isinstance(contract, dict) else [],
|
|
2949
|
+
}
|
|
2950
|
+
if details:
|
|
2951
|
+
public_details.update(deepcopy(details))
|
|
2371
2952
|
return {
|
|
2372
2953
|
"status": "failed",
|
|
2373
|
-
"error_code":
|
|
2954
|
+
"error_code": error_code,
|
|
2374
2955
|
"recoverable": True,
|
|
2375
2956
|
"message": message,
|
|
2376
2957
|
"normalized_args": {},
|
|
2377
2958
|
"missing_fields": [],
|
|
2378
|
-
"allowed_values":
|
|
2379
|
-
"details":
|
|
2380
|
-
"fix_hint": fix_hint,
|
|
2381
|
-
"allowed_keys": deepcopy(contract.get("allowed_keys", [])) if isinstance(contract, dict) else [],
|
|
2382
|
-
},
|
|
2959
|
+
"allowed_values": public_allowed_values,
|
|
2960
|
+
"details": public_details,
|
|
2383
2961
|
"suggested_next_call": None,
|
|
2384
2962
|
"request_id": None,
|
|
2385
2963
|
"backend_code": None,
|
|
@@ -2389,6 +2967,52 @@ def _config_failure(*, tool_name: str, message: str, fix_hint: str) -> JSONObjec
|
|
|
2389
2967
|
}
|
|
2390
2968
|
|
|
2391
2969
|
|
|
2970
|
+
def _workspace_icon_config_failure(
|
|
2971
|
+
*,
|
|
2972
|
+
tool_name: str,
|
|
2973
|
+
error_code: str,
|
|
2974
|
+
message: str,
|
|
2975
|
+
details: JSONObject,
|
|
2976
|
+
) -> JSONObject:
|
|
2977
|
+
return _config_failure(
|
|
2978
|
+
tool_name=tool_name,
|
|
2979
|
+
error_code=error_code,
|
|
2980
|
+
message=message,
|
|
2981
|
+
fix_hint="Call `qingflow --json builder icon catalog`, choose an explicit non-template icon and color, then retry.",
|
|
2982
|
+
details=details,
|
|
2983
|
+
allowed_values={
|
|
2984
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
2985
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
2986
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
2987
|
+
},
|
|
2988
|
+
)
|
|
2989
|
+
|
|
2990
|
+
|
|
2991
|
+
def _validate_workspace_icon_for_builder(
|
|
2992
|
+
*,
|
|
2993
|
+
tool_name: str,
|
|
2994
|
+
icon: str | None,
|
|
2995
|
+
color: str | None,
|
|
2996
|
+
creating: bool,
|
|
2997
|
+
) -> JSONObject | None:
|
|
2998
|
+
if not creating and not (str(icon or "").strip() or str(color or "").strip()):
|
|
2999
|
+
return None
|
|
3000
|
+
ok, error_code, message, details = validate_workspace_icon_choice(
|
|
3001
|
+
icon=icon,
|
|
3002
|
+
color=color,
|
|
3003
|
+
require_explicit=creating,
|
|
3004
|
+
disallow_generic=creating,
|
|
3005
|
+
)
|
|
3006
|
+
if ok:
|
|
3007
|
+
return None
|
|
3008
|
+
return _workspace_icon_config_failure(
|
|
3009
|
+
tool_name=tool_name,
|
|
3010
|
+
error_code=error_code or "WORKSPACE_ICON_INVALID",
|
|
3011
|
+
message=message or "invalid workspace icon configuration",
|
|
3012
|
+
details=details,
|
|
3013
|
+
)
|
|
3014
|
+
|
|
3015
|
+
|
|
2392
3016
|
def _safe_tool_call(
|
|
2393
3017
|
call,
|
|
2394
3018
|
*,
|
|
@@ -2437,6 +3061,7 @@ def _publicize_package_fields(value):
|
|
|
2437
3061
|
"tag_ids_after": "package_ids_after",
|
|
2438
3062
|
"tag_name": "package_name",
|
|
2439
3063
|
"tag_icon": "icon",
|
|
3064
|
+
"iconConfig": "icon_config",
|
|
2440
3065
|
"package_tag_id": "package_id",
|
|
2441
3066
|
"package_tag_ids": "package_ids",
|
|
2442
3067
|
"expected_package_tag_id": "expected_package_id",
|
|
@@ -2448,6 +3073,742 @@ def _publicize_package_fields(value):
|
|
|
2448
3073
|
return public
|
|
2449
3074
|
|
|
2450
3075
|
|
|
3076
|
+
def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) -> JSONObject:
|
|
3077
|
+
public = deepcopy(contract)
|
|
3078
|
+
if tool_name not in BUILDER_APPLY_TOOL_NAMES:
|
|
3079
|
+
return public
|
|
3080
|
+
notes = public.setdefault("execution_notes", [])
|
|
3081
|
+
if isinstance(notes, list):
|
|
3082
|
+
note = "apply/write output includes schema_version, operation, summary, and resources[]; UI and agents should read resources[].id/key/name first and use legacy fields only for compatibility/debugging"
|
|
3083
|
+
if note not in notes:
|
|
3084
|
+
notes.append(note)
|
|
3085
|
+
public["output_contract"] = {
|
|
3086
|
+
"schema_version": BUILDER_APPLY_SCHEMA_VERSION,
|
|
3087
|
+
"preferred_ui_fields": ["operation", "summary", "resources"],
|
|
3088
|
+
"resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "icon_config", "error_code", "message"],
|
|
3089
|
+
"legacy_fields_preserved": True,
|
|
3090
|
+
}
|
|
3091
|
+
return public
|
|
3092
|
+
|
|
3093
|
+
|
|
3094
|
+
def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONObject:
|
|
3095
|
+
if not isinstance(payload, dict):
|
|
3096
|
+
return payload
|
|
3097
|
+
resources = _builder_apply_resources(tool_name, payload)
|
|
3098
|
+
payload["schema_version"] = BUILDER_APPLY_SCHEMA_VERSION
|
|
3099
|
+
payload["operation"] = tool_name
|
|
3100
|
+
payload["resources"] = resources
|
|
3101
|
+
payload["summary"] = _builder_apply_summary(payload, resources)
|
|
3102
|
+
return payload
|
|
3103
|
+
|
|
3104
|
+
|
|
3105
|
+
def _builder_apply_summary(payload: JSONObject, resources: list[JSONObject]) -> JSONObject:
|
|
3106
|
+
status = str(payload.get("status") or "")
|
|
3107
|
+
failed = sum(1 for item in resources if str(item.get("status") or "") == "failed")
|
|
3108
|
+
created = sum(1 for item in resources if str(item.get("operation") or "") == "created" and str(item.get("status") or "") != "failed")
|
|
3109
|
+
removed = sum(1 for item in resources if str(item.get("operation") or "") == "removed" and str(item.get("status") or "") != "failed")
|
|
3110
|
+
updated = sum(
|
|
3111
|
+
1
|
|
3112
|
+
for item in resources
|
|
3113
|
+
if str(item.get("status") or "") != "failed"
|
|
3114
|
+
and str(item.get("operation") or "") in {"updated", "layout_updated", "workflow_updated", "verified", "published"}
|
|
3115
|
+
)
|
|
3116
|
+
published_value = payload.get("published")
|
|
3117
|
+
if published_value is None:
|
|
3118
|
+
publish_requested = payload.get("publish_requested")
|
|
3119
|
+
published_value = bool(publish_requested) and status in {"success", "partial_success"}
|
|
3120
|
+
verified_value = payload.get("verified")
|
|
3121
|
+
if verified_value is None:
|
|
3122
|
+
verification = payload.get("verification")
|
|
3123
|
+
verified_value = status == "success" and failed == 0 and _builder_verification_truthy(verification)
|
|
3124
|
+
summary: JSONObject = {
|
|
3125
|
+
"total": len(resources),
|
|
3126
|
+
"created": created,
|
|
3127
|
+
"updated": updated,
|
|
3128
|
+
"removed": removed,
|
|
3129
|
+
"failed": failed,
|
|
3130
|
+
"published": bool(published_value),
|
|
3131
|
+
"verified": bool(verified_value),
|
|
3132
|
+
}
|
|
3133
|
+
if "write_executed" in payload:
|
|
3134
|
+
summary["write_executed"] = bool(payload.get("write_executed"))
|
|
3135
|
+
if "safe_to_retry" in payload:
|
|
3136
|
+
summary["safe_to_retry"] = bool(payload.get("safe_to_retry"))
|
|
3137
|
+
return summary
|
|
3138
|
+
|
|
3139
|
+
|
|
3140
|
+
def _builder_verification_truthy(value: object) -> bool:
|
|
3141
|
+
if value is None:
|
|
3142
|
+
return True
|
|
3143
|
+
if isinstance(value, bool):
|
|
3144
|
+
return value
|
|
3145
|
+
if isinstance(value, dict):
|
|
3146
|
+
booleans = [item for item in value.values() if isinstance(item, bool)]
|
|
3147
|
+
return all(booleans) if booleans else True
|
|
3148
|
+
return True
|
|
3149
|
+
|
|
3150
|
+
|
|
3151
|
+
def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONObject]:
|
|
3152
|
+
resources: list[JSONObject]
|
|
3153
|
+
if tool_name == "package_apply":
|
|
3154
|
+
resources = _builder_package_resources(payload)
|
|
3155
|
+
elif tool_name == "app_schema_apply":
|
|
3156
|
+
resources = _builder_schema_resources(payload)
|
|
3157
|
+
elif tool_name == "app_layout_apply":
|
|
3158
|
+
resources = [_builder_app_resource(payload, operation="layout_updated")]
|
|
3159
|
+
elif tool_name == "app_flow_apply":
|
|
3160
|
+
resources = [_builder_app_resource(payload, operation="workflow_updated")]
|
|
3161
|
+
elif tool_name == "app_views_apply":
|
|
3162
|
+
resources = _builder_view_resources(payload)
|
|
3163
|
+
elif tool_name == "app_charts_apply":
|
|
3164
|
+
resources = _builder_chart_resources(payload)
|
|
3165
|
+
elif tool_name == "portal_apply":
|
|
3166
|
+
resources = _builder_portal_resources(payload)
|
|
3167
|
+
elif tool_name == "app_custom_buttons_apply":
|
|
3168
|
+
resources = _builder_button_resources(payload)
|
|
3169
|
+
elif tool_name == "app_associated_resources_apply":
|
|
3170
|
+
resources = _builder_associated_resource_resources(payload)
|
|
3171
|
+
elif tool_name == "app_publish_verify":
|
|
3172
|
+
resources = [_builder_app_resource(payload, operation="verified")]
|
|
3173
|
+
else:
|
|
3174
|
+
resources = []
|
|
3175
|
+
if not resources and _builder_status(payload, "") == "failed" and _builder_apply_tool_is_app_scoped(tool_name):
|
|
3176
|
+
app_key = _builder_payload_app_key(payload)
|
|
3177
|
+
if app_key not in (None, ""):
|
|
3178
|
+
resources = [_builder_app_resource(payload, operation="failed")]
|
|
3179
|
+
return resources
|
|
3180
|
+
|
|
3181
|
+
|
|
3182
|
+
def _builder_apply_tool_is_app_scoped(tool_name: str) -> bool:
|
|
3183
|
+
return tool_name in {
|
|
3184
|
+
"app_schema_apply",
|
|
3185
|
+
"app_layout_apply",
|
|
3186
|
+
"app_flow_apply",
|
|
3187
|
+
"app_views_apply",
|
|
3188
|
+
"app_custom_buttons_apply",
|
|
3189
|
+
"app_associated_resources_apply",
|
|
3190
|
+
"app_charts_apply",
|
|
3191
|
+
"app_publish_verify",
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
|
|
3195
|
+
def _builder_status(payload_or_item: JSONObject, fallback: str = "success") -> str:
|
|
3196
|
+
status = str(payload_or_item.get("status") or fallback or "success")
|
|
3197
|
+
return status
|
|
3198
|
+
|
|
3199
|
+
|
|
3200
|
+
def _builder_operation(value: object, fallback: str = "updated") -> str:
|
|
3201
|
+
raw = str(value or fallback or "updated").strip()
|
|
3202
|
+
mapping = {
|
|
3203
|
+
"create": "created",
|
|
3204
|
+
"created": "created",
|
|
3205
|
+
"add": "created",
|
|
3206
|
+
"update": "updated",
|
|
3207
|
+
"updated": "updated",
|
|
3208
|
+
"patch": "updated",
|
|
3209
|
+
"remove": "removed",
|
|
3210
|
+
"removed": "removed",
|
|
3211
|
+
"delete": "removed",
|
|
3212
|
+
"deleted": "removed",
|
|
3213
|
+
"unchanged": "unchanged",
|
|
3214
|
+
"failed": "failed",
|
|
3215
|
+
}
|
|
3216
|
+
return mapping.get(raw, raw)
|
|
3217
|
+
|
|
3218
|
+
|
|
3219
|
+
def _builder_parent(resource_type: str, *, key: object = None, name: object = None, id_value: object = None) -> JSONObject:
|
|
3220
|
+
return {
|
|
3221
|
+
"resource_type": resource_type,
|
|
3222
|
+
"id": id_value,
|
|
3223
|
+
"key": str(key) if key not in (None, "") else None,
|
|
3224
|
+
"name": str(name) if name not in (None, "") else None,
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
|
|
3228
|
+
def _builder_app_parent(payload: JSONObject) -> JSONObject | None:
|
|
3229
|
+
app_key = payload.get("app_key") or payload.get("appKey")
|
|
3230
|
+
app_name = payload.get("app_name_after") or payload.get("app_name") or payload.get("appTitle") or payload.get("app_title")
|
|
3231
|
+
if app_key in (None, "") and app_name in (None, ""):
|
|
3232
|
+
return None
|
|
3233
|
+
return _builder_parent("app", key=app_key, name=app_name)
|
|
3234
|
+
|
|
3235
|
+
|
|
3236
|
+
def _builder_icon_config(raw_icon: object = None, *, icon: object = None, color: object = None) -> JSONObject | None:
|
|
3237
|
+
raw = str(raw_icon).strip() if raw_icon not in (None, "") else ""
|
|
3238
|
+
explicit_icon = str(icon).strip() if icon not in (None, "") else ""
|
|
3239
|
+
explicit_color = str(color).strip() if color not in (None, "") else ""
|
|
3240
|
+
if raw:
|
|
3241
|
+
if raw.startswith("{") and raw.endswith("}"):
|
|
3242
|
+
config = workspace_icon_config(raw)
|
|
3243
|
+
else:
|
|
3244
|
+
config = {
|
|
3245
|
+
"icon_name": normalize_workspace_icon_name(raw),
|
|
3246
|
+
"icon_color": explicit_color or None,
|
|
3247
|
+
"icon_text": None,
|
|
3248
|
+
"raw": raw,
|
|
3249
|
+
}
|
|
3250
|
+
if any(config.get(key) for key in ("icon_name", "icon_color", "icon_text", "raw")):
|
|
3251
|
+
return config
|
|
3252
|
+
if explicit_icon or explicit_color:
|
|
3253
|
+
return {
|
|
3254
|
+
"icon_name": normalize_workspace_icon_name(explicit_icon) if explicit_icon else None,
|
|
3255
|
+
"icon_color": explicit_color or None,
|
|
3256
|
+
"icon_text": None,
|
|
3257
|
+
"raw": None,
|
|
3258
|
+
}
|
|
3259
|
+
return None
|
|
3260
|
+
|
|
3261
|
+
|
|
3262
|
+
def _builder_container_icon_config(container: object, *, raw_keys: tuple[str, ...], icon_keys: tuple[str, ...] = ("icon",), color_keys: tuple[str, ...] = ("color",)) -> JSONObject | None:
|
|
3263
|
+
if not isinstance(container, dict):
|
|
3264
|
+
return None
|
|
3265
|
+
raw_icon = next((container.get(key) for key in raw_keys if container.get(key) not in (None, "")), None)
|
|
3266
|
+
icon = next((container.get(key) for key in icon_keys if container.get(key) not in (None, "")), None)
|
|
3267
|
+
color = next((container.get(key) for key in color_keys if container.get(key) not in (None, "")), None)
|
|
3268
|
+
return _builder_icon_config(raw_icon, icon=icon, color=color)
|
|
3269
|
+
|
|
3270
|
+
|
|
3271
|
+
def _builder_resource(
|
|
3272
|
+
*,
|
|
3273
|
+
resource_type: str,
|
|
3274
|
+
operation: str,
|
|
3275
|
+
status: str,
|
|
3276
|
+
id_value: object = None,
|
|
3277
|
+
key: object = None,
|
|
3278
|
+
name: object = None,
|
|
3279
|
+
ids: JSONObject | None = None,
|
|
3280
|
+
parent: JSONObject | None = None,
|
|
3281
|
+
icon_config: JSONObject | None = None,
|
|
3282
|
+
error_code: object = None,
|
|
3283
|
+
message: object = None,
|
|
3284
|
+
) -> JSONObject:
|
|
3285
|
+
resource = {
|
|
3286
|
+
"resource_type": resource_type,
|
|
3287
|
+
"operation": operation,
|
|
3288
|
+
"status": status,
|
|
3289
|
+
"id": id_value,
|
|
3290
|
+
"key": str(key) if key not in (None, "") else None,
|
|
3291
|
+
"name": str(name) if name not in (None, "") else None,
|
|
3292
|
+
"ids": ids or {},
|
|
3293
|
+
"parent": parent,
|
|
3294
|
+
"error_code": str(error_code) if error_code not in (None, "") else None,
|
|
3295
|
+
"message": str(message) if message not in (None, "") else None,
|
|
3296
|
+
}
|
|
3297
|
+
if icon_config:
|
|
3298
|
+
resource["icon_config"] = icon_config
|
|
3299
|
+
return resource
|
|
3300
|
+
|
|
3301
|
+
|
|
3302
|
+
def _builder_app_resource(payload: JSONObject, *, operation: str) -> JSONObject:
|
|
3303
|
+
status = _builder_status(payload, "success")
|
|
3304
|
+
if status == "failed":
|
|
3305
|
+
operation = "failed"
|
|
3306
|
+
app_key = _builder_payload_app_key(payload)
|
|
3307
|
+
app_name = _builder_payload_app_name(payload)
|
|
3308
|
+
normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
|
|
3309
|
+
icon_config = (
|
|
3310
|
+
_builder_container_icon_config(payload, raw_keys=("app_icon", "appIcon"))
|
|
3311
|
+
or _builder_container_icon_config(normalized_args, raw_keys=("app_icon", "appIcon", "icon"))
|
|
3312
|
+
)
|
|
3313
|
+
return _builder_resource(
|
|
3314
|
+
resource_type="app",
|
|
3315
|
+
operation=operation,
|
|
3316
|
+
status=status,
|
|
3317
|
+
key=app_key,
|
|
3318
|
+
name=app_name,
|
|
3319
|
+
ids={"app_key": app_key} if app_key not in (None, "") else {},
|
|
3320
|
+
icon_config=icon_config,
|
|
3321
|
+
error_code=payload.get("error_code"),
|
|
3322
|
+
message=payload.get("message") if status == "failed" else None,
|
|
3323
|
+
)
|
|
3324
|
+
|
|
3325
|
+
|
|
3326
|
+
def _builder_payload_app_key(payload: JSONObject) -> object:
|
|
3327
|
+
return _builder_payload_identity_value(payload, ("app_key", "appKey"))
|
|
3328
|
+
|
|
3329
|
+
|
|
3330
|
+
def _builder_payload_app_name(payload: JSONObject) -> object:
|
|
3331
|
+
return _builder_payload_identity_value(payload, ("app_name_after", "app_name", "appName", "appTitle", "app_title", "name", "title"))
|
|
3332
|
+
|
|
3333
|
+
|
|
3334
|
+
def _builder_payload_identity_value(payload: JSONObject, keys: tuple[str, ...]) -> object:
|
|
3335
|
+
for key in keys:
|
|
3336
|
+
value = payload.get(key)
|
|
3337
|
+
if value not in (None, ""):
|
|
3338
|
+
return value
|
|
3339
|
+
for container_key in ("normalized_args", "canonical_arguments"):
|
|
3340
|
+
container = payload.get(container_key)
|
|
3341
|
+
if isinstance(container, dict):
|
|
3342
|
+
for key in keys:
|
|
3343
|
+
value = container.get(key)
|
|
3344
|
+
if value not in (None, ""):
|
|
3345
|
+
return value
|
|
3346
|
+
details = payload.get("details")
|
|
3347
|
+
if isinstance(details, dict):
|
|
3348
|
+
for container_key in ("normalized_args", "canonical_arguments"):
|
|
3349
|
+
container = details.get(container_key)
|
|
3350
|
+
if isinstance(container, dict):
|
|
3351
|
+
for key in keys:
|
|
3352
|
+
value = container.get(key)
|
|
3353
|
+
if value not in (None, ""):
|
|
3354
|
+
return value
|
|
3355
|
+
return None
|
|
3356
|
+
|
|
3357
|
+
|
|
3358
|
+
def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3359
|
+
package_id = payload.get("package_id") or payload.get("id")
|
|
3360
|
+
package_name = payload.get("package_name") or payload.get("name")
|
|
3361
|
+
status = _builder_status(payload, "success")
|
|
3362
|
+
operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
|
|
3363
|
+
normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
|
|
3364
|
+
icon_config = (
|
|
3365
|
+
_builder_container_icon_config(payload, raw_keys=("icon", "tagIcon", "tag_icon"))
|
|
3366
|
+
or _builder_container_icon_config(normalized_args, raw_keys=("icon", "tagIcon", "tag_icon"))
|
|
3367
|
+
)
|
|
3368
|
+
return [
|
|
3369
|
+
_builder_resource(
|
|
3370
|
+
resource_type="package",
|
|
3371
|
+
operation=operation,
|
|
3372
|
+
status=status,
|
|
3373
|
+
id_value=package_id,
|
|
3374
|
+
key=str(package_id) if package_id not in (None, "") else None,
|
|
3375
|
+
name=package_name,
|
|
3376
|
+
ids={"package_id": package_id} if package_id not in (None, "") else {},
|
|
3377
|
+
icon_config=icon_config,
|
|
3378
|
+
error_code=payload.get("error_code"),
|
|
3379
|
+
message=payload.get("message") if status == "failed" else None,
|
|
3380
|
+
)
|
|
3381
|
+
]
|
|
3382
|
+
|
|
3383
|
+
|
|
3384
|
+
def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3385
|
+
if payload.get("mode") == "multi_app" and isinstance(payload.get("apps"), list):
|
|
3386
|
+
resources: list[JSONObject] = []
|
|
3387
|
+
package_id = payload.get("package_id")
|
|
3388
|
+
package_parent = _builder_parent("package", id_value=package_id, key=package_id) if package_id not in (None, "") else None
|
|
3389
|
+
for item in payload.get("apps") or []:
|
|
3390
|
+
if not isinstance(item, dict):
|
|
3391
|
+
continue
|
|
3392
|
+
status = _builder_status(item, "success")
|
|
3393
|
+
operation = "failed" if status == "failed" else ("created" if bool(item.get("created")) else "updated")
|
|
3394
|
+
parent = _builder_parent("app", key=item.get("app_key"), name=item.get("app_name"))
|
|
3395
|
+
icon_config = (
|
|
3396
|
+
_builder_container_icon_config(item, raw_keys=("app_icon", "appIcon", "icon"))
|
|
3397
|
+
or _builder_container_icon_config(item.get("shell_result"), raw_keys=("app_icon", "appIcon", "icon"))
|
|
3398
|
+
)
|
|
3399
|
+
resources.append(
|
|
3400
|
+
_builder_resource(
|
|
3401
|
+
resource_type="app",
|
|
3402
|
+
operation=operation,
|
|
3403
|
+
status=status,
|
|
3404
|
+
key=item.get("app_key"),
|
|
3405
|
+
name=item.get("app_name"),
|
|
3406
|
+
ids={
|
|
3407
|
+
**({"app_key": item.get("app_key")} if item.get("app_key") else {}),
|
|
3408
|
+
**({"package_id": package_id} if package_id not in (None, "") else {}),
|
|
3409
|
+
},
|
|
3410
|
+
parent=package_parent,
|
|
3411
|
+
icon_config=icon_config,
|
|
3412
|
+
error_code=item.get("error_code"),
|
|
3413
|
+
message=item.get("message") if status == "failed" else None,
|
|
3414
|
+
)
|
|
3415
|
+
)
|
|
3416
|
+
resources.extend(_builder_field_resources(item.get("field_diff_details") or item.get("field_diff"), parent=parent))
|
|
3417
|
+
return resources
|
|
3418
|
+
|
|
3419
|
+
status = _builder_status(payload, "success")
|
|
3420
|
+
operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
|
|
3421
|
+
app_key = payload.get("app_key")
|
|
3422
|
+
app_name = payload.get("app_name_after") or payload.get("app_name")
|
|
3423
|
+
parent = _builder_parent("app", key=app_key, name=app_name)
|
|
3424
|
+
normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
|
|
3425
|
+
icon_config = (
|
|
3426
|
+
_builder_container_icon_config(payload, raw_keys=("app_icon", "appIcon", "icon"))
|
|
3427
|
+
or _builder_container_icon_config(normalized_args, raw_keys=("icon", "app_icon", "appIcon"))
|
|
3428
|
+
)
|
|
3429
|
+
resources = [
|
|
3430
|
+
_builder_resource(
|
|
3431
|
+
resource_type="app",
|
|
3432
|
+
operation=operation,
|
|
3433
|
+
status=status,
|
|
3434
|
+
key=app_key,
|
|
3435
|
+
name=app_name,
|
|
3436
|
+
ids={"app_key": app_key} if app_key else {},
|
|
3437
|
+
icon_config=icon_config,
|
|
3438
|
+
error_code=payload.get("error_code"),
|
|
3439
|
+
message=payload.get("message") if status == "failed" else None,
|
|
3440
|
+
)
|
|
3441
|
+
]
|
|
3442
|
+
resources.extend(_builder_field_resources(payload.get("field_diff_details") or payload.get("field_diff"), parent=parent))
|
|
3443
|
+
return resources
|
|
3444
|
+
|
|
3445
|
+
|
|
3446
|
+
def _builder_field_resources(field_diff: object, *, parent: JSONObject | None) -> list[JSONObject]:
|
|
3447
|
+
if not isinstance(field_diff, dict):
|
|
3448
|
+
return []
|
|
3449
|
+
resources: list[JSONObject] = []
|
|
3450
|
+
for key, operation in (("added", "created"), ("updated", "updated"), ("removed", "removed")):
|
|
3451
|
+
for field in field_diff.get(key) or []:
|
|
3452
|
+
if isinstance(field, dict):
|
|
3453
|
+
name = field.get("name") or field.get("title") or field.get("field_name") or field.get("queTitle")
|
|
3454
|
+
field_id = field.get("field_id") or field.get("queId")
|
|
3455
|
+
que_id = field.get("que_id") or field.get("queId")
|
|
3456
|
+
else:
|
|
3457
|
+
name = field
|
|
3458
|
+
field_id = None
|
|
3459
|
+
que_id = None
|
|
3460
|
+
resource_id = que_id if que_id not in (None, "") else field_id
|
|
3461
|
+
resources.append(
|
|
3462
|
+
_builder_resource(
|
|
3463
|
+
resource_type="field",
|
|
3464
|
+
operation=operation,
|
|
3465
|
+
status="success",
|
|
3466
|
+
id_value=resource_id,
|
|
3467
|
+
key=field_id if field_id not in (None, "") else name,
|
|
3468
|
+
name=name,
|
|
3469
|
+
ids={
|
|
3470
|
+
**({"field_id": field_id} if field_id not in (None, "") else {}),
|
|
3471
|
+
**({"que_id": que_id} if que_id not in (None, "") else {}),
|
|
3472
|
+
**({"app_key": parent.get("key")} if isinstance(parent, dict) and parent.get("key") else {}),
|
|
3473
|
+
},
|
|
3474
|
+
parent=parent,
|
|
3475
|
+
)
|
|
3476
|
+
)
|
|
3477
|
+
return resources
|
|
3478
|
+
|
|
3479
|
+
|
|
3480
|
+
def _builder_view_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3481
|
+
diff = payload.get("views_diff") if isinstance(payload.get("views_diff"), dict) else {}
|
|
3482
|
+
verification_by_name = _builder_view_verification_by_name(payload)
|
|
3483
|
+
parent = _builder_app_parent(payload)
|
|
3484
|
+
resources: list[JSONObject] = []
|
|
3485
|
+
for key, operation in (("created", "created"), ("updated", "updated"), ("removed", "removed")):
|
|
3486
|
+
for item in diff.get(key) or []:
|
|
3487
|
+
name, view_key, status, error_code, message = _builder_view_identity(item, verification_by_name)
|
|
3488
|
+
resources.append(
|
|
3489
|
+
_builder_resource(
|
|
3490
|
+
resource_type="view",
|
|
3491
|
+
operation=operation,
|
|
3492
|
+
status=status,
|
|
3493
|
+
key=view_key,
|
|
3494
|
+
name=name,
|
|
3495
|
+
ids={
|
|
3496
|
+
**({"view_key": view_key} if view_key else {}),
|
|
3497
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3498
|
+
},
|
|
3499
|
+
parent=parent,
|
|
3500
|
+
error_code=error_code,
|
|
3501
|
+
message=message,
|
|
3502
|
+
)
|
|
3503
|
+
)
|
|
3504
|
+
for item in diff.get("failed") or []:
|
|
3505
|
+
name, view_key, _status, error_code, message = _builder_view_identity(item, verification_by_name)
|
|
3506
|
+
resources.append(
|
|
3507
|
+
_builder_resource(
|
|
3508
|
+
resource_type="view",
|
|
3509
|
+
operation="failed",
|
|
3510
|
+
status="failed",
|
|
3511
|
+
key=view_key,
|
|
3512
|
+
name=name,
|
|
3513
|
+
ids={
|
|
3514
|
+
**({"view_key": view_key} if view_key else {}),
|
|
3515
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3516
|
+
},
|
|
3517
|
+
parent=parent,
|
|
3518
|
+
error_code=error_code,
|
|
3519
|
+
message=message,
|
|
3520
|
+
)
|
|
3521
|
+
)
|
|
3522
|
+
return resources
|
|
3523
|
+
|
|
3524
|
+
|
|
3525
|
+
def _builder_view_verification_by_name(payload: JSONObject) -> dict[str, JSONObject]:
|
|
3526
|
+
verification = payload.get("verification")
|
|
3527
|
+
by_view = verification.get("by_view") if isinstance(verification, dict) else None
|
|
3528
|
+
result: dict[str, JSONObject] = {}
|
|
3529
|
+
if isinstance(by_view, list):
|
|
3530
|
+
for item in by_view:
|
|
3531
|
+
if isinstance(item, dict):
|
|
3532
|
+
name = str(item.get("name") or "").strip()
|
|
3533
|
+
if name:
|
|
3534
|
+
result[name] = item
|
|
3535
|
+
return result
|
|
3536
|
+
|
|
3537
|
+
|
|
3538
|
+
def _builder_view_identity(item: object, verification_by_name: dict[str, JSONObject]) -> tuple[str | None, str | None, str, object, object]:
|
|
3539
|
+
verification: JSONObject | None = None
|
|
3540
|
+
if isinstance(item, dict):
|
|
3541
|
+
name = item.get("name") or item.get("view_name") or item.get("viewName")
|
|
3542
|
+
view_key = item.get("view_key") or item.get("viewKey")
|
|
3543
|
+
status = str(item.get("status") or "success")
|
|
3544
|
+
error_code = item.get("error_code")
|
|
3545
|
+
message = item.get("message")
|
|
3546
|
+
else:
|
|
3547
|
+
name = str(item) if item not in (None, "") else None
|
|
3548
|
+
view_key = None
|
|
3549
|
+
status = "success"
|
|
3550
|
+
error_code = None
|
|
3551
|
+
message = None
|
|
3552
|
+
if name and not view_key:
|
|
3553
|
+
verification = verification_by_name.get(str(name))
|
|
3554
|
+
if isinstance(verification, dict):
|
|
3555
|
+
view_key = verification.get("view_key") or verification.get("viewKey")
|
|
3556
|
+
if not view_key:
|
|
3557
|
+
matching = verification.get("matching_view_keys")
|
|
3558
|
+
if isinstance(matching, list) and matching:
|
|
3559
|
+
view_key = matching[0]
|
|
3560
|
+
if name and verification is None:
|
|
3561
|
+
verification = verification_by_name.get(str(name))
|
|
3562
|
+
if isinstance(verification, dict):
|
|
3563
|
+
verification_status = str(verification.get("status") or "").strip()
|
|
3564
|
+
if verification_status in {"removed", "readback_pending"}:
|
|
3565
|
+
status = "readback_pending"
|
|
3566
|
+
if verification_status == "removed":
|
|
3567
|
+
status = "removed"
|
|
3568
|
+
error_code = error_code or verification.get("error_code")
|
|
3569
|
+
message = message or verification.get("message")
|
|
3570
|
+
return (
|
|
3571
|
+
str(name) if name not in (None, "") else None,
|
|
3572
|
+
str(view_key) if view_key not in (None, "") else None,
|
|
3573
|
+
status,
|
|
3574
|
+
error_code,
|
|
3575
|
+
message,
|
|
3576
|
+
)
|
|
3577
|
+
|
|
3578
|
+
|
|
3579
|
+
def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3580
|
+
parent = _builder_app_parent(payload)
|
|
3581
|
+
resources: list[JSONObject] = []
|
|
3582
|
+
for item in payload.get("chart_results") or []:
|
|
3583
|
+
if not isinstance(item, dict):
|
|
3584
|
+
continue
|
|
3585
|
+
status = str(item.get("status") or "success")
|
|
3586
|
+
operation = _builder_operation(item.get("operation") or status, fallback="updated")
|
|
3587
|
+
if status == "failed":
|
|
3588
|
+
operation = "failed"
|
|
3589
|
+
resource_status = "failed" if status == "failed" else ("readback_pending" if status == "readback_pending" else "success")
|
|
3590
|
+
chart_id = item.get("chart_id") or item.get("chartId")
|
|
3591
|
+
chart_key = item.get("chart_key") or item.get("chartKey")
|
|
3592
|
+
resources.append(
|
|
3593
|
+
_builder_resource(
|
|
3594
|
+
resource_type="chart",
|
|
3595
|
+
operation=operation,
|
|
3596
|
+
status=resource_status,
|
|
3597
|
+
id_value=chart_id,
|
|
3598
|
+
key=chart_key or chart_id,
|
|
3599
|
+
name=item.get("name") or item.get("chart_name") or item.get("chartName"),
|
|
3600
|
+
ids={
|
|
3601
|
+
**({"chart_id": chart_id} if chart_id not in (None, "") else {}),
|
|
3602
|
+
**({"chart_key": chart_key} if chart_key else {}),
|
|
3603
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3604
|
+
**({"chart_type": item.get("chart_type")} if item.get("chart_type") else {}),
|
|
3605
|
+
},
|
|
3606
|
+
parent=parent,
|
|
3607
|
+
error_code=item.get("error_code"),
|
|
3608
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3609
|
+
)
|
|
3610
|
+
)
|
|
3611
|
+
return resources
|
|
3612
|
+
|
|
3613
|
+
|
|
3614
|
+
def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3615
|
+
status = _builder_status(payload, "success")
|
|
3616
|
+
draft_result = payload.get("draft_result") if isinstance(payload.get("draft_result"), dict) else {}
|
|
3617
|
+
live_result = payload.get("live_result") if isinstance(payload.get("live_result"), dict) else {}
|
|
3618
|
+
normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
|
|
3619
|
+
dash_key = (
|
|
3620
|
+
payload.get("dash_key")
|
|
3621
|
+
or payload.get("dashKey")
|
|
3622
|
+
or draft_result.get("dashKey")
|
|
3623
|
+
or draft_result.get("dash_key")
|
|
3624
|
+
or live_result.get("dashKey")
|
|
3625
|
+
or live_result.get("dash_key")
|
|
3626
|
+
)
|
|
3627
|
+
dash_name = (
|
|
3628
|
+
payload.get("dash_name")
|
|
3629
|
+
or payload.get("dashName")
|
|
3630
|
+
or payload.get("name")
|
|
3631
|
+
or draft_result.get("dashName")
|
|
3632
|
+
or draft_result.get("dash_name")
|
|
3633
|
+
or draft_result.get("name")
|
|
3634
|
+
or live_result.get("dashName")
|
|
3635
|
+
or live_result.get("dash_name")
|
|
3636
|
+
or live_result.get("name")
|
|
3637
|
+
or normalized_args.get("dash_name")
|
|
3638
|
+
or normalized_args.get("dashName")
|
|
3639
|
+
)
|
|
3640
|
+
package_id = payload.get("package_id") or normalized_args.get("package_id") or normalized_args.get("package_tag_id")
|
|
3641
|
+
if package_id in (None, "") and isinstance(draft_result.get("tags"), list) and draft_result.get("tags"):
|
|
3642
|
+
first_tag = draft_result.get("tags")[0]
|
|
3643
|
+
if isinstance(first_tag, dict):
|
|
3644
|
+
package_id = first_tag.get("tagId") or first_tag.get("tag_id") or first_tag.get("id")
|
|
3645
|
+
operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
|
|
3646
|
+
parent = None
|
|
3647
|
+
if package_id:
|
|
3648
|
+
parent = _builder_parent("package", id_value=package_id, key=package_id)
|
|
3649
|
+
icon_config = (
|
|
3650
|
+
_builder_container_icon_config(payload, raw_keys=("dash_icon", "dashIcon", "icon"))
|
|
3651
|
+
or _builder_container_icon_config(draft_result, raw_keys=("dashIcon", "dash_icon", "icon"))
|
|
3652
|
+
or _builder_container_icon_config(live_result, raw_keys=("dashIcon", "dash_icon", "icon"))
|
|
3653
|
+
or _builder_container_icon_config(normalized_args, raw_keys=("icon", "dash_icon", "dashIcon"))
|
|
3654
|
+
)
|
|
3655
|
+
return [
|
|
3656
|
+
_builder_resource(
|
|
3657
|
+
resource_type="portal",
|
|
3658
|
+
operation=operation,
|
|
3659
|
+
status=status,
|
|
3660
|
+
key=dash_key,
|
|
3661
|
+
name=dash_name,
|
|
3662
|
+
ids={
|
|
3663
|
+
**({"dash_key": dash_key} if dash_key else {}),
|
|
3664
|
+
**({"package_id": package_id} if package_id else {}),
|
|
3665
|
+
},
|
|
3666
|
+
parent=parent,
|
|
3667
|
+
icon_config=icon_config,
|
|
3668
|
+
error_code=payload.get("error_code"),
|
|
3669
|
+
message=payload.get("message") if status == "failed" else None,
|
|
3670
|
+
)
|
|
3671
|
+
]
|
|
3672
|
+
|
|
3673
|
+
|
|
3674
|
+
def _builder_button_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3675
|
+
parent = _builder_app_parent(payload)
|
|
3676
|
+
resources: list[JSONObject] = []
|
|
3677
|
+
for key, operation in (("created", "created"), ("updated", "updated"), ("removed", "removed"), ("failed", "failed")):
|
|
3678
|
+
for item in payload.get(key) or []:
|
|
3679
|
+
if not isinstance(item, dict):
|
|
3680
|
+
continue
|
|
3681
|
+
status = "failed" if operation == "failed" or item.get("status") == "failed" else str(item.get("status") or "success")
|
|
3682
|
+
button_id = item.get("button_id") or item.get("buttonId")
|
|
3683
|
+
resources.append(
|
|
3684
|
+
_builder_resource(
|
|
3685
|
+
resource_type="button",
|
|
3686
|
+
operation=operation if operation != "failed" else "failed",
|
|
3687
|
+
status=status,
|
|
3688
|
+
id_value=button_id,
|
|
3689
|
+
key=button_id,
|
|
3690
|
+
name=item.get("button_text") or item.get("buttonText"),
|
|
3691
|
+
ids={
|
|
3692
|
+
**({"button_id": button_id} if button_id not in (None, "") else {}),
|
|
3693
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3694
|
+
},
|
|
3695
|
+
parent=parent,
|
|
3696
|
+
error_code=item.get("error_code"),
|
|
3697
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3698
|
+
)
|
|
3699
|
+
)
|
|
3700
|
+
for item in payload.get("view_configs") or []:
|
|
3701
|
+
if isinstance(item, dict):
|
|
3702
|
+
status = str(item.get("status") or "success")
|
|
3703
|
+
view_key = item.get("view_key") or item.get("viewKey")
|
|
3704
|
+
resources.append(
|
|
3705
|
+
_builder_resource(
|
|
3706
|
+
resource_type="button_binding",
|
|
3707
|
+
operation="updated" if status != "failed" else "failed",
|
|
3708
|
+
status=status,
|
|
3709
|
+
key=view_key,
|
|
3710
|
+
name=item.get("view_name") or item.get("viewName") or view_key,
|
|
3711
|
+
ids={
|
|
3712
|
+
**({"view_key": view_key} if view_key else {}),
|
|
3713
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3714
|
+
},
|
|
3715
|
+
parent=parent,
|
|
3716
|
+
error_code=item.get("error_code"),
|
|
3717
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3718
|
+
)
|
|
3719
|
+
)
|
|
3720
|
+
return resources
|
|
3721
|
+
|
|
3722
|
+
|
|
3723
|
+
def _builder_associated_resource_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3724
|
+
parent = _builder_app_parent(payload)
|
|
3725
|
+
readback_by_id = _builder_associated_resource_readback_by_id(payload)
|
|
3726
|
+
resources: list[JSONObject] = []
|
|
3727
|
+
for key, operation in (
|
|
3728
|
+
("created", "created"),
|
|
3729
|
+
("updated", "updated"),
|
|
3730
|
+
("unchanged", "unchanged"),
|
|
3731
|
+
("removed", "removed"),
|
|
3732
|
+
("failed", "failed"),
|
|
3733
|
+
):
|
|
3734
|
+
for item in payload.get(key) or []:
|
|
3735
|
+
if not isinstance(item, dict):
|
|
3736
|
+
continue
|
|
3737
|
+
status = "failed" if operation == "failed" or item.get("status") == "failed" else str(item.get("status") or "success")
|
|
3738
|
+
associated_item_id = item.get("associated_item_id") or item.get("associatedItemId")
|
|
3739
|
+
readback = readback_by_id.get(str(associated_item_id)) if associated_item_id not in (None, "") else None
|
|
3740
|
+
view_key = item.get("view_key") or item.get("viewKey") or (readback or {}).get("view_key") or (readback or {}).get("viewKey")
|
|
3741
|
+
chart_key = item.get("chart_key") or item.get("chartKey") or (readback or {}).get("chart_key") or (readback or {}).get("chartKey")
|
|
3742
|
+
target_app_key = item.get("target_app_key") or (readback or {}).get("target_app_key")
|
|
3743
|
+
name = item.get("name") or item.get("resource_name") or (readback or {}).get("name") or view_key or chart_key
|
|
3744
|
+
resources.append(
|
|
3745
|
+
_builder_resource(
|
|
3746
|
+
resource_type="associated_resource",
|
|
3747
|
+
operation=operation if operation != "failed" else "failed",
|
|
3748
|
+
status=status,
|
|
3749
|
+
id_value=associated_item_id,
|
|
3750
|
+
key=view_key or chart_key or associated_item_id,
|
|
3751
|
+
name=name,
|
|
3752
|
+
ids={
|
|
3753
|
+
**({"associated_item_id": associated_item_id} if associated_item_id not in (None, "") else {}),
|
|
3754
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3755
|
+
**({"target_app_key": target_app_key} if target_app_key else {}),
|
|
3756
|
+
**({"view_key": view_key} if view_key else {}),
|
|
3757
|
+
**({"chart_key": chart_key} if chart_key else {}),
|
|
3758
|
+
},
|
|
3759
|
+
parent=parent,
|
|
3760
|
+
error_code=item.get("error_code"),
|
|
3761
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3762
|
+
)
|
|
3763
|
+
)
|
|
3764
|
+
for item in payload.get("view_configs") or []:
|
|
3765
|
+
if isinstance(item, dict):
|
|
3766
|
+
status = str(item.get("status") or "success")
|
|
3767
|
+
view_key = item.get("view_key") or item.get("viewKey")
|
|
3768
|
+
resources.append(
|
|
3769
|
+
_builder_resource(
|
|
3770
|
+
resource_type="associated_resource_binding",
|
|
3771
|
+
operation="updated" if status != "failed" else "failed",
|
|
3772
|
+
status=status,
|
|
3773
|
+
key=view_key,
|
|
3774
|
+
name=item.get("view_name") or item.get("viewName") or view_key,
|
|
3775
|
+
ids={
|
|
3776
|
+
**({"view_key": view_key} if view_key else {}),
|
|
3777
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3778
|
+
},
|
|
3779
|
+
parent=parent,
|
|
3780
|
+
error_code=item.get("error_code"),
|
|
3781
|
+
message=item.get("message") if status == "failed" else None,
|
|
3782
|
+
)
|
|
3783
|
+
)
|
|
3784
|
+
return resources
|
|
3785
|
+
|
|
3786
|
+
|
|
3787
|
+
def _builder_associated_resource_readback_by_id(payload: JSONObject) -> dict[str, JSONObject]:
|
|
3788
|
+
result: dict[str, JSONObject] = {}
|
|
3789
|
+
|
|
3790
|
+
def collect(items: object) -> None:
|
|
3791
|
+
if not isinstance(items, list):
|
|
3792
|
+
return
|
|
3793
|
+
for item in items:
|
|
3794
|
+
if not isinstance(item, dict):
|
|
3795
|
+
continue
|
|
3796
|
+
associated_item_id = item.get("associated_item_id") or item.get("associatedItemId")
|
|
3797
|
+
if associated_item_id in (None, ""):
|
|
3798
|
+
continue
|
|
3799
|
+
result[str(associated_item_id)] = item
|
|
3800
|
+
|
|
3801
|
+
collect(payload.get("associated_resources"))
|
|
3802
|
+
for config in payload.get("view_configs") or []:
|
|
3803
|
+
if not isinstance(config, dict):
|
|
3804
|
+
continue
|
|
3805
|
+
for key in ("actual", "expected"):
|
|
3806
|
+
value = config.get(key)
|
|
3807
|
+
if isinstance(value, dict):
|
|
3808
|
+
collect(value.get("items"))
|
|
3809
|
+
return result
|
|
3810
|
+
|
|
3811
|
+
|
|
2451
3812
|
def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
2452
3813
|
if isinstance(error, QingflowApiError):
|
|
2453
3814
|
return error
|
|
@@ -2596,6 +3957,40 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2596
3957
|
"tool_name": "chart_get",
|
|
2597
3958
|
},
|
|
2598
3959
|
},
|
|
3960
|
+
"workspace_icon_catalog_get": {
|
|
3961
|
+
"allowed_keys": [],
|
|
3962
|
+
"aliases": {},
|
|
3963
|
+
"allowed_values": {
|
|
3964
|
+
"icon": list(WORKSPACE_ICON_NAMES),
|
|
3965
|
+
"color": list(WORKSPACE_ICON_COLORS),
|
|
3966
|
+
"generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
3967
|
+
},
|
|
3968
|
+
"execution_notes": [
|
|
3969
|
+
"read this before creating app packages, apps, or portals when choosing supported workspace icons",
|
|
3970
|
+
"the CLI validates icon/color candidates but does not infer business defaults from resource names",
|
|
3971
|
+
"new app/package/portal creation requires explicit non-template icon + color",
|
|
3972
|
+
],
|
|
3973
|
+
"minimal_example": {
|
|
3974
|
+
"profile": "default",
|
|
3975
|
+
},
|
|
3976
|
+
},
|
|
3977
|
+
"package_list": {
|
|
3978
|
+
"allowed_keys": ["trial_status", "query"],
|
|
3979
|
+
"aliases": {"trialStatus": "trial_status", "keyword": "query"},
|
|
3980
|
+
"allowed_values": {"trial_status": ["all"]},
|
|
3981
|
+
"execution_notes": [
|
|
3982
|
+
"lists app packages visible to the current builder profile by calling backend GET /tag?trialStatus=...",
|
|
3983
|
+
"query is applied locally to package_id/tag_id/package_name/tag_name after /tag returns",
|
|
3984
|
+
"does not fall back to app list because app list cannot represent empty packages, duplicate package names, or package-level permissions",
|
|
3985
|
+
"returns package_id/package_name plus compatible tag_id/tag_name; use package_get for package detail before editing",
|
|
3986
|
+
"permission failures are returned as PACKAGE_LIST_FAILED with backend transport details",
|
|
3987
|
+
],
|
|
3988
|
+
"minimal_example": {
|
|
3989
|
+
"profile": "default",
|
|
3990
|
+
"trial_status": "all",
|
|
3991
|
+
"query": "产品研发",
|
|
3992
|
+
},
|
|
3993
|
+
},
|
|
2599
3994
|
"package_get": {
|
|
2600
3995
|
"allowed_keys": ["package_id"],
|
|
2601
3996
|
"aliases": {"packageId": "package_id"},
|
|
@@ -2620,9 +4015,17 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2620
4015
|
"iconColor": "color",
|
|
2621
4016
|
"allowDetach": "allow_detach",
|
|
2622
4017
|
},
|
|
2623
|
-
"allowed_values":
|
|
4018
|
+
"allowed_values": {
|
|
4019
|
+
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
4020
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
4021
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
4022
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
4023
|
+
},
|
|
2624
4024
|
"execution_notes": [
|
|
2625
4025
|
"create or update package metadata, visibility, grouping, and ordering in one call",
|
|
4026
|
+
"creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
4027
|
+
"updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4028
|
+
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
2626
4029
|
"metadata keys omitted on update are preserved",
|
|
2627
4030
|
"package_id maps internally to backend tagId; do not use tag_id in public calls",
|
|
2628
4031
|
"items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
|
|
@@ -2646,7 +4049,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2646
4049
|
"profile": "default",
|
|
2647
4050
|
"package_name": "项目管理",
|
|
2648
4051
|
"create_if_missing": True,
|
|
2649
|
-
"icon": "
|
|
4052
|
+
"icon": "briefcase",
|
|
2650
4053
|
"color": "azure",
|
|
2651
4054
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
2652
4055
|
},
|
|
@@ -2759,7 +4162,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2759
4162
|
},
|
|
2760
4163
|
"execution_notes": [
|
|
2761
4164
|
"use this read-only tool before button writes when an agent needs a supported icon or color choice",
|
|
2762
|
-
"current frontend only supports
|
|
4165
|
+
"current frontend only supports button icons and button colors from this catalog",
|
|
2763
4166
|
"text/icon color is unified through text_color; there is no separate icon_color",
|
|
2764
4167
|
],
|
|
2765
4168
|
"minimal_example": {
|
|
@@ -2861,6 +4264,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2861
4264
|
"default placements are header and detail; header maps to frontend top buttons",
|
|
2862
4265
|
"placement=list configures backend INSIDE row/list buttons; header maps to TOP and detail maps to DETAIL",
|
|
2863
4266
|
"remove_buttons supports button_id or exact unique button_text",
|
|
4267
|
+
"after a remove_buttons DELETE is sent, the tool verifies deletion by single button_id readback; removed[] returns delete_executed, readback_status, and safe_to_retry_delete=false",
|
|
4268
|
+
"if a removed button returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
|
|
2864
4269
|
"all operations share one edit context and publish after at least one write succeeds; there is no draft-only mode for this tool",
|
|
2865
4270
|
"background_color and text_color cannot both be white",
|
|
2866
4271
|
],
|
|
@@ -3003,6 +4408,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3003
4408
|
"match_mappings.source_field accepts source schema fields plus system fields 数据ID(-17) and 编号(0); match_mappings compiles to backend matchRules",
|
|
3004
4409
|
"do not write raw match_rules unless preserving a legacy backend config; match_mappings and match_rules are mutually exclusive",
|
|
3005
4410
|
"client_key only lets a view_config reference a resource created earlier in the same apply call through associated_item_refs; it is not persisted and cannot deduplicate later apply calls",
|
|
4411
|
+
"remove_associated_item_ids sends DELETE and verifies deletion with one associated-resource pool readback because the backend has no confirmed single-item GET; removed[] returns delete_executed, readback_status, and safe_to_retry_delete=false",
|
|
4412
|
+
"if an associated resource delete returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
|
|
3006
4413
|
"this tool publishes after at least one write succeeds; there is no draft-only mode",
|
|
3007
4414
|
"visible=false hides the associated-resource area without clearing previous selected ids; visible=true with limit_type=all shows the whole app-level pool",
|
|
3008
4415
|
],
|
|
@@ -3069,17 +4476,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3069
4476
|
"field.type": [member.value for member in PublicFieldType],
|
|
3070
4477
|
"field.relation_mode": [member.value for member in PublicRelationMode],
|
|
3071
4478
|
"field_type_ids": sorted(FIELD_TYPE_ID_ALIASES.keys()),
|
|
4479
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
4480
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
4481
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
3072
4482
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
3073
4483
|
},
|
|
3074
4484
|
"execution_notes": [
|
|
3075
4485
|
"create mode may set visibility for the new app; edit mode may update visibility on an existing app",
|
|
4486
|
+
"create mode should include explicit non-template icon + color; apply mode enforces this before writing",
|
|
4487
|
+
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
3076
4488
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3077
4489
|
],
|
|
3078
4490
|
"minimal_example": {
|
|
3079
4491
|
"profile": "default",
|
|
3080
4492
|
"app_name": "研发项目管理",
|
|
3081
4493
|
"package_id": 1001,
|
|
3082
|
-
"icon": "
|
|
4494
|
+
"icon": "briefcase",
|
|
3083
4495
|
"color": "emerald",
|
|
3084
4496
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
3085
4497
|
"create_if_missing": True,
|
|
@@ -3105,11 +4517,40 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3105
4517
|
},
|
|
3106
4518
|
},
|
|
3107
4519
|
"app_schema_apply": {
|
|
3108
|
-
"allowed_keys": [
|
|
4520
|
+
"allowed_keys": [
|
|
4521
|
+
"app_key",
|
|
4522
|
+
"package_id",
|
|
4523
|
+
"app_name",
|
|
4524
|
+
"icon",
|
|
4525
|
+
"color",
|
|
4526
|
+
"visibility",
|
|
4527
|
+
"create_if_missing",
|
|
4528
|
+
"publish",
|
|
4529
|
+
"add_fields",
|
|
4530
|
+
"update_fields",
|
|
4531
|
+
"remove_fields",
|
|
4532
|
+
"apps",
|
|
4533
|
+
"apps[].client_key",
|
|
4534
|
+
"apps[].app_key",
|
|
4535
|
+
"apps[].app_name",
|
|
4536
|
+
"apps[].icon",
|
|
4537
|
+
"apps[].color",
|
|
4538
|
+
"apps[].visibility",
|
|
4539
|
+
"apps[].add_fields",
|
|
4540
|
+
"apps[].update_fields",
|
|
4541
|
+
"apps[].remove_fields",
|
|
4542
|
+
"apps[].add_fields[].target_app_ref",
|
|
4543
|
+
],
|
|
3109
4544
|
"aliases": {
|
|
3110
4545
|
"app_title": "app_name",
|
|
3111
4546
|
"title": "app_name",
|
|
3112
4547
|
"packageId": "package_id",
|
|
4548
|
+
"apps[].clientKey": "apps[].client_key",
|
|
4549
|
+
"apps[].appKey": "apps[].app_key",
|
|
4550
|
+
"apps[].appName": "apps[].app_name",
|
|
4551
|
+
"apps[].appTitle": "apps[].app_name",
|
|
4552
|
+
"field.targetAppRef": "field.target_app_ref",
|
|
4553
|
+
"field.targetAppClientKey": "field.target_app_ref",
|
|
3113
4554
|
"field.title": "field.name",
|
|
3114
4555
|
"field.label": "field.name",
|
|
3115
4556
|
"field.fields": "field.subfields",
|
|
@@ -3146,7 +4587,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3146
4587
|
"use exactly one resource mode",
|
|
3147
4588
|
"edit mode: app_key, optional app_name to rename the existing app",
|
|
3148
4589
|
"create mode: package_id + app_name + create_if_missing=true",
|
|
4590
|
+
"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",
|
|
4591
|
+
"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",
|
|
4592
|
+
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
3149
4593
|
"create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
|
|
4594
|
+
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
4595
|
+
"multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
|
|
4596
|
+
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4597
|
+
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
3150
4598
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3151
4599
|
"update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
|
|
3152
4600
|
"multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
|
|
@@ -3171,7 +4619,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3171
4619
|
"profile": "default",
|
|
3172
4620
|
"app_name": "研发项目管理",
|
|
3173
4621
|
"package_id": 1001,
|
|
3174
|
-
"icon": "
|
|
4622
|
+
"icon": "briefcase",
|
|
3175
4623
|
"color": "emerald",
|
|
3176
4624
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
3177
4625
|
"create_if_missing": True,
|
|
@@ -3183,6 +4631,40 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3183
4631
|
"update_fields": [],
|
|
3184
4632
|
"remove_fields": [],
|
|
3185
4633
|
},
|
|
4634
|
+
"multi_app_example": {
|
|
4635
|
+
"profile": "default",
|
|
4636
|
+
"package_id": 1001,
|
|
4637
|
+
"create_if_missing": True,
|
|
4638
|
+
"publish": True,
|
|
4639
|
+
"apps": [
|
|
4640
|
+
{
|
|
4641
|
+
"client_key": "employee",
|
|
4642
|
+
"app_name": "员工花名册",
|
|
4643
|
+
"icon": "business-personalcard",
|
|
4644
|
+
"color": "emerald",
|
|
4645
|
+
"add_fields": [
|
|
4646
|
+
{"name": "员工名称", "type": "text", "as_data_title": True},
|
|
4647
|
+
{"name": "员工照片", "type": "attachment", "as_data_cover": True},
|
|
4648
|
+
],
|
|
4649
|
+
},
|
|
4650
|
+
{
|
|
4651
|
+
"client_key": "worklog",
|
|
4652
|
+
"app_name": "工时表",
|
|
4653
|
+
"icon": "clock",
|
|
4654
|
+
"color": "blue",
|
|
4655
|
+
"add_fields": [
|
|
4656
|
+
{"name": "工时标题", "type": "text", "as_data_title": True},
|
|
4657
|
+
{
|
|
4658
|
+
"name": "关联员工",
|
|
4659
|
+
"type": "relation",
|
|
4660
|
+
"target_app_ref": "employee",
|
|
4661
|
+
"display_field": {"name": "员工名称"},
|
|
4662
|
+
"visible_fields": [{"name": "员工名称"}],
|
|
4663
|
+
},
|
|
4664
|
+
],
|
|
4665
|
+
},
|
|
4666
|
+
],
|
|
4667
|
+
},
|
|
3186
4668
|
"rename_example": {
|
|
3187
4669
|
"profile": "default",
|
|
3188
4670
|
"app_key": "APP_PROJECT",
|
|
@@ -3451,7 +4933,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3451
4933
|
},
|
|
3452
4934
|
},
|
|
3453
4935
|
"app_views_plan": {
|
|
3454
|
-
"allowed_keys": [
|
|
4936
|
+
"allowed_keys": [
|
|
4937
|
+
"app_key",
|
|
4938
|
+
"upsert_views",
|
|
4939
|
+
"patch_views",
|
|
4940
|
+
"remove_views",
|
|
4941
|
+
"preset",
|
|
4942
|
+
"upsert_views[].view_key",
|
|
4943
|
+
"upsert_views[].name",
|
|
4944
|
+
"upsert_views[].type",
|
|
4945
|
+
"upsert_views[].columns",
|
|
4946
|
+
"upsert_views[].filters",
|
|
4947
|
+
"upsert_views[].buttons",
|
|
4948
|
+
"upsert_views[].visibility",
|
|
4949
|
+
"upsert_views[].query_conditions",
|
|
4950
|
+
"patch_views[].view_key",
|
|
4951
|
+
"patch_views[].name",
|
|
4952
|
+
"patch_views[].set",
|
|
4953
|
+
"patch_views[].unset",
|
|
4954
|
+
],
|
|
3455
4955
|
"aliases": {
|
|
3456
4956
|
"fields": "columns",
|
|
3457
4957
|
"column_names": "columns",
|
|
@@ -3483,7 +4983,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3483
4983
|
"view.type": [member.value for member in PublicViewType],
|
|
3484
4984
|
"view.filter.operator": [member.value for member in ViewFilterOperator],
|
|
3485
4985
|
"view.buttons.button_type": ["SYSTEM", "CUSTOM"],
|
|
3486
|
-
"view.buttons.config_type": ["TOP", "DETAIL"],
|
|
4986
|
+
"view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
|
|
3487
4987
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
3488
4988
|
},
|
|
3489
4989
|
"execution_notes": [
|
|
@@ -3491,7 +4991,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3491
4991
|
"filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
|
|
3492
4992
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
3493
4993
|
"use patch_views for partial parameter replacement on existing views; the tool reads current config, merges patch_views[].set/unset, then submits the backend full-save payload internally",
|
|
3494
|
-
"
|
|
4994
|
+
"remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
|
|
4995
|
+
"deleted views return verification.by_view[].delete_executed, readback_status, and safe_to_retry_delete=false; if readback is pending, do not blindly repeat the delete",
|
|
4996
|
+
"new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_views explicitly changes associated_resources",
|
|
4997
|
+
"associated report/view resource pool and per-view selected resources are configured through app_associated_resources_apply; app_views_apply only keeps legacy associated_resources input compatible",
|
|
3495
4998
|
"for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
|
|
3496
4999
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3497
5000
|
],
|
|
@@ -3554,7 +5057,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3554
5057
|
},
|
|
3555
5058
|
},
|
|
3556
5059
|
"app_views_apply": {
|
|
3557
|
-
"allowed_keys": [
|
|
5060
|
+
"allowed_keys": [
|
|
5061
|
+
"app_key",
|
|
5062
|
+
"publish",
|
|
5063
|
+
"upsert_views",
|
|
5064
|
+
"patch_views",
|
|
5065
|
+
"remove_views",
|
|
5066
|
+
"upsert_views[].view_key",
|
|
5067
|
+
"upsert_views[].name",
|
|
5068
|
+
"upsert_views[].type",
|
|
5069
|
+
"upsert_views[].columns",
|
|
5070
|
+
"upsert_views[].filters",
|
|
5071
|
+
"upsert_views[].buttons",
|
|
5072
|
+
"upsert_views[].visibility",
|
|
5073
|
+
"upsert_views[].query_conditions",
|
|
5074
|
+
"patch_views[].view_key",
|
|
5075
|
+
"patch_views[].name",
|
|
5076
|
+
"patch_views[].set",
|
|
5077
|
+
"patch_views[].unset",
|
|
5078
|
+
],
|
|
3558
5079
|
"aliases": {
|
|
3559
5080
|
"fields": "columns",
|
|
3560
5081
|
"column_names": "columns",
|
|
@@ -3585,7 +5106,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3585
5106
|
"view.type": [member.value for member in PublicViewType],
|
|
3586
5107
|
"view.filter.operator": [member.value for member in ViewFilterOperator],
|
|
3587
5108
|
"view.buttons.button_type": ["SYSTEM", "CUSTOM"],
|
|
3588
|
-
"view.buttons.config_type": ["TOP", "DETAIL"],
|
|
5109
|
+
"view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
|
|
3589
5110
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
3590
5111
|
},
|
|
3591
5112
|
"execution_notes": [
|
|
@@ -3598,7 +5119,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3598
5119
|
"filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
|
|
3599
5120
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
3600
5121
|
"use patch_views for partial parameter replacement on existing views; the public update mode is patch even though the backend save is still a full view payload",
|
|
3601
|
-
"
|
|
5122
|
+
"remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
|
|
5123
|
+
"deleted views return verification.by_view[].delete_executed, readback_status, and safe_to_retry_delete=false; if readback is pending, do not blindly repeat the delete",
|
|
5124
|
+
"new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_views explicitly changes associated_resources",
|
|
5125
|
+
"associated report/view resource pool and per-view selected resources are configured through app_associated_resources_apply; app_views_apply keeps legacy associated_resources input compatible but it is no longer the recommended public contract",
|
|
3602
5126
|
"for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
|
|
3603
5127
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3604
5128
|
],
|
|
@@ -3690,6 +5214,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3690
5214
|
"execution_notes": [
|
|
3691
5215
|
"returns compact current field configuration for one app",
|
|
3692
5216
|
"use this before app_schema_apply when you need exact field definitions",
|
|
5217
|
+
"also returns chart_fields from QingBI datasource fields; app_charts_apply field selectors should use chart_fields because record/schema-visible fields and QingBI fields are not the same schema",
|
|
5218
|
+
"chart_fields[].field_id supports field_<queId> selectors, while chart_fields[].bi_field_id is the raw QingBI fieldId accepted by report configs",
|
|
3693
5219
|
"subtable fields include nested subfields using the same compact field shape",
|
|
3694
5220
|
],
|
|
3695
5221
|
"minimal_example": {
|
|
@@ -3811,6 +5337,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3811
5337
|
"successful create results must return a real backend chart_id",
|
|
3812
5338
|
"upsert_charts[].visibility compiles to QingBI base visibleAuth only",
|
|
3813
5339
|
"visibility-only updates keep the existing chart config and do not rewrite rawDataConfigDTO.authInfo",
|
|
5340
|
+
"chart dimension/metric/filter/query fields are resolved from app_get_fields.chart_fields (QingBI datasource fields), not record schema or form-only fields",
|
|
5341
|
+
"system fields such as 申请人/申请时间/编号 are usable only when they appear in chart_fields; otherwise app_charts_apply returns CHART_FIELD_NOT_IN_QINGBI_SCHEMA",
|
|
5342
|
+
"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",
|
|
5343
|
+
"chart rule failures return chart_results[].diagnostics with rule_code, expected, actual, offending_fields, and next_action; backend 81002/81005 are translated when possible",
|
|
5344
|
+
"remove_chart_ids deletes by chart_id and verifies each deleted chart with single chart_id readback; pure delete does not read the full chart list",
|
|
5345
|
+
"if delete readback is unavailable or still finds the chart, chart_results[] returns delete_executed=true, readback_status, and safe_to_retry_delete=false; do not blindly repeat delete",
|
|
3814
5346
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3815
5347
|
],
|
|
3816
5348
|
"minimal_example": {
|
|
@@ -3854,9 +5386,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3854
5386
|
},
|
|
3855
5387
|
},
|
|
3856
5388
|
"portal_apply": {
|
|
3857
|
-
"allowed_keys": ["dash_key", "dash_name", "package_id", "publish", "sections", "visibility", "auth", "icon", "color", "hide_copyright", "dash_global_config", "config"],
|
|
5389
|
+
"allowed_keys": ["dash_key", "dash_name", "name", "package_id", "publish", "sections", "pages", "payload", "layout_preset", "visibility", "auth", "icon", "color", "hide_copyright", "dash_global_config", "config"],
|
|
3858
5390
|
"aliases": {
|
|
3859
5391
|
"packageId": "package_id",
|
|
5392
|
+
"name": "dash_name",
|
|
3860
5393
|
"sourceType": "source_type",
|
|
3861
5394
|
"chartRef": "chart_ref",
|
|
3862
5395
|
"viewRef": "view_ref",
|
|
@@ -3869,20 +5402,34 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3869
5402
|
"viewRef": "view_ref",
|
|
3870
5403
|
"dashStyleConfigBO": "dash_style_config",
|
|
3871
5404
|
},
|
|
3872
|
-
"allowed_values": {
|
|
3873
|
-
"
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
5405
|
+
"allowed_values": {
|
|
5406
|
+
"section.source_type": ["chart", "view", "grid", "filter", "text", "link"],
|
|
5407
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
5408
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
5409
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
5410
|
+
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
5411
|
+
},
|
|
5412
|
+
"execution_notes": [
|
|
5413
|
+
"use exactly one resource mode",
|
|
5414
|
+
"update mode: dash_key",
|
|
5415
|
+
"create mode: package_id + dash_name",
|
|
5416
|
+
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5417
|
+
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
5418
|
+
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
5419
|
+
"portal_apply uses replace semantics for sections",
|
|
5420
|
+
"when editing an existing portal, sections may be omitted to update only base info such as visibility, icon, or package",
|
|
5421
|
+
"portal section-level patch is not exposed; supplying sections means full sections replacement",
|
|
5422
|
+
"remove a section by omitting it from the new sections list",
|
|
5423
|
+
"package_id is required when creating a new portal",
|
|
5424
|
+
"publish=false only guarantees draft and base-info updates; it does not claim live has changed",
|
|
3883
5425
|
"chart_ref resolves by chart_id first, then exact unique chart_name",
|
|
3884
5426
|
"view_ref resolves by view_key first, then exact unique view_name",
|
|
5427
|
+
"pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
|
|
5428
|
+
"if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
|
|
5429
|
+
"two-column pc layout should use x=0/12 with cols=12; three-column pc layout should use x=0/8/16 with cols=8",
|
|
5430
|
+
"x=0/6 with cols=6 only occupies the left half of the pc portal and triggers PORTAL_LAYOUT_HALF_WIDTH",
|
|
3885
5431
|
"position.pc/mobile is the canonical portal layout shape",
|
|
5432
|
+
"compat payload accepts name -> dash_name and single pages[0].components -> sections",
|
|
3886
5433
|
"visibility is the canonical public auth shape; auth is kept only as a deprecated compatibility alias",
|
|
3887
5434
|
"passing visibility and auth together is rejected as VISIBILITY_CONFLICT",
|
|
3888
5435
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
@@ -3891,7 +5438,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3891
5438
|
"profile": "default",
|
|
3892
5439
|
"dash_name": "经营门户",
|
|
3893
5440
|
"package_id": 1001,
|
|
5441
|
+
"icon": "view-grid",
|
|
5442
|
+
"color": "blue",
|
|
3894
5443
|
"publish": True,
|
|
5444
|
+
"layout_preset": "dashboard_2col",
|
|
3895
5445
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
3896
5446
|
"sections": [
|
|
3897
5447
|
{
|
|
@@ -3905,6 +5455,23 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3905
5455
|
}
|
|
3906
5456
|
],
|
|
3907
5457
|
},
|
|
5458
|
+
"compat_payload_example": {
|
|
5459
|
+
"name": "经营门户",
|
|
5460
|
+
"package_id": 1001,
|
|
5461
|
+
"layout_preset": "dashboard_2col",
|
|
5462
|
+
"pages": [
|
|
5463
|
+
{
|
|
5464
|
+
"title": "经营总览",
|
|
5465
|
+
"components": [
|
|
5466
|
+
{
|
|
5467
|
+
"title": "销售趋势",
|
|
5468
|
+
"source_type": "chart",
|
|
5469
|
+
"chart_ref": {"app_key": "APP_KEY", "chart_id": "CHART_ID"},
|
|
5470
|
+
}
|
|
5471
|
+
],
|
|
5472
|
+
}
|
|
5473
|
+
],
|
|
5474
|
+
},
|
|
3908
5475
|
"minimal_section_example": {
|
|
3909
5476
|
"title": "订单概览",
|
|
3910
5477
|
"source_type": "view",
|