@josephyan/qingflow-cli 0.2.0-beta.63 → 0.2.0-beta.65

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.
@@ -16,7 +16,7 @@ from ..list_type_labels import RECORD_LIST_TYPE_LABELS, SYSTEM_VIEW_ID_TO_LIST_T
16
16
  from ..solution.build_assembly_store import BuildAssemblyStore, default_artifacts, default_manifest
17
17
  from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
18
18
  from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
19
- from ..solution.compiler.icon_utils import encode_workspace_icon
19
+ from ..solution.compiler.icon_utils import encode_workspace_icon_with_defaults
20
20
  from ..solution.compiler.view_compiler import VIEW_TYPE_MAP
21
21
  from ..solution.executor import _build_viewgraph_questions, _compact_dict, extract_field_map
22
22
  from ..solution.spec_models import FieldType, FormLayoutRowSpec, FormLayoutSectionSpec, ViewSpec
@@ -1406,6 +1406,15 @@ class AiBuilderFacade:
1406
1406
  def finalize(response: JSONObject) -> JSONObject:
1407
1407
  return _apply_permission_outcomes(response, *permission_outcomes)
1408
1408
 
1409
+ edit_version_no, edit_context_error = self._ensure_app_edit_context(
1410
+ profile=profile,
1411
+ app_key=app_key,
1412
+ normalized_args=normalized_args,
1413
+ failure_code="CUSTOM_BUTTON_CREATE_FAILED",
1414
+ )
1415
+ if edit_context_error is not None:
1416
+ return finalize(edit_context_error)
1417
+
1409
1418
  create_result = self.buttons.custom_button_create(
1410
1419
  profile=profile,
1411
1420
  app_key=app_key,
@@ -1419,7 +1428,7 @@ class AiBuilderFacade:
1419
1428
  "CUSTOM_BUTTON_CREATE_FAILED",
1420
1429
  "custom button create succeeded but no button_id was returned",
1421
1430
  normalized_args=normalized_args,
1422
- details={"app_key": app_key, "result": deepcopy(raw_result)},
1431
+ details={"app_key": app_key, "result": deepcopy(raw_result), "edit_version_no": edit_version_no},
1423
1432
  suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
1424
1433
  )
1425
1434
  )
@@ -1444,6 +1453,7 @@ class AiBuilderFacade:
1444
1453
  "details": {
1445
1454
  "app_key": app_key,
1446
1455
  "button_id": button_id,
1456
+ "edit_version_no": edit_version_no,
1447
1457
  "transport_error": _transport_error_payload(api_error),
1448
1458
  },
1449
1459
  "request_id": api_error.request_id,
@@ -1487,6 +1497,7 @@ class AiBuilderFacade:
1487
1497
  "verified": True,
1488
1498
  "app_key": app_key,
1489
1499
  "button_id": button_id,
1500
+ "edit_version_no": edit_version_no,
1490
1501
  "button": button,
1491
1502
  }
1492
1503
  )
@@ -1514,6 +1525,15 @@ class AiBuilderFacade:
1514
1525
  def finalize(response: JSONObject) -> JSONObject:
1515
1526
  return _apply_permission_outcomes(response, *permission_outcomes)
1516
1527
 
1528
+ edit_version_no, edit_context_error = self._ensure_app_edit_context(
1529
+ profile=profile,
1530
+ app_key=app_key,
1531
+ normalized_args=normalized_args,
1532
+ failure_code="CUSTOM_BUTTON_UPDATE_FAILED",
1533
+ )
1534
+ if edit_context_error is not None:
1535
+ return finalize(edit_context_error)
1536
+
1517
1537
  self.buttons.custom_button_update(
1518
1538
  profile=profile,
1519
1539
  app_key=app_key,
@@ -1541,6 +1561,7 @@ class AiBuilderFacade:
1541
1561
  "details": {
1542
1562
  "app_key": app_key,
1543
1563
  "button_id": button_id,
1564
+ "edit_version_no": edit_version_no,
1544
1565
  "transport_error": _transport_error_payload(api_error),
1545
1566
  },
1546
1567
  "request_id": api_error.request_id,
@@ -1584,6 +1605,7 @@ class AiBuilderFacade:
1584
1605
  "verified": True,
1585
1606
  "app_key": app_key,
1586
1607
  "button_id": button_id,
1608
+ "edit_version_no": edit_version_no,
1587
1609
  "button": button,
1588
1610
  }
1589
1611
  )
@@ -1946,6 +1968,7 @@ class AiBuilderFacade:
1946
1968
  response = AppReadSummaryResponse(
1947
1969
  app_key=app_key,
1948
1970
  title=state["base"].get("formTitle"),
1971
+ app_icon=str(state["base"].get("appIcon") or "").strip() or None,
1949
1972
  tag_ids=_coerce_int_list(state["base"].get("tagIds")),
1950
1973
  publish_status=state["base"].get("appPublishStatus"),
1951
1974
  field_count=len(parsed["fields"]),
@@ -2567,7 +2590,17 @@ class AiBuilderFacade:
2567
2590
  upsert_views = _build_views_preset(request.preset, list(field_names))
2568
2591
  blocking_issues: list[dict[str, Any]] = []
2569
2592
  for patch in upsert_views:
2570
- columns = patch.get("columns") or []
2593
+ raw_columns = [str(name or "").strip() for name in (patch.get("columns") or []) if str(name or "").strip()]
2594
+ columns = _filter_known_system_view_columns(raw_columns)
2595
+ if patch.get("type") in {"table", "card"} and raw_columns and not columns:
2596
+ blocking_issues.append(
2597
+ {
2598
+ "error_code": "VALIDATION_ERROR",
2599
+ "view_name": patch.get("name"),
2600
+ "message": "view columns must include at least one real app field; system columns cannot be applied directly",
2601
+ "ignored_system_columns": [name for name in raw_columns if name in _KNOWN_SYSTEM_VIEW_COLUMNS],
2602
+ }
2603
+ )
2571
2604
  missing_columns = [name for name in columns if name not in field_names]
2572
2605
  if missing_columns:
2573
2606
  blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_columns})
@@ -2642,6 +2675,7 @@ class AiBuilderFacade:
2642
2675
  "app": {
2643
2676
  "app_key": app_key,
2644
2677
  "title": base_result.get("formTitle"),
2678
+ "app_icon": str(base_result.get("appIcon") or "").strip() or None,
2645
2679
  "tag_ids": _coerce_int_list(base_result.get("tagIds")),
2646
2680
  "publish_status": base_result.get("appPublishStatus"),
2647
2681
  },
