@qingflow-tech/qingflow-app-builder-mcp 1.0.11 → 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 +6 -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 +12 -12
- package/skills/qingflow-app-builder/references/create-app.md +3 -3
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +1 -1
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +6 -5
- package/skills/qingflow-app-builder/references/update-views.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -3
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/service.py +1488 -288
- package/src/qingflow_mcp/cli/commands/builder.py +2 -2
- 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 +39 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +206 -7
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +21 -15
- package/src/qingflow_mcp/response_trim.py +68 -13
- package/src/qingflow_mcp/server.py +11 -9
- package/src/qingflow_mcp/server_app_builder.py +3 -2
- package/src/qingflow_mcp/server_app_user.py +15 -13
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +36 -11
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- 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 +118 -6
- 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 +1042 -338
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -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
|
@@ -5,7 +5,7 @@ from copy import deepcopy
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
|
-
from ..errors import QingflowApiError
|
|
8
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error
|
|
9
9
|
from ..tools.app_tools import AppTools
|
|
10
10
|
from ..tools.navigation_tools import NavigationTools
|
|
11
11
|
from ..tools.package_tools import PackageTools
|
|
@@ -76,7 +76,8 @@ class SolutionExecutor:
|
|
|
76
76
|
except Exception as exc: # noqa: BLE001
|
|
77
77
|
store.record_step_failed(step.step_name, str(exc), debug_context=debug_context)
|
|
78
78
|
return store.summary()
|
|
79
|
-
store.
|
|
79
|
+
final_status = "partial_success" if _artifacts_have_post_write_readback_pending(store.data.get("artifacts", {})) else "success"
|
|
80
|
+
store.mark_finished(status=final_status)
|
|
80
81
|
return store.summary()
|
|
81
82
|
|
|
82
83
|
def _repair_start_index(self, compiled: CompiledSolution, store: RunArtifactStore) -> int:
|
|
@@ -203,6 +204,11 @@ class SolutionExecutor:
|
|
|
203
204
|
dash_key = store.get_artifact("portal", "dash_key")
|
|
204
205
|
if dash_key:
|
|
205
206
|
self.portal_tools.portal_publish(profile=profile, dash_key=dash_key)
|
|
207
|
+
store.set_artifact(
|
|
208
|
+
"portal",
|
|
209
|
+
"publish",
|
|
210
|
+
{"published": True, "write_executed": True, "safe_to_retry": False},
|
|
211
|
+
)
|
|
206
212
|
self._refresh_portal_artifact(profile=profile, store=store, being_draft=False, artifact_key="published_result")
|
|
207
213
|
return
|
|
208
214
|
if step_name == "publish.navigation" and publish and compiled.normalized_spec.publish_policy.navigation:
|
|
@@ -347,7 +353,32 @@ class SolutionExecutor:
|
|
|
347
353
|
updated_items.insert(insert_at, item)
|
|
348
354
|
self.package_tools.package_sort_items(profile=profile, tag_id=tag_id, tag_items=updated_items)
|
|
349
355
|
|
|
350
|
-
|
|
356
|
+
try:
|
|
357
|
+
verified_detail = _verify_package_attachment(self.package_tools, profile=profile, tag_id=tag_id, app_key=app_key)
|
|
358
|
+
except Exception as exc: # noqa: BLE001
|
|
359
|
+
api_error = _coerce_qingflow_error(exc)
|
|
360
|
+
if api_error is None or not _is_permission_restricted_error(api_error):
|
|
361
|
+
raise
|
|
362
|
+
store.set_artifact(
|
|
363
|
+
"package",
|
|
364
|
+
"attachment_readback",
|
|
365
|
+
_post_write_readback_artifact(
|
|
366
|
+
resource="package_attach",
|
|
367
|
+
target={"tag_id": tag_id, "app_key": app_key},
|
|
368
|
+
error=api_error,
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
self._record_package_attachment(
|
|
372
|
+
store,
|
|
373
|
+
entity.entity_id,
|
|
374
|
+
app_artifact,
|
|
375
|
+
tag_id=tag_id,
|
|
376
|
+
attached=True,
|
|
377
|
+
reused=False,
|
|
378
|
+
readback_status="unavailable",
|
|
379
|
+
readback_verified=False,
|
|
380
|
+
)
|
|
381
|
+
return
|
|
351
382
|
verified_result = verified_detail.get("result") if isinstance(verified_detail.get("result"), dict) else {}
|
|
352
383
|
verified_items = [deepcopy(existing) for existing in verified_result.get("tagItems", []) if isinstance(existing, dict)]
|
|
353
384
|
if not any(_package_item_app_key(existing) == app_key for existing in verified_items):
|
|
@@ -369,12 +400,18 @@ class SolutionExecutor:
|
|
|
369
400
|
tag_id: int,
|
|
370
401
|
attached: bool,
|
|
371
402
|
reused: bool,
|
|
403
|
+
readback_status: str = "verified",
|
|
404
|
+
readback_verified: bool = True,
|
|
372
405
|
) -> None:
|
|
373
406
|
next_artifact = deepcopy(app_artifact)
|
|
374
407
|
next_artifact["package_attachment"] = {
|
|
375
408
|
"tag_id": tag_id,
|
|
376
409
|
"attached": attached,
|
|
377
410
|
"reused": reused,
|
|
411
|
+
"readback_status": readback_status,
|
|
412
|
+
"readback_verified": readback_verified,
|
|
413
|
+
"write_executed": not reused,
|
|
414
|
+
"safe_to_retry": reused or not attached,
|
|
378
415
|
}
|
|
379
416
|
store.set_artifact("apps", entity_id, next_artifact)
|
|
380
417
|
|
|
@@ -698,12 +735,23 @@ class SolutionExecutor:
|
|
|
698
735
|
api_error = _coerce_qingflow_error(exc)
|
|
699
736
|
if api_error is None or not _is_permission_restricted_error(api_error):
|
|
700
737
|
raise
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
738
|
+
if not result:
|
|
739
|
+
raise _required_state_read_blocked_error(
|
|
740
|
+
resource="portal",
|
|
741
|
+
message=f"portal update requires readable draft state for dash '{dash_key}'",
|
|
742
|
+
error=api_error,
|
|
743
|
+
details={"dash_key": dash_key},
|
|
744
|
+
) from exc
|
|
745
|
+
base_payload = {}
|
|
746
|
+
store.set_artifact(
|
|
747
|
+
"portal",
|
|
748
|
+
"draft_readback_before_update",
|
|
749
|
+
_post_write_readback_artifact(
|
|
750
|
+
resource="portal",
|
|
751
|
+
target={"dash_key": dash_key, "phase": "created_portal_draft_readback"},
|
|
752
|
+
error=api_error,
|
|
753
|
+
),
|
|
754
|
+
)
|
|
707
755
|
update_payload = self._resolve_portal_payload(compiled.portal_plan["update_payload"], store, base_payload=base_payload)
|
|
708
756
|
self.portal_tools.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
709
757
|
self._refresh_portal_artifact(profile=profile, store=store, being_draft=True, artifact_key="draft_result")
|
|
@@ -744,7 +792,19 @@ class SolutionExecutor:
|
|
|
744
792
|
return
|
|
745
793
|
try:
|
|
746
794
|
result = self.portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
|
|
747
|
-
except Exception: # noqa: BLE001
|
|
795
|
+
except Exception as exc: # noqa: BLE001
|
|
796
|
+
api_error = _coerce_qingflow_error(exc)
|
|
797
|
+
if api_error is None:
|
|
798
|
+
raise
|
|
799
|
+
store.set_artifact(
|
|
800
|
+
"portal",
|
|
801
|
+
f"{artifact_key}_readback",
|
|
802
|
+
_post_write_readback_artifact(
|
|
803
|
+
resource="portal",
|
|
804
|
+
target={"dash_key": dash_key, "being_draft": being_draft, "artifact_key": artifact_key},
|
|
805
|
+
error=api_error,
|
|
806
|
+
),
|
|
807
|
+
)
|
|
748
808
|
return
|
|
749
809
|
store.set_artifact("portal", artifact_key, result)
|
|
750
810
|
store.set_artifact("portal", "result", result)
|
|
@@ -2112,10 +2172,9 @@ def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | Non
|
|
|
2112
2172
|
|
|
2113
2173
|
|
|
2114
2174
|
def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
backend_code = None
|
|
2175
|
+
if is_auth_like_error(error):
|
|
2176
|
+
return False
|
|
2177
|
+
backend_code = backend_code_int(error)
|
|
2119
2178
|
if backend_code != 50004:
|
|
2120
2179
|
return False
|
|
2121
2180
|
message = error.message or ""
|
|
@@ -2152,7 +2211,9 @@ def _coerce_qingflow_error(error: Exception) -> QingflowApiError | None:
|
|
|
2152
2211
|
|
|
2153
2212
|
|
|
2154
2213
|
def _is_permission_restricted_error(error: QingflowApiError) -> bool:
|
|
2155
|
-
|
|
2214
|
+
if is_auth_like_error(error):
|
|
2215
|
+
return False
|
|
2216
|
+
return backend_code_int(error) in {40002, 40027}
|
|
2156
2217
|
|
|
2157
2218
|
|
|
2158
2219
|
def _required_state_read_blocked_error(
|
|
@@ -2182,6 +2243,42 @@ def _required_state_read_blocked_error(
|
|
|
2182
2243
|
)
|
|
2183
2244
|
|
|
2184
2245
|
|
|
2246
|
+
def _post_write_readback_artifact(
|
|
2247
|
+
*,
|
|
2248
|
+
resource: str,
|
|
2249
|
+
target: dict[str, Any],
|
|
2250
|
+
error: QingflowApiError,
|
|
2251
|
+
) -> dict[str, Any]:
|
|
2252
|
+
return {
|
|
2253
|
+
"resource": resource,
|
|
2254
|
+
"target": deepcopy(target),
|
|
2255
|
+
"readback_status": "unavailable",
|
|
2256
|
+
"readback_verified": False,
|
|
2257
|
+
"write_executed": True,
|
|
2258
|
+
"safe_to_retry": False,
|
|
2259
|
+
"transport_error": {
|
|
2260
|
+
"http_status": error.http_status,
|
|
2261
|
+
"backend_code": error.backend_code,
|
|
2262
|
+
"category": error.category,
|
|
2263
|
+
"request_id": error.request_id,
|
|
2264
|
+
},
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
|
|
2268
|
+
def _artifacts_have_post_write_readback_pending(value: Any) -> bool:
|
|
2269
|
+
if isinstance(value, dict):
|
|
2270
|
+
if (
|
|
2271
|
+
value.get("write_executed") is True
|
|
2272
|
+
and value.get("safe_to_retry") is False
|
|
2273
|
+
and value.get("readback_status") == "unavailable"
|
|
2274
|
+
):
|
|
2275
|
+
return True
|
|
2276
|
+
return any(_artifacts_have_post_write_readback_pending(item) for item in value.values())
|
|
2277
|
+
if isinstance(value, list):
|
|
2278
|
+
return any(_artifacts_have_post_write_readback_pending(item) for item in value)
|
|
2279
|
+
return False
|
|
2280
|
+
|
|
2281
|
+
|
|
2185
2282
|
def _portal_component_position(
|
|
2186
2283
|
source_type: Any,
|
|
2187
2284
|
*,
|
|
@@ -15,7 +15,7 @@ from ..builder_facade.button_style_catalog import (
|
|
|
15
15
|
)
|
|
16
16
|
from ..public_surface import public_builder_contract_tool_names
|
|
17
17
|
from ..config import DEFAULT_PROFILE
|
|
18
|
-
from ..errors import QingflowApiError
|
|
18
|
+
from ..errors import QingflowApiError, backend_code_int
|
|
19
19
|
from ..json_types import JSONObject
|
|
20
20
|
from ..builder_facade.models import (
|
|
21
21
|
AssociatedResourcesApplyRequest,
|
|
@@ -854,16 +854,23 @@ class AiBuilderTools(ToolBase):
|
|
|
854
854
|
contain_disable: bool = False,
|
|
855
855
|
) -> JSONObject:
|
|
856
856
|
"""执行工具方法逻辑。"""
|
|
857
|
+
normalized_query = str(query or "").strip()
|
|
857
858
|
normalized_args = {
|
|
858
|
-
"query":
|
|
859
|
+
"query": normalized_query,
|
|
859
860
|
"page_num": page_num,
|
|
860
861
|
"page_size": page_size,
|
|
861
862
|
"contain_disable": contain_disable,
|
|
862
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
|
+
)
|
|
863
870
|
return _safe_tool_call(
|
|
864
871
|
lambda: self._facade.member_search(
|
|
865
872
|
profile=profile,
|
|
866
|
-
query=
|
|
873
|
+
query=normalized_query,
|
|
867
874
|
page_num=page_num,
|
|
868
875
|
page_size=page_size,
|
|
869
876
|
contain_disable=contain_disable,
|
|
@@ -876,9 +883,16 @@ class AiBuilderTools(ToolBase):
|
|
|
876
883
|
@tool_cn_name("角色检索")
|
|
877
884
|
def role_search(self, *, profile: str, keyword: str, page_num: int = 1, page_size: int = 20) -> JSONObject:
|
|
878
885
|
"""执行角色相关逻辑。"""
|
|
879
|
-
|
|
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
|
+
)
|
|
880
894
|
return _safe_tool_call(
|
|
881
|
-
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),
|
|
882
896
|
error_code="ROLE_SEARCH_FAILED",
|
|
883
897
|
normalized_args=normalized_args,
|
|
884
898
|
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, **normalized_args}},
|
|
@@ -1820,6 +1834,9 @@ class AiBuilderTools(ToolBase):
|
|
|
1820
1834
|
)
|
|
1821
1835
|
public_shell = _publicize_package_fields(shell)
|
|
1822
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
|
|
1823
1840
|
if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
|
|
1824
1841
|
results.append({
|
|
1825
1842
|
"index": index,
|
|
@@ -1831,13 +1848,12 @@ class AiBuilderTools(ToolBase):
|
|
|
1831
1848
|
"stage": "resolve_or_create_shell",
|
|
1832
1849
|
"error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
|
|
1833
1850
|
"message": public_shell.get("message") or "app shell resolve/create failed",
|
|
1834
|
-
"
|
|
1851
|
+
"write_executed": shell_write_executed,
|
|
1852
|
+
"safe_to_retry": not shell_write_executed and not any_write_executed,
|
|
1835
1853
|
})
|
|
1836
1854
|
continue
|
|
1837
1855
|
if bool(public_shell.get("created")):
|
|
1838
1856
|
created_app_keys.append(resolved_key)
|
|
1839
|
-
if _schema_apply_result_has_write(public_shell):
|
|
1840
|
-
any_write_executed = True
|
|
1841
1857
|
if client_key:
|
|
1842
1858
|
client_key_to_app_key[client_key] = resolved_key
|
|
1843
1859
|
results.append({
|
|
@@ -2842,6 +2858,8 @@ def _merge_schema_field_diffs(*diffs: object) -> JSONObject:
|
|
|
2842
2858
|
|
|
2843
2859
|
|
|
2844
2860
|
def _schema_apply_result_has_write(result: JSONObject) -> bool:
|
|
2861
|
+
if "write_executed" in result:
|
|
2862
|
+
return bool(result.get("write_executed"))
|
|
2845
2863
|
if bool(result.get("created")) or bool(result.get("published")) or bool(result.get("app_base_updated")):
|
|
2846
2864
|
return True
|
|
2847
2865
|
field_diff = result.get("field_diff")
|
|
@@ -3831,7 +3849,7 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
|
3831
3849
|
|
|
3832
3850
|
|
|
3833
3851
|
def _public_error_message(error_code: str, error: QingflowApiError) -> str:
|
|
3834
|
-
if error
|
|
3852
|
+
if backend_code_int(error) == 40074 or error_code == "APP_EDIT_LOCKED":
|
|
3835
3853
|
return "app is currently locked by another active editor session"
|
|
3836
3854
|
if error.http_status != 404:
|
|
3837
3855
|
return error.message
|
|
@@ -4030,6 +4048,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4030
4048
|
"package_id maps internally to backend tagId; do not use tag_id in public calls",
|
|
4031
4049
|
"items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
|
|
4032
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",
|
|
4033
4052
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4034
4053
|
],
|
|
4035
4054
|
"minimal_example": {
|
|
@@ -4256,6 +4275,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4256
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)",
|
|
4257
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",
|
|
4258
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",
|
|
4259
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",
|
|
4260
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",
|
|
4261
4281
|
"view_configs[].buttons is required in merge mode; omitting buttons is blocked to avoid no-op writes and accidental publish",
|
|
@@ -4399,6 +4419,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4399
4419
|
"this tool manages Qingflow in-app associated report/view display; it does not create or edit QingBI report bodies/configs",
|
|
4400
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",
|
|
4401
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",
|
|
4402
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",
|
|
4403
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",
|
|
4404
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",
|
|
@@ -4587,6 +4608,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4587
4608
|
"use exactly one resource mode",
|
|
4588
4609
|
"edit mode: app_key, optional app_name to rename the existing app",
|
|
4589
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",
|
|
4590
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",
|
|
4591
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",
|
|
4592
4614
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
@@ -4987,6 +5009,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4987
5009
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
4988
5010
|
},
|
|
4989
5011
|
"execution_notes": [
|
|
5012
|
+
"creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
|
|
4990
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",
|
|
4991
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",
|
|
4992
5015
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
@@ -5111,6 +5134,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5111
5134
|
},
|
|
5112
5135
|
"execution_notes": [
|
|
5113
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",
|
|
5114
5138
|
"when duplicate view names exist, supply view_key to target the exact view",
|
|
5115
5139
|
"read back app_get after any failed or partial view apply",
|
|
5116
5140
|
"view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
|
|
@@ -5196,8 +5220,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5196
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",
|
|
5197
5221
|
"use this as the default builder discovery read before view_get/chart_get/apply detail work",
|
|
5198
5222
|
"editability is route-aware builder capability summary, not end-user data visibility",
|
|
5199
|
-
"can_edit_app_base covers app base-info writes such as app_name, icon, and visibility",
|
|
5200
|
-
"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",
|
|
5201
5225
|
"returns normalized app visibility when backend auth is readable",
|
|
5202
5226
|
"custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
|
|
5203
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",
|
|
@@ -5413,6 +5437,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5413
5437
|
"use exactly one resource mode",
|
|
5414
5438
|
"update mode: dash_key",
|
|
5415
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",
|
|
5416
5441
|
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5417
5442
|
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
5418
5443
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|