@qingflow-tech/qingflow-app-builder-mcp 1.0.13 → 1.0.15
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/service.py +116 -0
- package/src/qingflow_mcp/cli/commands/builder.py +11 -0
- package/src/qingflow_mcp/cli/main.py +5 -1
- package/src/qingflow_mcp/public_surface.py +1 -0
- package/src/qingflow_mcp/response_trim.py +1 -0
- package/src/qingflow_mcp/server_app_builder.py +11 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +47 -3
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.15
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.15 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -11803,6 +11803,96 @@ class AiBuilderFacade:
|
|
|
11803
11803
|
"live_result": live_result,
|
|
11804
11804
|
})
|
|
11805
11805
|
|
|
11806
|
+
def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
|
|
11807
|
+
normalized_args = {"dash_key": dash_key}
|
|
11808
|
+
dash_key = str(dash_key or "").strip()
|
|
11809
|
+
if not dash_key:
|
|
11810
|
+
return _failed(
|
|
11811
|
+
"PORTAL_DASH_KEY_REQUIRED",
|
|
11812
|
+
"dash_key is required when deleting a portal",
|
|
11813
|
+
normalized_args=normalized_args,
|
|
11814
|
+
missing_fields=["dash_key"],
|
|
11815
|
+
suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, "dash_key": "DASH_KEY"}},
|
|
11816
|
+
)
|
|
11817
|
+
try:
|
|
11818
|
+
self.portals.portal_delete(profile=profile, dash_key=dash_key)
|
|
11819
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
11820
|
+
api_error = _coerce_api_error(error)
|
|
11821
|
+
return _failed_from_api_error(
|
|
11822
|
+
"PORTAL_DELETE_FAILED",
|
|
11823
|
+
api_error,
|
|
11824
|
+
normalized_args=normalized_args,
|
|
11825
|
+
details={"dash_key": dash_key},
|
|
11826
|
+
suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, **normalized_args}},
|
|
11827
|
+
)
|
|
11828
|
+
|
|
11829
|
+
delete_readback = self._verify_portal_deleted_by_key(profile=profile, dash_key=dash_key)
|
|
11830
|
+
verified = delete_readback.get("readback_status") == "deleted"
|
|
11831
|
+
return {
|
|
11832
|
+
"status": "success" if verified else "partial_success",
|
|
11833
|
+
"error_code": None if verified else delete_readback.get("error_code") or "PORTAL_DELETE_READBACK_PENDING",
|
|
11834
|
+
"recoverable": not verified,
|
|
11835
|
+
"message": "deleted portal" if verified else "portal delete completed; readback pending",
|
|
11836
|
+
"normalized_args": normalized_args,
|
|
11837
|
+
"missing_fields": [],
|
|
11838
|
+
"allowed_values": {},
|
|
11839
|
+
"details": {} if verified else {"delete_readback": delete_readback},
|
|
11840
|
+
"request_id": delete_readback.get("request_id"),
|
|
11841
|
+
"backend_code": delete_readback.get("backend_code"),
|
|
11842
|
+
"http_status": delete_readback.get("http_status"),
|
|
11843
|
+
"suggested_next_call": None if verified else {"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key}},
|
|
11844
|
+
"noop": False,
|
|
11845
|
+
"warnings": [] if verified else [_warning("PORTAL_DELETE_READBACK_PENDING", "portal delete was sent, but deletion readback is not fully verified")],
|
|
11846
|
+
"verification": {"portal_deleted": verified, "delete_readback": delete_readback},
|
|
11847
|
+
"verified": verified,
|
|
11848
|
+
"dash_key": dash_key,
|
|
11849
|
+
"deleted": verified,
|
|
11850
|
+
"delete_executed": True,
|
|
11851
|
+
"readback_status": delete_readback.get("readback_status"),
|
|
11852
|
+
"safe_to_retry_delete": False,
|
|
11853
|
+
"write_executed": True,
|
|
11854
|
+
"safe_to_retry": False,
|
|
11855
|
+
}
|
|
11856
|
+
|
|
11857
|
+
def _verify_portal_deleted_by_key(self, *, profile: str, dash_key: str) -> JSONObject:
|
|
11858
|
+
try:
|
|
11859
|
+
self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True)
|
|
11860
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
11861
|
+
api_error = _coerce_api_error(error)
|
|
11862
|
+
if _delete_readback_is_not_found(api_error):
|
|
11863
|
+
return {
|
|
11864
|
+
"dash_key": dash_key,
|
|
11865
|
+
"operation": "delete",
|
|
11866
|
+
"status": "removed",
|
|
11867
|
+
"delete_executed": True,
|
|
11868
|
+
"readback_status": "deleted",
|
|
11869
|
+
"safe_to_retry_delete": False,
|
|
11870
|
+
}
|
|
11871
|
+
return {
|
|
11872
|
+
"dash_key": dash_key,
|
|
11873
|
+
"operation": "delete",
|
|
11874
|
+
"status": "readback_pending",
|
|
11875
|
+
"delete_executed": True,
|
|
11876
|
+
"readback_status": "unavailable",
|
|
11877
|
+
"safe_to_retry_delete": False,
|
|
11878
|
+
"error_code": "PORTAL_DELETE_READBACK_UNAVAILABLE",
|
|
11879
|
+
"message": "delete request completed, but portal existence could not be verified by dash_key readback",
|
|
11880
|
+
"request_id": api_error.request_id,
|
|
11881
|
+
"backend_code": api_error.backend_code,
|
|
11882
|
+
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
11883
|
+
"transport_error": _transport_error_payload(api_error),
|
|
11884
|
+
}
|
|
11885
|
+
return {
|
|
11886
|
+
"dash_key": dash_key,
|
|
11887
|
+
"operation": "delete",
|
|
11888
|
+
"status": "readback_pending",
|
|
11889
|
+
"delete_executed": True,
|
|
11890
|
+
"readback_status": "still_exists",
|
|
11891
|
+
"safe_to_retry_delete": False,
|
|
11892
|
+
"error_code": "PORTAL_DELETE_READBACK_STILL_EXISTS",
|
|
11893
|
+
"message": "delete request completed, but the portal still exists during dash_key readback",
|
|
11894
|
+
}
|
|
11895
|
+
|
|
11806
11896
|
def _publish_current_edit_version(self, *, profile: str, app_key: str, edit_version_no: int | None = None) -> JSONObject:
|
|
11807
11897
|
normalized_args = {"app_key": app_key}
|
|
11808
11898
|
if edit_version_no is None:
|
|
@@ -17721,8 +17811,23 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
17721
17811
|
field["config"] = deepcopy(payload["code_block_config"])
|
|
17722
17812
|
question_rebuild_required = True
|
|
17723
17813
|
if "code_block_binding" in payload:
|
|
17814
|
+
existing_code_block_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {})
|
|
17815
|
+
next_code_block_binding = _normalize_code_block_binding(payload["code_block_binding"])
|
|
17724
17816
|
field["code_block_binding"] = payload["code_block_binding"]
|
|
17725
17817
|
field["_explicit_code_block_binding"] = True
|
|
17818
|
+
if (
|
|
17819
|
+
"code_block_config" not in payload
|
|
17820
|
+
and existing_code_block_config is not None
|
|
17821
|
+
and next_code_block_binding is not None
|
|
17822
|
+
):
|
|
17823
|
+
existing_code = _normalize_code_block_output_assignment(
|
|
17824
|
+
_strip_code_block_generated_input_prelude(str(existing_code_block_config.get("code_content") or ""))
|
|
17825
|
+
)
|
|
17826
|
+
next_code = _normalize_code_block_output_assignment(str(next_code_block_binding.get("code") or ""))
|
|
17827
|
+
if existing_code != next_code:
|
|
17828
|
+
existing_code_block_config["code_content"] = next_code
|
|
17829
|
+
field["code_block_config"] = existing_code_block_config
|
|
17830
|
+
field["config"] = deepcopy(existing_code_block_config)
|
|
17726
17831
|
question_rebuild_required = True
|
|
17727
17832
|
if "auto_trigger" in payload:
|
|
17728
17833
|
field["auto_trigger"] = payload["auto_trigger"]
|
|
@@ -18228,6 +18333,15 @@ def _compile_code_block_binding_fields(
|
|
|
18228
18333
|
"alias_id": _coerce_positive_int(output_item.get("alias_id")),
|
|
18229
18334
|
}
|
|
18230
18335
|
)
|
|
18336
|
+
current_field_refs: set[int] = set()
|
|
18337
|
+
for field in next_fields:
|
|
18338
|
+
if not isinstance(field, dict):
|
|
18339
|
+
continue
|
|
18340
|
+
field_ref = _coerce_positive_int(field.get("que_id"))
|
|
18341
|
+
if field_ref is None:
|
|
18342
|
+
field_ref = _coerce_any_int(field.get("que_temp_id"))
|
|
18343
|
+
if field_ref is not None:
|
|
18344
|
+
current_field_refs.add(field_ref)
|
|
18231
18345
|
carried_relations: list[dict[str, Any]] = []
|
|
18232
18346
|
for relation in existing_relations:
|
|
18233
18347
|
if not isinstance(relation, dict):
|
|
@@ -18238,6 +18352,8 @@ def _compile_code_block_binding_fields(
|
|
|
18238
18352
|
alias_config = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
|
|
18239
18353
|
source_ref = _coerce_positive_int(alias_config.get("queId"))
|
|
18240
18354
|
target_ref = _coerce_positive_int(relation.get("queId"))
|
|
18355
|
+
if source_ref not in current_field_refs or target_ref not in current_field_refs:
|
|
18356
|
+
continue
|
|
18241
18357
|
if (source_ref is not None and source_ref in affected_source_refs) or (target_ref is not None and target_ref in affected_target_refs):
|
|
18242
18358
|
continue
|
|
18243
18359
|
carried_relations.append(deepcopy(relation))
|
|
@@ -179,6 +179,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
179
179
|
portal_apply.add_argument("--config-file")
|
|
180
180
|
portal_apply.set_defaults(handler=_handle_portal_apply, format_hint="builder_summary", force_json_output=True)
|
|
181
181
|
|
|
182
|
+
portal_delete = portal_subparsers.add_parser("delete", help="删除门户")
|
|
183
|
+
portal_delete.add_argument("--dash-key", required=True)
|
|
184
|
+
portal_delete.set_defaults(handler=_handle_portal_delete, format_hint="builder_summary", force_json_output=True)
|
|
185
|
+
|
|
182
186
|
schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
|
|
183
187
|
schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
|
|
184
188
|
schema_apply_apply = schema_apply_subparsers.add_parser("apply", help="执行字段变更")
|
|
@@ -614,6 +618,13 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
614
618
|
)
|
|
615
619
|
|
|
616
620
|
|
|
621
|
+
def _handle_portal_delete(args: argparse.Namespace, context: CliContext) -> dict:
|
|
622
|
+
return context.builder.portal_delete(
|
|
623
|
+
profile=args.profile,
|
|
624
|
+
dash_key=args.dash_key,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
617
628
|
def _handle_publish_verify(args: argparse.Namespace, context: CliContext) -> dict:
|
|
618
629
|
return context.builder.app_publish_verify(
|
|
619
630
|
profile=args.profile,
|
|
@@ -208,6 +208,8 @@ def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
|
|
|
208
208
|
return "app_associated_resources_apply"
|
|
209
209
|
if section == "portal" and getattr(args, "builder_portal_command", "") == "apply":
|
|
210
210
|
return "portal_apply"
|
|
211
|
+
if section == "portal" and getattr(args, "builder_portal_command", "") == "delete":
|
|
212
|
+
return "portal_delete"
|
|
211
213
|
if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
|
|
212
214
|
return "app_schema_apply"
|
|
213
215
|
if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
|
|
@@ -229,7 +231,7 @@ def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
|
|
|
229
231
|
return None
|
|
230
232
|
section = tokens[1]
|
|
231
233
|
action = tokens[2]
|
|
232
|
-
if action != "apply" and not (section == "publish" and action == "verify"):
|
|
234
|
+
if action != "apply" and not (section == "publish" and action == "verify") and not (section == "portal" and action == "delete"):
|
|
233
235
|
return None
|
|
234
236
|
mapping = {
|
|
235
237
|
"package": "package_apply",
|
|
@@ -244,6 +246,8 @@ def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
|
|
|
244
246
|
"charts": "app_charts_apply",
|
|
245
247
|
"publish": "app_publish_verify",
|
|
246
248
|
}
|
|
249
|
+
if section == "portal" and action == "delete":
|
|
250
|
+
return "portal_delete"
|
|
247
251
|
return mapping.get(section)
|
|
248
252
|
|
|
249
253
|
|
|
@@ -162,6 +162,7 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
162
162
|
PublicToolSpec(BUILDER_DOMAIN, "app_views_apply", ("app_views_apply",), ("builder", "views", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
163
163
|
PublicToolSpec(BUILDER_DOMAIN, "app_charts_apply", ("app_charts_apply",), ("builder", "charts", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
164
164
|
PublicToolSpec(BUILDER_DOMAIN, "portal_apply", ("portal_apply",), ("builder", "portal", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
165
|
+
PublicToolSpec(BUILDER_DOMAIN, "portal_delete", ("portal_delete",), ("builder", "portal", "delete"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
165
166
|
PublicToolSpec(BUILDER_DOMAIN, "app_publish_verify", ("app_publish_verify",), ("builder", "publish", "verify"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
166
167
|
)
|
|
167
168
|
|
|
@@ -39,7 +39,7 @@ def build_builder_server() -> FastMCP:
|
|
|
39
39
|
"Use workspace_icon_catalog_get before creating app packages, apps, or portals when supported icon/color candidates are needed; new workspace resources require explicit non-template icon + color, and the CLI validates choices without inferring business defaults. "
|
|
40
40
|
"app_get as the default app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for focused configuration reads, "
|
|
41
41
|
"member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
|
|
42
|
-
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_custom_buttons_apply/app_associated_resources_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
|
|
42
|
+
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_custom_buttons_apply/app_associated_resources_apply/app_charts_apply/portal_apply/portal_delete to execute normalized patches; these apply/delete tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal delete separates DELETE execution from readback verification, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
|
|
43
43
|
"Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
|
|
44
44
|
"For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
|
|
45
45
|
"For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
|
|
@@ -595,6 +595,16 @@ def build_builder_server() -> FastMCP:
|
|
|
595
595
|
payload=payload,
|
|
596
596
|
)
|
|
597
597
|
|
|
598
|
+
@server.tool()
|
|
599
|
+
def portal_delete(
|
|
600
|
+
profile: str = DEFAULT_PROFILE,
|
|
601
|
+
dash_key: str = "",
|
|
602
|
+
) -> dict:
|
|
603
|
+
return ai_builder.portal_delete(
|
|
604
|
+
profile=profile,
|
|
605
|
+
dash_key=dash_key,
|
|
606
|
+
)
|
|
607
|
+
|
|
598
608
|
@server.tool()
|
|
599
609
|
def app_publish_verify(
|
|
600
610
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -91,6 +91,7 @@ BUILDER_APPLY_TOOL_NAMES = {
|
|
|
91
91
|
"app_associated_resources_apply",
|
|
92
92
|
"app_charts_apply",
|
|
93
93
|
"portal_apply",
|
|
94
|
+
"portal_delete",
|
|
94
95
|
"app_publish_verify",
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -553,6 +554,13 @@ class AiBuilderTools(ToolBase):
|
|
|
553
554
|
payload=payload,
|
|
554
555
|
)
|
|
555
556
|
|
|
557
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="delete", target="portal"))
|
|
558
|
+
def portal_delete(
|
|
559
|
+
profile: str = DEFAULT_PROFILE,
|
|
560
|
+
dash_key: str = "",
|
|
561
|
+
) -> JSONObject:
|
|
562
|
+
return self.portal_delete(profile=profile, dash_key=dash_key)
|
|
563
|
+
|
|
556
564
|
@mcp.tool()
|
|
557
565
|
def app_publish_verify(
|
|
558
566
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -2600,6 +2608,18 @@ class AiBuilderTools(ToolBase):
|
|
|
2600
2608
|
))
|
|
2601
2609
|
return _attach_builder_apply_envelope("portal_apply", result)
|
|
2602
2610
|
|
|
2611
|
+
@tool_cn_name("门户删除")
|
|
2612
|
+
def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
|
|
2613
|
+
"""执行门户删除逻辑。"""
|
|
2614
|
+
normalized_args = {"dash_key": dash_key}
|
|
2615
|
+
result = _publicize_package_fields(_safe_tool_call(
|
|
2616
|
+
lambda: self._facade.portal_delete(profile=profile, dash_key=dash_key),
|
|
2617
|
+
error_code="PORTAL_DELETE_FAILED",
|
|
2618
|
+
normalized_args=normalized_args,
|
|
2619
|
+
suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, **normalized_args}},
|
|
2620
|
+
))
|
|
2621
|
+
return _attach_builder_apply_envelope("portal_delete", result)
|
|
2622
|
+
|
|
2603
2623
|
@tool_cn_name("应用发布校验")
|
|
2604
2624
|
def app_publish_verify(
|
|
2605
2625
|
self,
|
|
@@ -3182,6 +3202,8 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
|
|
|
3182
3202
|
resources = _builder_chart_resources(payload)
|
|
3183
3203
|
elif tool_name == "portal_apply":
|
|
3184
3204
|
resources = _builder_portal_resources(payload)
|
|
3205
|
+
elif tool_name == "portal_delete":
|
|
3206
|
+
resources = _builder_portal_resources(payload, operation_override="removed")
|
|
3185
3207
|
elif tool_name == "app_custom_buttons_apply":
|
|
3186
3208
|
resources = _builder_button_resources(payload)
|
|
3187
3209
|
elif tool_name == "app_associated_resources_apply":
|
|
@@ -3377,7 +3399,15 @@ def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3377
3399
|
package_id = payload.get("package_id") or payload.get("id")
|
|
3378
3400
|
package_name = payload.get("package_name") or payload.get("name")
|
|
3379
3401
|
status = _builder_status(payload, "success")
|
|
3380
|
-
operation =
|
|
3402
|
+
operation = (
|
|
3403
|
+
"failed"
|
|
3404
|
+
if status == "failed"
|
|
3405
|
+
else "removed"
|
|
3406
|
+
if bool(payload.get("deleted")) or bool(payload.get("delete_executed"))
|
|
3407
|
+
else "created"
|
|
3408
|
+
if bool(payload.get("created"))
|
|
3409
|
+
else "updated"
|
|
3410
|
+
)
|
|
3381
3411
|
normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
|
|
3382
3412
|
icon_config = (
|
|
3383
3413
|
_builder_container_icon_config(payload, raw_keys=("icon", "tagIcon", "tag_icon"))
|
|
@@ -3629,7 +3659,7 @@ def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3629
3659
|
return resources
|
|
3630
3660
|
|
|
3631
3661
|
|
|
3632
|
-
def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3662
|
+
def _builder_portal_resources(payload: JSONObject, *, operation_override: str | None = None) -> list[JSONObject]:
|
|
3633
3663
|
status = _builder_status(payload, "success")
|
|
3634
3664
|
draft_result = payload.get("draft_result") if isinstance(payload.get("draft_result"), dict) else {}
|
|
3635
3665
|
live_result = payload.get("live_result") if isinstance(payload.get("live_result"), dict) else {}
|
|
@@ -3660,7 +3690,7 @@ def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3660
3690
|
first_tag = draft_result.get("tags")[0]
|
|
3661
3691
|
if isinstance(first_tag, dict):
|
|
3662
3692
|
package_id = first_tag.get("tagId") or first_tag.get("tag_id") or first_tag.get("id")
|
|
3663
|
-
operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
|
|
3693
|
+
operation = operation_override or ("failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated"))
|
|
3664
3694
|
parent = None
|
|
3665
3695
|
if package_id:
|
|
3666
3696
|
parent = _builder_parent("package", id_value=package_id, key=package_id)
|
|
@@ -5507,6 +5537,20 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5507
5537
|
},
|
|
5508
5538
|
},
|
|
5509
5539
|
},
|
|
5540
|
+
"portal_delete": {
|
|
5541
|
+
"allowed_keys": ["dash_key"],
|
|
5542
|
+
"aliases": {"dashKey": "dash_key"},
|
|
5543
|
+
"allowed_values": {},
|
|
5544
|
+
"execution_notes": [
|
|
5545
|
+
"deletes one portal by dash_key",
|
|
5546
|
+
"delete results separate DELETE execution from readback verification",
|
|
5547
|
+
"if delete_executed=true and readback_status=unavailable or still_exists, do not blindly repeat delete; confirm later with portal_get or portal_list",
|
|
5548
|
+
],
|
|
5549
|
+
"minimal_example": {
|
|
5550
|
+
"profile": "default",
|
|
5551
|
+
"dash_key": "DASH_KEY",
|
|
5552
|
+
},
|
|
5553
|
+
},
|
|
5510
5554
|
"app_publish_verify": {
|
|
5511
5555
|
"allowed_keys": ["app_key", "expected_package_id"],
|
|
5512
5556
|
"aliases": {},
|