@qingflow-tech/qingflow-app-builder-mcp 1.0.10 → 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/src/qingflow_mcp/builder_facade/models.py +41 -2
- package/src/qingflow_mcp/builder_facade/service.py +1261 -141
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +28 -2
- package/src/qingflow_mcp/cli/commands/record.py +16 -1
- package/src/qingflow_mcp/cli/formatters.py +32 -1
- package/src/qingflow_mcp/public_surface.py +3 -1
- package/src/qingflow_mcp/response_trim.py +55 -3
- package/src/qingflow_mcp/server.py +10 -9
- package/src/qingflow_mcp/server_app_builder.py +26 -5
- 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 +461 -54
- 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 +1262 -103
- 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
|
|
@@ -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 {}),
|
|
@@ -1657,6 +1715,68 @@ class AiBuilderTools(ToolBase):
|
|
|
1657
1715
|
message="app_schema_apply multi-app mode requires non-empty apps.",
|
|
1658
1716
|
fix_hint="Pass apps as a non-empty list of app schema items.",
|
|
1659
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
|
+
)
|
|
1660
1780
|
|
|
1661
1781
|
client_key_to_app_key: dict[str, str] = {}
|
|
1662
1782
|
created_app_keys: list[str] = []
|
|
@@ -1868,6 +1988,14 @@ class AiBuilderTools(ToolBase):
|
|
|
1868
1988
|
) -> JSONObject:
|
|
1869
1989
|
"""执行内部辅助逻辑。"""
|
|
1870
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
|
|
1871
1999
|
plan_result = self._rewrite_plan_result_for_apply(
|
|
1872
2000
|
result=self.app_schema_plan(
|
|
1873
2001
|
profile=profile,
|
|
@@ -2364,9 +2492,12 @@ class AiBuilderTools(ToolBase):
|
|
|
2364
2492
|
profile: str,
|
|
2365
2493
|
dash_key: str = "",
|
|
2366
2494
|
dash_name: str = "",
|
|
2495
|
+
name: str = "",
|
|
2367
2496
|
package_id: int | None = None,
|
|
2368
2497
|
publish: bool = True,
|
|
2369
2498
|
sections: list[JSONObject] | None = None,
|
|
2499
|
+
pages: list[JSONObject] | None = None,
|
|
2500
|
+
layout_preset: str = "",
|
|
2370
2501
|
visibility: JSONObject | None = None,
|
|
2371
2502
|
auth: JSONObject | None = None,
|
|
2372
2503
|
icon: str | None = None,
|
|
@@ -2374,25 +2505,44 @@ class AiBuilderTools(ToolBase):
|
|
|
2374
2505
|
hide_copyright: bool | None = None,
|
|
2375
2506
|
dash_global_config: JSONObject | None = None,
|
|
2376
2507
|
config: JSONObject | None = None,
|
|
2508
|
+
payload: JSONObject | None = None,
|
|
2377
2509
|
) -> JSONObject:
|
|
2378
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
|
|
2379
2544
|
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
|
-
)
|
|
2545
|
+
request = PortalApplyRequest.model_validate(request_payload)
|
|
2396
2546
|
except ValidationError as exc:
|
|
2397
2547
|
return _attach_builder_apply_envelope("portal_apply", _visibility_validation_failure(
|
|
2398
2548
|
str(exc),
|
|
@@ -2405,6 +2555,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2405
2555
|
"dash_name": dash_name or "业务门户",
|
|
2406
2556
|
"package_id": package_id or 1001,
|
|
2407
2557
|
"publish": True,
|
|
2558
|
+
"layout_preset": "dashboard_2col",
|
|
2408
2559
|
"sections": [
|
|
2409
2560
|
{
|
|
2410
2561
|
"title": "经营概览",
|
|
@@ -2417,6 +2568,14 @@ class AiBuilderTools(ToolBase):
|
|
|
2417
2568
|
))
|
|
2418
2569
|
normalized_args = request.model_dump(mode="json")
|
|
2419
2570
|
normalized_args["package_id"] = normalized_args.pop("package_tag_id", package_id)
|
|
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)
|
|
2420
2579
|
result = _publicize_package_fields(_safe_tool_call(
|
|
2421
2580
|
lambda: self._facade.portal_apply(profile=profile, request=request),
|
|
2422
2581
|
error_code="PORTAL_APPLY_FAILED",
|
|
@@ -2771,20 +2930,34 @@ def _visibility_validation_failure(
|
|
|
2771
2930
|
return result
|
|
2772
2931
|
|
|
2773
2932
|
|
|
2774
|
-
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:
|
|
2775
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))
|
|
2776
2952
|
return {
|
|
2777
2953
|
"status": "failed",
|
|
2778
|
-
"error_code":
|
|
2954
|
+
"error_code": error_code,
|
|
2779
2955
|
"recoverable": True,
|
|
2780
2956
|
"message": message,
|
|
2781
2957
|
"normalized_args": {},
|
|
2782
2958
|
"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
|
-
},
|
|
2959
|
+
"allowed_values": public_allowed_values,
|
|
2960
|
+
"details": public_details,
|
|
2788
2961
|
"suggested_next_call": None,
|
|
2789
2962
|
"request_id": None,
|
|
2790
2963
|
"backend_code": None,
|
|
@@ -2794,6 +2967,52 @@ def _config_failure(*, tool_name: str, message: str, fix_hint: str) -> JSONObjec
|
|
|
2794
2967
|
}
|
|
2795
2968
|
|
|
2796
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
|
+
|
|
2797
3016
|
def _safe_tool_call(
|
|
2798
3017
|
call,
|
|
2799
3018
|
*,
|
|
@@ -2842,6 +3061,7 @@ def _publicize_package_fields(value):
|
|
|
2842
3061
|
"tag_ids_after": "package_ids_after",
|
|
2843
3062
|
"tag_name": "package_name",
|
|
2844
3063
|
"tag_icon": "icon",
|
|
3064
|
+
"iconConfig": "icon_config",
|
|
2845
3065
|
"package_tag_id": "package_id",
|
|
2846
3066
|
"package_tag_ids": "package_ids",
|
|
2847
3067
|
"expected_package_tag_id": "expected_package_id",
|
|
@@ -2865,7 +3085,7 @@ def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) ->
|
|
|
2865
3085
|
public["output_contract"] = {
|
|
2866
3086
|
"schema_version": BUILDER_APPLY_SCHEMA_VERSION,
|
|
2867
3087
|
"preferred_ui_fields": ["operation", "summary", "resources"],
|
|
2868
|
-
"resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "error_code", "message"],
|
|
3088
|
+
"resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "icon_config", "error_code", "message"],
|
|
2869
3089
|
"legacy_fields_preserved": True,
|
|
2870
3090
|
}
|
|
2871
3091
|
return public
|
|
@@ -3013,6 +3233,41 @@ def _builder_app_parent(payload: JSONObject) -> JSONObject | None:
|
|
|
3013
3233
|
return _builder_parent("app", key=app_key, name=app_name)
|
|
3014
3234
|
|
|
3015
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
|
+
|
|
3016
3271
|
def _builder_resource(
|
|
3017
3272
|
*,
|
|
3018
3273
|
resource_type: str,
|
|
@@ -3023,10 +3278,11 @@ def _builder_resource(
|
|
|
3023
3278
|
name: object = None,
|
|
3024
3279
|
ids: JSONObject | None = None,
|
|
3025
3280
|
parent: JSONObject | None = None,
|
|
3281
|
+
icon_config: JSONObject | None = None,
|
|
3026
3282
|
error_code: object = None,
|
|
3027
3283
|
message: object = None,
|
|
3028
3284
|
) -> JSONObject:
|
|
3029
|
-
|
|
3285
|
+
resource = {
|
|
3030
3286
|
"resource_type": resource_type,
|
|
3031
3287
|
"operation": operation,
|
|
3032
3288
|
"status": status,
|
|
@@ -3038,6 +3294,9 @@ def _builder_resource(
|
|
|
3038
3294
|
"error_code": str(error_code) if error_code not in (None, "") else None,
|
|
3039
3295
|
"message": str(message) if message not in (None, "") else None,
|
|
3040
3296
|
}
|
|
3297
|
+
if icon_config:
|
|
3298
|
+
resource["icon_config"] = icon_config
|
|
3299
|
+
return resource
|
|
3041
3300
|
|
|
3042
3301
|
|
|
3043
3302
|
def _builder_app_resource(payload: JSONObject, *, operation: str) -> JSONObject:
|
|
@@ -3046,6 +3305,11 @@ def _builder_app_resource(payload: JSONObject, *, operation: str) -> JSONObject:
|
|
|
3046
3305
|
operation = "failed"
|
|
3047
3306
|
app_key = _builder_payload_app_key(payload)
|
|
3048
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
|
+
)
|
|
3049
3313
|
return _builder_resource(
|
|
3050
3314
|
resource_type="app",
|
|
3051
3315
|
operation=operation,
|
|
@@ -3053,6 +3317,7 @@ def _builder_app_resource(payload: JSONObject, *, operation: str) -> JSONObject:
|
|
|
3053
3317
|
key=app_key,
|
|
3054
3318
|
name=app_name,
|
|
3055
3319
|
ids={"app_key": app_key} if app_key not in (None, "") else {},
|
|
3320
|
+
icon_config=icon_config,
|
|
3056
3321
|
error_code=payload.get("error_code"),
|
|
3057
3322
|
message=payload.get("message") if status == "failed" else None,
|
|
3058
3323
|
)
|
|
@@ -3095,6 +3360,11 @@ def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3095
3360
|
package_name = payload.get("package_name") or payload.get("name")
|
|
3096
3361
|
status = _builder_status(payload, "success")
|
|
3097
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
|
+
)
|
|
3098
3368
|
return [
|
|
3099
3369
|
_builder_resource(
|
|
3100
3370
|
resource_type="package",
|
|
@@ -3104,6 +3374,7 @@ def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3104
3374
|
key=str(package_id) if package_id not in (None, "") else None,
|
|
3105
3375
|
name=package_name,
|
|
3106
3376
|
ids={"package_id": package_id} if package_id not in (None, "") else {},
|
|
3377
|
+
icon_config=icon_config,
|
|
3107
3378
|
error_code=payload.get("error_code"),
|
|
3108
3379
|
message=payload.get("message") if status == "failed" else None,
|
|
3109
3380
|
)
|
|
@@ -3121,6 +3392,10 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3121
3392
|
status = _builder_status(item, "success")
|
|
3122
3393
|
operation = "failed" if status == "failed" else ("created" if bool(item.get("created")) else "updated")
|
|
3123
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
|
+
)
|
|
3124
3399
|
resources.append(
|
|
3125
3400
|
_builder_resource(
|
|
3126
3401
|
resource_type="app",
|
|
@@ -3133,6 +3408,7 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3133
3408
|
**({"package_id": package_id} if package_id not in (None, "") else {}),
|
|
3134
3409
|
},
|
|
3135
3410
|
parent=package_parent,
|
|
3411
|
+
icon_config=icon_config,
|
|
3136
3412
|
error_code=item.get("error_code"),
|
|
3137
3413
|
message=item.get("message") if status == "failed" else None,
|
|
3138
3414
|
)
|
|
@@ -3145,6 +3421,11 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3145
3421
|
app_key = payload.get("app_key")
|
|
3146
3422
|
app_name = payload.get("app_name_after") or payload.get("app_name")
|
|
3147
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
|
+
)
|
|
3148
3429
|
resources = [
|
|
3149
3430
|
_builder_resource(
|
|
3150
3431
|
resource_type="app",
|
|
@@ -3153,6 +3434,7 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3153
3434
|
key=app_key,
|
|
3154
3435
|
name=app_name,
|
|
3155
3436
|
ids={"app_key": app_key} if app_key else {},
|
|
3437
|
+
icon_config=icon_config,
|
|
3156
3438
|
error_code=payload.get("error_code"),
|
|
3157
3439
|
message=payload.get("message") if status == "failed" else None,
|
|
3158
3440
|
)
|
|
@@ -3254,6 +3536,7 @@ def _builder_view_verification_by_name(payload: JSONObject) -> dict[str, JSONObj
|
|
|
3254
3536
|
|
|
3255
3537
|
|
|
3256
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
|
|
3257
3540
|
if isinstance(item, dict):
|
|
3258
3541
|
name = item.get("name") or item.get("view_name") or item.get("viewName")
|
|
3259
3542
|
view_key = item.get("view_key") or item.get("viewKey")
|
|
@@ -3274,6 +3557,16 @@ def _builder_view_identity(item: object, verification_by_name: dict[str, JSONObj
|
|
|
3274
3557
|
matching = verification.get("matching_view_keys")
|
|
3275
3558
|
if isinstance(matching, list) and matching:
|
|
3276
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")
|
|
3277
3570
|
return (
|
|
3278
3571
|
str(name) if name not in (None, "") else None,
|
|
3279
3572
|
str(view_key) if view_key not in (None, "") else None,
|
|
@@ -3290,16 +3583,17 @@ def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3290
3583
|
if not isinstance(item, dict):
|
|
3291
3584
|
continue
|
|
3292
3585
|
status = str(item.get("status") or "success")
|
|
3293
|
-
operation = _builder_operation(status, fallback="updated")
|
|
3586
|
+
operation = _builder_operation(item.get("operation") or status, fallback="updated")
|
|
3294
3587
|
if status == "failed":
|
|
3295
3588
|
operation = "failed"
|
|
3589
|
+
resource_status = "failed" if status == "failed" else ("readback_pending" if status == "readback_pending" else "success")
|
|
3296
3590
|
chart_id = item.get("chart_id") or item.get("chartId")
|
|
3297
3591
|
chart_key = item.get("chart_key") or item.get("chartKey")
|
|
3298
3592
|
resources.append(
|
|
3299
3593
|
_builder_resource(
|
|
3300
3594
|
resource_type="chart",
|
|
3301
3595
|
operation=operation,
|
|
3302
|
-
status=
|
|
3596
|
+
status=resource_status,
|
|
3303
3597
|
id_value=chart_id,
|
|
3304
3598
|
key=chart_key or chart_id,
|
|
3305
3599
|
name=item.get("name") or item.get("chart_name") or item.get("chartName"),
|
|
@@ -3311,7 +3605,7 @@ def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3311
3605
|
},
|
|
3312
3606
|
parent=parent,
|
|
3313
3607
|
error_code=item.get("error_code"),
|
|
3314
|
-
message=item.get("message") if status
|
|
3608
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3315
3609
|
)
|
|
3316
3610
|
)
|
|
3317
3611
|
return resources
|
|
@@ -3352,6 +3646,12 @@ def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3352
3646
|
parent = None
|
|
3353
3647
|
if package_id:
|
|
3354
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
|
+
)
|
|
3355
3655
|
return [
|
|
3356
3656
|
_builder_resource(
|
|
3357
3657
|
resource_type="portal",
|
|
@@ -3364,6 +3664,7 @@ def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3364
3664
|
**({"package_id": package_id} if package_id else {}),
|
|
3365
3665
|
},
|
|
3366
3666
|
parent=parent,
|
|
3667
|
+
icon_config=icon_config,
|
|
3367
3668
|
error_code=payload.get("error_code"),
|
|
3368
3669
|
message=payload.get("message") if status == "failed" else None,
|
|
3369
3670
|
)
|
|
@@ -3393,7 +3694,7 @@ def _builder_button_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3393
3694
|
},
|
|
3394
3695
|
parent=parent,
|
|
3395
3696
|
error_code=item.get("error_code"),
|
|
3396
|
-
message=item.get("message") if status
|
|
3697
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3397
3698
|
)
|
|
3398
3699
|
)
|
|
3399
3700
|
for item in payload.get("view_configs") or []:
|
|
@@ -3413,7 +3714,7 @@ def _builder_button_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3413
3714
|
},
|
|
3414
3715
|
parent=parent,
|
|
3415
3716
|
error_code=item.get("error_code"),
|
|
3416
|
-
message=item.get("message") if status
|
|
3717
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3417
3718
|
)
|
|
3418
3719
|
)
|
|
3419
3720
|
return resources
|
|
@@ -3457,7 +3758,7 @@ def _builder_associated_resource_resources(payload: JSONObject) -> list[JSONObje
|
|
|
3457
3758
|
},
|
|
3458
3759
|
parent=parent,
|
|
3459
3760
|
error_code=item.get("error_code"),
|
|
3460
|
-
message=item.get("message") if status
|
|
3761
|
+
message=item.get("message") if status in {"failed", "readback_pending"} else None,
|
|
3461
3762
|
)
|
|
3462
3763
|
)
|
|
3463
3764
|
for item in payload.get("view_configs") or []:
|
|
@@ -3656,6 +3957,40 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3656
3957
|
"tool_name": "chart_get",
|
|
3657
3958
|
},
|
|
3658
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
|
+
},
|
|
3659
3994
|
"package_get": {
|
|
3660
3995
|
"allowed_keys": ["package_id"],
|
|
3661
3996
|
"aliases": {"packageId": "package_id"},
|
|
@@ -3680,9 +4015,17 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3680
4015
|
"iconColor": "color",
|
|
3681
4016
|
"allowDetach": "allow_detach",
|
|
3682
4017
|
},
|
|
3683
|
-
"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
|
+
},
|
|
3684
4024
|
"execution_notes": [
|
|
3685
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",
|
|
3686
4029
|
"metadata keys omitted on update are preserved",
|
|
3687
4030
|
"package_id maps internally to backend tagId; do not use tag_id in public calls",
|
|
3688
4031
|
"items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
|
|
@@ -3706,7 +4049,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3706
4049
|
"profile": "default",
|
|
3707
4050
|
"package_name": "项目管理",
|
|
3708
4051
|
"create_if_missing": True,
|
|
3709
|
-
"icon": "
|
|
4052
|
+
"icon": "briefcase",
|
|
3710
4053
|
"color": "azure",
|
|
3711
4054
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
3712
4055
|
},
|
|
@@ -3819,7 +4162,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3819
4162
|
},
|
|
3820
4163
|
"execution_notes": [
|
|
3821
4164
|
"use this read-only tool before button writes when an agent needs a supported icon or color choice",
|
|
3822
|
-
"current frontend only supports
|
|
4165
|
+
"current frontend only supports button icons and button colors from this catalog",
|
|
3823
4166
|
"text/icon color is unified through text_color; there is no separate icon_color",
|
|
3824
4167
|
],
|
|
3825
4168
|
"minimal_example": {
|
|
@@ -3921,6 +4264,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3921
4264
|
"default placements are header and detail; header maps to frontend top buttons",
|
|
3922
4265
|
"placement=list configures backend INSIDE row/list buttons; header maps to TOP and detail maps to DETAIL",
|
|
3923
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",
|
|
3924
4269
|
"all operations share one edit context and publish after at least one write succeeds; there is no draft-only mode for this tool",
|
|
3925
4270
|
"background_color and text_color cannot both be white",
|
|
3926
4271
|
],
|
|
@@ -4063,6 +4408,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4063
4408
|
"match_mappings.source_field accepts source schema fields plus system fields 数据ID(-17) and 编号(0); match_mappings compiles to backend matchRules",
|
|
4064
4409
|
"do not write raw match_rules unless preserving a legacy backend config; match_mappings and match_rules are mutually exclusive",
|
|
4065
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",
|
|
4066
4413
|
"this tool publishes after at least one write succeeds; there is no draft-only mode",
|
|
4067
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",
|
|
4068
4415
|
],
|
|
@@ -4129,17 +4476,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4129
4476
|
"field.type": [member.value for member in PublicFieldType],
|
|
4130
4477
|
"field.relation_mode": [member.value for member in PublicRelationMode],
|
|
4131
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),
|
|
4132
4482
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
4133
4483
|
},
|
|
4134
4484
|
"execution_notes": [
|
|
4135
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",
|
|
4136
4488
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4137
4489
|
],
|
|
4138
4490
|
"minimal_example": {
|
|
4139
4491
|
"profile": "default",
|
|
4140
4492
|
"app_name": "研发项目管理",
|
|
4141
4493
|
"package_id": 1001,
|
|
4142
|
-
"icon": "
|
|
4494
|
+
"icon": "briefcase",
|
|
4143
4495
|
"color": "emerald",
|
|
4144
4496
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
4145
4497
|
"create_if_missing": True,
|
|
@@ -4239,6 +4591,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4239
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",
|
|
4240
4592
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
4241
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",
|
|
4242
4598
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4243
4599
|
"update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
|
|
4244
4600
|
"multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
|
|
@@ -4263,7 +4619,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4263
4619
|
"profile": "default",
|
|
4264
4620
|
"app_name": "研发项目管理",
|
|
4265
4621
|
"package_id": 1001,
|
|
4266
|
-
"icon": "
|
|
4622
|
+
"icon": "briefcase",
|
|
4267
4623
|
"color": "emerald",
|
|
4268
4624
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
4269
4625
|
"create_if_missing": True,
|
|
@@ -4284,6 +4640,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4284
4640
|
{
|
|
4285
4641
|
"client_key": "employee",
|
|
4286
4642
|
"app_name": "员工花名册",
|
|
4643
|
+
"icon": "business-personalcard",
|
|
4644
|
+
"color": "emerald",
|
|
4287
4645
|
"add_fields": [
|
|
4288
4646
|
{"name": "员工名称", "type": "text", "as_data_title": True},
|
|
4289
4647
|
{"name": "员工照片", "type": "attachment", "as_data_cover": True},
|
|
@@ -4292,6 +4650,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4292
4650
|
{
|
|
4293
4651
|
"client_key": "worklog",
|
|
4294
4652
|
"app_name": "工时表",
|
|
4653
|
+
"icon": "clock",
|
|
4654
|
+
"color": "blue",
|
|
4295
4655
|
"add_fields": [
|
|
4296
4656
|
{"name": "工时标题", "type": "text", "as_data_title": True},
|
|
4297
4657
|
{
|
|
@@ -4631,6 +4991,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4631
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",
|
|
4632
4992
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
4633
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",
|
|
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",
|
|
4634
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",
|
|
4635
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",
|
|
4636
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",
|
|
@@ -4757,6 +5119,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4757
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",
|
|
4758
5120
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
4759
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",
|
|
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",
|
|
4760
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",
|
|
4761
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",
|
|
4762
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",
|
|
@@ -4850,6 +5214,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4850
5214
|
"execution_notes": [
|
|
4851
5215
|
"returns compact current field configuration for one app",
|
|
4852
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",
|
|
4853
5219
|
"subtable fields include nested subfields using the same compact field shape",
|
|
4854
5220
|
],
|
|
4855
5221
|
"minimal_example": {
|
|
@@ -4971,6 +5337,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4971
5337
|
"successful create results must return a real backend chart_id",
|
|
4972
5338
|
"upsert_charts[].visibility compiles to QingBI base visibleAuth only",
|
|
4973
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",
|
|
4974
5346
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4975
5347
|
],
|
|
4976
5348
|
"minimal_example": {
|
|
@@ -5014,9 +5386,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5014
5386
|
},
|
|
5015
5387
|
},
|
|
5016
5388
|
"portal_apply": {
|
|
5017
|
-
"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"],
|
|
5018
5390
|
"aliases": {
|
|
5019
5391
|
"packageId": "package_id",
|
|
5392
|
+
"name": "dash_name",
|
|
5020
5393
|
"sourceType": "source_type",
|
|
5021
5394
|
"chartRef": "chart_ref",
|
|
5022
5395
|
"viewRef": "view_ref",
|
|
@@ -5029,20 +5402,34 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5029
5402
|
"viewRef": "view_ref",
|
|
5030
5403
|
"dashStyleConfigBO": "dash_style_config",
|
|
5031
5404
|
},
|
|
5032
|
-
"allowed_values": {
|
|
5033
|
-
"
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
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",
|
|
5043
5425
|
"chart_ref resolves by chart_id first, then exact unique chart_name",
|
|
5044
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",
|
|
5045
5431
|
"position.pc/mobile is the canonical portal layout shape",
|
|
5432
|
+
"compat payload accepts name -> dash_name and single pages[0].components -> sections",
|
|
5046
5433
|
"visibility is the canonical public auth shape; auth is kept only as a deprecated compatibility alias",
|
|
5047
5434
|
"passing visibility and auth together is rejected as VISIBILITY_CONFLICT",
|
|
5048
5435
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
@@ -5051,7 +5438,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5051
5438
|
"profile": "default",
|
|
5052
5439
|
"dash_name": "经营门户",
|
|
5053
5440
|
"package_id": 1001,
|
|
5441
|
+
"icon": "view-grid",
|
|
5442
|
+
"color": "blue",
|
|
5054
5443
|
"publish": True,
|
|
5444
|
+
"layout_preset": "dashboard_2col",
|
|
5055
5445
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
5056
5446
|
"sections": [
|
|
5057
5447
|
{
|
|
@@ -5065,6 +5455,23 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5065
5455
|
}
|
|
5066
5456
|
],
|
|
5067
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
|
+
},
|
|
5068
5475
|
"minimal_section_example": {
|
|
5069
5476
|
"title": "订单概览",
|
|
5070
5477
|
"source_type": "view",
|