@@ -2674,6 +2708,8 @@ class AiBuilderFacade:
2674
2708
  app_key: str = "",
2675
2709
  package_tag_id: int | None = None,
2676
2710
  app_name: str = "",
2711
+ icon: str | None = None,
2712
+ color: str | None = None,
2677
2713
  create_if_missing: bool = False,
2678
2714
  publish: bool = True,
2679
2715
  add_fields: list[FieldPatch],
@@ -2684,6 +2720,8 @@ class AiBuilderFacade:
2684
2720
  "app_key": app_key,
2685
2721
  "package_tag_id": package_tag_id,
2686
2722
  "app_name": app_name,
2723
+ "icon": icon,
2724
+ "color": color,
2687
2725
  "create_if_missing": create_if_missing,
2688
2726
  "publish": publish,
2689
2727
  "add_fields": [patch.model_dump(mode="json") for patch in add_fields],
@@ -2734,6 +2772,8 @@ class AiBuilderFacade:
2734
2772
  profile=profile,
2735
2773
  app_name=app_name,
2736
2774
  package_tag_id=package_tag_id,
2775
+ icon=icon,
2776
+ color=color,
2737
2777
  )
2738
2778
  if resolved.get("status") == "failed":
2739
2779
  if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
@@ -2757,6 +2797,23 @@ class AiBuilderFacade:
2757
2797
  if permission_outcome.block is not None:
2758
2798
  return permission_outcome.block
2759
2799
  permission_outcomes.append(permission_outcome)
2800
+ visual_result = self._ensure_app_base_visuals(
2801
+ profile=profile,
2802
+ app_key=target.app_key,
2803
+ fallback_title=target.app_name,
2804
+ icon=icon,
2805
+ color=color,
2806
+ normalized_args=normalized_args,
2807
+ )
2808
+ if visual_result.get("status") == "failed":
2809
+ return finalize(visual_result)
2810
+ else:
2811
+ visual_result = {
2812
+ "status": "success",
2813
+ "updated": False,
2814
+ "app_icon": str(resolved.get("app_icon") or "").strip() or None,
2815
+ "request_id": None,
2816
+ }
2760
2817
  if bool(resolved.get("created")) and not requested_field_changes:
2761
2818
  return finalize({
2762
2819
  "status": "success",
@@ -2775,9 +2832,11 @@ class AiBuilderFacade:
2775
2832
  "fields_verified": True,
2776
2833
  "package_attached": None if package_tag_id is None else package_tag_id in target.tag_ids,
2777
2834
  "relation_field_limit_verified": True,
2835
+ "app_visuals_verified": True,
2778
2836
  "publish_skipped": True,
2779
2837
  },
2780
2838
  "app_key": target.app_key,
2839
+ "app_icon": str(resolved.get("app_icon") or visual_result.get("app_icon") or "").strip() or None,
2781
2840
  "created": True,
2782
2841
  "field_diff": {"added": [], "updated": [], "removed": []},
2783
2842
  "verified": True,
@@ -2899,17 +2958,22 @@ class AiBuilderFacade:
2899
2958
  "status": "success",
2900
2959
  "error_code": None,
2901
2960
  "recoverable": False,
2902
- "message": "schema already matches requested state",
2961
+ "message": "updated app visuals; schema already matches requested state" if bool(visual_result.get("updated")) else "schema already matches requested state",
2903
2962
  "normalized_args": normalized_args,
2904
2963
  "missing_fields": [],
2905
2964
  "allowed_values": {"field_types": [item.value for item in PublicFieldType]},
2906
2965
  "details": {},
2907
2966
  "request_id": None,
2908
2967
  "suggested_next_call": None if package_attached is not False else {"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": package_tag_id, "app_key": target.app_key}},
2909
- "noop": True,
2968
+ "noop": not bool(visual_result.get("updated")),
2910
2969
  "warnings": relation_warnings,
2911
- "verification": {"fields_verified": True, "relation_field_limit_verified": relation_limit_verified},
2970
+ "verification": {
2971
+ "fields_verified": True,
2972
+ "relation_field_limit_verified": relation_limit_verified,
2973
+ "app_visuals_verified": True,
2974
+ },
2912
2975
  "app_key": target.app_key,
2976
+ "app_icon": str(visual_result.get("app_icon") or "").strip() or None,
2913
2977
  "created": False,
2914
2978
  "field_diff": {"added": [], "updated": [], "removed": []},
2915
2979
  "verified": True,
@@ -2981,9 +3045,11 @@ class AiBuilderFacade:
2981
3045
  "verification": {
2982
3046
  "fields_verified": False,
2983
3047
  "package_attached": None,
3048
+ "app_visuals_verified": True,
2984
3049
  "relation_field_limit_verified": relation_limit_verified,
2985
3050
  },
2986
3051
  "app_key": target.app_key,
3052
+ "app_icon": str(visual_result.get("app_icon") or resolved.get("app_icon") or "").strip() or None,
2987
3053
  "created": bool(resolved.get("created")),
2988
3054
  "field_diff": {
2989
3055
  "added": added,
@@ -3644,6 +3710,7 @@ class AiBuilderFacade:
3644
3710
  for patch in upsert_views
3645
3711
  )
3646
3712
  valid_custom_button_ids: set[int] = set()
3713
+ custom_button_details_by_id: dict[int, dict[str, Any]] = {}
3647
3714
  if requires_custom_button_validation:
3648
3715
  try:
3649
3716
  button_listing = self.buttons.custom_button_list(
@@ -3668,6 +3735,26 @@ class AiBuilderFacade:
3668
3735
  for item in (button_listing.get("items") or [])
3669
3736
  if isinstance(item, dict) and (button_id := _coerce_positive_int(item.get("button_id"))) is not None
3670
3737
  }
3738
+ referenced_custom_button_ids = {
3739
+ binding.button_id
3740
+ for patch in upsert_views
3741
+ for binding in (patch.buttons or [])
3742
+ if binding.button_type == PublicViewButtonType.custom and binding.button_id in valid_custom_button_ids
3743
+ }
3744
+ for button_id in sorted(referenced_custom_button_ids):
3745
+ try:
3746
+ detail = self.buttons.custom_button_get(
3747
+ profile=profile,
3748
+ app_key=app_key,
3749
+ button_id=button_id,
3750
+ being_draft=True,
3751
+ include_raw=False,
3752
+ )
3753
+ except (QingflowApiError, RuntimeError):
3754
+ continue
3755
+ detail_result = detail.get("result")
3756
+ if isinstance(detail_result, dict):
3757
+ custom_button_details_by_id[button_id] = _normalize_custom_button_detail(detail_result)
3671
3758
  removed: list[str] = []
