@qingflow-tech/qingflow-app-user-mcp 1.0.9 → 1.0.10

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.
@@ -2564,6 +2564,7 @@ class AiBuilderFacade:
2564
2564
  details=_with_state_read_blocked_details({"app_key": app_key}, resource="custom_buttons", error=api_error),
2565
2565
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
2566
2566
  ))
2567
+ app_name = self._read_app_name_for_builder_output(profile=profile, app_key=app_key)
2567
2568
 
2568
2569
  existing_by_id = {
2569
2570
  button_id: item
@@ -3013,6 +3014,7 @@ class AiBuilderFacade:
3013
3014
  },
3014
3015
  "verified": verified,
3015
3016
  "app_key": app_key,
3017
+ "app_name": app_name,
3016
3018
  "mode": "apply",
3017
3019
  "created": created,
3018
3020
  "updated": updated,
@@ -3663,6 +3665,7 @@ class AiBuilderFacade:
3663
3665
  details=_with_state_read_blocked_details({"app_key": app_key}, resource="associated_resources", error=api_error),
3664
3666
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
3665
3667
  ))
3668
+ app_name = self._read_app_name_for_builder_output(profile=profile, app_key=app_key)
3666
3669
 
3667
3670
  existing_by_id = _associated_resource_index(existing_resources)
3668
3671
  blocking_issues: list[dict[str, Any]] = []
@@ -3671,7 +3674,6 @@ class AiBuilderFacade:
3671
3674
  client_key_to_patch: dict[str, AssociatedResourceUpsertPatch] = {}
3672
3675
  client_key_to_id: dict[str, int] = {}
3673
3676
  used_client_keys: set[str] = set()
3674
- force_update_resource_ids: set[int] = set()
3675
3677
  resolved_associated_item_refs: dict[str, list[int]] = {}
3676
3678
 
3677
3679
  upsert_resources = list(request.upsert_resources)
@@ -3691,11 +3693,6 @@ class AiBuilderFacade:
3691
3693
  )
3692
3694
  )
3693
3695
  upsert_resources.extend(expanded_resources)
3694
- force_update_resource_ids.update(
3695
- item_id
3696
- for item_id in (_coerce_positive_int(patch.associated_item_id) for patch in expanded_resources)
3697
- if item_id is not None
3698
- )
3699
3696
  normalized_args["upsert_resources"] = [patch.model_dump(mode="json") for patch in upsert_resources]
3700
3697
  normalized_args["patch_results"] = patch_results
3701
3698
 
@@ -3733,14 +3730,9 @@ class AiBuilderFacade:
3733
3730
  touched_ids.add(associated_item_id)
3734
3731
  if client_key:
3735
3732
  client_key_to_id[client_key] = associated_item_id
3736
- operation = (
3737
- "update"
3738
- if associated_item_id in force_update_resource_ids
3739
- or _associated_resource_patch_has_match_config(patch)
3740
- else "unchanged"
3741
- if _associated_resource_matches_patch(existing_by_id[associated_item_id], patch)
3742
- else "update"
3743
- )
3733
+ has_match_config = _associated_resource_patch_has_match_config(patch)
3734
+ identity_matches = _associated_resource_matches_patch(existing_by_id[associated_item_id], patch)
3735
+ operation = "update" if has_match_config or not identity_matches else "unchanged"
3744
3736
  upsert_ops.append({"operation": operation, "associated_item_id": associated_item_id, "patch": patch, "index": index})
3745
3737
  continue
