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

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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@0.2.0-beta.63
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.64
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.63 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.64 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.63",
3
+ "version": "0.2.0-beta.64",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b63"
7
+ version = "0.2.0b64"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
  )
@@ -2567,7 +2589,17 @@ class AiBuilderFacade:
2567
2589
  upsert_views = _build_views_preset(request.preset, list(field_names))
2568
2590
  blocking_issues: list[dict[str, Any]] = []
2569
2591
  for patch in upsert_views:
2570
- columns = patch.get("columns") or []
2592
+ raw_columns = [str(name or "").strip() for name in (patch.get("columns") or []) if str(name or "").strip()]
2593
+ columns = _filter_known_system_view_columns(raw_columns)
2594
+ if patch.get("type") in {"table", "card"} and raw_columns and not columns:
2595
+ blocking_issues.append(
2596
+ {
2597
+ "error_code": "VALIDATION_ERROR",
2598
+ "view_name": patch.get("name"),
2599
+ "message": "view columns must include at least one real app field; system columns cannot be applied directly",
2600
+ "ignored_system_columns": [name for name in raw_columns if name in _KNOWN_SYSTEM_VIEW_COLUMNS],
2601
+ }
2602
+ )
2571
2603
  missing_columns = [name for name in columns if name not in field_names]
2572
2604
  if missing_columns:
2573
2605
  blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_columns})
@@ -3644,6 +3676,7 @@ class AiBuilderFacade:
3644
3676
  for patch in upsert_views
3645
3677
  )
3646
3678
  valid_custom_button_ids: set[int] = set()
3679
+ custom_button_details_by_id: dict[int, dict[str, Any]] = {}
3647
3680
  if requires_custom_button_validation:
3648
3681
  try:
3649
3682
  button_listing = self.buttons.custom_button_list(
@@ -3668,6 +3701,26 @@ class AiBuilderFacade:
3668
3701
  for item in (button_listing.get("items") or [])
3669
3702
  if isinstance(item, dict) and (button_id := _coerce_positive_int(item.get("button_id"))) is not None
3670
3703
  }
3704
+ referenced_custom_button_ids = {
3705
+ binding.button_id
3706
+ for patch in upsert_views
3707
+ for binding in (patch.buttons or [])
3708
+ if binding.button_type == PublicViewButtonType.custom and binding.button_id in valid_custom_button_ids
3709
+ }
3710
+ for button_id in sorted(referenced_custom_button_ids):
3711
+ try:
3712
+ detail = self.buttons.custom_button_get(
3713
+ profile=profile,
3714
+ app_key=app_key,
3715
+ button_id=button_id,
3716
+ being_draft=True,
3717
+ include_raw=False,
3718
+ )
3719
+ except (QingflowApiError, RuntimeError):
3720
+ continue
3721
+ detail_result = detail.get("result")
3722
+ if isinstance(detail_result, dict):
3723
+ custom_button_details_by_id[button_id] = _normalize_custom_button_detail(detail_result)
3671
3724
  removed: list[str] = []
3672
3725
  view_results: list[dict[str, Any]] = []
3673
3726
  for name in remove_views:
@@ -3704,7 +3757,24 @@ class AiBuilderFacade:
3704
3757
  and _extract_view_name(view) not in remove_views
3705
3758
  ]
3706
3759
  for ordinal, patch in enumerate(upsert_views, start=1):
3707
- missing_columns = [name for name in patch.columns if name not in field_names]
3760
+ apply_columns = _resolve_view_visible_field_names(patch)
3761
+ ignored_system_columns = [
3762
+ name
3763
+ for name in [str(value or "").strip() for value in patch.columns]
3764
+ if name in _KNOWN_SYSTEM_VIEW_COLUMNS
3765
+ ]
3766
+ if patch.type in {PublicViewType.table, PublicViewType.card} and patch.columns and not apply_columns:
3767
+ return _failed(
3768
+ "VALIDATION_ERROR",
3769
+ "view columns must include at least one real app field; system columns cannot be applied directly",
3770
+ normalized_args=normalized_args,
3771
+ details={
3772
+ "app_key": app_key,
3773
+ "view_name": patch.name,
3774
+ "ignored_system_columns": ignored_system_columns,
3775
+ },
3776
+ )
3777
+ missing_columns = [name for name in apply_columns if name not in field_names]
3708
3778
  if missing_columns:
3709
3779
  return _failed(
3710
3780
  "UNKNOWN_VIEW_FIELD",
@@ -3714,6 +3784,7 @@ class AiBuilderFacade:
3714
3784
  "app_key": app_key,
3715
3785
  "view_name": patch.name,
3716
3786
  "missing_fields": missing_columns,
3787
+ "ignored_system_columns": ignored_system_columns,
3717
3788
  },
3718
3789
  missing_fields=missing_columns,
3719
3790
  suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": app_key}},
@@ -3784,7 +3855,10 @@ class AiBuilderFacade:
3784
3855
  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
3856
  suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
3786
3857
  )
3787
- expected_button_summary = _normalize_view_buttons_for_compare(explicit_button_dtos or [])
3858
+ expected_button_summary = _normalize_expected_view_buttons_for_compare(
3859
+ explicit_button_dtos or [],
3860
+ custom_button_details_by_id=custom_button_details_by_id,
3861
+ )
3788
3862
  matched_existing_view: dict[str, Any] | None = None
3789
3863
  existing_key: str | None = None
3790
3864
  if patch.view_key:
@@ -3837,12 +3911,14 @@ class AiBuilderFacade:
3837
3911
  system_view_sync: dict[str, Any] | None = None
3838
3912
  if system_view_list_type is not None and patch.type.value == "table":
3839
3913
  operation_phase = "default_view_apply_config_sync"
3840
- system_view_sync = self._sync_system_view_apply_config(
3914
+ system_view_sync = self._sync_system_view_and_restore_buttons(
3841
3915
  profile=profile,
3842
3916
  app_key=app_key,
3917
+ viewgraph_key=existing_key,
3918
+ payload=payload,
3843
3919
  list_type=system_view_list_type,
3844
3920
  schema=schema,
3845
- visible_field_names=patch.columns,
3921
+ visible_field_names=apply_columns,
3846
3922
  )
3847
3923
  if not bool(system_view_sync.get("verified")):
3848
3924
  failure_entry = {
@@ -3864,6 +3940,7 @@ class AiBuilderFacade:
3864
3940
  "list_type": system_view_list_type,
3865
3941
  "expected_visible_order": system_view_sync.get("expected_visible_order"),
3866
3942
  "actual_visible_order": system_view_sync.get("actual_visible_order"),
3943
+ "apply_columns": apply_columns,
3867
3944
  },
3868
3945
  }
3869
3946
  failed_views.append(failure_entry)
@@ -3879,6 +3956,7 @@ class AiBuilderFacade:
3879
3956
  "expected_filters": deepcopy(translated_filters),
3880
3957
  "expected_buttons": deepcopy(expected_button_summary),
3881
3958
  "system_view_sync": system_view_sync,
3959
+ "apply_columns": deepcopy(apply_columns),
3882
3960
  }
3883
3961
  )
3884
3962
  else:
@@ -3956,6 +4034,49 @@ class AiBuilderFacade:
3956
4034
  explicit_button_dtos=fallback_button_dtos,
3957
4035
  )
3958
4036
  self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
4037
+ system_view_sync: dict[str, Any] | None = None
4038
+ fallback_system_view_list_type = (
4039
+ _resolve_system_view_list_type(view_key=target_key, view_name=patch.name)
4040
+ if patch.type.value == "table" and target_key
4041
+ else None
4042
+ )
4043
+ if fallback_system_view_list_type is not None:
4044
+ operation_phase = "default_view_apply_config_sync"
4045
+ system_view_sync = self._sync_system_view_and_restore_buttons(
4046
+ profile=profile,
4047
+ app_key=app_key,
4048
+ viewgraph_key=target_key,
4049
+ payload=fallback_payload,
4050
+ list_type=fallback_system_view_list_type,
4051
+ schema=schema,
4052
+ visible_field_names=apply_columns,
4053
+ )
4054
+ if not bool(system_view_sync.get("verified")):
4055
+ failure_entry = {
4056
+ "name": patch.name,
4057
+ "view_key": target_key,
4058
+ "type": patch.type.value,
4059
+ "status": "failed",
4060
+ "error_code": "SYSTEM_VIEW_ORDER_SYNC_FAILED",
4061
+ "message": "default view column order did not verify through app apply/baseInfo readback",
4062
+ "request_id": None,
4063
+ "backend_code": None,
4064
+ "http_status": None,
4065
+ "operation": "sync_default_view",
4066
+ "details": {
4067
+ "app_key": app_key,
4068
+ "view_name": patch.name,
4069
+ "view_key": target_key,
4070
+ "view_type": patch.type.value,
4071
+ "list_type": fallback_system_view_list_type,
4072
+ "expected_visible_order": system_view_sync.get("expected_visible_order"),
4073
+ "actual_visible_order": system_view_sync.get("actual_visible_order"),
4074
+ "apply_columns": apply_columns,
4075
+ },
4076
+ }
4077
+ failed_views.append(failure_entry)
4078
+ view_results.append(failure_entry)
4079
+ continue
3959
4080
  if existing_key:
3960
4081
  updated.append(patch.name)
3961
4082
  view_results.append(
@@ -3967,6 +4088,8 @@ class AiBuilderFacade:
3967
4088
  "fallback_applied": True,
3968
4089
  "expected_filters": deepcopy(translated_filters),
3969
4090
  "expected_buttons": deepcopy(expected_button_summary),
4091
+ "system_view_sync": system_view_sync,
4092
+ "apply_columns": deepcopy(apply_columns),
3970
4093
  }
3971
4094
  )
3972
4095
  else:
@@ -3980,6 +4103,8 @@ class AiBuilderFacade:
3980
4103
  "fallback_applied": True,
3981
4104
  "expected_filters": deepcopy(translated_filters),
3982
4105
  "expected_buttons": deepcopy(expected_button_summary),
4106
+ "system_view_sync": system_view_sync,
4107
+ "apply_columns": deepcopy(apply_columns),
3983
4108
  }
3984
4109
  )
3985
4110
  continue
@@ -4079,6 +4204,8 @@ class AiBuilderFacade:
4079
4204
  filter_mismatches: list[dict[str, Any]] = []
4080
4205
  button_readback_pending = False
4081
4206
  button_mismatches: list[dict[str, Any]] = []
4207
+ custom_button_readback_pending = False
4208
+ custom_button_readback_pending_entries: list[dict[str, Any]] = []
4082
4209
  for item in view_results:
4083
4210
  status = str(item.get("status") or "")
4084
4211
  name = str(item.get("name") or "")
@@ -4181,12 +4308,28 @@ class AiBuilderFacade:
4181
4308
  config_response = self.views.view_get_config(profile=profile, viewgraph_key=verification_key)
4182
4309
  config_result = (config_response.get("result") or {}) if isinstance(config_response.get("result"), dict) else {}
4183
4310
  actual_buttons = _normalize_view_buttons_for_compare(config_result)
4184
- buttons_verified = actual_buttons == expected_buttons
4311
+ button_comparison = _compare_view_button_summaries(
4312
+ expected=expected_buttons,
4313
+ actual=actual_buttons,
4314
+ )
4315
+ buttons_verified = bool(button_comparison.get("verified"))
4185
4316
  verification_entry["buttons_verified"] = buttons_verified
4186
4317
  verification_entry["view_key"] = verification_key
4187
4318
  verification_entry["expected_buttons"] = deepcopy(expected_buttons)
4188
4319
  verification_entry["actual_buttons"] = actual_buttons