3672
3759
  view_results: list[dict[str, Any]] = []
3673
3760
  for name in remove_views:
@@ -3704,7 +3791,24 @@ class AiBuilderFacade:
3704
3791
  and _extract_view_name(view) not in remove_views
3705
3792
  ]
3706
3793
  for ordinal, patch in enumerate(upsert_views, start=1):
3707
- missing_columns = [name for name in patch.columns if name not in field_names]
3794
+ apply_columns = _resolve_view_visible_field_names(patch)
3795
+ ignored_system_columns = [
3796
+ name
3797
+ for name in [str(value or "").strip() for value in patch.columns]
3798
+ if name in _KNOWN_SYSTEM_VIEW_COLUMNS
3799
+ ]
3800
+ if patch.type in {PublicViewType.table, PublicViewType.card} and patch.columns and not apply_columns:
3801
+ return _failed(
3802
+ "VALIDATION_ERROR",
3803
+ "view columns must include at least one real app field; system columns cannot be applied directly",
3804
+ normalized_args=normalized_args,
3805
+ details={
3806
+ "app_key": app_key,
3807
+ "view_name": patch.name,
3808
+ "ignored_system_columns": ignored_system_columns,
3809
+ },
3810
+ )
3811
+ missing_columns = [name for name in apply_columns if name not in field_names]
3708
3812
  if missing_columns:
3709
3813
  return _failed(
3710
3814
  "UNKNOWN_VIEW_FIELD",
@@ -3714,6 +3818,7 @@ class AiBuilderFacade:
3714
3818
  "app_key": app_key,
3715
3819
  "view_name": patch.name,
3716
3820
  "missing_fields": missing_columns,
3821
+ "ignored_system_columns": ignored_system_columns,
3717
3822
  },
3718
3823
  missing_fields=missing_columns,
3719
3824
  suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": app_key}},
@@ -3784,7 +3889,10 @@ class AiBuilderFacade:
3784
3889
  allowed_values=first_issue.get("allowed_values") or {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
3785
3890
  suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
3786
3891
  )
3787
- expected_button_summary = _normalize_view_buttons_for_compare(explicit_button_dtos or [])
3892
+ expected_button_summary = _normalize_expected_view_buttons_for_compare(
3893
+ explicit_button_dtos or [],
3894
+ custom_button_details_by_id=custom_button_details_by_id,
3895
+ )
3788
3896
  matched_existing_view: dict[str, Any] | None = None
3789
3897
  existing_key: str | None = None
3790
3898
  if patch.view_key:
@@ -3837,12 +3945,14 @@ class AiBuilderFacade:
3837
3945
  system_view_sync: dict[str, Any] | None = None
3838
3946
  if system_view_list_type is not None and patch.type.value == "table":
3839
3947
  operation_phase = "default_view_apply_config_sync"
3840
- system_view_sync = self._sync_system_view_apply_config(
3948
+ system_view_sync = self._sync_system_view_and_restore_buttons(
3841
3949
  profile=profile,
3842
3950
  app_key=app_key,
3951
+ viewgraph_key=existing_key,
3952
+ payload=payload,
3843
3953
  list_type=system_view_list_type,
3844
3954
  schema=schema,
3845
- visible_field_names=patch.columns,
3955
+ visible_field_names=apply_columns,
3846
3956
  )
3847
3957
  if not bool(system_view_sync.get("verified")):
3848
3958
  failure_entry = {
@@ -3864,6 +3974,7 @@ class AiBuilderFacade:
3864
3974
  "list_type": system_view_list_type,
3865
3975
  "expected_visible_order": system_view_sync.get("expected_visible_order"),
3866
3976
  "actual_visible_order": system_view_sync.get("actual_visible_order"),
3977
+ "apply_columns": apply_columns,
3867
3978
  },
3868
3979
  }
3869
3980
  failed_views.append(failure_entry)
@@ -3879,6 +3990,7 @@ class AiBuilderFacade:
3879
3990
  "expected_filters": deepcopy(translated_filters),
3880
3991
  "expected_buttons": deepcopy(expected_button_summary),
3881
3992
  "system_view_sync": system_view_sync,
3993
+ "apply_columns": deepcopy(apply_columns),
3882
3994
  }
3883
3995
  )
3884
3996
  else:
@@ -3956,6 +4068,49 @@ class AiBuilderFacade:
3956
4068
  explicit_button_dtos=fallback_button_dtos,
3957
4069
  )
3958
4070
  self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
4071
+ system_view_sync: dict[str, Any] | None = None
4072
+ fallback_system_view_list_type = (
4073
+ _resolve_system_view_list_type(view_key=target_key, view_name=patch.name)
4074
+ if patch.type.value == "table" and target_key
4075
+ else None
4076
+ )
4077
+ if fallback_system_view_list_type is not None:
4078
+ operation_phase = "default_view_apply_config_sync"
4079
+ system_view_sync = self._sync_system_view_and_restore_buttons(
4080
+ profile=profile,
4081
+ app_key=app_key,
4082
+ viewgraph_key=target_key,
4083
+ payload=fallback_payload,
4084
+ list_type=fallback_system_view_list_type,
4085
+ schema=schema,
4086
+ visible_field_names=apply_columns,
4087
+ )
4088
+ if not bool(system_view_sync.get("verified")):
4089
+ failure_entry = {
4090
+ "name": patch.name,
4091
+ "view_key": target_key,
4092
+ "type": patch.type.value,
4093
+ "status": "failed",
4094
+ "error_code": "SYSTEM_VIEW_ORDER_SYNC_FAILED",
4095
+ "message": "default view column order did not verify through app apply/baseInfo readback",
4096
+ "request_id": None,
4097
+ "backend_code": None,
4098
+ "http_status": None,
4099
+ "operation": "sync_default_view",
4100
+ "details": {
4101
+ "app_key": app_key,
4102
+ "view_name": patch.name,
4103
+ "view_key": target_key,
4104
+ "view_type": patch.type.value,
4105
+ "list_type": fallback_system_view_list_type,
4106
+ "expected_visible_order": system_view_sync.get("expected_visible_order"),
4107
+ "actual_visible_order": system_view_sync.get("actual_visible_order"),
4108
+ "apply_columns": apply_columns,
4109
+ },
4110
+ }
4111
+ failed_views.append(failure_entry)
4112
+ view_results.append(failure_entry)
4113
+ continue
3959
4114
  if existing_key:
3960
4115
  updated.append(patch.name)
3961
4116
  view_results.append(
@@ -3967,6 +4122,8 @@ class AiBuilderFacade:
3967
4122
  "fallback_applied": True,
3968
4123
  "expected_filters": deepcopy(translated_filters),
3969
4124
  "expected_buttons": deepcopy(expected_button_summary),
4125
+ "system_view_sync": system_view_sync,
4126
+ "apply_columns": deepcopy(apply_columns),
3970
4127
  }
3971
4128
  )
3972
4129
  else:
@@ -3980,6 +4137,8 @@ class AiBuilderFacade:
3980
4137
  "fallback_applied": True,
3981
4138
  "expected_filters": deepcopy(translated_filters),
3982
4139
  "expected_buttons": deepcopy(expected_button_summary),
4140
+ "system_view_sync": system_view_sync,
4141
+ "apply_columns": deepcopy(apply_columns),
3983
4142
  }
3984
4143
  )
3985
4144
  continue
@@ -4079,6 +4238,8 @@ class AiBuilderFacade:
4079
4238
  filter_mismatches: list[dict[str, Any]] = []
4080
4239
  button_readback_pending = False
4081
4240
  button_mismatches: list[dict[str, Any]] = []
4241
+ custom_button_readback_pending = False
4242
+ custom_button_readback_pending_entries: list[dict[str, Any]] = []
4082
4243
  for item in view_results:
4083
4244
  status = str(item.get("status") or "")
4084
4245
  name = str(item.get("name") or "")
@@ -4181,12 +4342,28 @@ class AiBuilderFacade:
4181
4342
  config_response = self.views.view_get_config(profile=profile, viewgraph_key=verification_key)
4182
4343
  config_result = (config_response.get("result") or {}) if isinstance(config_response.get("result"), dict) else {}
4183
4344
  actual_buttons = _normalize_view_buttons_for_compare(config_result)
4184
- buttons_verified = actual_buttons == expected_buttons
4345
+ button_comparison = _compare_view_button_summaries(
4346
+ expected=expected_buttons,
4347
+ actual=actual_buttons,
4348
+ )
4349
+ buttons_verified = bool(button_comparison.get("verified"))
4185
4350
  verification_entry["buttons_verified"] = buttons_verified
4186
4351
  verification_entry["view_key"] = verification_key
4187
4352
  verification_entry["expected_buttons"] = deepcopy(expected_buttons)
4188
4353
  verification_entry["actual_buttons"] = actual_buttons