3746
3738
  matches = [
@@ -4018,6 +4010,26 @@ class AiBuilderFacade:
4018
4010
 
4019
4011
  for index, view_config in enumerate(request.view_configs):
4020
4012
  resolved_view_config_ids = resolved_associated_item_refs.get(f"view_configs[{index}].associated_item_ids", [])
4013
+ view_name = str(view_config.view_key or "").strip()
4014
+ missing_ref_ids = [
4015
+ str(ref or "").strip()
4016
+ for ref in view_config.associated_item_refs
4017
+ if str(ref or "").strip() and str(ref or "").strip() not in client_key_to_id
4018
+ ]
4019
+ if missing_ref_ids:
4020
+ failed.append(
4021
+ {
4022
+ "operation": "view_config",
4023
+ "index": index,
4024
+ "view_key": view_config.view_key,
4025
+ "view_name": view_name,
4026
+ "status": "failed",
4027
+ "error_code": "ASSOCIATED_RESOURCE_REF_UNRESOLVED",
4028
+ "missing_refs": missing_ref_ids,
4029
+ "message": "view_config references associated resources that were not successfully created or resolved; skipped view config write to avoid clearing existing associations",
4030
+ }
4031
+ )
4032
+ continue
4021
4033
  selected_ids = [
4022
4034
  item_id
4023
4035
  for item_id in (
@@ -4034,19 +4046,21 @@ class AiBuilderFacade:
4034
4046
  available_resources=resources_after,
4035
4047
  )
4036
4048
  if issues:
4037
- failed.append({"operation": "view_config", "index": index, "view_key": view_config.view_key, "status": "failed", "issues": issues})
4049
+ failed.append({"operation": "view_config", "index": index, "view_key": view_config.view_key, "view_name": view_name, "status": "failed", "issues": issues})
4038
4050
  continue
4039
4051
  try:
4040
4052
  write_executed = True
4041
4053
  self._update_view_associated_resources_config(profile=profile, view_key=view_config.view_key, associated_resources_payload=view_payload)
4042
4054
  try:
4043
4055
  config = self.views.view_get_config(profile=profile, viewgraph_key=view_config.view_key).get("result") or {}
4056
+ view_name = _extract_view_name(config if isinstance(config, dict) else {}) or view_name
4044
4057
  actual_config = _extract_view_associated_resources_config(config if isinstance(config, dict) else {}, available_resources=resources_after)
4045
4058
  verified_config = expected_config is not None and _associated_resources_config_matches(expected_config, actual_config)
4046
4059
  if not verified_config and expected_config is not None:
4047
4060
  self._update_view_associated_resources_config(profile=profile, view_key=view_config.view_key, associated_resources_payload=view_payload)
4048
4061
  refreshed_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
4049
4062
  refreshed_config = self.views.view_get_config(profile=profile, viewgraph_key=view_config.view_key).get("result") or {}
4063
+ view_name = _extract_view_name(refreshed_config if isinstance(refreshed_config, dict) else {}) or view_name
4050
4064
  actual_config = _extract_view_associated_resources_config(
4051
4065
  refreshed_config if isinstance(refreshed_config, dict) else {},
4052
4066
  available_resources=refreshed_resources,
@@ -4055,10 +4069,10 @@ class AiBuilderFacade:
4055
4069
  except (QingflowApiError, RuntimeError):
4056
4070
  actual_config = {}
4057
4071
  verified_config = False
4058
- view_config_results.append({"index": index, "view_key": view_config.view_key, "status": "success" if verified_config else "partial_success", "associated_resources_verified": verified_config, "expected": expected_config, "actual": actual_config})
4072
+ view_config_results.append({"index": index, "view_key": view_config.view_key, "view_name": view_name, "status": "success" if verified_config else "partial_success", "associated_resources_verified": verified_config, "expected": expected_config, "actual": actual_config})
4059
4073
  except (QingflowApiError, RuntimeError) as error:
4060
4074
  api_error = _coerce_api_error(error)
4061
- failed.append({"operation": "view_config", "index": index, "view_key": view_config.view_key, "status": "failed", "error_code": "VIEW_ASSOCIATED_RESOURCES_WRITE_FAILED", "message": api_error.message, "transport_error": _transport_error_payload(api_error)})
4075
+ failed.append({"operation": "view_config", "index": index, "view_key": view_config.view_key, "view_name": view_name, "status": "failed", "error_code": "VIEW_ASSOCIATED_RESOURCES_WRITE_FAILED", "message": api_error.message, "transport_error": _transport_error_payload(api_error)})
4062
4076
 
4063
4077
  final_resources: list[dict[str, Any]] = []
4064
4078
  readback_failed = False
@@ -4134,6 +4148,7 @@ class AiBuilderFacade:
4134
4148
  "verification": {"associated_resources_verified": pool_verified, "associated_resource_view_configs_verified": view_configs_verified, "readback_loaded": not readback_failed},
4135
4149
  "verified": verified,
4136
4150
  "app_key": app_key,
4151
+ "app_name": app_name,
4137
4152
  "mode": "apply",
4138
4153
  "created": created,
4139
4154
  "updated": updated,
@@ -4612,9 +4627,18 @@ class AiBuilderFacade:
4612
4627
  "associated_resources": len(associated_resources),
4613
4628
  "custom_buttons": len(custom_buttons),
4614
4629
  }
4630
+ app_name = str(
4631
+ base_result.get("formTitle")
4632
+ or base_result.get("title")
4633
+ or base_result.get("appName")
4634
+ or base_result.get("name")
4635
+ or app_key
4636
+ ).strip() or app_key
4615
4637
  response = AppReadSummaryResponse(
4616
4638
  app_key=app_key,
4617
- title=base_result.get("formTitle"),
4639
+ app_name=app_name,
4640
+ name=app_name,
4641
+ title=app_name,
4618
4642
  app_icon=str(base_result.get("appIcon") or "").strip() or None,
4619
4643
  visibility=_public_visibility_from_member_auth(base_result.get("auth")),
4620
4644
  tag_ids=_coerce_int_list(base_result.get("tagIds")),
@@ -5441,7 +5465,7 @@ class AiBuilderFacade:
5441
5465
  for index, patch, config in semantic_patches:
5442
5466
  config_payload = config.model_dump(mode="json", exclude_none=True)
5443
5467
  reason_base = f"upsert_buttons[{index}].trigger_add_data_config"
5444
- if config.que_relation:
5468
+ if "que_relation" in config.model_fields_set:
5445
5469
  issues.append(
5446
5470
  {
5447
5471
  "error_code": "MIXED_CUSTOM_BUTTON_MAPPING_MODES",
@@ -5745,11 +5769,12 @@ class AiBuilderFacade:
5745
5769
  )
5746
5770
  return {"verified": False, "write_executed": False, "write_succeeded": False, "view_configs": results, "failed": failed, "warnings": warnings}
5747
5771
 
5748
- view_keys = {
5749
- _extract_view_key(view)
5772
+ existing_views_by_key = {
5773
+ _extract_view_key(view): view
5750
5774
  for view in (existing_views if isinstance(existing_views, list) else [])
5751
5775
  if isinstance(view, dict) and _extract_view_key(view)
5752
5776
  }
5777
+ view_keys = set(existing_views_by_key)
5753
5778
  button_inventory: dict[int, dict[str, Any]] = {}
5754
5779
  for item in [*existing_buttons, *readback_buttons]:
5755
5780
  if not isinstance(item, dict):
@@ -5781,6 +5806,7 @@ class AiBuilderFacade:
5781
5806
  try:
5782
5807
  current_response = self.views.view_get_config(profile=profile, viewgraph_key=view_key)
5783
5808
  current_config = current_response.get("result") if isinstance(current_response.get("result"), dict) else {}
5809
+ view_name = _extract_view_name(current_config) or _extract_view_name(existing_views_by_key.get(view_key) or {}) or view_key
5784
5810
  except (QingflowApiError, RuntimeError) as error:
5785
5811
  api_error = _coerce_api_error(error)
5786
5812
  issue = {
@@ -5789,6 +5815,7 @@ class AiBuilderFacade:
5789
5815
  "status": "failed",
5790
5816
  "error_code": "VIEW_CONFIG_READ_FAILED",
5791
5817
  "view_key": view_key,
5818
+ "view_name": _extract_view_name(existing_views_by_key.get(view_key) or {}) or view_key,
5792
5819
  "message": api_error.message,
5793
5820
  "transport_error": _transport_error_payload(api_error),
5794
5821
  }
@@ -5879,6 +5906,7 @@ class AiBuilderFacade:
5879
5906
  "status": "failed",
5880
5907
  "error_code": "INSIDE_BUTTON_BACKEND_UNSUPPORTED",
5881
5908
  "view_key": view_key,
5909
+ "view_name": view_name,
5882
5910
  "message": "backend rejected inside/list-button placement; header/detail placements were retried without inside buttons",
5883
5911
  "backend_message": api_error.message,
5884
5912
  "transport_error": _transport_error_payload(api_error),
@@ -5891,9 +5919,10 @@ class AiBuilderFacade:
5891
5919
  "index": config_index,
5892
5920
  "operation": "view_config",
5893
5921
  "status": "failed",
5894
- "error_code": "VIEW_BUTTON_CONFIG_WRITE_FAILED",
5895
- "view_key": view_key,
5896
- "message": fallback_api_error.message,
5922
+ "error_code": "VIEW_BUTTON_CONFIG_WRITE_FAILED",
5923
+ "view_key": view_key,
5924
+ "view_name": view_name,
5925
+ "message": fallback_api_error.message,
5897
5926
  "transport_error": _transport_error_payload(fallback_api_error),
5898
5927
  "initial_error": _transport_error_payload(api_error),
5899
5928
  }
@@ -5908,6 +5937,7 @@ class AiBuilderFacade:
5908
5937
  "status": "failed",
5909
5938
  "error_code": "INSIDE_BUTTON_BACKEND_UNSUPPORTED",
5910
5939
  "view_key": view_key,
5940
+ "view_name": view_name,
5911
5941
  "message": "backend rejected inside/list-button placement",
5912
5942
  "backend_message": api_error.message,
5913
5943
  "transport_error": _transport_error_payload(api_error),
@@ -5924,6 +5954,7 @@ class AiBuilderFacade:
5924
5954
  "status": "failed",
5925
5955
  "error_code": "VIEW_BUTTON_CONFIG_WRITE_FAILED",
5926
5956
  "view_key": view_key,
5957
+ "view_name": view_name,
5927
5958
  "message": api_error.message,
5928
5959
  "transport_error": _transport_error_payload(api_error),
5929
5960
  }
@@ -5953,6 +5984,7 @@ class AiBuilderFacade:
5953
5984
  "operation": "view_config",
5954
5985
  "status": "success" if verified else ("partial_success" if unsupported_list_issue is not None else "unverified"),
5955
5986
  "view_key": view_key,
5987
+ "view_name": view_name,
5956
5988
  "mode": "replace" if replace_existing else "merge",
5957
5989
  "buttons_configured": len(new_dtos),
5958
5990
  "view_buttons_verified": verified,
@@ -5975,6 +6007,7 @@ class AiBuilderFacade:
5975
6007
  "operation": "view_config",
5976
6008
  "status": "unverified",
5977
6009
  "view_key": view_key,
6010
+ "view_name": view_name,
5978
6011
  "mode": "replace" if replace_existing else "merge",
5979
6012
  "buttons_configured": len(new_dtos),
5980
6013
  "view_buttons_verified": None,
@@ -7055,6 +7088,7 @@ class AiBuilderFacade:
7055
7088
  },
7056
7089
  "app_key": target.app_key,
7057
7090
  "app_icon": str(resolved.get("app_icon") or visual_result.get("app_icon") or "").strip() or None,
7091
+ "app_name": str(visual_result.get("app_name_after") or target.app_name),
7058
7092
  "app_name_before": str(visual_result.get("app_name_before") or target.app_name),
7059
7093
  "app_name_after": str(visual_result.get("app_name_after") or target.app_name),
7060
7094
  "app_base_updated": bool(visual_result.get("updated")),
@@ -7295,7 +7329,8 @@ class AiBuilderFacade:
7295
7329
  actual_app_name = str(base_info.get("formTitle") or effective_app_name).strip() or effective_app_name
7296
7330
  actual_app_icon = str(base_info.get("appIcon") or visual_result.get("app_icon") or "").strip() or None
7297
7331
  expected_app_icon = str(visual_result.get("app_icon") or "").strip() or None
7298
- app_base_verified = actual_app_name == effective_app_name and actual_app_icon == expected_app_icon
7332
+ app_icon_verified = True if expected_app_icon is None else actual_app_icon == expected_app_icon
7333
+ app_base_verified = actual_app_name == effective_app_name and app_icon_verified
7299
7334
  verified = app_base_verified and relation_target_metadata_verified
7300
7335
  response = {
7301
7336
  "status": "success" if verified else "partial_success",
@@ -7319,6 +7354,7 @@ class AiBuilderFacade:
7319
7354
  },
7320
7355
  "app_key": target.app_key,
7321
7356
  "app_icon": str(visual_result.get("app_icon") or "").strip() or None,
7357
+ "app_name": effective_app_name,
7322
7358
  "app_name_before": str(visual_result.get("app_name_before") or target.app_name),
7323
7359
  "app_name_after": effective_app_name,
7324
7360
  "app_base_updated": bool(visual_result.get("updated")),
@@ -7516,6 +7552,7 @@ class AiBuilderFacade:
7516
7552
  },
7517
7553
  "app_key": target.app_key,
7518
7554
  "app_icon": str(visual_result.get("app_icon") or resolved.get("app_icon") or "").strip() or None,
7555
+ "app_name": effective_app_name,
7519
7556
  "app_name_before": str(visual_result.get("app_name_before") or target.app_name),
7520
7557
  "app_name_after": effective_app_name,
7521
7558
  "app_base_updated": bool(visual_result.get("updated")),
@@ -7525,6 +7562,13 @@ class AiBuilderFacade:
7525
7562
  "updated": updated,
7526
7563
  "removed": removed,
7527
7564
  },
