@qingflow-tech/qingflow-app-user-mcp 1.0.10 → 1.0.12
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 +9 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +255 -0
- package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder/references/create-app.md +149 -0
- package/skills/qingflow-app-builder/references/environments.md +63 -0
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +107 -0
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
- package/skills/qingflow-app-builder/references/update-flow.md +158 -0
- package/skills/qingflow-app-builder/references/update-layout.md +68 -0
- package/skills/qingflow-app-builder/references/update-schema.md +72 -0
- package/skills/qingflow-app-builder/references/update-views.md +284 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/skills/qingflow-app-user/SKILL.md +12 -11
- package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
- package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
- package/skills/qingflow-app-user/references/record-patterns.md +5 -5
- package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
- package/skills/qingflow-mcp-setup/SKILL.md +113 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-record-analysis/SKILL.md +6 -7
- package/skills/qingflow-record-analysis/manifest.yaml +10 -0
- package/skills/qingflow-record-delete/SKILL.md +5 -3
- package/skills/qingflow-record-import/SKILL.md +6 -2
- package/skills/qingflow-record-insert/SKILL.md +48 -4
- package/skills/qingflow-record-insert/manifest.yaml +6 -0
- package/skills/qingflow-record-update/SKILL.md +36 -24
- package/skills/qingflow-task-ops/SKILL.md +25 -25
- package/skills/qingflow-task-ops/references/environments.md +0 -1
- package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +41 -2
- package/src/qingflow_mcp/builder_facade/service.py +2743 -423
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +30 -4
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +54 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +238 -8
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +24 -16
- package/src/qingflow_mcp/response_trim.py +119 -12
- package/src/qingflow_mcp/server.py +17 -14
- package/src/qingflow_mcp/server_app_builder.py +29 -7
- package/src/qingflow_mcp/server_app_user.py +23 -24
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
- package/src/qingflow_mcp/tools/app_tools.py +237 -51
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +230 -33
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +293 -40
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +134 -8
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +2305 -442
- package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -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
|
|
|
@@ -14,7 +15,7 @@ from ..builder_facade.button_style_catalog import (
|
|
|
14
15
|
)
|
|
15
16
|
from ..public_surface import public_builder_contract_tool_names
|
|
16
17
|
from ..config import DEFAULT_PROFILE
|
|
17
|
-
from ..errors import QingflowApiError
|
|
18
|
+
from ..errors import QingflowApiError, backend_code_int
|
|
18
19
|
from ..json_types import JSONObject
|
|
19
20
|
from ..builder_facade.models import (
|
|
20
21
|
AssociatedResourcesApplyRequest,
|
|
@@ -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
|
|
@@ -117,6 +127,14 @@ class AiBuilderTools(ToolBase):
|
|
|
117
127
|
def builder_tool_contract(tool_name: str = "") -> JSONObject:
|
|
118
128
|
return self.builder_tool_contract(tool_name=tool_name)
|
|
119
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
|
+
|
|
120
138
|
@mcp.tool()
|
|
121
139
|
def package_get(profile: str = DEFAULT_PROFILE, package_id: int = 0) -> JSONObject:
|
|
122
140
|
return self.package_get(profile=profile, package_id=package_id)
|
|
@@ -482,9 +500,12 @@ class AiBuilderTools(ToolBase):
|
|
|
482
500
|
profile: str = DEFAULT_PROFILE,
|
|
483
501
|
dash_key: str = "",
|
|
484
502
|
dash_name: str = "",
|
|
503
|
+
name: str = "",
|
|
485
504
|
package_id: int | None = None,
|
|
486
505
|
publish: bool = True,
|
|
487
506
|
sections: list[JSONObject] | None = None,
|
|
507
|
+
pages: list[JSONObject] | None = None,
|
|
508
|
+
layout_preset: str = "",
|
|
488
509
|
visibility: JSONObject | None = None,
|
|
489
510
|
auth: JSONObject | None = None,
|
|
490
511
|
icon: str | None = None,
|
|
@@ -492,10 +513,14 @@ class AiBuilderTools(ToolBase):
|
|
|
492
513
|
hide_copyright: bool | None = None,
|
|
493
514
|
dash_global_config: JSONObject | None = None,
|
|
494
515
|
config: JSONObject | None = None,
|
|
516
|
+
payload: JSONObject | None = None,
|
|
495
517
|
) -> JSONObject:
|
|
518
|
+
payload = payload if isinstance(payload, dict) else {}
|
|
496
519
|
has_dash_key = bool((dash_key or "").strip())
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
499
524
|
if has_dash_key and has_package_id:
|
|
500
525
|
return _config_failure(
|
|
501
526
|
tool_name="portal_apply",
|
|
@@ -512,9 +537,12 @@ class AiBuilderTools(ToolBase):
|
|
|
512
537
|
profile=profile,
|
|
513
538
|
dash_key=dash_key,
|
|
514
539
|
dash_name=dash_name,
|
|
540
|
+
name=name,
|
|
515
541
|
package_id=package_id,
|
|
516
542
|
publish=publish,
|
|
517
543
|
sections=sections or [],
|
|
544
|
+
pages=pages or [],
|
|
545
|
+
layout_preset=layout_preset,
|
|
518
546
|
visibility=visibility,
|
|
519
547
|
auth=auth,
|
|
520
548
|
icon=icon,
|
|
@@ -522,6 +550,7 @@ class AiBuilderTools(ToolBase):
|
|
|
522
550
|
hide_copyright=hide_copyright,
|
|
523
551
|
dash_global_config=dash_global_config,
|
|
524
552
|
config=config or {},
|
|
553
|
+
payload=payload,
|
|
525
554
|
)
|
|
526
555
|
|
|
527
556
|
@mcp.tool()
|
|
@@ -537,14 +566,14 @@ class AiBuilderTools(ToolBase):
|
|
|
537
566
|
)
|
|
538
567
|
|
|
539
568
|
@tool_cn_name("分组列表查询")
|
|
540
|
-
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:
|
|
541
570
|
"""执行分组与包相关逻辑。"""
|
|
542
|
-
normalized_args = {"trial_status": trial_status}
|
|
571
|
+
normalized_args = {"trial_status": trial_status, "query": query}
|
|
543
572
|
return _safe_tool_call(
|
|
544
|
-
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),
|
|
545
574
|
error_code="PACKAGE_LIST_FAILED",
|
|
546
575
|
normalized_args=normalized_args,
|
|
547
|
-
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}},
|
|
548
577
|
)
|
|
549
578
|
|
|
550
579
|
@tool_cn_name("分组解析")
|
|
@@ -611,6 +640,27 @@ class AiBuilderTools(ToolBase):
|
|
|
611
640
|
"contract": contract,
|
|
612
641
|
}
|
|
613
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
|
+
|
|
614
664
|
@tool_cn_name("分组创建")
|
|
615
665
|
def package_create(
|
|
616
666
|
self,
|
|
@@ -691,6 +741,14 @@ class AiBuilderTools(ToolBase):
|
|
|
691
741
|
"package_apply",
|
|
692
742
|
_visibility_validation_failure(str(exc), tool_name="package_apply", exc=exc),
|
|
693
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)
|
|
694
752
|
normalized_args = {
|
|
695
753
|
"package_id": package_id,
|
|
696
754
|
**({"package_name": package_name} if str(package_name or "").strip() else {}),
|
|
@@ -796,16 +854,23 @@ class AiBuilderTools(ToolBase):
|
|
|
796
854
|
contain_disable: bool = False,
|
|
797
855
|
) -> JSONObject:
|
|
798
856
|
"""执行工具方法逻辑。"""
|
|
857
|
+
normalized_query = str(query or "").strip()
|
|
799
858
|
normalized_args = {
|
|
800
|
-
"query":
|
|
859
|
+
"query": normalized_query,
|
|
801
860
|
"page_num": page_num,
|
|
802
861
|
"page_size": page_size,
|
|
803
862
|
"contain_disable": contain_disable,
|
|
804
863
|
}
|
|
864
|
+
if not normalized_query:
|
|
865
|
+
return _config_failure(
|
|
866
|
+
tool_name="member_search",
|
|
867
|
+
message="query is required for member_search; builder member lookup is a contact-directory path, not a record candidate fallback.",
|
|
868
|
+
fix_hint="For record member/department field ambiguity, use record member-candidates / department-candidates instead.",
|
|
869
|
+
)
|
|
805
870
|
return _safe_tool_call(
|
|
806
871
|
lambda: self._facade.member_search(
|
|
807
872
|
profile=profile,
|
|
808
|
-
query=
|
|
873
|
+
query=normalized_query,
|
|
809
874
|
page_num=page_num,
|
|
810
875
|
page_size=page_size,
|
|
811
876
|
contain_disable=contain_disable,
|
|
@@ -818,9 +883,16 @@ class AiBuilderTools(ToolBase):
|
|
|
818
883
|
@tool_cn_name("角色检索")
|
|
819
884
|
def role_search(self, *, profile: str, keyword: str, page_num: int = 1, page_size: int = 20) -> JSONObject:
|
|
820
885
|
"""执行角色相关逻辑。"""
|
|
821
|
-
|
|
886
|
+
normalized_keyword = str(keyword or "").strip()
|
|
887
|
+
normalized_args = {"keyword": normalized_keyword, "page_num": page_num, "page_size": page_size}
|
|
888
|
+
if not normalized_keyword:
|
|
889
|
+
return _config_failure(
|
|
890
|
+
tool_name="role_search",
|
|
891
|
+
message="keyword is required for role_search; builder role lookup is a contact-management path, not a record candidate fallback.",
|
|
892
|
+
fix_hint="For record member/department field ambiguity, use record member-candidates / department-candidates instead.",
|
|
893
|
+
)
|
|
822
894
|
return _safe_tool_call(
|
|
823
|
-
lambda: self._facade.role_search(profile=profile, keyword=
|
|
895
|
+
lambda: self._facade.role_search(profile=profile, keyword=normalized_keyword, page_num=page_num, page_size=page_size),
|
|
824
896
|
error_code="ROLE_SEARCH_FAILED",
|
|
825
897
|
normalized_args=normalized_args,
|
|
826
898
|
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, **normalized_args}},
|
|
@@ -1657,6 +1729,68 @@ class AiBuilderTools(ToolBase):
|
|
|
1657
1729
|
message="app_schema_apply multi-app mode requires non-empty apps.",
|
|
1658
1730
|
fix_hint="Pass apps as a non-empty list of app schema items.",
|
|
1659
1731
|
)
|
|
1732
|
+
icon_errors: list[JSONObject] = []
|
|
1733
|
+
seen_new_app_icons: dict[str, int] = {}
|
|
1734
|
+
for index, raw_item in enumerate(apps):
|
|
1735
|
+
if not isinstance(raw_item, dict):
|
|
1736
|
+
continue
|
|
1737
|
+
app_key = str(raw_item.get("app_key") or raw_item.get("appKey") or "").strip()
|
|
1738
|
+
creating_item = not app_key
|
|
1739
|
+
icon_failure = _validate_workspace_icon_for_builder(
|
|
1740
|
+
tool_name="app_schema_apply",
|
|
1741
|
+
icon=str(raw_item.get("icon") or ""),
|
|
1742
|
+
color=str(raw_item.get("color") or ""),
|
|
1743
|
+
creating=creating_item,
|
|
1744
|
+
)
|
|
1745
|
+
if icon_failure is not None:
|
|
1746
|
+
icon_errors.append(
|
|
1747
|
+
{
|
|
1748
|
+
"index": index,
|
|
1749
|
+
"row_number": index + 1,
|
|
1750
|
+
"error_code": icon_failure.get("error_code"),
|
|
1751
|
+
"message": icon_failure.get("message"),
|
|
1752
|
+
"details": icon_failure.get("details"),
|
|
1753
|
+
}
|
|
1754
|
+
)
|
|
1755
|
+
continue
|
|
1756
|
+
_ok, _error_code, _message, icon_details = validate_workspace_icon_choice(
|
|
1757
|
+
icon=str(raw_item.get("icon") or ""),
|
|
1758
|
+
color=str(raw_item.get("color") or ""),
|
|
1759
|
+
require_explicit=creating_item,
|
|
1760
|
+
disallow_generic=creating_item,
|
|
1761
|
+
)
|
|
1762
|
+
normalized_icon = str(icon_details.get("normalized_icon") or "").strip()
|
|
1763
|
+
if creating_item and normalized_icon:
|
|
1764
|
+
if normalized_icon in seen_new_app_icons:
|
|
1765
|
+
icon_errors.append(
|
|
1766
|
+
{
|
|
1767
|
+
"index": index,
|
|
1768
|
+
"row_number": index + 1,
|
|
1769
|
+
"error_code": "DUPLICATE_WORKSPACE_ICON_IN_BATCH",
|
|
1770
|
+
"message": f"apps[{index}] reuses icon '{normalized_icon}' from apps[{seen_new_app_icons[normalized_icon]}]",
|
|
1771
|
+
"details": {
|
|
1772
|
+
"icon": normalized_icon,
|
|
1773
|
+
"first_index": seen_new_app_icons[normalized_icon],
|
|
1774
|
+
"duplicate_index": index,
|
|
1775
|
+
"icon_catalog_command": "qingflow --json builder icon catalog",
|
|
1776
|
+
},
|
|
1777
|
+
}
|
|
1778
|
+
)
|
|
1779
|
+
else:
|
|
1780
|
+
seen_new_app_icons[normalized_icon] = index
|
|
1781
|
+
if icon_errors:
|
|
1782
|
+
return _config_failure(
|
|
1783
|
+
tool_name="app_schema_apply",
|
|
1784
|
+
error_code="WORKSPACE_ICON_BATCH_INVALID",
|
|
1785
|
+
message="one or more apps have invalid workspace icon configuration",
|
|
1786
|
+
fix_hint="Call `qingflow --json builder icon catalog`, choose a distinct non-template icon and color for each new app, then retry.",
|
|
1787
|
+
details={"icon_errors": icon_errors},
|
|
1788
|
+
allowed_values={
|
|
1789
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
1790
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
1791
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
1792
|
+
},
|
|
1793
|
+
)
|
|
1660
1794
|
|
|
1661
1795
|
client_key_to_app_key: dict[str, str] = {}
|
|
1662
1796
|
created_app_keys: list[str] = []
|
|
@@ -1700,6 +1834,9 @@ class AiBuilderTools(ToolBase):
|
|
|
1700
1834
|
)
|
|
1701
1835
|
public_shell = _publicize_package_fields(shell)
|
|
1702
1836
|
resolved_key = str(public_shell.get("app_key") or "").strip()
|
|
1837
|
+
shell_write_executed = _schema_apply_result_has_write(public_shell)
|
|
1838
|
+
if shell_write_executed:
|
|
1839
|
+
any_write_executed = True
|
|
1703
1840
|
if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
|
|
1704
1841
|
results.append({
|
|
1705
1842
|
"index": index,
|
|
@@ -1711,13 +1848,12 @@ class AiBuilderTools(ToolBase):
|
|
|
1711
1848
|
"stage": "resolve_or_create_shell",
|
|
1712
1849
|
"error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
|
|
1713
1850
|
"message": public_shell.get("message") or "app shell resolve/create failed",
|
|
1714
|
-
"
|
|
1851
|
+
"write_executed": shell_write_executed,
|
|
1852
|
+
"safe_to_retry": not shell_write_executed and not any_write_executed,
|
|
1715
1853
|
})
|
|
1716
1854
|
continue
|
|
1717
1855
|
if bool(public_shell.get("created")):
|
|
1718
1856
|
created_app_keys.append(resolved_key)
|
|
1719
|
-
if _schema_apply_result_has_write(public_shell):
|
|
1720
|
-
any_write_executed = True
|
|
1721
1857
|
if client_key:
|
|
1722
1858
|
client_key_to_app_key[client_key] = resolved_key
|
|
1723
1859
|
results.append({
|
|
@@ -1868,6 +2004,14 @@ class AiBuilderTools(ToolBase):
|
|
|
1868
2004
|
) -> JSONObject:
|
|
1869
2005
|
"""执行内部辅助逻辑。"""
|
|
1870
2006
|
effective_app_name = app_name or app_title
|
|
2007
|
+
icon_failure = _validate_workspace_icon_for_builder(
|
|
2008
|
+
tool_name="app_schema_apply",
|
|
2009
|
+
icon=icon,
|
|
2010
|
+
color=color,
|
|
2011
|
+
creating=not bool(str(app_key or "").strip()) and bool(create_if_missing),
|
|
2012
|
+
)
|
|
2013
|
+
if icon_failure is not None:
|
|
2014
|
+
return icon_failure
|
|
1871
2015
|
plan_result = self._rewrite_plan_result_for_apply(
|
|
1872
2016
|
result=self.app_schema_plan(
|
|
1873
2017
|
profile=profile,
|
|
@@ -2364,9 +2508,12 @@ class AiBuilderTools(ToolBase):
|
|
|
2364
2508
|
profile: str,
|
|
2365
2509
|
dash_key: str = "",
|
|
2366
2510
|
dash_name: str = "",
|
|
2511
|
+
name: str = "",
|
|
2367
2512
|
package_id: int | None = None,
|
|
2368
2513
|
publish: bool = True,
|
|
2369
2514
|
sections: list[JSONObject] | None = None,
|
|
2515
|
+
pages: list[JSONObject] | None = None,
|
|
2516
|
+
layout_preset: str = "",
|
|
2370
2517
|
visibility: JSONObject | None = None,
|
|
2371
2518
|
auth: JSONObject | None = None,
|
|
2372
2519
|
icon: str | None = None,
|
|
@@ -2374,25 +2521,44 @@ class AiBuilderTools(ToolBase):
|
|
|
2374
2521
|
hide_copyright: bool | None = None,
|
|
2375
2522
|
dash_global_config: JSONObject | None = None,
|
|
2376
2523
|
config: JSONObject | None = None,
|
|
2524
|
+
payload: JSONObject | None = None,
|
|
2377
2525
|
) -> JSONObject:
|
|
2378
2526
|
"""执行门户相关逻辑。"""
|
|
2527
|
+
request_payload: dict[str, Any] = dict(payload) if isinstance(payload, dict) else {}
|
|
2528
|
+
if dash_key:
|
|
2529
|
+
request_payload["dash_key"] = dash_key
|
|
2530
|
+
if dash_name:
|
|
2531
|
+
request_payload["dash_name"] = dash_name
|
|
2532
|
+
elif name:
|
|
2533
|
+
request_payload["name"] = name
|
|
2534
|
+
if package_id is not None:
|
|
2535
|
+
request_payload["package_id"] = package_id
|
|
2536
|
+
if "publish" not in request_payload or publish is False:
|
|
2537
|
+
request_payload["publish"] = publish
|
|
2538
|
+
if sections:
|
|
2539
|
+
request_payload["sections"] = sections
|
|
2540
|
+
if pages:
|
|
2541
|
+
request_payload["pages"] = pages
|
|
2542
|
+
if layout_preset:
|
|
2543
|
+
request_payload["layout_preset"] = layout_preset
|
|
2544
|
+
if visibility is not None:
|
|
2545
|
+
request_payload["visibility"] = visibility
|
|
2546
|
+
if auth is not None:
|
|
2547
|
+
request_payload["auth"] = auth
|
|
2548
|
+
if icon is not None:
|
|
2549
|
+
request_payload["icon"] = icon
|
|
2550
|
+
if color is not None:
|
|
2551
|
+
request_payload["color"] = color
|
|
2552
|
+
if hide_copyright is not None:
|
|
2553
|
+
request_payload["hide_copyright"] = hide_copyright
|
|
2554
|
+
if dash_global_config is not None:
|
|
2555
|
+
request_payload["dash_global_config"] = dash_global_config
|
|
2556
|
+
if config:
|
|
2557
|
+
merged_config = dict(request_payload.get("config") or {}) if isinstance(request_payload.get("config"), dict) else {}
|
|
2558
|
+
merged_config.update(config)
|
|
2559
|
+
request_payload["config"] = merged_config
|
|
2379
2560
|
try:
|
|
2380
|
-
request = PortalApplyRequest.model_validate(
|
|
2381
|
-
{
|
|
2382
|
-
"dash_key": dash_key or None,
|
|
2383
|
-
"dash_name": dash_name or None,
|
|
2384
|
-
"package_tag_id": package_id,
|
|
2385
|
-
"publish": publish,
|
|
2386
|
-
"sections": sections or [],
|
|
2387
|
-
"visibility": visibility,
|
|
2388
|
-
"auth": auth,
|
|
2389
|
-
"icon": icon,
|
|
2390
|
-
"color": color,
|
|
2391
|
-
"hide_copyright": hide_copyright,
|
|
2392
|
-
"dash_global_config": dash_global_config,
|
|
2393
|
-
"config": config or {},
|
|
2394
|
-
}
|
|
2395
|
-
)
|
|
2561
|
+
request = PortalApplyRequest.model_validate(request_payload)
|
|
2396
2562
|
except ValidationError as exc:
|
|
2397
2563
|
return _attach_builder_apply_envelope("portal_apply", _visibility_validation_failure(
|
|
2398
2564
|
str(exc),
|
|
@@ -2405,6 +2571,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2405
2571
|
"dash_name": dash_name or "业务门户",
|
|
2406
2572
|
"package_id": package_id or 1001,
|
|
2407
2573
|
"publish": True,
|
|
2574
|
+
"layout_preset": "dashboard_2col",
|
|
2408
2575
|
"sections": [
|
|
2409
2576
|
{
|
|
2410
2577
|
"title": "经营概览",
|
|
@@ -2417,6 +2584,14 @@ class AiBuilderTools(ToolBase):
|
|
|
2417
2584
|
))
|
|
2418
2585
|
normalized_args = request.model_dump(mode="json")
|
|
2419
2586
|
normalized_args["package_id"] = normalized_args.pop("package_tag_id", package_id)
|
|
2587
|
+
icon_failure = _validate_workspace_icon_for_builder(
|
|
2588
|
+
tool_name="portal_apply",
|
|
2589
|
+
icon=str(request.icon or ""),
|
|
2590
|
+
color=str(request.color or ""),
|
|
2591
|
+
creating=not bool(str(request.dash_key or "").strip()),
|
|
2592
|
+
)
|
|
2593
|
+
if icon_failure is not None:
|
|
2594
|
+
return _attach_builder_apply_envelope("portal_apply", icon_failure)
|
|
2420
2595
|
result = _publicize_package_fields(_safe_tool_call(
|
|
2421
2596
|
lambda: self._facade.portal_apply(profile=profile, request=request),
|
|
2422
2597
|
error_code="PORTAL_APPLY_FAILED",
|
|
@@ -2683,6 +2858,8 @@ def _merge_schema_field_diffs(*diffs: object) -> JSONObject:
|
|
|
2683
2858
|
|
|
2684
2859
|
|
|
2685
2860
|
def _schema_apply_result_has_write(result: JSONObject) -> bool:
|
|
2861
|
+
if "write_executed" in result:
|
|
2862
|
+
return bool(result.get("write_executed"))
|
|
2686
2863
|
if bool(result.get("created")) or bool(result.get("published")) or bool(result.get("app_base_updated")):
|
|
2687
2864
|
return True
|
|
2688
2865
|
field_diff = result.get("field_diff")
|
|
@@ -2771,20 +2948,34 @@ def _visibility_validation_failure(
|
|
|
2771
2948
|
return result
|
|
2772
2949
|
|
|
2773
2950
|
|
|
2774
|
-
def _config_failure(
|
|
2951
|
+
def _config_failure(
|
|
2952
|
+
*,
|
|
2953
|
+
tool_name: str,
|
|
2954
|
+
message: str,
|
|
2955
|
+
fix_hint: str,
|
|
2956
|
+
error_code: str = "CONFIG_ERROR",
|
|
2957
|
+
details: JSONObject | None = None,
|
|
2958
|
+
allowed_values: JSONObject | None = None,
|
|
2959
|
+
) -> JSONObject:
|
|
2775
2960
|
contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
|
|
2961
|
+
public_allowed_values = deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {}
|
|
2962
|
+
if allowed_values:
|
|
2963
|
+
public_allowed_values.update(deepcopy(allowed_values))
|
|
2964
|
+
public_details: JSONObject = {
|
|
2965
|
+
"fix_hint": fix_hint,
|
|
2966
|
+
"allowed_keys": deepcopy(contract.get("allowed_keys", [])) if isinstance(contract, dict) else [],
|
|
2967
|
+
}
|
|
2968
|
+
if details:
|
|
2969
|
+
public_details.update(deepcopy(details))
|
|
2776
2970
|
return {
|
|
2777
2971
|
"status": "failed",
|
|
2778
|
-
"error_code":
|
|
2972
|
+
"error_code": error_code,
|
|
2779
2973
|
"recoverable": True,
|
|
2780
2974
|
"message": message,
|
|
2781
2975
|
"normalized_args": {},
|
|
2782
2976
|
"missing_fields": [],
|
|
2783
|
-
"allowed_values":
|
|
2784
|
-
"details":
|
|
2785
|
-
"fix_hint": fix_hint,
|
|
2786
|
-
"allowed_keys": deepcopy(contract.get("allowed_keys", [])) if isinstance(contract, dict) else [],
|
|
2787
|
-
},
|
|
2977
|
+
"allowed_values": public_allowed_values,
|
|
2978
|
+
"details": public_details,
|
|
2788
2979
|
"suggested_next_call": None,
|
|
2789
2980
|
"request_id": None,
|
|
2790
2981
|
"backend_code": None,
|
|
@@ -2794,6 +2985,52 @@ def _config_failure(*, tool_name: str, message: str, fix_hint: str) -> JSONObjec
|
|
|
2794
2985
|
}
|
|
2795
2986
|
|
|
2796
2987
|
|
|
2988
|
+
def _workspace_icon_config_failure(
|
|
2989
|
+
*,
|
|
2990
|
+
tool_name: str,
|
|
2991
|
+
error_code: str,
|
|
2992
|
+
message: str,
|
|
2993
|
+
details: JSONObject,
|
|
2994
|
+
) -> JSONObject:
|
|
2995
|
+
return _config_failure(
|
|
2996
|
+
tool_name=tool_name,
|
|
2997
|
+
error_code=error_code,
|
|
2998
|
+
message=message,
|
|
2999
|
+
fix_hint="Call `qingflow --json builder icon catalog`, choose an explicit non-template icon and color, then retry.",
|
|
3000
|
+
details=details,
|
|
3001
|
+
allowed_values={
|
|
3002
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
3003
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
3004
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
3005
|
+
},
|
|
3006
|
+
)
|
|
3007
|
+
|
|
3008
|
+
|
|
3009
|
+
def _validate_workspace_icon_for_builder(
|
|
3010
|
+
*,
|
|
3011
|
+
tool_name: str,
|
|
3012
|
+
icon: str | None,
|
|
3013
|
+
color: str | None,
|
|
3014
|
+
creating: bool,
|
|
3015
|
+
) -> JSONObject | None:
|
|
3016
|
+
if not creating and not (str(icon or "").strip() or str(color or "").strip()):
|
|
3017
|
+
return None
|
|
3018
|
+
ok, error_code, message, details = validate_workspace_icon_choice(
|
|
3019
|
+
icon=icon,
|
|
3020
|
+
color=color,
|
|
3021
|
+
require_explicit=creating,
|
|
3022
|
+
disallow_generic=creating,
|
|
3023
|
+
)
|
|
3024
|
+
if ok:
|
|
3025
|
+
return None
|
|
3026
|
+
return _workspace_icon_config_failure(
|
|
3027
|
+
tool_name=tool_name,
|
|
3028
|
+
error_code=error_code or "WORKSPACE_ICON_INVALID",
|
|
3029
|
+
message=message or "invalid workspace icon configuration",
|
|
3030
|
+
details=details,
|
|
3031
|
+
)
|
|
3032
|
+
|
|
3033
|
+
|
|
2797
3034
|
def _safe_tool_call(
|
|
2798
3035
|
call,
|
|
2799
3036
|
*,
|
|
@@ -2842,6 +3079,7 @@ def _publicize_package_fields(value):
|
|
|
2842
3079
|
"tag_ids_after": "package_ids_after",
|
|
2843
3080
|
"tag_name": "package_name",
|
|
2844
3081
|
"tag_icon": "icon",
|
|
3082
|
+
"iconConfig": "icon_config",
|
|
2845
3083
|
"package_tag_id": "package_id",
|
|
2846
3084
|
"package_tag_ids": "package_ids",
|
|
2847
3085
|
"expected_package_tag_id": "expected_package_id",
|
|
@@ -2865,7 +3103,7 @@ def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) ->
|
|
|
2865
3103
|
public["output_contract"] = {
|
|
2866
3104
|
"schema_version": BUILDER_APPLY_SCHEMA_VERSION,
|
|
2867
3105
|
"preferred_ui_fields": ["operation", "summary", "resources"],
|
|
2868
|
-
"resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "error_code", "message"],
|
|
3106
|
+
"resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "icon_config", "error_code", "message"],
|
|
2869
3107
|
"legacy_fields_preserved": True,
|
|
2870
3108
|
}
|
|
2871
3109
|
return public
|
|
@@ -3013,6 +3251,41 @@ def _builder_app_parent(payload: JSONObject) -> JSONObject | None:
|
|
|
3013
3251
|
return _builder_parent("app", key=app_key, name=app_name)
|
|
3014
3252
|
|
|
3015
3253
|
|
|
3254
|
+
def _builder_icon_config(raw_icon: object = None, *, icon: object = None, color: object = None) -> JSONObject | None:
|
|
3255
|
+
raw = str(raw_icon).strip() if raw_icon not in (None, "") else ""
|
|
3256
|
+
explicit_icon = str(icon).strip() if icon not in (None, "") else ""
|
|
3257
|
+
explicit_color = str(color).strip() if color not in (None, "") else ""
|
|
3258
|
+
if raw:
|
|
3259
|
+
if raw.startswith("{") and raw.endswith("}"):
|
|
3260
|
+
config = workspace_icon_config(raw)
|
|
3261
|
+
else:
|
|
3262
|
+
config = {
|
|
3263
|
+
"icon_name": normalize_workspace_icon_name(raw),
|
|
3264
|
+
"icon_color": explicit_color or None,
|
|
3265
|
+
"icon_text": None,
|
|
3266
|
+
"raw": raw,
|
|
3267
|
+
}
|
|
3268
|
+
if any(config.get(key) for key in ("icon_name", "icon_color", "icon_text", "raw")):
|
|
3269
|
+
return config
|
|
3270
|
+
if explicit_icon or explicit_color:
|
|
3271
|
+
return {
|
|
3272
|
+
"icon_name": normalize_workspace_icon_name(explicit_icon) if explicit_icon else None,
|
|
3273
|
+
"icon_color": explicit_color or None,
|
|
3274
|
+
"icon_text": None,
|
|
3275
|
+
"raw": None,
|
|
3276
|
+
}
|
|
3277
|
+
return None
|
|
3278
|
+
|
|
3279
|
+
|
|
3280
|
+
def _builder_container_icon_config(container: object, *, raw_keys: tuple[str, ...], icon_keys: tuple[str, ...] = ("icon",), color_keys: tuple[str, ...] = ("color",)) -> JSONObject | None:
|
|
3281
|
+
if not isinstance(container, dict):
|
|
3282
|
+
return None
|
|
3283
|
+
raw_icon = next((container.get(key) for key in raw_keys if container.get(key) not in (None, "")), None)
|
|
3284
|
+
icon = next((container.get(key) for key in icon_keys if container.get(key) not in (None, "")), None)
|
|
3285
|
+
color = next((container.get(key) for key in color_keys if container.get(key) not in (None, "")), None)
|
|
3286
|
+
return _builder_icon_config(raw_icon, icon=icon, color=color)
|
|
3287
|
+
|
|
3288
|
+
|
|
3016
3289
|
def _builder_resource(
|
|
3017
3290
|
*,
|
|
3018
3291
|
resource_type: str,
|
|
@@ -3023,10 +3296,11 @@ def _builder_resource(
|
|
|
3023
3296
|
name: object = None,
|
|
3024
3297
|
ids: JSONObject | None = None,
|
|
3025
3298
|
parent: JSONObject | None = None,
|
|
3299
|
+
icon_config: JSONObject | None = None,
|
|
3026
3300
|
error_code: object = None,
|
|
3027
3301
|
message: object = None,
|
|
3028
3302
|
) -> JSONObject:
|
|
3029
|
-
|
|
3303
|
+
resource = {
|
|
3030
3304
|
"resource_type": resource_type,
|
|
3031
3305
|
"operation": operation,
|
|
3032
3306
|
"status": status,
|
|
@@ -3038,6 +3312,9 @@ def _builder_resource(
|
|
|
3038
3312
|
"error_code": str(error_code) if error_code not in (None, "") else None,
|
|
3039
3313
|
"message": str(message) if message not in (None, "") else None,
|
|
3040
3314
|
}
|
|
3315
|
+
if icon_config:
|
|
3316
|
+
resource["icon_config"] = icon_config
|
|
3317
|
+
return resource
|
|
3041
3318
|
|
|
3042
3319
|
|
|
3043
3320
|
def _builder_app_resource(payload: JSONObject, *, operation: str) -> JSONObject:
|
|
@@ -3046,6 +3323,11 @@ def _builder_app_resource(payload: JSONObject, *, operation: str) -> JSONObject:
|
|
|
3046
3323
|
operation = "failed"
|
|
3047
3324
|
app_key = _builder_payload_app_key(payload)
|
|
3048
3325
|
app_name = _builder_payload_app_name(payload)
|
|
3326
|
+
normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
|
|
3327
|
+
icon_config = (
|
|
3328
|
+
_builder_container_icon_config(payload, raw_keys=("app_icon", "appIcon"))
|
|
3329
|
+
or _builder_container_icon_config(normalized_args, raw_keys=("app_icon", "appIcon", "icon"))
|
|
3330
|
+
)
|
|
3049
3331
|
return _builder_resource(
|
|
3050
3332
|
resource_type="app",
|
|
3051
3333
|
operation=operation,
|
|
@@ -3053,6 +3335,7 @@ def _builder_app_resource(payload: JSONObject, *, operation: str) -> JSONObject:
|
|
|
3053
3335
|
key=app_key,
|
|
3054
3336
|
name=app_name,
|
|
3055
3337
|
ids={"app_key": app_key} if app_key not in (None, "") else {},
|
|
3338
|
+
icon_config=icon_config,
|
|
3056
3339
|
error_code=payload.get("error_code"),
|
|
3057
3340
|
message=payload.get("message") if status == "failed" else None,
|
|
3058
3341
|
)
|
|
@@ -3095,6 +3378,11 @@ def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3095
3378
|
package_name = payload.get("package_name") or payload.get("name")
|
|
3096
3379
|
status = _builder_status(payload, "success")
|
|
3097
3380
|
operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
|
|
3381
|
+
normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
|
|
3382
|
+
icon_config = (
|
|
3383
|
+
_builder_container_icon_config(payload, raw_keys=("icon", "tagIcon", "tag_icon"))
|
|
3384
|
+
or _builder_container_icon_config(normalized_args, raw_keys=("icon", "tagIcon", "tag_icon"))
|
|
3385
|
+
)
|
|
3098
3386
|
return [
|
|
3099
3387
|
_builder_resource(
|
|
3100
3388
|
resource_type="package",
|
|
@@ -3104,6 +3392,7 @@ def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3104
3392
|
key=str(package_id) if package_id not in (None, "") else None,
|
|
3105
3393
|
name=package_name,
|
|
3106
3394
|
ids={"package_id": package_id} if package_id not in (None, "") else {},
|
|
3395
|
+
icon_config=icon_config,
|
|
3107
3396
|
error_code=payload.get("error_code"),
|
|
3108
3397
|
message=payload.get("message") if status == "failed" else None,
|
|
3109
3398
|
)
|
|
@@ -3121,6 +3410,10 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3121
3410
|
status = _builder_status(item, "success")
|
|
3122
3411
|
operation = "failed" if status == "failed" else ("created" if bool(item.get("created")) else "updated")
|
|
3123
3412
|
parent = _builder_parent("app", key=item.get("app_key"), name=item.get("app_name"))
|
|
3413
|
+
icon_config = (
|
|
3414
|
+
_builder_container_icon_config(item, raw_keys=("app_icon", "appIcon", "icon"))
|
|
3415
|
+
or _builder_container_icon_config(item.get("shell_result"), raw_keys=("app_icon", "appIcon", "icon"))
|
|
3416
|
+
)
|
|
3124
3417
|
resources.append(
|
|
3125
3418
|
_builder_resource(
|
|
3126
3419
|
resource_type="app",
|
|
@@ -3133,6 +3426,7 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3133
3426
|
**({"package_id": package_id} if package_id not in (None, "") else {}),
|
|
3134
3427
|
},
|
|
3135
3428
|
parent=package_parent,
|
|
3429
|
+
icon_config=icon_config,
|
|
3136
3430
|
error_code=item.get("error_code"),
|
|
3137
3431
|
message=item.get("message") if status == "failed" else None,
|
|
3138
3432
|
)
|
|
@@ -3145,6 +3439,11 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3145
3439
|
app_key = payload.get("app_key")
|
|
3146
3440
|
app_name = payload.get("app_name_after") or payload.get("app_name")
|
|
3147
3441
|
parent = _builder_parent("app", key=app_key, name=app_name)
|
|
3442
|
+
normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
|
|
3443
|
+
icon_config = (
|
|
3444
|
+
_builder_container_icon_config(payload, raw_keys=("app_icon", "appIcon", "icon"))
|
|
3445
|
+
or _builder_container_icon_config(normalized_args, raw_keys=("icon", "app_icon", "appIcon"))
|
|
3446
|
+
)
|
|
3148
3447
|
resources = [
|
|
3149
3448
|
_builder_resource(
|
|
3150
3449
|
resource_type="app",
|
|
@@ -3153,6 +3452,7 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3153
3452
|
key=app_key,
|
|
3154
3453
|
name=app_name,
|
|
3155
3454
|
ids={"app_key": app_key} if app_key else {},
|
|
3455
|
+
icon_config=icon_config,
|
|
3156
3456
|
error_code=payload.get("error_code"),
|
|
3157
3457
|
message=payload.get("message") if status == "failed" else None,
|
|
3158
3458
|
)
|
|
@@ -3254,6 +3554,7 @@ def _builder_view_verification_by_name(payload: JSONObject) -> dict[str, JSONObj
|
|
|
3254
3554
|
|
|
3255
3555
|
|
|
3256
3556
|
def _builder_view_identity(item: object, verification_by_name: dict[str, JSONObject]) -> tuple[str | None, str | None, str, object, object]:
|
|
3557
|
+
verification: JSONObject | None = None
|
|
3257
3558
|
if isinstance(item, dict):
|
|
3258
3559
|
name = item.get("name") or item.get("view_name") or item.get("viewName")
|
|
3259
3560
|
view_key = item.get("view_key") or item.get("viewKey")
|
|
@@ -3274,6 +3575,16 @@ def _builder_view_identity(item: object, verification_by_name: dict[str, JSONObj
|
|
|
3274
3575
|
matching = verification.get("matching_view_keys")
|
|
3275
3576
|
if isinstance(matching, list) and matching:
|
|
3276
3577
|
view_key = matching[0]
|
|
3578
|
+
if name and verification is None:
|
|
3579
|
+
verification = verification_by_name.get(str(name))
|
|
3580
|
+
if isinstance(verification, dict):
|
|
3581
|
+
verification_status = str(verification.get("status") or "").strip()
|
|
3582
|
+
if verification_status in {"removed", "readback_pending"}:
|
|
3583
|
+
status = "readback_pending"
|
|
3584
|
+
if verification_status == "removed":
|
|
3585
|
+
status = "removed"
|
|
3586
|
+
error_code = error_code or verification.get("error_code")
|
|
3587
|
+
message = message or verification.get("message")
|
|
3277
3588
|
return (
|
|
3278
3589
|
str(name) if name not in (None, "") else None,
|
|
3279
3590
|
str(view_key) if view_key not in (None, "") else None,
|
|
@@ -3290,16 +3601,17 @@ def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3290
3601
|
if not isinstance(item, dict):
|
|
3291
3602
|
continue
|
|
3292
3603
|
status = str(item.get("status") or "success")
|
|
3293
|
-
operation = _builder_operation(status, fallback="updated")
|
|
3604
|
+
operation = _builder_operation(item.get("operation") or status, fallback="updated")
|
|
3294
3605
|
if status == "failed":
|
|
3295
3606
|
operation = "failed"
|
|
3607
|
+
resource_status = "failed" if status == "failed" else ("readback_pending" if status == "readback_pending" else "success")
|
|
3296
3608
|
chart_id = item.get("chart_id") or item.get("chartId")
|
|
3297
3609
|
chart_key = item.get("chart_key") or item.get("chartKey")
|
|
3298
3610
|
resources.append(
|
|
3299
3611
|
_builder_resource(
|
|
3300
3612
|
resource_type="chart",
|
|
3301
3613
|
operation=operation,
|
|
3302
|
-
status=
|
|
3614
|
+
status=resource_status,
|
|
3303
3615
|
id_value=chart_id,
|
|
3304
3616
|
key=chart_key or chart_id,
|
|
3305
3617
|
name=item.get("name") or item.get("chart_name") or item.get("chartName"),
|
|
@@ -3311,7 +3623,7 @@ def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3311
3623
|
},
|
|
3312
3624
|
parent=parent,
|
|
3313
3625
|
error_code=item.get("error_code"),
|
|
3314
|
-
message=item.get("message") if status
|
|
3626
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3315
3627
|
)
|
|
3316
3628
|
)
|
|
3317
3629
|
return resources
|
|
@@ -3352,6 +3664,12 @@ def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3352
3664
|
parent = None
|
|
3353
3665
|
if package_id:
|
|
3354
3666
|
parent = _builder_parent("package", id_value=package_id, key=package_id)
|
|
3667
|
+
icon_config = (
|
|
3668
|
+
_builder_container_icon_config(payload, raw_keys=("dash_icon", "dashIcon", "icon"))
|
|
3669
|
+
or _builder_container_icon_config(draft_result, raw_keys=("dashIcon", "dash_icon", "icon"))
|
|
3670
|
+
or _builder_container_icon_config(live_result, raw_keys=("dashIcon", "dash_icon", "icon"))
|
|
3671
|
+
or _builder_container_icon_config(normalized_args, raw_keys=("icon", "dash_icon", "dashIcon"))
|
|
3672
|
+
)
|
|
3355
3673
|
return [
|
|
3356
3674
|
_builder_resource(
|
|
3357
3675
|
resource_type="portal",
|
|
@@ -3364,6 +3682,7 @@ def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3364
3682
|
**({"package_id": package_id} if package_id else {}),
|
|
3365
3683
|
},
|
|
3366
3684
|
parent=parent,
|
|
3685
|
+
icon_config=icon_config,
|
|
3367
3686
|
error_code=payload.get("error_code"),
|
|
3368
3687
|
message=payload.get("message") if status == "failed" else None,
|
|
3369
3688
|
)
|
|
@@ -3393,7 +3712,7 @@ def _builder_button_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3393
3712
|
},
|
|
3394
3713
|
parent=parent,
|
|
3395
3714
|
error_code=item.get("error_code"),
|
|
3396
|
-
message=item.get("message") if status
|
|
3715
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3397
3716
|
)
|
|
3398
3717
|
)
|
|
3399
3718
|
for item in payload.get("view_configs") or []:
|
|
@@ -3413,7 +3732,7 @@ def _builder_button_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3413
3732
|
},
|
|
3414
3733
|
parent=parent,
|
|
3415
3734
|
error_code=item.get("error_code"),
|
|
3416
|
-
message=item.get("message") if status
|
|
3735
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3417
3736
|
)
|
|
3418
3737
|
)
|
|
3419
3738
|
return resources
|
|
@@ -3457,7 +3776,7 @@ def _builder_associated_resource_resources(payload: JSONObject) -> list[JSONObje
|
|
|
3457
3776
|
},
|
|
3458
3777
|
parent=parent,
|
|
3459
3778
|
error_code=item.get("error_code"),
|
|
3460
|
-
message=item.get("message") if status
|
|
3779
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3461
3780
|
)
|
|
3462
3781
|
)
|
|
3463
3782
|
for item in payload.get("view_configs") or []:
|
|
@@ -3530,7 +3849,7 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
|
3530
3849
|
|
|
3531
3850
|
|
|
3532
3851
|
def _public_error_message(error_code: str, error: QingflowApiError) -> str:
|
|
3533
|
-
if error
|
|
3852
|
+
if backend_code_int(error) == 40074 or error_code == "APP_EDIT_LOCKED":
|
|
3534
3853
|
return "app is currently locked by another active editor session"
|
|
3535
3854
|
if error.http_status != 404:
|
|
3536
3855
|
return error.message
|
|
@@ -3656,6 +3975,40 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3656
3975
|
"tool_name": "chart_get",
|
|
3657
3976
|
},
|
|
3658
3977
|
},
|
|
3978
|
+
"workspace_icon_catalog_get": {
|
|
3979
|
+
"allowed_keys": [],
|
|
3980
|
+
"aliases": {},
|
|
3981
|
+
"allowed_values": {
|
|
3982
|
+
"icon": list(WORKSPACE_ICON_NAMES),
|
|
3983
|
+
"color": list(WORKSPACE_ICON_COLORS),
|
|
3984
|
+
"generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
3985
|
+
},
|
|
3986
|
+
"execution_notes": [
|
|
3987
|
+
"read this before creating app packages, apps, or portals when choosing supported workspace icons",
|
|
3988
|
+
"the CLI validates icon/color candidates but does not infer business defaults from resource names",
|
|
3989
|
+
"new app/package/portal creation requires explicit non-template icon + color",
|
|
3990
|
+
],
|
|
3991
|
+
"minimal_example": {
|
|
3992
|
+
"profile": "default",
|
|
3993
|
+
},
|
|
3994
|
+
},
|
|
3995
|
+
"package_list": {
|
|
3996
|
+
"allowed_keys": ["trial_status", "query"],
|
|
3997
|
+
"aliases": {"trialStatus": "trial_status", "keyword": "query"},
|
|
3998
|
+
"allowed_values": {"trial_status": ["all"]},
|
|
3999
|
+
"execution_notes": [
|
|
4000
|
+
"lists app packages visible to the current builder profile by calling backend GET /tag?trialStatus=...",
|
|
4001
|
+
"query is applied locally to package_id/tag_id/package_name/tag_name after /tag returns",
|
|
4002
|
+
"does not fall back to app list because app list cannot represent empty packages, duplicate package names, or package-level permissions",
|
|
4003
|
+
"returns package_id/package_name plus compatible tag_id/tag_name; use package_get for package detail before editing",
|
|
4004
|
+
"permission failures are returned as PACKAGE_LIST_FAILED with backend transport details",
|
|
4005
|
+
],
|
|
4006
|
+
"minimal_example": {
|
|
4007
|
+
"profile": "default",
|
|
4008
|
+
"trial_status": "all",
|
|
4009
|
+
"query": "产品研发",
|
|
4010
|
+
},
|
|
4011
|
+
},
|
|
3659
4012
|
"package_get": {
|
|
3660
4013
|
"allowed_keys": ["package_id"],
|
|
3661
4014
|
"aliases": {"packageId": "package_id"},
|
|
@@ -3680,13 +4033,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3680
4033
|
"iconColor": "color",
|
|
3681
4034
|
"allowDetach": "allow_detach",
|
|
3682
4035
|
},
|
|
3683
|
-
"allowed_values":
|
|
4036
|
+
"allowed_values": {
|
|
4037
|
+
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
4038
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
4039
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
4040
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
4041
|
+
},
|
|
3684
4042
|
"execution_notes": [
|
|
3685
4043
|
"create or update package metadata, visibility, grouping, and ordering in one call",
|
|
4044
|
+
"creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
4045
|
+
"updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4046
|
+
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
3686
4047
|
"metadata keys omitted on update are preserved",
|
|
3687
4048
|
"package_id maps internally to backend tagId; do not use tag_id in public calls",
|
|
3688
4049
|
"items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
|
|
3689
4050
|
"item shapes: {type:'app', app_key}, {type:'portal', dash_key}, or {type:'group', group_id?, name, items:[...]}",
|
|
4051
|
+
"layout apply calls backend package ordering (MoveGroupAuth), so it requires package edit_app permission even when items=[] only clears/deletes existing groups",
|
|
3690
4052
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3691
4053
|
],
|
|
3692
4054
|
"minimal_example": {
|
|
@@ -3706,7 +4068,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3706
4068
|
"profile": "default",
|
|
3707
4069
|
"package_name": "项目管理",
|
|
3708
4070
|
"create_if_missing": True,
|
|
3709
|
-
"icon": "
|
|
4071
|
+
"icon": "briefcase",
|
|
3710
4072
|
"color": "azure",
|
|
3711
4073
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
3712
4074
|
},
|
|
@@ -3819,7 +4181,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3819
4181
|
},
|
|
3820
4182
|
"execution_notes": [
|
|
3821
4183
|
"use this read-only tool before button writes when an agent needs a supported icon or color choice",
|
|
3822
|
-
"current frontend only supports
|
|
4184
|
+
"current frontend only supports button icons and button colors from this catalog",
|
|
3823
4185
|
"text/icon color is unified through text_color; there is no separate icon_color",
|
|
3824
4186
|
],
|
|
3825
4187
|
"minimal_example": {
|
|
@@ -3913,6 +4275,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3913
4275
|
"field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id maps to current record id (-17), 编号/record_number maps to visible record number (0)",
|
|
3914
4276
|
"to fill a target relation field with the current source record, map source_field='数据ID' to the target relation field; default_values is for static constants, not dynamic current-record values",
|
|
3915
4277
|
"do not write raw que_relation unless maintaining a legacy config; field_mappings/default_values and que_relation are mutually exclusive",
|
|
4278
|
+
"permission split follows backend routes: upsert_buttons/patch_buttons/remove_buttons require EditAppAuth; view_configs also requires ViewManagementAuth (beingViewManageStatus), which falls back to DataManageAuth when advanced app permissions are not enabled",
|
|
3916
4279
|
"view_configs binds custom buttons into views in the same apply call; button_ref may be a same-call client_key, a button_id, or an exact unique existing button_text",
|
|
3917
4280
|
"view_configs[].view_key is the raw builder view key from app_get.views[].view_key; do not pass record-data view_id values like custom:VIEW_KEY",
|
|
3918
4281
|
"view_configs[].buttons is required in merge mode; omitting buttons is blocked to avoid no-op writes and accidental publish",
|
|
@@ -3921,6 +4284,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3921
4284
|
"default placements are header and detail; header maps to frontend top buttons",
|
|
3922
4285
|
"placement=list configures backend INSIDE row/list buttons; header maps to TOP and detail maps to DETAIL",
|
|
3923
4286
|
"remove_buttons supports button_id or exact unique button_text",
|
|
4287
|
+
"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",
|
|
4288
|
+
"if a removed button returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
|
|
3924
4289
|
"all operations share one edit context and publish after at least one write succeeds; there is no draft-only mode for this tool",
|
|
3925
4290
|
"background_color and text_color cannot both be white",
|
|
3926
4291
|
],
|
|
@@ -4054,6 +4419,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4054
4419
|
"this tool manages Qingflow in-app associated report/view display; it does not create or edit QingBI report bodies/configs",
|
|
4055
4420
|
"create or edit app-source BI report bodies first with app_charts_apply, then attach the resulting chart_id here with graph_type=chart; dataset BI reports can only be attached when they already exist",
|
|
4056
4421
|
"this is the default associated report/view path; it manages both the app-level associated resource pool and per-view display config",
|
|
4422
|
+
"permission split follows backend routes: upsert_resources/patch_resources/remove/reorder require EditAppAuth; view_configs require ViewManagementAuth (beingViewManageStatus), which falls back to DataManageAuth when advanced app permissions are not enabled",
|
|
4057
4423
|
"use patch_resources for partial parameter replacement on existing associated resources; the tool reads the current resource including backend-required raw fields, merges patch_resources[].set/unset, then submits the backend full-save payload internally",
|
|
4058
4424
|
"associated_item_id is form_asos_chart.id from app_get.associated_resources[].associated_item_id; view_configs/remove/reorder may also pass an existing associated resource's chart_id/chart_key/view_key and the tool resolves it to the internal id",
|
|
4059
4425
|
"before creating an associated resource, read app_get.associated_resources and reuse an existing item with patch_resources when target_app_key + view_key/chart_key already matches; repeated upsert_resources without associated_item_id can create duplicate associated items",
|
|
@@ -4063,6 +4429,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4063
4429
|
"match_mappings.source_field accepts source schema fields plus system fields 数据ID(-17) and 编号(0); match_mappings compiles to backend matchRules",
|
|
4064
4430
|
"do not write raw match_rules unless preserving a legacy backend config; match_mappings and match_rules are mutually exclusive",
|
|
4065
4431
|
"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",
|
|
4432
|
+
"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",
|
|
4433
|
+
"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",
|
|
4066
4434
|
"this tool publishes after at least one write succeeds; there is no draft-only mode",
|
|
4067
4435
|
"visible=false hides the associated-resource area without clearing previous selected ids; visible=true with limit_type=all shows the whole app-level pool",
|
|
4068
4436
|
],
|
|
@@ -4129,17 +4497,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4129
4497
|
"field.type": [member.value for member in PublicFieldType],
|
|
4130
4498
|
"field.relation_mode": [member.value for member in PublicRelationMode],
|
|
4131
4499
|
"field_type_ids": sorted(FIELD_TYPE_ID_ALIASES.keys()),
|
|
4500
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
4501
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
4502
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
4132
4503
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
4133
4504
|
},
|
|
4134
4505
|
"execution_notes": [
|
|
4135
4506
|
"create mode may set visibility for the new app; edit mode may update visibility on an existing app",
|
|
4507
|
+
"create mode should include explicit non-template icon + color; apply mode enforces this before writing",
|
|
4508
|
+
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
4136
4509
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4137
4510
|
],
|
|
4138
4511
|
"minimal_example": {
|
|
4139
4512
|
"profile": "default",
|
|
4140
4513
|
"app_name": "研发项目管理",
|
|
4141
4514
|
"package_id": 1001,
|
|
4142
|
-
"icon": "
|
|
4515
|
+
"icon": "briefcase",
|
|
4143
4516
|
"color": "emerald",
|
|
4144
4517
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
4145
4518
|
"create_if_missing": True,
|
|
@@ -4235,10 +4608,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4235
4608
|
"use exactly one resource mode",
|
|
4236
4609
|
"edit mode: app_key, optional app_name to rename the existing app",
|
|
4237
4610
|
"create mode: package_id + app_name + create_if_missing=true",
|
|
4611
|
+
"create mode follows backend CreateAppBean: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
|
|
4238
4612
|
"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",
|
|
4239
4613
|
"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",
|
|
4240
4614
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
4241
4615
|
"create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
|
|
4616
|
+
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
4617
|
+
"multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
|
|
4618
|
+
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4619
|
+
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
4242
4620
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4243
4621
|
"update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
|
|
4244
4622
|
"multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
|
|
@@ -4263,7 +4641,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4263
4641
|
"profile": "default",
|
|
4264
4642
|
"app_name": "研发项目管理",
|
|
4265
4643
|
"package_id": 1001,
|
|
4266
|
-
"icon": "
|
|
4644
|
+
"icon": "briefcase",
|
|
4267
4645
|
"color": "emerald",
|
|
4268
4646
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
4269
4647
|
"create_if_missing": True,
|
|
@@ -4284,6 +4662,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4284
4662
|
{
|
|
4285
4663
|
"client_key": "employee",
|
|
4286
4664
|
"app_name": "员工花名册",
|
|
4665
|
+
"icon": "business-personalcard",
|
|
4666
|
+
"color": "emerald",
|
|
4287
4667
|
"add_fields": [
|
|
4288
4668
|
{"name": "员工名称", "type": "text", "as_data_title": True},
|
|
4289
4669
|
{"name": "员工照片", "type": "attachment", "as_data_cover": True},
|
|
@@ -4292,6 +4672,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4292
4672
|
{
|
|
4293
4673
|
"client_key": "worklog",
|
|
4294
4674
|
"app_name": "工时表",
|
|
4675
|
+
"icon": "clock",
|
|
4676
|
+
"color": "blue",
|
|
4295
4677
|
"add_fields": [
|
|
4296
4678
|
{"name": "工时标题", "type": "text", "as_data_title": True},
|
|
4297
4679
|
{
|
|
@@ -4627,10 +5009,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4627
5009
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
4628
5010
|
},
|
|
4629
5011
|
"execution_notes": [
|
|
5012
|
+
"creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
|
|
4630
5013
|
"upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
|
|
4631
5014
|
"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",
|
|
4632
5015
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
4633
5016
|
"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",
|
|
5017
|
+
"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",
|
|
5018
|
+
"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",
|
|
4634
5019
|
"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",
|
|
4635
5020
|
"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",
|
|
4636
5021
|
"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",
|
|
@@ -4749,6 +5134,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4749
5134
|
},
|
|
4750
5135
|
"execution_notes": [
|
|
4751
5136
|
"apply may return partial_success when some views land and others fail",
|
|
5137
|
+
"creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
|
|
4752
5138
|
"when duplicate view names exist, supply view_key to target the exact view",
|
|
4753
5139
|
"read back app_get after any failed or partial view apply",
|
|
4754
5140
|
"view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
|
|
@@ -4757,6 +5143,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4757
5143
|
"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",
|
|
4758
5144
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
4759
5145
|
"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",
|
|
5146
|
+
"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",
|
|
5147
|
+
"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",
|
|
4760
5148
|
"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",
|
|
4761
5149
|
"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",
|
|
4762
5150
|
"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",
|
|
@@ -4832,8 +5220,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4832
5220
|
"returns builder-side app map: base summary, editability, field/view/chart/button counts, compact views, compact charts, custom_buttons, and app-level associated_resources",
|
|
4833
5221
|
"use this as the default builder discovery read before view_get/chart_get/apply detail work",
|
|
4834
5222
|
"editability is route-aware builder capability summary, not end-user data visibility",
|
|
4835
|
-
"can_edit_app_base covers app base-info writes such as app_name, icon, and visibility",
|
|
4836
|
-
"can_edit_form covers form/schema routes
|
|
5223
|
+
"can_edit_app_base covers app base-info writes such as app_name, icon, and visibility; it follows backend EditAppAuth and does not require package edit_tag",
|
|
5224
|
+
"can_edit_form covers form/schema routes and also follows backend EditAppAuth",
|
|
4837
5225
|
"returns normalized app visibility when backend auth is readable",
|
|
4838
5226
|
"custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
|
|
4839
5227
|
"associated_resources[].associated_item_id is the internal id; app_associated_resources_apply.view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key",
|
|
@@ -4850,6 +5238,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4850
5238
|
"execution_notes": [
|
|
4851
5239
|
"returns compact current field configuration for one app",
|
|
4852
5240
|
"use this before app_schema_apply when you need exact field definitions",
|
|
5241
|
+
"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",
|
|
5242
|
+
"chart_fields[].field_id supports field_<queId> selectors, while chart_fields[].bi_field_id is the raw QingBI fieldId accepted by report configs",
|
|
4853
5243
|
"subtable fields include nested subfields using the same compact field shape",
|
|
4854
5244
|
],
|
|
4855
5245
|
"minimal_example": {
|
|
@@ -4971,6 +5361,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4971
5361
|
"successful create results must return a real backend chart_id",
|
|
4972
5362
|
"upsert_charts[].visibility compiles to QingBI base visibleAuth only",
|
|
4973
5363
|
"visibility-only updates keep the existing chart config and do not rewrite rawDataConfigDTO.authInfo",
|
|
5364
|
+
"chart dimension/metric/filter/query fields are resolved from app_get_fields.chart_fields (QingBI datasource fields), not record schema or form-only fields",
|
|
5365
|
+
"system fields such as 申请人/申请时间/编号 are usable only when they appear in chart_fields; otherwise app_charts_apply returns CHART_FIELD_NOT_IN_QINGBI_SCHEMA",
|
|
5366
|
+
"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",
|
|
5367
|
+
"chart rule failures return chart_results[].diagnostics with rule_code, expected, actual, offending_fields, and next_action; backend 81002/81005 are translated when possible",
|
|
5368
|
+
"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",
|
|
5369
|
+
"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",
|
|
4974
5370
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4975
5371
|
],
|
|
4976
5372
|
"minimal_example": {
|
|
@@ -5014,9 +5410,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5014
5410
|
},
|
|
5015
5411
|
},
|
|
5016
5412
|
"portal_apply": {
|
|
5017
|
-
"allowed_keys": ["dash_key", "dash_name", "package_id", "publish", "sections", "visibility", "auth", "icon", "color", "hide_copyright", "dash_global_config", "config"],
|
|
5413
|
+
"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"],
|
|
5018
5414
|
"aliases": {
|
|
5019
5415
|
"packageId": "package_id",
|
|
5416
|
+
"name": "dash_name",
|
|
5020
5417
|
"sourceType": "source_type",
|
|
5021
5418
|
"chartRef": "chart_ref",
|
|
5022
5419
|
"viewRef": "view_ref",
|
|
@@ -5029,20 +5426,35 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5029
5426
|
"viewRef": "view_ref",
|
|
5030
5427
|
"dashStyleConfigBO": "dash_style_config",
|
|
5031
5428
|
},
|
|
5032
|
-
"allowed_values": {
|
|
5033
|
-
"
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5429
|
+
"allowed_values": {
|
|
5430
|
+
"section.source_type": ["chart", "view", "grid", "filter", "text", "link"],
|
|
5431
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
5432
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
5433
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
5434
|
+
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
5435
|
+
},
|
|
5436
|
+
"execution_notes": [
|
|
5437
|
+
"use exactly one resource mode",
|
|
5438
|
+
"update mode: dash_key",
|
|
5439
|
+
"create mode: package_id + dash_name",
|
|
5440
|
+
"create mode follows backend DashCtrl.createDash: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
|
|
5441
|
+
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5442
|
+
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
5443
|
+
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
5444
|
+
"portal_apply uses replace semantics for sections",
|
|
5445
|
+
"when editing an existing portal, sections may be omitted to update only base info such as visibility, icon, or package",
|
|
5446
|
+
"portal section-level patch is not exposed; supplying sections means full sections replacement",
|
|
5447
|
+
"remove a section by omitting it from the new sections list",
|
|
5448
|
+
"package_id is required when creating a new portal",
|
|
5449
|
+
"publish=false only guarantees draft and base-info updates; it does not claim live has changed",
|
|
5043
5450
|
"chart_ref resolves by chart_id first, then exact unique chart_name",
|
|
5044
5451
|
"view_ref resolves by view_key first, then exact unique view_name",
|
|
5452
|
+
"pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
|
|
5453
|
+
"if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
|
|
5454
|
+
"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",
|
|
5455
|
+
"x=0/6 with cols=6 only occupies the left half of the pc portal and triggers PORTAL_LAYOUT_HALF_WIDTH",
|
|
5045
5456
|
"position.pc/mobile is the canonical portal layout shape",
|
|
5457
|
+
"compat payload accepts name -> dash_name and single pages[0].components -> sections",
|
|
5046
5458
|
"visibility is the canonical public auth shape; auth is kept only as a deprecated compatibility alias",
|
|
5047
5459
|
"passing visibility and auth together is rejected as VISIBILITY_CONFLICT",
|
|
5048
5460
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
@@ -5051,7 +5463,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5051
5463
|
"profile": "default",
|
|
5052
5464
|
"dash_name": "经营门户",
|
|
5053
5465
|
"package_id": 1001,
|
|
5466
|
+
"icon": "view-grid",
|
|
5467
|
+
"color": "blue",
|
|
5054
5468
|
"publish": True,
|
|
5469
|
+
"layout_preset": "dashboard_2col",
|
|
5055
5470
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
5056
5471
|
"sections": [
|
|
5057
5472
|
{
|
|
@@ -5065,6 +5480,23 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5065
5480
|
}
|
|
5066
5481
|
],
|
|
5067
5482
|
},
|
|
5483
|
+
"compat_payload_example": {
|
|
5484
|
+
"name": "经营门户",
|
|
5485
|
+
"package_id": 1001,
|
|
5486
|
+
"layout_preset": "dashboard_2col",
|
|
5487
|
+
"pages": [
|
|
5488
|
+
{
|
|
5489
|
+
"title": "经营总览",
|
|
5490
|
+
"components": [
|
|
5491
|
+
{
|
|
5492
|
+
"title": "销售趋势",
|
|
5493
|
+
"source_type": "chart",
|
|
5494
|
+
"chart_ref": {"app_key": "APP_KEY", "chart_id": "CHART_ID"},
|
|
5495
|
+
}
|
|
5496
|
+
],
|
|
5497
|
+
}
|
|
5498
|
+
],
|
|
5499
|
+
},
|
|
5068
5500
|
"minimal_section_example": {
|
|
5069
5501
|
"title": "订单概览",
|
|
5070
5502
|
"source_type": "view",
|