4189
- if not buttons_verified:
4354
+ if button_comparison.get("custom_button_readback_pending"):
4355
+ verification_entry["custom_button_readback_pending"] = True
4356
+ verification_entry["pending_custom_buttons"] = deepcopy(button_comparison.get("pending_custom_buttons") or [])
4357
+ custom_button_readback_pending = True
4358
+ custom_button_readback_pending_entries.append(
4359
+ {
4360
+ "name": name,
4361
+ "type": item.get("type"),
4362
+ "view_key": verification_key,
4363
+ "pending_custom_buttons": deepcopy(button_comparison.get("pending_custom_buttons") or []),
4364
+ }
4365
+ )
4366
+ elif not buttons_verified:
4190
4367
  button_mismatches.append(
4191
4368
  {
4192
4369
  "name": name,
@@ -4249,6 +4426,11 @@ class AiBuilderFacade:
4249
4426
  "per_view_results": view_results,
4250
4427
  "filter_mismatches": filter_mismatches,
4251
4428
  "button_mismatches": button_mismatches,
4429
+ **(
4430
+ {"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
4431
+ if custom_button_readback_pending_entries
4432
+ else {}
4433
+ ),
4252
4434
  },
4253
4435
  "request_id": first_failure.get("request_id"),
4254
4436
  "suggested_next_call": {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
@@ -4256,10 +4438,21 @@ class AiBuilderFacade:
4256
4438
  "http_status": first_failure.get("http_status"),
4257
4439
  "noop": noop,
4258
4440
  "warnings": (
4259
- [_warning("VIEW_FILTERS_UNVERIFIED", "view definitions may exist, but saved filter behavior is not fully verified")] if (filter_readback_pending or filter_mismatches) else []
4260
- )
4261
- + (
4262
- [_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions may exist, but saved button behavior is not fully verified")] if (button_readback_pending or button_mismatches) else []
4441
+ (
4442
+ [_warning("VIEW_FILTERS_UNVERIFIED", "view definitions may exist, but saved filter behavior is not fully verified")]
4443
+ if (filter_readback_pending or filter_mismatches)
4444
+ else []
4445
+ )
4446
+ + (
4447
+ [_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions may exist, but saved button behavior is not fully verified")]
4448
+ if (button_readback_pending or button_mismatches)
4449
+ else []
4450
+ )
4451
+ + (
4452
+ [_warning("VIEW_CUSTOM_BUTTON_READBACK_PENDING", "system buttons verified, but draft custom button bindings are not fully visible through view readback yet")]
4453
+ if custom_button_readback_pending
4454
+ else []
4455
+ )
4263
4456
  ),
4264
4457
  "verification": {
4265
4458
  "views_verified": verified,
@@ -4267,6 +4460,8 @@ class AiBuilderFacade:
4267
4460
  "view_buttons_verified": view_buttons_verified,
4268
4461
  "views_read_unavailable": verified_views_unavailable,
4269
4462
  "by_view": verification_by_view,
4463
+ "custom_button_readback_pending": custom_button_readback_pending,
4464
+ "custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
4270
4465
  },
4271
4466
  "app_key": app_key,
4272
4467
  "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
@@ -4278,6 +4473,13 @@ class AiBuilderFacade:
4278
4473
  warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
4279
4474
  if button_readback_pending or button_mismatches:
4280
4475
  warnings.append(_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions were applied, but saved button behavior is not fully verified"))
4476
+ if custom_button_readback_pending:
4477
+ warnings.append(
4478
+ _warning(
4479
+ "VIEW_CUSTOM_BUTTON_READBACK_PENDING",
4480
+ "system buttons verified, but draft custom button bindings are not fully visible through view readback yet",
4481
+ )
4482
+ )
4281
4483
  response = {
4282
4484
  "status": "success" if verified and view_filters_verified and view_buttons_verified else "partial_success",
4283
4485
  "error_code": None if verified and view_filters_verified and view_buttons_verified else ("VIEW_BUTTON_READBACK_MISMATCH" if button_mismatches else "VIEW_FILTER_READBACK_MISMATCH" if filter_mismatches else "VIEWS_READBACK_PENDING"),
@@ -4297,6 +4499,11 @@ class AiBuilderFacade:
4297
4499
  "details": {
4298
4500
  **({"filter_mismatches": filter_mismatches} if filter_mismatches else {}),
4299
4501
  **({"button_mismatches": button_mismatches} if button_mismatches else {}),
4502
+ **(
4503
+ {"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
4504
+ if custom_button_readback_pending_entries
4505
+ else {}
4506
+ ),
4300
4507
  },
4301
4508
  "request_id": None,
4302
4509
  "suggested_next_call": None if verified and view_filters_verified and view_buttons_verified else {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
@@ -4309,6 +4516,8 @@ class AiBuilderFacade:
4309
4516
  "views_read_unavailable": verified_views_unavailable,
4310
4517
  "filter_readback_pending": filter_readback_pending,
4311
4518
  "button_readback_pending": button_readback_pending,
4519
+ "custom_button_readback_pending": custom_button_readback_pending,
4520
+ "custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
4312
4521
  "by_view": verification_by_view,
4313
4522
  },
4314
4523
  "app_key": app_key,
@@ -5055,6 +5264,28 @@ class AiBuilderFacade:
5055
5264
  version_result = {}
5056
5265
  return _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or int(current_schema.get("editVersionNo") or 1)
5057
5266
 
5267
+ def _ensure_app_edit_context(
5268
+ self,
5269
+ *,
5270
+ profile: str,
5271
+ app_key: str,
5272
+ normalized_args: dict[str, Any],
5273
+ failure_code: str,
5274
+ ) -> tuple[int | None, JSONObject | None]:
5275
+ try:
5276
+ version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
5277
+ except (QingflowApiError, RuntimeError) as error:
5278
+ api_error = _coerce_api_error(error)
5279
+ return None, _failed_from_api_error(
5280
+ failure_code,
5281
+ api_error,
5282
+ normalized_args=normalized_args,
5283
+ details={"app_key": app_key, "phase": "prepare_edit_context"},
5284
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
5285
+ )
5286
+ edit_version_no = _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or 1
5287
+ return edit_version_no, None
5288
+
5058
5289
  def _append_publish_result(self, *, profile: str, app_key: str, publish: bool, response: JSONObject) -> JSONObject:
5059
5290
  verification = response.get("verification")
5060
5291
  if not isinstance(verification, dict):
@@ -5134,6 +5365,29 @@ class AiBuilderFacade:
5134
5365
  "verified": verified,
5135
5366
  }
5136
5367
 
5368
+ def _sync_system_view_and_restore_buttons(
5369
+ self,
5370
+ *,
5371
+ profile: str,
5372
+ app_key: str,
5373
+ viewgraph_key: str,
5374
+ payload: dict[str, Any],
5375
+ list_type: int,
5376
+ schema: dict[str, Any],
5377
+ visible_field_names: list[str],
5378
+ ) -> dict[str, Any]:
5379
+ sync_result = self._sync_system_view_apply_config(
5380
+ profile=profile,
5381
+ app_key=app_key,
5382
+ list_type=list_type,
5383
+ schema=schema,
5384
+ visible_field_names=visible_field_names,
5385
+ )
5386
+ if bool(sync_result.get("verified")) and "buttonConfigDTOList" in payload:
5387
+ self.views.view_update(profile=profile, viewgraph_key=viewgraph_key, payload=payload)
5388
+ sync_result = {**sync_result, "button_config_restored": True}
5389
+ return sync_result
5390
+
5137
5391
  def _load_views_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
5138
5392
  try:
5139
5393
  views = self.views.view_list_flat(profile=profile, app_key=app_key)
@@ -5262,9 +5516,17 @@ class AiBuilderFacade:
5262
5516
  profile: str,
5263
5517
  app_name: str,
5264
5518
  package_tag_id: int | None,
5519
+ icon: str | None,
5520
+ color: str | None,
5265
5521
  ) -> JSONObject:
5266
5522
  payload: JSONObject = {
5267
5523
  "appName": app_name or "未命名应用",
5524
+ "appIcon": encode_workspace_icon_with_defaults(
5525
+ icon=icon,
5526
+ color=color,
5527
+ title=app_name or "未命名应用",
5528
+ fallback_icon_name="template",
5529
+ ),
5268
5530
  "auth": default_member_auth(),
5269
5531
  "tagIds": [package_tag_id] if package_tag_id and package_tag_id > 0 else [],
5270
5532
  }
@@ -5310,6 +5572,7 @@ class AiBuilderFacade:
5310
5572
  "suggested_next_call": None,
5311
5573
  "app_key": new_app_key,
5312
5574
  "app_name": app_name or "未命名应用",
5575
+ "app_icon": payload.get("appIcon"),
5313
5576
  "tag_ids": [package_tag_id] if package_tag_id and package_tag_id > 0 else [],
5314
5577
  "created": True,
5315
5578
  }
@@ -5321,10 +5584,103 @@ class AiBuilderFacade:
5321
5584
  "suggested_next_call": None,
5322
5585
  "app_key": new_app_key,
5323
5586
  "app_name": base.get("formTitle") or app_name or "未命名应用",
5587
+ "app_icon": str(base.get("appIcon") or payload.get("appIcon") or "").strip() or None,
5324
5588
  "tag_ids": _coerce_int_list(base.get("tagIds")),
5325
5589
  "created": True,
5326
5590
  }
5327
5591
 
5592
+ def _build_app_base_update_payload(
5593
+ self,
5594
+ *,
5595
+ raw_base: dict[str, Any],
5596
+ form_title: str,
5597
+ app_icon: str,
5598
+ ) -> JSONObject | None:
5599
+ auth = deepcopy(raw_base.get("auth"))
5600
+ if not isinstance(auth, dict):
5601
+ return None
5602
+ payload: JSONObject = {
5603
+ "formTitle": form_title,
5604
+ "auth": auth,
5605
+ "appIcon": app_icon,
5606
+ }
5607
+ tag_ids = _coerce_int_list(raw_base.get("tagIds"))
5608
+ if tag_ids:
5609
+ payload["tagIds"] = tag_ids
5610
+ return payload
5611
+
5612
+ def _ensure_app_base_visuals(
5613
+ self,
5614
+ *,
5615
+ profile: str,
5616
+ app_key: str,
5617
+ fallback_title: str,
5618
+ icon: str | None,
5619
+ color: str | None,
5620
+ normalized_args: dict[str, Any],
5621
+ ) -> JSONObject:
5622
+ if not icon and not color:
5623
+ return {
5624
+ "status": "success",
5625
+ "updated": False,
5626
+ "app_icon": None,
5627
+ "request_id": None,
5628
+ }
5629
+ try:
5630
+ base_result = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
5631
+ except (QingflowApiError, RuntimeError) as error:
5632
+ api_error = _coerce_api_error(error)
5633
+ return _failed_from_api_error(
5634
+ "APP_VISUAL_READ_FAILED",
5635
+ api_error,
5636
+ normalized_args=normalized_args,
5637
+ details={"app_key": app_key},
5638
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
5639
+ )
5640
+ raw_base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
5641
+ effective_title = str(raw_base.get("formTitle") or fallback_title or "未命名应用").strip() or "未命名应用"
5642
+ existing_icon = str(raw_base.get("appIcon") or "").strip() or None
5643
+ desired_icon = encode_workspace_icon_with_defaults(
5644
+ icon=icon,
5645
+ color=color,
5646
+ title=effective_title,
5647
+ fallback_icon_name="template",
5648
+ existing_icon=existing_icon,
5649
+ )
5650
+ if desired_icon == existing_icon:
5651
+ return {
5652
+ "status": "success",
5653
+ "updated": False,
5654
+ "app_icon": desired_icon,
5655
+ "request_id": None,
5656
+ }
5657
+ payload = self._build_app_base_update_payload(raw_base=raw_base, form_title=effective_title, app_icon=desired_icon)
5658
+ if payload is None:
5659
+ return _failed(
5660
+ "APP_VISUAL_UPDATE_UNSUPPORTED",
5661
+ "app base info did not include editable auth payload required for icon update",
5662
+ normalized_args=normalized_args,
5663
+ details={"app_key": app_key},
5664
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
5665
+ )
5666
+ try:
5667
+ update_result = self.apps.app_update_base(profile=profile, app_key=app_key, payload=payload)
5668
+ except (QingflowApiError, RuntimeError) as error:
5669
+ api_error = _coerce_api_error(error)
5670
+ return _failed_from_api_error(
5671
+ "APP_VISUAL_UPDATE_FAILED",
5672
+ api_error,
5673
+ normalized_args=normalized_args,
5674
+ details={"app_key": app_key, "app_icon": desired_icon},
5675
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
5676
+ )
5677
+ return {
5678
+ "status": "success",
5679
+ "updated": True,
5680
+ "app_icon": desired_icon,
5681
+ "request_id": update_result.get("request_id") if isinstance(update_result, dict) else None,
5682
+ }
5683
+
5328
5684
  def _current_request_route(self, profile: str) -> JSONObject:
5329
5685
  session_profile = self.apps.sessions.get_profile(profile)
5330
5686
  backend_session = self.apps.sessions.get_backend_session(profile)
@@ -5507,11 +5863,15 @@ def _serialize_custom_button_payload(payload: CustomButtonPatch) -> dict[str, An
5507
5863
  "buttonIcon": data["button_icon"],
5508
5864
  "triggerAction": data["trigger_action"],
5509
5865
  }
5866
+ if str(data.get("icon_color") or "").strip():
5867
+ serialized["iconColor"] = data["icon_color"]
5510
5868
  if str(data.get("trigger_link_url") or "").strip():
5511
5869
  serialized["triggerLinkUrl"] = data["trigger_link_url"]
5512
5870
  trigger_add_data_config = data.get("trigger_add_data_config")
5513
5871
  if isinstance(trigger_add_data_config, dict):
5514
5872
  serialized["triggerAddDataConfig"] = _serialize_custom_button_add_data_config(trigger_add_data_config)
5873
+ else:
5874
+ serialized["triggerAddDataConfig"] = _serialize_custom_button_add_data_config({})
5515
5875
  external_qrobot_config = data.get("external_qrobot_config")
5516
5876
  if isinstance(external_qrobot_config, dict):
5517
5877
  serialized["customButtonExternalQRobotRelationVO"] = _serialize_custom_button_external_qrobot_config(external_qrobot_config)
@@ -5594,6 +5954,7 @@ def _normalize_custom_button_summary(item: dict[str, Any]) -> dict[str, Any]:
5594
5954
  "button_id": _coerce_positive_int(item.get("button_id") or item.get("buttonId") or item.get("id")),
5595
5955
  "button_text": str(item.get("button_text") or item.get("buttonText") or "").strip() or None,
5596
5956
  "button_icon": str(item.get("button_icon") or item.get("buttonIcon") or "").strip() or None,
5957
+ "icon_color": str(item.get("icon_color") or item.get("iconColor") or "").strip() or None,
5597
5958
  "background_color": str(item.get("background_color") or item.get("backgroundColor") or "").strip() or None,
5598
5959
  "text_color": str(item.get("text_color") or item.get("textColor") or "").strip() or None,
5599
5960
  "used_in_chart_count": _coerce_nonnegative_int(item.get("used_in_chart_count") or item.get("userInChartCount")),
@@ -6350,11 +6711,12 @@ def _build_public_portal_base_payload(
6350
6711
  effective_name = str(dash_name or data.get("dashName") or "").strip() or "未命名门户"
6351
6712
  effective_hide_copyright = hide_copyright if hide_copyright is not None else bool(data.get("hideCopyright", False))
6352
6713
  if icon or color or not data.get("dashIcon"):
6353
- data["dashIcon"] = encode_workspace_icon(
6714
+ data["dashIcon"] = encode_workspace_icon_with_defaults(
6354
6715
  icon=icon,
6355
6716
  color=color,
6356
6717
  title=effective_name,
6357
6718
  fallback_icon_name="view-grid",
6719
+ existing_icon=str(data.get("dashIcon") or "").strip() or None,
6358
6720
  )
6359
6721
  data["dashName"] = effective_name
6360
6722
  data["auth"] = deepcopy(auth if auth is not None else data.get("auth") or default_member_auth())
@@ -8072,6 +8434,36 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
8072
8434
  for entry in display_entries
8073
8435
  if str(entry.get("name") or "").strip()
8074
8436
  ]
8437
+ apply_entries = [
8438
+ entry
8439
+ for entry in display_entries
8440
+ if _coerce_nonnegative_int(entry.get("field_id")) is not None
8441
+ and str(entry.get("name") or "").strip()
8442
+ and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
8443
+ ]
8444
+ apply_column_ids = [
8445
+ field_id
8446
+ for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in apply_entries)
8447
+ if field_id is not None
8448
+ ]
8449
+ apply_columns = [
8450
+ str(entry.get("name") or "").strip()
8451
+ for entry in apply_entries
8452
+ if str(entry.get("name") or "").strip()
8453
+ ]
8454
+ if not apply_columns and configured_column_ids:
8455
+ apply_columns = [
8456
+ str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
8457
+ for field_id in configured_column_ids
8458
+ if str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
8459
+ and str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
8460
+ ]
8461
+ apply_column_ids = [
8462
+ field_id
8463
+ for field_id in configured_column_ids
8464
+ if str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
8465
+ and str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
8466
+ ]
8075
8467
  if not display_columns and configured_columns:
8076
8468
  display_columns = configured_columns
8077
8469
  display_column_ids = list(configured_column_ids)
@@ -8087,6 +8479,10 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
8087
8479
  summary["configured_columns"] = configured_columns
8088
8480
  summary["configured_column_ids"] = configured_column_ids
8089
8481
  config_enriched = True
8482
+ if apply_columns:
8483
+ summary["apply_columns"] = apply_columns
8484
+ summary["apply_column_ids"] = apply_column_ids
8485
+ config_enriched = True
8090
8486
  if question_entries:
8091
8487
  summary["column_details"] = display_entries or _sort_view_question_entries(question_entries)
8092
8488
  config_enriched = True
@@ -8337,6 +8733,7 @@ def _normalize_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
8337
8733
  for public_key, source_key in (
8338
8734
  ("default_button_text", "defaultButtonText"),
8339
8735
  ("button_icon", "buttonIcon"),
8736
+ ("icon_color", "iconColor"),
8340
8737
  ("background_color", "backgroundColor"),
8341
8738
  ("text_color", "textColor"),
8342
8739
  ("trigger_link_url", "triggerLinkUrl"),
@@ -8366,17 +8763,21 @@ def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
8366
8763
  normalized_entries: list[dict[str, Any]] = []
8367
8764
  for entry in entries:
8368
8765
  normalized = _normalize_view_button_entry(entry)
8766
+ is_custom = normalized.get("button_type") == "CUSTOM"
8369
8767
  normalized_entries.append(
8370
8768
  {
8371
8769
  "button_type": normalized.get("button_type"),
8372
8770
  "config_type": normalized.get("config_type"),
8373
- "button_id": normalized.get("button_id"),
8771
+ "button_id": normalized.get("button_id") if is_custom else None,
8374
8772
  "button_text": normalized.get("button_text"),
8375
8773
  "button_icon": normalized.get("button_icon"),
8774
+ "icon_color": normalized.get("icon_color"),
8376
8775
  "background_color": normalized.get("background_color"),
8377
8776
  "text_color": normalized.get("text_color"),
8378
8777
  "trigger_action": normalized.get("trigger_action"),
8379
- "trigger_link_url": normalized.get("trigger_link_url"),
8778
+ # Custom button trigger details are verified by dedicated button CRUD readback.
8779
+ # View binding verification should focus on binding/display semantics and tolerate resource-owned URLs.
8780
+ "trigger_link_url": None if is_custom else normalized.get("trigger_link_url"),
8380
8781
  "being_main": bool(normalized.get("being_main", False)),
8381
8782
  "print_tpls": _normalize_print_tpls_for_compare(normalized.get("print_tpls")),
8382
8783
  "button_formula": str(normalized.get("button_formula") or ""),
@@ -8387,12 +8788,127 @@ def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
8387
8788
  return normalized_entries
8388
8789
 
8389
8790
 
8791
+ _SYSTEM_VIEW_BUTTON_ID_BY_ACTION: dict[tuple[str, str], int] = {
8792
+ ("TOP", "set"): 1,
8793
+ ("TOP", "switchView"): 2,
8794
+ ("TOP", "setRowHeight"): 3,
8795
+ ("TOP", "search"): 6,
8796
+ ("DETAIL", "share"): 7,
8797
+ ("DETAIL", "edit"): 8,
8798
+ }
8799
+
8800
+ _SYSTEM_VIEW_BUTTON_ID_BY_TEXT: dict[tuple[str, str], int] = {
8801
+ ("TOP", "字段管理"): 1,
8802
+ ("TOP", "视图类型"): 2,
8803
+ ("TOP", "行高"): 3,
8804
+ ("TOP", "搜索"): 6,
8805
+ ("DETAIL", "分享"): 7,
8806
+ ("DETAIL", "修改"): 8,
8807
+ ("DETAIL", "修改记录"): 8,
8808
+ }
8809
+
8810
+
8811
+ def _resolve_system_view_button_logical_id(entry: dict[str, Any]) -> int | None:
8812
+ config_type = _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type")) or ""
8813
+ trigger_action = str(entry.get("triggerAction") or entry.get("trigger_action") or "").strip()
8814
+ if config_type and trigger_action:
8815
+ mapped = _SYSTEM_VIEW_BUTTON_ID_BY_ACTION.get((config_type, trigger_action))
8816
+ if mapped is not None:
8817
+ return mapped
8818
+ button_text = str(entry.get("buttonText") or entry.get("button_text") or "").strip()
8819
+ default_button_text = str(entry.get("defaultButtonText") or entry.get("default_button_text") or "").strip()
8820
+ for candidate in (button_text, default_button_text):
8821
+ if config_type and candidate:
8822
+ mapped = _SYSTEM_VIEW_BUTTON_ID_BY_TEXT.get((config_type, candidate))
8823
+ if mapped is not None:
8824
+ return mapped
8825
+ button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
8826
+ if button_id is not None and button_id < 1000:
8827
+ return button_id
8828
+ return None
8829
+
8830
+
8831
+ def _normalize_expected_view_buttons_for_compare(
8832
+ value: Any,
8833
+ *,
8834
+ custom_button_details_by_id: dict[int, dict[str, Any]] | None = None,
8835
+ ) -> list[dict[str, Any]]:
8836
+ normalized_entries = _normalize_view_buttons_for_compare(value)
8837
+ if not custom_button_details_by_id:
8838
+ return normalized_entries
8839
+ enriched_entries: list[dict[str, Any]] = []
8840
+ for item in normalized_entries:
8841
+ enriched = deepcopy(item)
8842
+ if enriched.get("button_type") == "CUSTOM":
8843
+ button_id = _coerce_positive_int(enriched.get("button_id"))
8844
+ detail = custom_button_details_by_id.get(button_id or -1)
8845
+ if isinstance(detail, dict):
8846
+ for key in (
8847
+ "button_text",
8848
+ "button_icon",
8849
+ "icon_color",
8850
+ "background_color",
8851
+ "text_color",
8852
+ "trigger_action",
8853
+ ):
8854
+ value = detail.get(key)
8855
+ if enriched.get(key) in {None, ""} and value not in {None, ""}:
8856
+ enriched[key] = deepcopy(value)
8857
+ enriched_entries.append(enriched)
8858
+ return enriched_entries
8859
+
8860
+
8861
+ def _partition_view_button_summaries(
8862
+ items: list[dict[str, Any]],
8863
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
8864
+ system_buttons: list[dict[str, Any]] = []
8865
+ custom_buttons: list[dict[str, Any]] = []
8866
+ other_buttons: list[dict[str, Any]] = []
8867
+ for item in items:
8868
+ button_type = str(item.get("button_type") or "").strip().upper()
8869
+ if button_type == "SYSTEM":
8870
+ system_buttons.append(item)
8871
+ elif button_type == "CUSTOM":
8872
+ custom_buttons.append(item)
8873
+ else:
8874
+ other_buttons.append(item)
8875
+ return system_buttons, custom_buttons, other_buttons
8876
+
8877
+
8878
+ def _compare_view_button_summaries(
8879
+ *,
8880
+ expected: list[dict[str, Any]],
8881
+ actual: list[dict[str, Any]],
8882
+ ) -> dict[str, Any]:
8883
+ if actual == expected:
8884
+ return {
8885
+ "verified": True,
8886
+ "custom_button_readback_pending": False,
8887
+ "pending_custom_buttons": [],
8888
+ }
8889
+ expected_system, expected_custom, expected_other = _partition_view_button_summaries(expected)
8890
+ actual_system, actual_custom, actual_other = _partition_view_button_summaries(actual)
8891
+ custom_button_readback_pending = (
8892
+ bool(expected_custom)
8893
+ and not actual_custom
8894
+ and actual_system == expected_system
8895
+ and actual_other == expected_other
8896
+ )
8897
+ return {
8898
+ "verified": custom_button_readback_pending,
8899
+ "custom_button_readback_pending": custom_button_readback_pending,
8900
+ "pending_custom_buttons": deepcopy(expected_custom) if custom_button_readback_pending else [],
8901
+ }
8902
+
8903
+
8390
8904
  def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
8391
8905
  dto: dict[str, Any] = {}
8906
+ button_type = _normalize_view_button_type(entry.get("buttonType") or entry.get("button_type"))
8392
8907
  button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
8908
+ if button_type == "SYSTEM":
8909
+ button_id = _resolve_system_view_button_logical_id(entry)
8393
8910
  if button_id is not None:
8394
8911
  dto["buttonId"] = button_id
8395
- button_type = _normalize_view_button_type(entry.get("buttonType") or entry.get("button_type"))
8396
8912
  if button_type is not None:
8397
8913
  dto["buttonType"] = button_type
8398
8914
  config_type = _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type"))
@@ -8405,6 +8921,7 @@ def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, An
8405
8921
  dto["printTpls"] = _serialize_print_tpl_ids(entry.get("printTpls"))
8406
8922
  for source_key, target_key in (
8407
8923
  ("buttonText", "buttonText"),
8924
+ ("defaultButtonText", "defaultButtonText"),
8408
8925
  ("buttonIcon", "buttonIcon"),
8409
8926
  ("backgroundColor", "backgroundColor"),
8410
8927
  ("textColor", "textColor"),
@@ -8423,10 +8940,9 @@ def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, An
8423
8940
  def _extract_existing_view_button_dtos(config: dict[str, Any]) -> list[dict[str, Any]]:
8424
8941
  if not isinstance(config, dict):
8425
8942
  return []
8426
- button_config_dtos = config.get("buttonConfigDTOList")
8427
- if isinstance(button_config_dtos, list):
8428
- return [deepcopy(item) for item in button_config_dtos if isinstance(item, dict)]
8429
- entries, _ = _extract_view_button_entries(config)
8943
+ entries, source = _extract_view_button_entries(config)
8944
+ if source == "buttonConfigDTOList":
8945
+ return [deepcopy(item) for item in entries if isinstance(item, dict)]
8430
8946
  return [_serialize_existing_view_button_entry(entry) for entry in entries if isinstance(entry, dict)]
8431
8947
 
8432
8948
 
@@ -8510,9 +9026,11 @@ def _serialize_view_button_binding(
8510
9026
  "buttonFormulaType": binding.button_formula_type,
8511
9027
  "printTpls": _serialize_print_tpl_ids(binding.print_tpls),
8512
9028
  }
8513
- if binding.button_type == PublicViewButtonType.system:
9029
+ if binding.button_type in {PublicViewButtonType.system, PublicViewButtonType.custom}:
8514
9030
  dto["buttonText"] = binding.button_text
8515
9031
  dto["buttonIcon"] = binding.button_icon
9032
+ if str(binding.icon_color or "").strip():
9033
+ dto["iconColor"] = binding.icon_color
8516
9034
  dto["backgroundColor"] = binding.background_color
8517
9035
  dto["textColor"] = binding.text_color
8518
9036
  dto["triggerAction"] = binding.trigger_action
@@ -9171,6 +9689,22 @@ def _build_view_update_payload(
9171
9689
  )
9172
9690
 
9173
9691
 
9692
+ _KNOWN_SYSTEM_VIEW_COLUMNS = {
9693
+ "编号",
9694
+ "当前流程状态",
9695
+ "申请人",
9696
+ "申请时间",
9697
+ "更新时间",
9698
+ "流程标题",
9699
+ "当前处理人",
9700
+ "当前处理节点",
9701
+ }
9702
+
9703
+
9704
+ def _filter_known_system_view_columns(columns: list[str]) -> list[str]:
9705
+ return [name for name in columns if str(name or "").strip() and str(name).strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS]
9706
+
9707
+
9174
9708
  def _build_minimal_view_payload(
9175
9709
  *,
9176
9710
  app_key: str,
@@ -9299,7 +9833,7 @@ def _resolve_view_visible_field_names(patch: ViewUpsertPatch) -> list[str]:
9299
9833
  ordered: list[str] = []
9300
9834
  for value in [*patch.columns, patch.title_field, patch.start_field, patch.end_field, patch.group_by]:
9301
9835
  name = str(value or "").strip()
9302
- if name and name not in ordered:
9836
+ if name and name not in _KNOWN_SYSTEM_VIEW_COLUMNS and name not in ordered:
9303
9837
  ordered.append(name)
9304
9838
  return ordered
9305
9839