7565
+ "field_diff_details": _schema_field_diff_details(
7566
+ added=added,
7567
+ updated=updated,
7568
+ removed=removed,
7569
+ before_fields=original_fields,
7570
+ after_fields=current_fields,
7571
+ ),
7528
7572
  "verified": False,
7529
7573
  "tag_ids_after": [],
7530
7574
  "package_attached": None,
@@ -7544,6 +7588,13 @@ class AiBuilderFacade:
7544
7588
  try:
7545
7589
  verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
7546
7590
  verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
7591
+ response["field_diff_details"] = _schema_field_diff_details(
7592
+ added=added,
7593
+ updated=updated,
7594
+ removed=removed,
7595
+ before_fields=original_fields,
7596
+ after_fields=cast(list[dict[str, Any]], verified["schema"]["fields"]),
7597
+ )
7547
7598
  verification_ok = all(name in verified_field_names for name in added + updated) and all(name not in verified_field_names for name in removed)
7548
7599
  data_display_verification = _verify_data_display_readback(
7549
7600
  form_settings=verified.get("form_settings"),
@@ -7575,7 +7626,8 @@ class AiBuilderFacade:
7575
7626
  actual_app_name = str(base_info.get("formTitle") or effective_app_name).strip() or effective_app_name
7576
7627
  actual_app_icon = str(base_info.get("appIcon") or visual_result.get("app_icon") or "").strip() or None
7577
7628
  expected_app_icon = str(visual_result.get("app_icon") or "").strip() or None
7578
- app_base_verified = actual_app_name == effective_app_name and actual_app_icon == expected_app_icon
7629
+ app_icon_verified = True if expected_app_icon is None else actual_app_icon == expected_app_icon
7630
+ app_base_verified = actual_app_name == effective_app_name and app_icon_verified
7579
7631
  except (QingflowApiError, RuntimeError) as error:
7580
7632
  base_error = _coerce_api_error(error)
7581
7633
  if verification_error is None:
@@ -7669,6 +7721,7 @@ class AiBuilderFacade:
7669
7721
  details=_with_state_read_blocked_details({"app_key": app_key}, resource="schema", error=api_error),
7670
7722
  suggested_next_call={"tool_name": "app_get_layout", "arguments": {"profile": profile, "app_key": app_key}},
7671
7723
  ))