4189
- if not buttons_verified:
4320
+ if button_comparison.get("custom_button_readback_pending"):
4321
+ verification_entry["custom_button_readback_pending"] = True
4322
+ verification_entry["pending_custom_buttons"] = deepcopy(button_comparison.get("pending_custom_buttons") or [])
4323
+ custom_button_readback_pending = True
4324
+ custom_button_readback_pending_entries.append(
4325
+ {
4326
+ "name": name,
4327
+ "type": item.get("type"),
4328
+ "view_key": verification_key,
4329
+ "pending_custom_buttons": deepcopy(button_comparison.get("pending_custom_buttons") or []),
4330
+ }
4331
+ )
4332
+ elif not buttons_verified:
4190
4333
  button_mismatches.append(
4191
4334
  {
4192
4335
  "name": name,
@@ -4249,6 +4392,11 @@ class AiBuilderFacade:
4249
4392
  "per_view_results": view_results,
4250
4393
  "filter_mismatches": filter_mismatches,
4251
4394
  "button_mismatches": button_mismatches,
4395
+ **(
4396
+ {"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
4397
+ if custom_button_readback_pending_entries
4398
+ else {}
4399
+ ),
4252
4400
  },
4253
4401
  "request_id": first_failure.get("request_id"),
4254
4402
  "suggested_next_call": {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
@@ -4256,10 +4404,21 @@ class AiBuilderFacade:
4256
4404
  "http_status": first_failure.get("http_status"),
4257
4405
  "noop": noop,
4258
4406
  "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 []
4407
+ (
4408
+ [_warning("VIEW_FILTERS_UNVERIFIED", "view definitions may exist, but saved filter behavior is not fully verified")]
4409
+ if (filter_readback_pending or filter_mismatches)
4410
+ else []
4411
+ )
4412
+ + (
4413
+ [_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions may exist, but saved button behavior is not fully verified")]
4414
+ if (button_readback_pending or button_mismatches)
4415
+ else []
4416
+ )
4417
+ + (
4418
+ [_warning("VIEW_CUSTOM_BUTTON_READBACK_PENDING", "system buttons verified, but draft custom button bindings are not fully visible through view readback yet")]
4419
+ if custom_button_readback_pending
4420
+ else []
4421
+ )
4263
4422
  ),
4264
4423
  "verification": {
4265
4424
  "views_verified": verified,
@@ -4267,6 +4426,8 @@ class AiBuilderFacade:
4267
4426
  "view_buttons_verified": view_buttons_verified,
4268
4427
  "views_read_unavailable": verified_views_unavailable,
4269
4428
  "by_view": verification_by_view,
4429
+ "custom_button_readback_pending": custom_button_readback_pending,
4430
+ "custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
4270
4431
  },
4271
4432
  "app_key": app_key,
4272
4433
  "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
@@ -4278,6 +4439,13 @@ class AiBuilderFacade:
4278
4439
  warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
4279
4440
  if button_readback_pending or button_mismatches:
4280
4441
  warnings.append(_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions were applied, but saved button behavior is not fully verified"))
4442
+ if custom_button_readback_pending:
4443
+ warnings.append(
4444
+ _warning(
4445
+ "VIEW_CUSTOM_BUTTON_READBACK_PENDING",
4446
+ "system buttons verified, but draft custom button bindings are not fully visible through view readback yet",
4447
+ )
4448
+ )
4281
4449
  response = {
4282
4450
  "status": "success" if verified and view_filters_verified and view_buttons_verified else "partial_success",
4283
4451
  "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 +4465,11 @@ class AiBuilderFacade:
4297
4465
  "details": {
4298
4466
  **({"filter_mismatches": filter_mismatches} if filter_mismatches else {}),
4299
4467
  **({"button_mismatches": button_mismatches} if button_mismatches else {}),
4468
+ **(
4469
+ {"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
4470
+ if custom_button_readback_pending_entries
4471
+ else {}
4472
+ ),
4300
4473
  },
4301
4474
  "request_id": None,
4302
4475
  "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 +4482,8 @@ class AiBuilderFacade:
4309
4482
  "views_read_unavailable": verified_views_unavailable,
4310
4483
  "filter_readback_pending": filter_readback_pending,
4311
4484
  "button_readback_pending": button_readback_pending,
4485
+ "custom_button_readback_pending": custom_button_readback_pending,
4486
+ "custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
4312
4487
  "by_view": verification_by_view,
4313
4488
  },
4314
4489
  "app_key": app_key,
@@ -5055,6 +5230,28 @@ class AiBuilderFacade:
5055
5230
  version_result = {}
5056
5231
  return _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or int(current_schema.get("editVersionNo") or 1)
5057
5232
 
5233
+ def _ensure_app_edit_context(
5234
+ self,
5235
+ *,
5236
+ profile: str,
5237
+ app_key: str,
5238
+ normalized_args: dict[str, Any],
5239
+ failure_code: str,
5240
+ ) -> tuple[int | None, JSONObject | None]:
5241
+ try:
5242
+ version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
5243
+ except (QingflowApiError, RuntimeError) as error:
5244
+ api_error = _coerce_api_error(error)
5245
+ return None, _failed_from_api_error(
5246
+ failure_code,
5247
+ api_error,
5248
+ normalized_args=normalized_args,
5249
+ details={"app_key": app_key, "phase": "prepare_edit_context"},
5250
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
5251
+ )
5252
+ edit_version_no = _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or 1
5253
+ return edit_version_no, None
5254
+
5058
5255
  def _append_publish_result(self, *, profile: str, app_key: str, publish: bool, response: JSONObject) -> JSONObject:
5059
5256
  verification = response.get("verification")
5060
5257
  if not isinstance(verification, dict):
@@ -5134,6 +5331,29 @@ class AiBuilderFacade:
5134
5331
  "verified": verified,
5135
5332
  }
5136
5333
 
5334
+ def _sync_system_view_and_restore_buttons(
5335
+ self,
5336
+ *,
5337
+ profile: str,
5338
+ app_key: str,
5339
+ viewgraph_key: str,
5340
+ payload: dict[str, Any],
5341
+ list_type: int,
5342
+ schema: dict[str, Any],
5343
+ visible_field_names: list[str],
5344
+ ) -> dict[str, Any]:
5345
+ sync_result = self._sync_system_view_apply_config(
5346
+ profile=profile,
5347
+ app_key=app_key,
5348
+ list_type=list_type,
5349
+ schema=schema,
5350
+ visible_field_names=visible_field_names,
5351
+ )
5352
+ if bool(sync_result.get("verified")) and "buttonConfigDTOList" in payload:
5353
+ self.views.view_update(profile=profile, viewgraph_key=viewgraph_key, payload=payload)
5354
+ sync_result = {**sync_result, "button_config_restored": True}
5355
+ return sync_result
5356
+
5137
5357
  def _load_views_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
5138
5358
  try:
5139
5359
  views = self.views.view_list_flat(profile=profile, app_key=app_key)
@@ -5512,6 +5732,8 @@ def _serialize_custom_button_payload(payload: CustomButtonPatch) -> dict[str, An
5512
5732
  trigger_add_data_config = data.get("trigger_add_data_config")
5513
5733
  if isinstance(trigger_add_data_config, dict):
5514
5734
  serialized["triggerAddDataConfig"] = _serialize_custom_button_add_data_config(trigger_add_data_config)
5735
+ else:
5736
+ serialized["triggerAddDataConfig"] = _serialize_custom_button_add_data_config({})
5515
5737
  external_qrobot_config = data.get("external_qrobot_config")
5516
5738
  if isinstance(external_qrobot_config, dict):
5517
5739
  serialized["customButtonExternalQRobotRelationVO"] = _serialize_custom_button_external_qrobot_config(external_qrobot_config)
@@ -8072,6 +8294,36 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
8072
8294
  for entry in display_entries
8073
8295
  if str(entry.get("name") or "").strip()
8074
8296
  ]
8297
+ apply_entries = [
8298
+ entry
8299
+ for entry in display_entries
8300
+ if _coerce_nonnegative_int(entry.get("field_id")) is not None
8301
+ and str(entry.get("name") or "").strip()
8302
+ and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
8303
+ ]
8304
+ apply_column_ids = [
8305
+ field_id
8306
+ for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in apply_entries)
8307
+ if field_id is not None
8308
+ ]
8309
+ apply_columns = [
8310
+ str(entry.get("name") or "").strip()
8311
+ for entry in apply_entries
8312
+ if str(entry.get("name") or "").strip()
8313
+ ]
8314
+ if not apply_columns and configured_column_ids:
8315
+ apply_columns = [
8316
+ str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
8317
+ for field_id in configured_column_ids
8318
+ if str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
8319
+ and str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
8320
+ ]
8321
+ apply_column_ids = [
8322
+ field_id
8323
+ for field_id in configured_column_ids
8324
+ if str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
8325
+ and str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
8326
+ ]
8075
8327
  if not display_columns and configured_columns:
8076
8328
  display_columns = configured_columns
8077
8329
  display_column_ids = list(configured_column_ids)
@@ -8087,6 +8339,10 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
8087
8339
  summary["configured_columns"] = configured_columns
8088
8340
  summary["configured_column_ids"] = configured_column_ids
8089
8341
  config_enriched = True
8342
+ if apply_columns:
8343
+ summary["apply_columns"] = apply_columns
8344
+ summary["apply_column_ids"] = apply_column_ids
8345
+ config_enriched = True
8090
8346
  if question_entries:
8091
8347
  summary["column_details"] = display_entries or _sort_view_question_entries(question_entries)
8092
8348
  config_enriched = True
@@ -8370,7 +8626,7 @@ def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
8370
8626
  {
8371
8627
  "button_type": normalized.get("button_type"),
8372
8628
  "config_type": normalized.get("config_type"),
8373
- "button_id": normalized.get("button_id"),
8629
+ "button_id": normalized.get("button_id") if normalized.get("button_type") == "CUSTOM" else None,
8374
8630
  "button_text": normalized.get("button_text"),
8375
8631
  "button_icon": normalized.get("button_icon"),
8376
8632
  "background_color": normalized.get("background_color"),
@@ -8387,12 +8643,127 @@ def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
8387
8643
  return normalized_entries
8388
8644
 
8389
8645
 
8646
+ _SYSTEM_VIEW_BUTTON_ID_BY_ACTION: dict[tuple[str, str], int] = {
8647
+ ("TOP", "set"): 1,
8648
+ ("TOP", "switchView"): 2,
8649
+ ("TOP", "setRowHeight"): 3,
8650
+ ("TOP", "search"): 6,
8651
+ ("DETAIL", "share"): 7,
8652
+ ("DETAIL", "edit"): 8,
8653
+ }
8654
+
8655
+ _SYSTEM_VIEW_BUTTON_ID_BY_TEXT: dict[tuple[str, str], int] = {
8656
+ ("TOP", "字段管理"): 1,
8657
+ ("TOP", "视图类型"): 2,
8658
+ ("TOP", "行高"): 3,
8659
+ ("TOP", "搜索"): 6,
8660
+ ("DETAIL", "分享"): 7,
8661
+ ("DETAIL", "修改"): 8,
8662
+ ("DETAIL", "修改记录"): 8,
8663
+ }
8664
+
8665
+
8666
+ def _resolve_system_view_button_logical_id(entry: dict[str, Any]) -> int | None:
8667
+ config_type = _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type")) or ""
8668
+ trigger_action = str(entry.get("triggerAction") or entry.get("trigger_action") or "").strip()
8669
+ if config_type and trigger_action:
8670
+ mapped = _SYSTEM_VIEW_BUTTON_ID_BY_ACTION.get((config_type, trigger_action))
8671
+ if mapped is not None:
8672
+ return mapped
8673
+ button_text = str(entry.get("buttonText") or entry.get("button_text") or "").strip()
8674
+ default_button_text = str(entry.get("defaultButtonText") or entry.get("default_button_text") or "").strip()
8675
+ for candidate in (button_text, default_button_text):
8676
+ if config_type and candidate:
8677
+ mapped = _SYSTEM_VIEW_BUTTON_ID_BY_TEXT.get((config_type, candidate))
8678
+ if mapped is not None:
8679
+ return mapped
8680
+ button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
8681
+ if button_id is not None and button_id < 1000:
8682
+ return button_id
8683
+ return None
8684
+
8685
+
8686
+ def _normalize_expected_view_buttons_for_compare(
8687
+ value: Any,
8688
+ *,
8689
+ custom_button_details_by_id: dict[int, dict[str, Any]] | None = None,
8690
+ ) -> list[dict[str, Any]]:
8691
+ normalized_entries = _normalize_view_buttons_for_compare(value)
8692
+ if not custom_button_details_by_id:
8693
+ return normalized_entries
8694
+ enriched_entries: list[dict[str, Any]] = []
8695
+ for item in normalized_entries:
8696
+ enriched = deepcopy(item)
8697
+ if enriched.get("button_type") == "CUSTOM":
8698
+ button_id = _coerce_positive_int(enriched.get("button_id"))
8699
+ detail = custom_button_details_by_id.get(button_id or -1)
8700
+ if isinstance(detail, dict):
8701
+ for key in (
8702
+ "button_text",
8703
+ "button_icon",
8704
+ "background_color",
8705
+ "text_color",
8706
+ "trigger_action",
8707
+ "trigger_link_url",
8708
+ ):
8709
+ value = detail.get(key)
8710
+ if value not in {None, ""}:
8711
+ enriched[key] = deepcopy(value)
8712
+ enriched_entries.append(enriched)
8713
+ return enriched_entries
8714
+
8715
+
8716
+ def _partition_view_button_summaries(
8717
+ items: list[dict[str, Any]],
8718
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
8719
+ system_buttons: list[dict[str, Any]] = []
8720
+ custom_buttons: list[dict[str, Any]] = []
8721
+ other_buttons: list[dict[str, Any]] = []
8722
+ for item in items:
8723
+ button_type = str(item.get("button_type") or "").strip().upper()
8724
+ if button_type == "SYSTEM":
8725
+ system_buttons.append(item)
8726
+ elif button_type == "CUSTOM":
8727
+ custom_buttons.append(item)
8728
+ else:
8729
+ other_buttons.append(item)
8730
+ return system_buttons, custom_buttons, other_buttons
8731
+
8732
+
8733
+ def _compare_view_button_summaries(
8734
+ *,
8735
+ expected: list[dict[str, Any]],
8736
+ actual: list[dict[str, Any]],
8737
+ ) -> dict[str, Any]:
8738
+ if actual == expected:
8739
+ return {
8740
+ "verified": True,
8741
+ "custom_button_readback_pending": False,
8742
+ "pending_custom_buttons": [],
8743
+ }
8744
+ expected_system, expected_custom, expected_other = _partition_view_button_summaries(expected)
8745
+ actual_system, actual_custom, actual_other = _partition_view_button_summaries(actual)
8746
+ custom_button_readback_pending = (
8747
+ bool(expected_custom)
8748
+ and not actual_custom
8749
+ and actual_system == expected_system
8750
+ and actual_other == expected_other
8751
+ )
8752
+ return {
8753
+ "verified": custom_button_readback_pending,
8754
+ "custom_button_readback_pending": custom_button_readback_pending,
8755
+ "pending_custom_buttons": deepcopy(expected_custom) if custom_button_readback_pending else [],
8756
+ }
8757
+
8758
+
8390
8759
  def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
8391
8760
  dto: dict[str, Any] = {}
8761
+ button_type = _normalize_view_button_type(entry.get("buttonType") or entry.get("button_type"))
8392
8762
  button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
8763
+ if button_type == "SYSTEM":
8764
+ button_id = _resolve_system_view_button_logical_id(entry)
8393
8765
  if button_id is not None:
8394
8766
  dto["buttonId"] = button_id
8395
- button_type = _normalize_view_button_type(entry.get("buttonType") or entry.get("button_type"))
8396
8767
  if button_type is not None:
8397
8768
  dto["buttonType"] = button_type
8398
8769
  config_type = _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type"))
@@ -8405,6 +8776,7 @@ def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, An
8405
8776
  dto["printTpls"] = _serialize_print_tpl_ids(entry.get("printTpls"))
8406
8777
  for source_key, target_key in (
8407
8778
  ("buttonText", "buttonText"),
8779
+ ("defaultButtonText", "defaultButtonText"),
8408
8780
  ("buttonIcon", "buttonIcon"),
8409
8781
  ("backgroundColor", "backgroundColor"),
8410
8782
  ("textColor", "textColor"),
@@ -8423,10 +8795,9 @@ def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, An
8423
8795
  def _extract_existing_view_button_dtos(config: dict[str, Any]) -> list[dict[str, Any]]:
8424
8796
  if not isinstance(config, dict):
8425
8797
  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)
8798
+ entries, source = _extract_view_button_entries(config)
8799
+ if source == "buttonConfigDTOList":
8800
+ return [deepcopy(item) for item in entries if isinstance(item, dict)]
8430
8801
  return [_serialize_existing_view_button_entry(entry) for entry in entries if isinstance(entry, dict)]
8431
8802
 
8432
8803
 
@@ -9171,6 +9542,22 @@ def _build_view_update_payload(
9171
9542
  )
9172
9543
 
9173
9544
 
9545
+ _KNOWN_SYSTEM_VIEW_COLUMNS = {
9546
+ "编号",
9547
+ "当前流程状态",
9548
+ "申请人",
9549
+ "申请时间",
9550
+ "更新时间",
9551
+ "流程标题",
9552
+ "当前处理人",
9553
+ "当前处理节点",
9554
+ }
9555
+
9556
+
9557
+ def _filter_known_system_view_columns(columns: list[str]) -> list[str]:
9558
+ return [name for name in columns if str(name or "").strip() and str(name).strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS]
9559
+
9560
+
9174
9561
  def _build_minimal_view_payload(
9175
9562
  *,
9176
9563
  app_key: str,
@@ -9299,7 +9686,7 @@ def _resolve_view_visible_field_names(patch: ViewUpsertPatch) -> list[str]:
9299
9686
  ordered: list[str] = []
9300
9687
  for value in [*patch.columns, patch.title_field, patch.start_field, patch.end_field, patch.group_by]:
9301
9688
  name = str(value or "").strip()
9302
- if name and name not in ordered:
9689
+ if name and name not in _KNOWN_SYSTEM_VIEW_COLUMNS and name not in ordered:
9303
9690
  ordered.append(name)
9304
9691
  return ordered
9305
9692