7724
+ app_name = str(schema_result.get("formTitle") or schema_result.get("title") or schema_result.get("appName") or app_key).strip() or app_key
7672
7725
  parsed = _parse_schema(schema_result)
7673
7726
  current_fields = parsed["fields"]
7674
7727
  requested_sections, missing_selectors = _resolve_layout_sections_to_names(requested_sections, current_fields)
@@ -7751,6 +7804,7 @@ class AiBuilderFacade:
7751
7804
  "warnings": [],
7752
7805
  "verification": {"layout_verified": True, "layout_summary_verified": True},
7753
7806
  "app_key": app_key,
7807
+ "app_name": app_name,
7754
7808
  "layout_diff": {
7755
7809
  "mode": mode.value,
7756
7810
  "replaced": mode == LayoutApplyMode.replace,
@@ -7839,6 +7893,7 @@ class AiBuilderFacade:
7839
7893
  "warnings": [],
7840
7894
  "verification": {"layout_verified": False, "layout_summary_verified": False, "layout_read_unavailable": True},
7841
7895
  "app_key": app_key,
7896
+ "app_name": app_name,
7842
7897
  "layout_diff": {
7843
7898
  "mode": mode.value,
7844
7899
  "replaced": mode == LayoutApplyMode.replace,
@@ -7895,6 +7950,7 @@ class AiBuilderFacade:
7895
7950
  "warnings": warnings,
7896
7951
  "verification": {"layout_verified": layout_verified, "layout_summary_verified": layout_summary_verified},
7897
7952
  "app_key": app_key,
7953
+ "app_name": app_name,
7898
7954
  "layout_diff": {
7899
7955
  "mode": mode.value,
7900
7956
  "replaced": mode == LayoutApplyMode.replace,
@@ -7957,6 +8013,7 @@ class AiBuilderFacade:
7957
8013
  details=_with_state_read_blocked_details({"app_key": app_key}, resource="workflow", error=api_error),
7958
8014
  suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
7959
8015
  ))
8016
+ app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_key).strip() or app_key
7960
8017
  entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
7961
8018
  current_fields = _parse_schema(schema)["fields"]
7962
8019
  normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
@@ -8124,6 +8181,7 @@ class AiBuilderFacade:
8124
8181
  "workflow_read_unavailable": verified_nodes_unavailable,
8125
8182
  },
8126
8183
  "app_key": app_key,
8184
+ "app_name": app_name,
8127
8185
  "flow_diff": {"mode": "replace", "node_count": desired_node_count},
8128
8186
  "verified": workflow_verified,
8129
8187
  }
@@ -8199,6 +8257,7 @@ class AiBuilderFacade:
8199
8257
  details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
8200
8258
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
8201
8259
  ))
8260
+ app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or "").strip() or None
8202
8261
  existing_views = existing_views or []
8203
8262
  existing_by_key: dict[str, dict[str, Any]] = {}
8204
8263
  existing_by_name: dict[str, list[dict[str, Any]]] = {}
@@ -8306,9 +8365,15 @@ class AiBuilderFacade:
8306
8365
  )
8307
8366
  )
8308
8367
  removed: list[str] = []
8368
+ removed_keys: set[str] = set()
8309
8369
  view_results: list[dict[str, Any]] = []
8310
- for name in remove_views:
8311
- matches = existing_by_name.get(name, [])
8370
+ failed_views: list[dict[str, Any]] = []
8371
+ for selector in remove_views:
8372
+ selector_text = str(selector or "").strip()
8373
+ if not selector_text:
8374
+ continue
8375
+ key_match = existing_by_key.get(selector_text)
8376
+ matches = [key_match] if isinstance(key_match, dict) else existing_by_name.get(selector_text, [])
8312
8377
  if len(matches) > 1:
8313
8378
  return _failed(
8314
8379
  "AMBIGUOUS_VIEW",
@@ -8316,7 +8381,7 @@ class AiBuilderFacade:
8316
8381
  normalized_args=normalized_args,
8317
8382
  details={
8318
8383
  "app_key": app_key,
8319
- "view_name": name,
8384
+ "view_name": selector_text,
8320
8385
  "matches": [
8321
8386
  {"name": _extract_view_name(view), "view_key": _extract_view_key(view), "type": _normalize_view_type_name(view.get("viewgraphType") or view.get("type"))}
8322
8387
  for view in matches
@@ -8324,21 +8389,36 @@ class AiBuilderFacade:
8324
8389
  },
8325
8390
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
8326
8391
  )
8392
+ if not matches:
8393
+ failed_view = {
8394
+ "name": selector_text,
8395
+ "view_key": selector_text,
8396
+ "type": None,
8397
+ "status": "failed",
8398
+ "error_code": "VIEW_NOT_FOUND",
8399
+ "message": "remove_views item did not match an existing view name or view_key",
8400
+ }
8401
+ failed_views.append(failed_view)
8402
+ view_results.append(deepcopy(failed_view))
8403
+ continue
8327
8404
  if len(matches) == 1:
8328
8405
  key = _extract_view_key(matches[0])
8406
+ removed_name = _extract_view_name(matches[0]) or selector_text
8329
8407
  self.views.view_delete(profile=profile, viewgraph_key=key)
8330
- removed.append(name)
8331
- existing_by_name.pop(name, None)
8332
- existing_by_key.pop(key, None)
8333
- view_results.append({"name": name, "view_key": key, "type": None, "status": "removed"})
8408
+ removed.append(removed_name)
8409
+ if key:
8410
+ removed_keys.add(key)
8411
+ existing_by_key.pop(key, None)
8412
+ existing_by_name.pop(removed_name, None)
8413
+ view_results.append({"name": removed_name, "view_key": key, "type": None, "status": "removed"})
8334
8414
  created: list[str] = []
8335
8415
  updated: list[str] = []
8336
- failed_views: list[dict[str, Any]] = []
8337
8416
  existing_view_list = [
8338
8417
  view
8339
8418
  for view in (existing_views if isinstance(existing_views, list) else [])
8340
8419
  if isinstance(view, dict)
8341
- and _extract_view_name(view) not in remove_views
8420
+ and _extract_view_name(view) not in removed
8421
+ and _extract_view_key(view) not in removed_keys
8342
8422
  ]
8343
8423
  for ordinal, patch in enumerate(upsert_views, start=1):
8344
8424
  apply_columns = _resolve_view_visible_field_names(patch)
@@ -8551,6 +8631,11 @@ class AiBuilderFacade:
8551
8631
  if not existing_key and query_condition_payload_for_apply is None:
8552
8632
  query_condition_payload_for_apply = _empty_view_query_conditions_payload()
8553
8633
  expected_query_conditions_for_verify = _normalize_view_query_conditions_for_compare(query_condition_payload_for_apply)
8634
+ if not existing_key and associated_resources_payload_for_apply is None:
8635
+ associated_resources_payload_for_apply, _, _ = _build_view_associated_resources_payload(
8636
+ associated_resources={"visible": True, "limit_type": "all", "associated_item_ids": []},
8637
+ available_resources=associated_resources,
8638
+ )
8554
8639
  try:
8555
8640
  view_auth_override = (
8556
8641
  self._compile_visibility_to_member_auth(profile=profile, visibility=patch.visibility)
@@ -9160,9 +9245,11 @@ class AiBuilderFacade:
9160
9245
  verification_by_view.append(
9161
9246
  {
9162
9247
  "name": name,
9248
+ "view_key": item.get("view_key"),
9163
9249
  "type": item.get("type"),
9164
9250
  "status": "removed",
9165
- "present_in_readback": None if verified_views_unavailable else name not in verified_names,
9251
+ "present_in_readback": None if verified_views_unavailable else name in verified_names,
9252
+ "removed_verified": None if verified_views_unavailable else name not in verified_names,
9166
9253
  }
9167
9254
  )
9168
9255
  else:
@@ -9183,7 +9270,7 @@ class AiBuilderFacade:
9183
9270
  view_filters_verified = verified and not filter_readback_pending and not filter_mismatches
9184
9271
  view_query_conditions_verified = verified and not query_condition_readback_pending and not query_condition_mismatches
9185
9272
  view_associated_resources_verified = verified and not associated_resource_readback_pending and not associated_resource_mismatches
9186
- view_buttons_verified = verified and not button_readback_pending and not button_mismatches
9273
+ view_buttons_verified = verified and not button_readback_pending and not button_mismatches and not custom_button_readback_pending
9187
9274
  noop = not created and not updated and not removed
9188
9275
  if failed_views:
9189
9276
  successful_changes = bool(created or updated or removed)
@@ -9252,6 +9339,7 @@ class AiBuilderFacade:
9252
9339
  "custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
9253
9340
  },
9254
9341
  "app_key": app_key,
9342
+ "app_name": app_name,
9255
9343
  "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
9256
9344
  "verified": verified and view_filters_verified and view_query_conditions_verified and view_associated_resources_verified and view_buttons_verified,
9257
9345
  }
@@ -9324,6 +9412,7 @@ class AiBuilderFacade:
9324
9412
  "by_view": verification_by_view,
9325
9413
  },
9326
9414
  "app_key": app_key,
9415
+ "app_name": app_name,
9327
9416
  "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
9328
9417
  "verified": all_verified,
9329
9418
  }
@@ -9349,6 +9438,7 @@ class AiBuilderFacade:
9349
9438
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
9350
9439
  )
9351
9440
  tag_ids_before = _coerce_int_list(base_before.get("tagIds"))
9441
+ app_name_before = str(base_before.get("formTitle") or base_before.get("title") or base_before.get("appName") or "").strip() or None
9352
9442
  already_published = bool(base_before.get("appPublishStatus") in {1, 2})
9353
9443
  package_already_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_before
9354
9444
  try:
@@ -9379,6 +9469,7 @@ class AiBuilderFacade:
9379
9469
  "warnings": [],
9380
9470
  "verification": {"published": True, "package_attached": package_already_attached, "views_ok": True},
9381
9471
  "app_key": app_key,
9472
+ "app_name": app_name_before,
9382
9473
  "published": True,
9383
9474
  "package_attached": package_already_attached,
9384
9475
  "tag_ids_after": tag_ids_before,
@@ -9411,6 +9502,7 @@ class AiBuilderFacade:
9411
9502
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
9412
9503
  )
9413
9504
  tag_ids_after = _coerce_int_list(base.get("tagIds"))
9505
+ app_name_after = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_name_before or "").strip() or None
9414
9506
  package_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_after
9415
9507
  try:
9416
9508
  views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
@@ -9451,6 +9543,7 @@ class AiBuilderFacade:
9451
9543
  "warnings": warnings,
9452
9544
  "verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok, "views_read_unavailable": views_unavailable},
9453
9545
  "app_key": app_key,
9546
+ "app_name": app_name_after,
9454
9547
  "published": bool(base.get("appPublishStatus") in {1, 2}),
9455
9548
  "package_attached": package_attached,
9456
9549
  "tag_ids_after": tag_ids_after,
@@ -9588,6 +9681,7 @@ class AiBuilderFacade:
9588
9681
  if resolved_outcome is not None:
9589
9682
  permission_outcomes.append(resolved_outcome)
9590
9683
  app_key = str(app_result.get("app_key") or request.app_key)
9684
+ app_name = str(app_result.get("app_name") or "").strip() or None
9591
9685
  permission_outcome = self._guard_app_permission(
9592
9686
  profile=profile,
9593
9687
  app_key=app_key,
@@ -9933,6 +10027,7 @@ class AiBuilderFacade:
9933
10027
  "chart_list_source": readback_list_source or existing_chart_list_source,
9934
10028
  },
9935
10029
  "app_key": app_key,
10030
+ "app_name": app_name,
9936
10031
  "chart_results": chart_results,
9937
10032
  "verified": False if failed_items else verified,
9938
10033
  })
@@ -9961,6 +10056,7 @@ class AiBuilderFacade:
9961
10056
  "chart_list_source": existing_chart_list_source if noop else readback_list_source,
9962
10057
  },
9963
10058
  "app_key": app_key,
10059
+ "app_name": app_name,
9964
10060
  "chart_results": chart_results,
9965
10061
  "verified": result_verified,
9966
10062
  })
@@ -10246,6 +10342,9 @@ class AiBuilderFacade:
10246
10342
  "publish_failed": publish_failed,
10247
10343
  },
10248
10344
  "dash_key": dash_key,
10345
+ "dash_name": update_payload.get("dashName"),
10346
+ "package_id": target_package_tag_id,
10347
+ "created": creating,
10249
10348
  "published": published,
10250
10349
  "verified": verified,
10251
10350
  "draft_result": draft_result,
@@ -10338,6 +10437,49 @@ class AiBuilderFacade:
10338
10437
  verification["published"] = False
10339
10438
  return response
10340
10439
  publish_result = self._publish_current_edit_version(profile=profile, app_key=app_key, edit_version_no=edit_version_no)
10440
+ if publish_result.get("status") == "failed" and publish_result.get("error_code") == "APP_EDIT_LOCKED":
10441
+ details = response.get("details")
10442
+ if not isinstance(details, dict):
10443
+ details = {}
10444
+ response["details"] = details
10445
+ suggested = publish_result.get("suggested_next_call")
10446
+ release_args = suggested.get("arguments") if isinstance(suggested, dict) else {}
10447
+ if isinstance(release_args, dict):
10448
+ release_result = self.app_release_edit_lock_if_mine(
10449
+ profile=profile,
10450
+ app_key=app_key,
10451
+ lock_owner_email=str(release_args.get("lock_owner_email") or ""),
10452
+ lock_owner_name=str(release_args.get("lock_owner_name") or ""),
10453
+ )
10454
+ details["edit_lock_release_result"] = release_result
10455
+ if release_result.get("status") == "success":
10456
+ retry_result = self._publish_current_edit_version(profile=profile, app_key=app_key, edit_version_no=None)
10457
+ details["publish_retry_after_edit_lock_release"] = retry_result
10458
+ publish_result = retry_result
10459
+ response["retried_after_edit_lock_release"] = True
10460
+ response["edit_lock_released"] = True
10461
+ elif release_result.get("error_code") == "EDIT_LOCK_OWNER_UNKNOWN" and edit_version_no is not None:
10462
+ try:
10463
+ self.apps.app_edit_finished(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
10464
+ release_result = {
10465
+ "status": "success",
10466
+ "error_code": None,
10467
+ "recoverable": False,
10468
+ "message": "released current apply edit context before publish retry",
10469
+ "details": {"app_key": app_key, "edit_version_no": edit_version_no, "release_strategy": "current_apply_edit_context"},
10470
+ "verification": {"released": True},
10471
+ "app_key": app_key,
10472
+ "verified": True,
10473
+ }
10474
+ details["edit_lock_release_result"] = release_result
10475
+ retry_result = self._publish_current_edit_version(profile=profile, app_key=app_key, edit_version_no=None)
10476
+ details["publish_retry_after_edit_lock_release"] = retry_result
10477
+ publish_result = retry_result
10478
+ response["retried_after_edit_lock_release"] = True
10479
+ response["edit_lock_released"] = True
10480
+ except (QingflowApiError, RuntimeError) as error:
10481
+ api_error = _coerce_api_error(error)
10482
+ details["current_apply_edit_context_release_error"] = _transport_error_payload(api_error)
10341
10483
  response["publish_result"] = publish_result
10342
10484
  response["published"] = bool(publish_result.get("published"))
10343
10485
  verification["published"] = bool(publish_result.get("published"))
@@ -10362,6 +10504,14 @@ class AiBuilderFacade:
10362
10504
  "schema_source": schema_source,
10363
10505
  }
10364
10506
 
10507
+ def _read_app_name_for_builder_output(self, *, profile: str, app_key: str) -> str | None:
10508
+ try:
10509
+ base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
10510
+ except (QingflowApiError, RuntimeError):
10511
+ return None
10512
+ app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or "").strip()
10513
+ return app_name or None
10514
+
10365
10515
  def _sync_system_view_apply_config(
10366
10516
  self,
10367
10517
  *,
@@ -12217,6 +12367,8 @@ def _extract_edit_lock_owner(message: str) -> JSONObject:
12217
12367
  patterns = [
12218
12368
  r"应用已被\s*(?P<name>[^((]+?)\s*[((](?P<email>[^))]+)[))]\s*编辑",
12219
12369
  r"edited by\s*(?P<name>[^<(]+?)\s*<(?P<email>[^>]+)>",
12370
+ r"active editor\s+(?P<email>[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})",
12371
+ r"(?P<email>[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})",
12220
12372
  ]
12221
12373
  for pattern in patterns:
12222
12374
  match = re.search(pattern, text)
@@ -14193,6 +14345,38 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
14193
14345
  return parsed
14194
14346
 
14195
14347
 
14348
+ def _schema_field_identity(field: dict[str, Any] | None, *, fallback_name: str | None = None) -> dict[str, Any]:
14349
+ field = field or {}
14350
+ name = str(field.get("name") or fallback_name or "").strip() or None
14351
+ field_id = str(field.get("field_id") or "").strip() or None
14352
+ que_id = _coerce_positive_int(field.get("que_id") or field.get("queId"))
14353
+ return {
14354
+ "name": name,
14355
+ "field_id": field_id,
14356
+ "que_id": que_id,
14357
+ }
14358
+
14359
+
14360
+ def _schema_field_diff_details(
14361
+ *,
14362
+ added: list[str],
14363
+ updated: list[str],
14364
+ removed: list[str],
14365
+ before_fields: list[dict[str, Any]],
14366
+ after_fields: list[dict[str, Any]],
14367
+ ) -> dict[str, list[dict[str, Any]]]:
14368
+ before_by_name = {str(field.get("name") or ""): field for field in before_fields if str(field.get("name") or "").strip()}
14369
+ after_by_name = {str(field.get("name") or ""): field for field in after_fields if str(field.get("name") or "").strip()}
14370
+ return {
14371
+ "added": [_schema_field_identity(after_by_name.get(name), fallback_name=name) for name in added],
14372
+ "updated": [
14373
+ _schema_field_identity(after_by_name.get(name) or before_by_name.get(name), fallback_name=name)
14374
+ for name in updated
14375
+ ],
14376
+ "removed": [_schema_field_identity(before_by_name.get(name), fallback_name=name) for name in removed],
14377
+ }
14378
+
14379
+
14196
14380
  def _resolve_layout_sections_to_names(
14197
14381
  requested_sections: list[dict[str, Any]],
14198
14382
  fields: list[dict[str, Any]],
@@ -19360,7 +19544,7 @@ def _compare_view_button_summaries(
19360
19544
  expected_without_pending = [item for index, item in enumerate(expected) if index not in pending_indexes]
19361
19545
  if _sorted_view_button_compare_signatures(actual) == _sorted_view_button_compare_signatures(expected_without_pending):
19362
19546
  return {
19363
- "verified": True,
19547
+ "verified": False,
19364
19548
  "custom_button_readback_pending": True,
19365
19549
  "pending_custom_buttons": deepcopy(pending_custom_buttons),
19366
19550
  }
@@ -19371,7 +19555,7 @@ def _compare_view_button_summaries(
19371
19555
  and _sorted_view_button_compare_signatures(actual_other) == _sorted_view_button_compare_signatures(expected_other)
19372
19556
  )
19373
19557
  return {
19374
- "verified": custom_button_readback_pending,
19558
+ "verified": False,
19375
19559
  "custom_button_readback_pending": custom_button_readback_pending,
19376
19560
  "pending_custom_buttons": deepcopy(expected_custom) if custom_button_readback_pending else [],
19377
19561
  }