@qingflow-tech/qingflow-cli 1.1.50-beta.2 → 1.1.50-beta.4

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.
Files changed (37) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +7 -2
  4. package/skills/qingflow-cli/SKILL.md +74 -0
  5. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
  6. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +1 -0
  7. package/skills/qingflow-cli/reference/builder/50-views.md +180 -191
  8. package/skills/qingflow-cli/reference/builder/README.md +1 -1
  9. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
  10. package/skills/qingflow-cli/reference/examples/README.md +1 -1
  11. package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
  12. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
  13. package/src/qingflow_mcp/builder_facade/models.py +227 -17
  14. package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +42 -0
  15. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1245 -0
  16. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
  17. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
  18. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +73 -0
  19. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
  20. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
  21. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +319 -0
  22. package/src/qingflow_mcp/builder_facade/service.py +1460 -410
  23. package/src/qingflow_mcp/builder_facade/views_extract.py +592 -0
  24. package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
  25. package/src/qingflow_mcp/cli/commands/builder.py +72 -53
  26. package/src/qingflow_mcp/public_surface.py +3 -0
  27. package/src/qingflow_mcp/response_trim.py +11 -2
  28. package/src/qingflow_mcp/server_app_builder.py +32 -8
  29. package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
  30. package/src/qingflow_mcp/solution/executor.py +96 -8
  31. package/src/qingflow_mcp/solution/spec_models.py +1 -0
  32. package/src/qingflow_mcp/tools/ai_builder_tools.py +211 -393
  33. package/src/qingflow_mcp/tools/solution_tools.py +2 -0
  34. package/src/qingflow_mcp/tools/view_tools.py +0 -6
  35. package/src/qingflow_mcp/tools/view_writer.py +44 -0
  36. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
  37. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +0 -42
@@ -15,6 +15,8 @@ from typing import Any, NoReturn, cast
15
15
  from urllib.parse import quote_plus, unquote_plus
16
16
  from uuid import uuid4
17
17
 
18
+ from pydantic import ValidationError
19
+
18
20
  from ..backend_client import BackendRequestContext
19
21
  from ..config import get_flow_edit_maintenance_threshold
20
22
  from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error
@@ -26,7 +28,7 @@ from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
26
28
  from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
27
29
  from ..solution.compiler.icon_utils import encode_workspace_icon_with_defaults, workspace_icon_config
28
30
  from ..solution.compiler.view_compiler import VIEW_TYPE_MAP
29
- from ..solution.executor import _build_viewgraph_questions, _compact_dict, extract_field_map
31
+ from ..solution.executor import _build_viewgraph_questions, _compact_dict, _flatten_viewgraph_questions, extract_field_map
30
32
  from ..solution.spec_models import FieldType, FormLayoutRowSpec, FormLayoutSectionSpec, ViewSpec
31
33
  from ..tools.app_tools import AppTools
32
34
  from ..tools.custom_button_tools import CustomButtonTools
@@ -41,6 +43,8 @@ from ..tools.workflow_tools import WorkflowTools
41
43
  from . import flow_validation
42
44
  from . import workflow_spec as workflow_spec_api
43
45
  from .button_style_catalog import button_style_catalog_payload
46
+ from ..tools.view_writer import ViewWriter
47
+ from .views_schema import ViewsContractError, compact_none_values, views_schema_registry
44
48
  from .models import (
45
49
  AppChartsReadResponse,
46
50
  AppFieldsReadResponse,
@@ -290,6 +294,7 @@ class AiBuilderFacade:
290
294
  self.buttons = buttons
291
295
  self.packages = packages
292
296
  self.views = views
297
+ self._view_writer = ViewWriter(views.sessions, views.backend)
293
298
  self.workflows = workflows
294
299
  self.portals = portals
295
300
  self.charts = charts
@@ -3072,14 +3077,27 @@ class AiBuilderFacade:
3072
3077
  member_ids=visibility.external_selectors.member_ids,
3073
3078
  member_emails=visibility.external_selectors.member_emails,
3074
3079
  )
3075
- self._raise_visibility_resolution_issues(
3076
- [
3080
+ # 冗余引用降级:提取器对同一成员同时产出 uid/email/name 三通道(uid 必有且直通解析,
3081
+ # 保证成员不丢);email/name 走后端通讯录搜索,受数据/权限边界影响解析可能失败。
3082
+ # 这类失败降级为非阻断——只有 uid/dept_id 直通也覆盖不了的引用才真正阻断。
3083
+ _SOFT_RESOLUTION_KINDS = {"member_email", "member_name", "department_name", "role", "external_member_email"}
3084
+ _NOT_FOUND_RESOLUTION_CODES = {"MEMBER_NOT_FOUND", "AMBIGUOUS_MEMBER", "ROLE_NOT_FOUND", "AMBIGUOUS_ROLE", "DEPARTMENT_NOT_FOUND", "AMBIGUOUS_DEPARTMENT"}
3085
+ blocking_issues = [
3086
+ issue
3087
+ for issue in [
3077
3088
  *member_resolution["issues"],
3078
3089
  *role_resolution["issues"],
3079
3090
  *department_resolution["issues"],
3080
3091
  *external_member_resolution["issues"],
3081
3092
  ]
3082
- )
3093
+ # 冗余 email/name 引用「找不到」降级为非阻断(uid/id 直通已保主体不丢);
3094
+ # 权限拒绝/查询失败类(LOOKUP_FAILED/PERMISSION_DENIED)仍阻断
3095
+ if not (
3096
+ str(issue.get("kind") or "") in _SOFT_RESOLUTION_KINDS
3097
+ and str(issue.get("error_code") or "") in _NOT_FOUND_RESOLUTION_CODES
3098
+ )
3099
+ ]
3100
+ self._raise_visibility_resolution_issues(blocking_issues)
3083
3101
 
3084
3102
  external_depart_entries: list[dict[str, Any]] = []
3085
3103
  for dept_id in visibility.external_selectors.dept_ids:
@@ -3220,7 +3238,7 @@ class AiBuilderFacade:
3220
3238
  }
3221
3239
  )
3222
3240
  condition_matrix, condition_issues = _build_flow_condition_matrix(
3223
- current_fields_by_name=field_name_to_field,
3241
+ fields_by_que_id=field_name_to_field,
3224
3242
  node=normalized_node,
3225
3243
  )
3226
3244
  issues.extend({**issue, "node_id": node.get("id")} for issue in condition_issues)
@@ -8430,11 +8448,7 @@ class AiBuilderFacade:
8430
8448
  try:
8431
8449
  schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
8432
8450
  parsed_schema = _parse_schema(schema)
8433
- current_fields_by_name = {
8434
- str(field.get("name") or ""): field
8435
- for field in parsed_schema.get("fields") or []
8436
- if isinstance(field, dict) and str(field.get("name") or "")
8437
- }
8451
+ fields_by_que_id = _collect_fields_by_que_id([], schema=schema)
8438
8452
  existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
8439
8453
  except (QingflowApiError, RuntimeError) as error:
8440
8454
  api_error = _coerce_api_error(error)
@@ -8525,7 +8539,7 @@ class AiBuilderFacade:
8525
8539
  view_binding = _custom_button_view_binding_to_view_button_patch(binding=binding, button_id=button_id)
8526
8540
  dto, binding_issues = _serialize_view_button_binding(
8527
8541
  binding=view_binding,
8528
- current_fields_by_name=current_fields_by_name,
8542
+ fields_by_que_id=fields_by_que_id,
8529
8543
  valid_custom_button_ids=valid_custom_button_ids,
8530
8544
  allow_unverified_custom_button_id=allow_unverified_numeric_button_ids,
8531
8545
  )
@@ -9455,49 +9469,37 @@ class AiBuilderFacade:
9455
9469
  return fields_result
9456
9470
  current_fields = fields_result.get("fields", [])
9457
9471
  field_names = {str(field.get("name") or "") for field in current_fields}
9458
- current_fields_by_name = {
9459
- str(field.get("name") or ""): field
9460
- for field in current_fields
9461
- if isinstance(field, dict) and str(field.get("name") or "")
9462
- }
9472
+ fields_by_que_id = _collect_fields_by_que_id(current_fields)
9463
9473
  upsert_views = [public_view_upsert_payload(view) for view in request.upsert_views]
9464
9474
  patch_views = [public_view_partial_payload(patch) for patch in request.patch_views]
9465
9475
  if request.preset is not None:
9466
9476
  upsert_views = _build_views_preset(request.preset, list(field_names))
9467
9477
  blocking_issues: list[dict[str, Any]] = []
9468
9478
  for patch in upsert_views:
9469
- raw_columns = [str(name or "").strip() for name in (patch.get("columns") or []) if str(name or "").strip()]
9470
- columns = _filter_known_system_view_columns(raw_columns)
9471
- if patch.get("type") in {"table", "card"} and raw_columns and not columns:
9472
- blocking_issues.append(
9473
- {
9474
- "error_code": "VALIDATION_ERROR",
9475
- "view_name": patch.get("name"),
9476
- "message": "view columns must include at least one real app field; system columns cannot be applied directly",
9477
- "ignored_system_columns": [name for name in raw_columns if name in _KNOWN_SYSTEM_VIEW_COLUMNS],
9478
- }
9479
- )
9480
- missing_columns = [name for name in columns if name not in field_names]
9479
+ # queId 形态:columns 是 int 数组,系统列 queId<=0 天然排除
9480
+ raw_columns = [que for que in (patch.get("columns") or []) if isinstance(que, int) and que > 0]
9481
+ columns = raw_columns
9482
+ missing_columns = [que for que in columns if que not in fields_by_que_id]
9481
9483
  if missing_columns:
9482
9484
  blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_columns})
9483
- group_by = patch.get("group_by")
9484
- if group_by and group_by not in field_names:
9485
- blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": [group_by]})
9486
- start_field = str(patch.get("start_field") or "").strip()
9487
- end_field = str(patch.get("end_field") or "").strip()
9488
- title_field = str(patch.get("title_field") or "").strip()
9485
+ group_que_id = patch.get("group_que_id")
9486
+ if group_que_id and group_que_id not in fields_by_que_id:
9487
+ blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": [group_que_id]})
9488
+ start_que_id = patch.get("start_que_id")
9489
+ end_que_id = patch.get("end_que_id")
9490
+ title_que_id = patch.get("title_que_id")
9489
9491
  if patch.get("type") == "gantt":
9490
9492
  missing_required = []
9491
- if not start_field:
9492
- missing_required.append("start_field")
9493
- if not end_field:
9494
- missing_required.append("end_field")
9493
+ if not start_que_id:
9494
+ missing_required.append("start_que_id")
9495
+ if not end_que_id:
9496
+ missing_required.append("end_que_id")
9495
9497
  if missing_required:
9496
9498
  blocking_issues.append({"error_code": "INVALID_GANTT_CONFIG", "view_name": patch.get("name"), "missing_fields": missing_required})
9497
- missing_gantt_fields = [name for name in (start_field, end_field, title_field) if name and name not in field_names]
9499
+ missing_gantt_fields = [que for que in (start_que_id, end_que_id, title_que_id) if que and que not in fields_by_que_id]
9498
9500
  if missing_gantt_fields:
9499
9501
  blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_gantt_fields})
9500
- translated_filters, filter_issues = _build_view_filter_groups(current_fields_by_name=current_fields_by_name, filters=patch.get("filters") or [])
9502
+ translated_filters, filter_issues = _build_view_filter_groups(fields_by_que_id=fields_by_que_id, filters=patch.get("filters") or [])
9501
9503
  if filter_issues:
9502
9504
  blocking_issues.extend(
9503
9505
  {
@@ -9509,7 +9511,7 @@ class AiBuilderFacade:
9509
9511
  if translated_filters:
9510
9512
  patch["filters"] = [dict(rule) for rule in (patch.get("filters") or [])]
9511
9513
  _, _, query_condition_issues = _build_view_query_conditions_payload(
9512
- current_fields_by_name=current_fields_by_name,
9514
+ fields_by_que_id=fields_by_que_id,
9513
9515
  query_conditions=patch.get("query_conditions"),
9514
9516
  )
9515
9517
  if query_condition_issues:
@@ -11796,6 +11798,26 @@ class AiBuilderFacade:
11796
11798
  result["retry_payload"] = self._view_action_buttons_retry_payload(profile=profile, app_key=app_key, intents=retry_intents) if retry_intents else None
11797
11799
  return result
11798
11800
 
11801
+ def _apply_view_create(self, *, profile: str, payload: JSONObject) -> str | None:
11802
+ """创建一个视图并从后端响应中提取 view key。"""
11803
+ create_result = self._view_writer.create_view(profile=profile, payload=payload)
11804
+ raw_created = create_result.get("result")
11805
+ if isinstance(raw_created, dict):
11806
+ raw_view_key = raw_created.get("viewgraphKey") or raw_created.get("viewKey")
11807
+ return str(raw_view_key).strip() if raw_view_key is not None and str(raw_view_key).strip() else None
11808
+ if isinstance(raw_created, str):
11809
+ return raw_created.strip() or None
11810
+ return None
11811
+
11812
+ def _apply_view_update(self, *, profile: str, view_key: str, payload: JSONObject) -> None:
11813
+ """更新一个已有视图。"""
11814
+ self.views.view_update(profile=profile, viewgraph_key=view_key, payload=payload)
11815
+
11816
+ def _apply_view_remove(self, *, profile: str, view_key: str) -> JSONObject:
11817
+ """删除一个视图并执行单 view key 读回验证。"""
11818
+ self.views.view_delete(profile=profile, viewgraph_key=view_key)
11819
+ return self._verify_view_deleted_by_key(profile=profile, view_key=view_key)
11820
+
11799
11821
  def app_views_apply(
11800
11822
  self,
11801
11823
  *,
@@ -11805,15 +11827,91 @@ class AiBuilderFacade:
11805
11827
  patch_views: list[ViewPartialPatch] | None = None,
11806
11828
  remove_views: list[str],
11807
11829
  publish: bool = True,
11830
+ schema_version: str | None = None,
11808
11831
  ) -> JSONObject:
11809
11832
  patch_views = patch_views or []
11833
+ operation_values = (
11834
+ ("upsert", "upsert_views", upsert_views, "VIEW_BATCH_CREATE_UNSUPPORTED"),
11835
+ ("patch", "patch_views", patch_views, "VIEW_BATCH_OPERATION_UNSUPPORTED"),
11836
+ ("remove", "remove_views", remove_views, "VIEW_BATCH_OPERATION_UNSUPPORTED"),
11837
+ )
11838
+ selected_operation_types = [operation for operation, _field_name, values, _error_code in operation_values if values]
11839
+ selected_operations = [field_name for _operation, field_name, values, _error_code in operation_values if values]
11810
11840
  normalized_args = {
11811
11841
  "app_key": app_key,
11812
- "upsert_views": [public_view_upsert_payload(patch) for patch in upsert_views],
11842
+ "upsert_view": public_view_upsert_payload(upsert_views[0]) if upsert_views else None,
11813
11843
  "patch_views": [public_view_partial_payload(patch) for patch in patch_views],
11814
11844
  "remove_views": list(remove_views),
11815
11845
  "publish": publish,
11846
+ "schema_version": schema_version,
11816
11847
  }
11848
+ if len(selected_operations) > 1:
11849
+ return _failed(
11850
+ "VIEW_SINGLE_OPERATION_UNSUPPORTED",
11851
+ "app_views_apply supports one view operation per call; choose upsert_view, patch_views, or remove_views",
11852
+ normalized_args=normalized_args,
11853
+ details={"app_key": app_key, "operations": selected_operations},
11854
+ suggested_next_call=None,
11855
+ )
11856
+ operation = selected_operation_types[0] if selected_operation_types else None
11857
+ for operation_name, _field_name, values, error_code in operation_values:
11858
+ if len(values) <= 1:
11859
+ continue
11860
+ first_value = values[0]
11861
+ if operation_name == "upsert":
11862
+ suggested_value: Any = public_view_upsert_payload(first_value)
11863
+ elif operation_name == "patch":
11864
+ suggested_value = [public_view_partial_payload(first_value)]
11865
+ else:
11866
+ suggested_value = [str(first_value)]
11867
+ argument_name = "upsert_view" if operation_name == "upsert" else f"{operation_name}_views"
11868
+ suggested_arguments: JSONObject = {
11869
+ "profile": profile,
11870
+ "app_key": app_key,
11871
+ argument_name: suggested_value,
11872
+ "publish": publish,
11873
+ }
11874
+ if schema_version is not None:
11875
+ suggested_arguments["schema_version"] = schema_version
11876
+ return _failed(
11877
+ error_code,
11878
+ (
11879
+ "app_views_apply supports creating or fully updating one view per call"
11880
+ if operation_name == "upsert"
11881
+ else f"app_views_apply supports at most one {operation_name} per call"
11882
+ ),
11883
+ normalized_args=normalized_args,
11884
+ details={
11885
+ "app_key": app_key,
11886
+ f"max_{operation_name}_views": 1,
11887
+ f"received_{operation_name}_views": len(values),
11888
+ },
11889
+ suggested_next_call={
11890
+ "tool_name": "app_views_apply",
11891
+ "arguments": suggested_arguments,
11892
+ },
11893
+ )
11894
+ schema_context = None
11895
+ if schema_version is not None:
11896
+ try:
11897
+ schema_context = views_schema_registry().resolve_schema_version(schema_version)
11898
+ for upsert in upsert_views:
11899
+ schema_context.validate_upsert(
11900
+ compact_none_values(public_view_upsert_payload(upsert))
11901
+ )
11902
+ except ViewsContractError as error:
11903
+ return _failed(
11904
+ error.error_code,
11905
+ str(error.message),
11906
+ normalized_args=normalized_args,
11907
+ details={
11908
+ "app_key": app_key,
11909
+ "schema_version": schema_version,
11910
+ "reason_path": error.path or "",
11911
+ **(error.details or {}),
11912
+ },
11913
+ suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_version": schema_version, "schema_kind": "complete"}},
11914
+ )
11817
11915
  apply_started_at = time.perf_counter()
11818
11916
  permission_ms = 0
11819
11917
  view_state_read_ms = 0
@@ -11828,6 +11926,8 @@ class AiBuilderFacade:
11828
11926
  permission_outcomes: list[PermissionCheckOutcome] = []
11829
11927
 
11830
11928
  def finalize(response: JSONObject) -> JSONObject:
11929
+ if schema_context is not None:
11930
+ response["schemaVersion"] = schema_context.schema_version
11831
11931
  return _apply_permission_outcomes(response, *permission_outcomes)
11832
11932
 
11833
11933
  def attach_duration(response: JSONObject) -> JSONObject:
@@ -12044,7 +12144,25 @@ class AiBuilderFacade:
12044
12144
  return finish(create_permission_outcome.block)
12045
12145
  permission_outcomes.append(create_permission_outcome)
12046
12146
  parsed_schema = _parse_schema(schema)
12047
- field_names = {field["name"] for field in parsed_schema["fields"]}
12147
+
12148
+ def _register_subfield_names(field: dict[str, Any], index: dict[str, dict[str, Any]]) -> None:
12149
+ # 表格字段(queType 18)的子字段是视图列/筛选/排序的合法引用面(viewgraph_question 有独立行);
12150
+ # _parse_schema 只把顶层字段入列,这里递归并入引用索引(不改 fields 布局面)
12151
+ for sub in field.get("subfields") or []:
12152
+ if isinstance(sub, dict) and str(sub.get("name") or "").strip():
12153
+ index.setdefault(str(sub["name"]), sub)
12154
+ _register_subfield_names(sub, index)
12155
+
12156
+ field_name_to_field: dict[str, dict[str, Any]] = {}
12157
+ for field in parsed_schema["fields"]:
12158
+ if isinstance(field, dict) and str(field.get("name") or "").strip():
12159
+ field_name_to_field[str(field["name"])] = field
12160
+ _register_subfield_names(field, field_name_to_field)
12161
+ field_names = set(field_name_to_field)
12162
+ # views 契约全 queId 化后,列/筛选/排序/配置引用的是 queId(int);
12163
+ # fields_by_que_id 必须以 que_id 为键,与 _build_view_filter_groups 等消费方对齐。
12164
+ # 递归收集(顶层+表格子字段;段落内字段经 _parse_schema 已展平到 fields)。
12165
+ fields_by_que_id = _collect_fields_by_que_id([], schema=schema)
12048
12166
  if patch_views:
12049
12167
  expanded_views, patch_issues, patch_results = self._expand_view_partial_patches(
12050
12168
  profile=profile,
@@ -12064,14 +12182,18 @@ class AiBuilderFacade:
12064
12182
  suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": patch_issues[0].get("view_key") or "VIEW_KEY"}},
12065
12183
  )
12066
12184
  )
12067
- upsert_views = [*upsert_views, *expanded_views]
12068
- normalized_args["upsert_views"] = [public_view_upsert_payload(patch) for patch in upsert_views]
12185
+ if len(expanded_views) > 1:
12186
+ return finalize(_failed(
12187
+ "VIEW_BATCH_OPERATION_UNSUPPORTED",
12188
+ "app_views_apply supports hydrating one patch view per call",
12189
+ normalized_args=normalized_args,
12190
+ details={"app_key": app_key, "received_patch_views": len(expanded_views)},
12191
+ suggested_next_call=None,
12192
+ ))
12193
+ upsert_views = expanded_views
12194
+ operation = "upsert" if expanded_views else "patch"
12195
+ normalized_args["upsert_view"] = public_view_upsert_payload(expanded_views[0]) if expanded_views else None
12069
12196
  normalized_args["patch_results"] = patch_results
12070
- current_fields_by_name = {
12071
- str(field.get("name") or ""): field
12072
- for field in parsed_schema["fields"]
12073
- if isinstance(field, dict) and str(field.get("name") or "")
12074
- }
12075
12197
  requires_custom_button_validation = any(
12076
12198
  any(binding.button_type == PublicViewButtonType.custom for binding in (patch.buttons or []))
12077
12199
  for patch in upsert_views
@@ -12167,8 +12289,8 @@ class AiBuilderFacade:
12167
12289
  associated_resource_inventory_ms += _duration_ms(associated_resource_inventory_started_at)
12168
12290
  removed: list[str] = []
12169
12291
  removed_keys: set[str] = set()
12170
- view_results: list[dict[str, Any]] = []
12171
- failed_views: list[dict[str, Any]] = []
12292
+ view_result: dict[str, Any] | None = None
12293
+ failure_result: dict[str, Any] | None = None
12172
12294
  action_button_intents: list[dict[str, Any]] = list(patch_action_button_intents)
12173
12295
 
12174
12296
  def record_action_button_intent(*, patch: ViewUpsertPatch, view_key: str | None, index: int) -> None:
@@ -12186,10 +12308,17 @@ class AiBuilderFacade:
12186
12308
  )
12187
12309
 
12188
12310
  view_mutation_started_at = time.perf_counter()
12189
- for selector in remove_views:
12190
- selector_text = str(selector or "").strip()
12311
+ if operation == "remove" and remove_views:
12312
+ selector_text = str(remove_views[0] or "").strip()
12191
12313
  if not selector_text:
12192
- continue
12314
+ view_mutation_ms += _duration_ms(view_mutation_started_at)
12315
+ return finish(_failed(
12316
+ "VIEW_NOT_FOUND",
12317
+ "remove_views item did not match an existing view name or view_key",
12318
+ normalized_args=normalized_args,
12319
+ details={"app_key": app_key},
12320
+ suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
12321
+ ))
12193
12322
  key_match = existing_by_key.get(selector_text)
12194
12323
  matches = [key_match] if isinstance(key_match, dict) else existing_by_name.get(selector_text, [])
12195
12324
  if len(matches) > 1:
@@ -12216,44 +12345,40 @@ class AiBuilderFacade:
12216
12345
  "error_code": "VIEW_NOT_FOUND",
12217
12346
  "message": "remove_views item did not match an existing view name or view_key",
12218
12347
  }
12219
- failed_views.append(failed_view)
12220
- view_results.append(deepcopy(failed_view))
12221
- continue
12222
- if len(matches) == 1:
12348
+ failure_result = failed_view
12349
+ view_result = deepcopy(failed_view)
12350
+ else:
12223
12351
  key = _extract_view_key(matches[0])
12224
12352
  removed_name = _extract_view_name(matches[0]) or selector_text
12225
12353
  try:
12226
- self.views.view_delete(profile=profile, viewgraph_key=key)
12227
- delete_readback = self._verify_view_deleted_by_key(profile=profile, view_key=key)
12354
+ delete_readback = self._apply_view_remove(profile=profile, view_key=key)
12228
12355
  removed.append(removed_name)
12229
12356
  if key:
12230
12357
  removed_keys.add(key)
12231
12358
  existing_by_key.pop(key, None)
12232
12359
  existing_by_name.pop(removed_name, None)
12233
- view_results.append(
12234
- {
12235
- "name": removed_name,
12236
- "view_key": key,
12237
- "type": None,
12238
- "status": delete_readback.get("status") or "readback_pending",
12239
- "operation": "delete",
12240
- "delete_executed": True,
12241
- "readback_status": delete_readback.get("readback_status"),
12242
- "safe_to_retry_delete": False,
12243
- **(
12244
- {
12245
- "error_code": delete_readback.get("error_code"),
12246
- "message": delete_readback.get("message"),
12247
- "request_id": delete_readback.get("request_id"),
12248
- "backend_code": delete_readback.get("backend_code"),
12249
- "http_status": delete_readback.get("http_status"),
12250
- "transport_error": delete_readback.get("transport_error"),
12251
- }
12252
- if delete_readback.get("readback_status") != "deleted"
12253
- else {}
12254
- ),
12255
- }
12256
- )
12360
+ view_result = {
12361
+ "name": removed_name,
12362
+ "view_key": key,
12363
+ "type": None,
12364
+ "status": delete_readback.get("status") or "readback_pending",
12365
+ "operation": "delete",
12366
+ "delete_executed": True,
12367
+ "readback_status": delete_readback.get("readback_status"),
12368
+ "safe_to_retry_delete": False,
12369
+ **(
12370
+ {
12371
+ "error_code": delete_readback.get("error_code"),
12372
+ "message": delete_readback.get("message"),
12373
+ "request_id": delete_readback.get("request_id"),
12374
+ "backend_code": delete_readback.get("backend_code"),
12375
+ "http_status": delete_readback.get("http_status"),
12376
+ "transport_error": delete_readback.get("transport_error"),
12377
+ }
12378
+ if delete_readback.get("readback_status") != "deleted"
12379
+ else {}
12380
+ ),
12381
+ }
12257
12382
  except (QingflowApiError, RuntimeError) as error:
12258
12383
  api_error = _coerce_api_error(error)
12259
12384
  failed_view = {
@@ -12270,8 +12395,8 @@ class AiBuilderFacade:
12270
12395
  "backend_code": api_error.backend_code,
12271
12396
  "http_status": None if api_error.http_status == 404 else api_error.http_status,
12272
12397
  }
12273
- failed_views.append(failed_view)
12274
- view_results.append(deepcopy(failed_view))
12398
+ failure_result = failed_view
12399
+ view_result = deepcopy(failed_view)
12275
12400
  created: list[str] = []
12276
12401
  updated: list[str] = []
12277
12402
  existing_view_list = [
@@ -12281,25 +12406,19 @@ class AiBuilderFacade:
12281
12406
  and _extract_view_name(view) not in removed
12282
12407
  and _extract_view_key(view) not in removed_keys
12283
12408
  ]
12284
- for ordinal, patch in enumerate(upsert_views, start=1):
12285
- apply_columns = _resolve_view_visible_field_names(patch)
12286
- ignored_system_columns = [
12287
- name
12288
- for name in [str(value or "").strip() for value in patch.columns]
12289
- if name in _KNOWN_SYSTEM_VIEW_COLUMNS
12290
- ]
12409
+ for ordinal, patch in ((1, upsert_views[0]),) if operation == "upsert" and upsert_views else ():
12410
+ apply_columns = _resolve_view_visible_que_ids(patch)
12291
12411
  if patch.type in {PublicViewType.table, PublicViewType.card} and patch.columns and not apply_columns:
12292
12412
  return _failed(
12293
12413
  "VALIDATION_ERROR",
12294
12414
  "view columns must include at least one real app field; system columns cannot be applied directly",
12295
12415
  normalized_args=normalized_args,
12296
- details={
12297
- "app_key": app_key,
12298
- "view_name": patch.name,
12299
- "ignored_system_columns": ignored_system_columns,
12300
- },
12416
+ details={"app_key": app_key, "view_name": patch.name},
12301
12417
  )
12302
- missing_columns = [name for name in apply_columns if name not in field_names]
12418
+ missing_columns = [
12419
+ que for que in apply_columns
12420
+ if not _is_known_system_view_que_id(que) and que not in fields_by_que_id
12421
+ ]
12303
12422
  if missing_columns:
12304
12423
  return _failed(
12305
12424
  "UNKNOWN_VIEW_FIELD",
@@ -12309,45 +12428,94 @@ class AiBuilderFacade:
12309
12428
  "app_key": app_key,
12310
12429
  "view_name": patch.name,
12311
12430
  "missing_fields": missing_columns,
12312
- "ignored_system_columns": ignored_system_columns,
12313
12431
  },
12314
12432
  missing_fields=missing_columns,
12315
12433
  suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
12316
12434
  )
12317
- if patch.group_by and patch.group_by not in field_names:
12435
+ if patch.group_que_id and patch.group_que_id not in fields_by_que_id:
12318
12436
  return _failed(
12319
12437
  "UNKNOWN_VIEW_FIELD",
12320
- f"group_by references unknown field '{patch.group_by}'",
12438
+ f"group_by references unknown field '{patch.group_que_id}'",
12321
12439
  normalized_args=normalized_args,
12322
12440
  details={
12323
12441
  "app_key": app_key,
12324
12442
  "view_name": patch.name,
12325
- "missing_fields": [patch.group_by],
12443
+ "missing_fields": [patch.group_que_id],
12326
12444
  },
12327
- missing_fields=[patch.group_by],
12445
+ missing_fields=[patch.group_que_id],
12328
12446
  suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
12329
12447
  )
12330
- for gantt_field_name in (patch.start_field, patch.end_field, patch.title_field):
12331
- if gantt_field_name and gantt_field_name not in field_names:
12448
+ for gantt_que_id in (patch.start_que_id, patch.end_que_id, patch.title_que_id):
12449
+ if gantt_que_id and gantt_que_id not in fields_by_que_id:
12332
12450
  return _failed(
12333
12451
  "UNKNOWN_VIEW_FIELD",
12334
- f"gantt configuration references unknown field '{gantt_field_name}'",
12452
+ f"gantt configuration references unknown field '{gantt_que_id}'",
12335
12453
  normalized_args=normalized_args,
12336
12454
  details={
12337
12455
  "app_key": app_key,
12338
12456
  "view_name": patch.name,
12339
- "missing_fields": [gantt_field_name],
12457
+ "missing_fields": [gantt_que_id],
12340
12458
  },
12341
- missing_fields=[gantt_field_name],
12459
+ missing_fields=[gantt_que_id],
12342
12460
  suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
12343
12461
  )
12462
+ for group_name, config_que_ids in _iter_view_config_group_field_names(patch):
12463
+ calendar_system_date = (
12464
+ {patch.calendar.date_que_id}
12465
+ if group_name == "calendar" and patch.calendar is not None and patch.calendar.date_que_id in {2, 3}
12466
+ else set()
12467
+ )
12468
+ missing_config_fields = [que for que in config_que_ids if que not in fields_by_que_id and que not in calendar_system_date]
12469
+ if missing_config_fields:
12470
+ return _failed(
12471
+ "UNKNOWN_VIEW_FIELD",
12472
+ f"view {group_name} configuration references unknown fields",
12473
+ normalized_args=normalized_args,
12474
+ details={
12475
+ "app_key": app_key,
12476
+ "view_name": patch.name,
12477
+ "missing_fields": missing_config_fields,
12478
+ "reason_path": f"{group_name}",
12479
+ },
12480
+ missing_fields=missing_config_fields,
12481
+ suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
12482
+ )
12483
+ hierarchy_root_filter_groups: list[list[dict[str, Any]]] | None = None
12484
+ if patch.hierarchy is not None and patch.hierarchy.root_filter:
12485
+ hierarchy_root_filter_groups = []
12486
+ for root_filter_group in patch.hierarchy.root_filter:
12487
+ translated_group, hierarchy_filter_issues = _build_view_filter_groups(
12488
+ fields_by_que_id=fields_by_que_id,
12489
+ filters=root_filter_group,
12490
+ )
12491
+ if hierarchy_filter_issues:
12492
+ first_issue = hierarchy_filter_issues[0]
12493
+ return _failed(
12494
+ str(first_issue.get("error_code") or "UNKNOWN_VIEW_FIELD"),
12495
+ "view hierarchy root_filter references invalid fields or values",
12496
+ normalized_args=normalized_args,
12497
+ details={
12498
+ "app_key": app_key,
12499
+ "view_name": patch.name,
12500
+ **first_issue,
12501
+ },
12502
+ missing_fields=list(first_issue.get("missing_fields") or []),
12503
+ suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
12504
+ )
12505
+ hierarchy_root_filter_groups.extend(translated_group)
12344
12506
  translated_filters: list[list[dict[str, Any]]] | None
12345
12507
  filter_issues: list[dict[str, Any]]
12346
12508
  if patch.preserve_filters:
12347
12509
  translated_filters = None
12348
12510
  filter_issues = []
12511
+ elif patch.filters and isinstance(patch.filters[0], list):
12512
+ # 嵌套形态 [[rule,...],[rule,...]]:组间 OR、组内 AND(viewgraphLimit 多组),
12513
+ # 复用 button_limit 的多组编译;后端简易模式会从多组规则自动派生公式
12514
+ translated_filters, filter_issues = _build_view_button_limit_groups(
12515
+ fields_by_que_id=fields_by_que_id, groups=patch.filters
12516
+ )
12349
12517
  else:
12350
- translated_filters, filter_issues = _build_view_filter_groups(current_fields_by_name=current_fields_by_name, filters=patch.filters)
12518
+ translated_filters, filter_issues = _build_view_filter_groups(fields_by_que_id=fields_by_que_id, filters=patch.filters)
12351
12519
  if filter_issues:
12352
12520
  first_issue = filter_issues[0]
12353
12521
  return _failed(
@@ -12369,7 +12537,7 @@ class AiBuilderFacade:
12369
12537
  query_condition_issues = []
12370
12538
  else:
12371
12539
  query_condition_payload, expected_query_conditions, query_condition_issues = _build_view_query_conditions_payload(
12372
- current_fields_by_name=current_fields_by_name,
12540
+ fields_by_que_id=fields_by_que_id,
12373
12541
  query_conditions=patch.query_conditions,
12374
12542
  )
12375
12543
  if query_condition_issues:
@@ -12426,7 +12594,7 @@ class AiBuilderFacade:
12426
12594
  expected_button_summary = None
12427
12595
  elif patch.buttons is not None:
12428
12596
  explicit_button_dtos, button_issues = _build_view_button_dtos(
12429
- current_fields_by_name=current_fields_by_name,
12597
+ fields_by_que_id=fields_by_que_id,
12430
12598
  bindings=patch.buttons,
12431
12599
  valid_custom_button_ids=valid_custom_button_ids,
12432
12600
  )
@@ -12484,6 +12652,10 @@ class AiBuilderFacade:
12484
12652
  existing_key = _extract_view_key(matched_existing_view)
12485
12653
  created_key: str | None = None
12486
12654
  system_view_list_type = _resolve_system_view_list_type(view_key=existing_key, view_name=patch.name) if existing_key else None
12655
+ if system_view_list_type is not None and os.environ.get("QINGFLOW_VIEWS_APPLY_SKIP_SYSTEM_VIEW_SYNC", "").strip().lower() in {"1", "true", "yes", "on"}:
12656
+ # 环境受限开关:默认视图列序同步依赖 app apply/baseInfo 回读,在快照/脱敏环境上该读面可能不可用;
12657
+ # 视图写主体(view_update)不受影响,仅跳过同步与尾部校验。
12658
+ system_view_list_type = None
12487
12659
  operation_phase = "view_update" if existing_key else "view_create"
12488
12660
  query_condition_payload_for_apply = deepcopy(query_condition_payload)
12489
12661
  expected_query_conditions_for_verify = deepcopy(expected_query_conditions)
@@ -12517,16 +12689,17 @@ class AiBuilderFacade:
12517
12689
  views=self.views,
12518
12690
  profile=profile,
12519
12691
  source_viewgraph_key=existing_key,
12692
+ hierarchy_filter_groups=hierarchy_root_filter_groups,
12520
12693
  schema=schema,
12521
12694
  patch=patch,
12522
12695
  view_filters=translated_filters,
12523
- current_fields_by_name=current_fields_by_name,
12696
+ fields_by_que_id=fields_by_que_id,
12524
12697
  auth_override=view_auth_override,
12525
12698
  explicit_button_dtos=explicit_button_dtos,
12526
12699
  query_condition_payload=query_condition_payload_for_apply,
12527
12700
  associated_resources_payload=associated_resources_payload_for_apply,
12528
12701
  )
12529
- self.views.view_update(profile=profile, viewgraph_key=existing_key, payload=payload)
12702
+ self._apply_view_update(profile=profile, view_key=existing_key, payload=payload)
12530
12703
  system_view_sync: dict[str, Any] | None = None
12531
12704
  if system_view_list_type is not None and patch.type.value == "table":
12532
12705
  operation_phase = "default_view_apply_config_sync"
@@ -12537,7 +12710,9 @@ class AiBuilderFacade:
12537
12710
  payload=payload,
12538
12711
  list_type=system_view_list_type,
12539
12712
  schema=schema,
12540
- visible_field_names=apply_columns,
12713
+ visible_field_names=[
12714
+ str((fields_by_que_id.get(que) or {}).get("name") or "") for que in apply_columns if que in fields_by_que_id
12715
+ ],
12541
12716
  )
12542
12717
  if not bool(system_view_sync.get("verified")):
12543
12718
  failure_entry = {
@@ -12562,24 +12737,23 @@ class AiBuilderFacade:
12562
12737
  "apply_columns": apply_columns,
12563
12738
  },
12564
12739
  }
12565
- failed_views.append(failure_entry)
12566
- view_results.append(failure_entry)
12740
+ failure_result = failure_entry
12741
+ view_result = failure_entry
12567
12742
  continue
12568
12743
  updated.append(patch.name)
12569
- view_results.append(
12570
- {
12571
- "name": patch.name,
12572
- "view_key": existing_key,
12573
- "type": patch.type.value,
12574
- "status": "updated",
12575
- "expected_filters": deepcopy(translated_filters),
12576
- "expected_buttons": deepcopy(expected_button_summary),
12577
- "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12578
- "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12579
- "system_view_sync": system_view_sync,
12580
- "apply_columns": deepcopy(apply_columns),
12581
- }
12582
- )
12744
+ view_result = {
12745
+ "name": patch.name,
12746
+ "view_key": existing_key,
12747
+ "type": patch.type.value,
12748
+ "status": "updated",
12749
+ "expected_filters": deepcopy(translated_filters),
12750
+ "expected_buttons": deepcopy(expected_button_summary),
12751
+ "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12752
+ "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12753
+ "expected_pinned": patch.pinned,
12754
+ "system_view_sync": system_view_sync,
12755
+ "apply_columns": deepcopy(apply_columns),
12756
+ }
12583
12757
  record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
12584
12758
  else:
12585
12759
  template_key = _pick_view_template_key(existing_view_list, desired_type=patch.type.value)
@@ -12591,50 +12765,45 @@ class AiBuilderFacade:
12591
12765
  views=self.views,
12592
12766
  profile=profile,
12593
12767
  source_viewgraph_key=created_key,
12768
+ hierarchy_filter_groups=hierarchy_root_filter_groups,
12594
12769
  schema=schema,
12595
12770
  patch=patch,
12596
12771
  view_filters=translated_filters,
12597
- current_fields_by_name=current_fields_by_name,
12772
+ fields_by_que_id=fields_by_que_id,
12598
12773
  auth_override=view_auth_override,
12599
12774
  explicit_button_dtos=explicit_button_dtos,
12600
12775
  query_condition_payload=query_condition_payload_for_apply,
12601
12776
  associated_resources_payload=associated_resources_payload_for_apply,
12602
12777
  )
12603
- self.views.view_update(profile=profile, viewgraph_key=created_key, payload=payload)
12778
+ self._apply_view_update(profile=profile, view_key=created_key, payload=payload)
12604
12779
  else:
12605
- payload = _build_view_create_payload(
12780
+ payload = _build_view_creation_payload(
12606
12781
  app_key=app_key,
12607
12782
  base_info=base,
12608
12783
  schema=schema,
12609
12784
  patch=patch,
12610
12785
  ordinal=ordinal,
12611
12786
  view_filters=translated_filters,
12612
- current_fields_by_name=current_fields_by_name,
12787
+ fields_by_que_id=fields_by_que_id,
12613
12788
  auth_override=view_auth_override,
12614
12789
  explicit_button_dtos=explicit_button_dtos,
12615
12790
  query_condition_payload=query_condition_payload_for_apply,
12616
12791
  associated_resources_payload=associated_resources_payload_for_apply,
12792
+ hierarchy_filter_groups=hierarchy_root_filter_groups,
12617
12793
  )
12618
- create_result = self.views.view_create(profile=profile, payload=payload)
12619
- raw_created = create_result.get("result")
12620
- if isinstance(raw_created, dict):
12621
- raw_view_key = raw_created.get("viewgraphKey") or raw_created.get("viewKey")
12622
- created_key = str(raw_view_key).strip() if raw_view_key is not None and str(raw_view_key).strip() else None
12623
- elif isinstance(raw_created, str):
12624
- created_key = raw_created.strip() or None
12794
+ created_key = self._apply_view_create(profile=profile, payload=payload)
12625
12795
  created.append(patch.name)
12626
- view_results.append(
12627
- {
12628
- "name": patch.name,
12629
- "view_key": created_key,
12630
- "type": patch.type.value,
12631
- "status": "created",
12632
- "expected_filters": deepcopy(translated_filters),
12633
- "expected_buttons": deepcopy(expected_button_summary),
12634
- "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12635
- "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12636
- }
12637
- )
12796
+ view_result = {
12797
+ "name": patch.name,
12798
+ "view_key": created_key,
12799
+ "type": patch.type.value,
12800
+ "status": "created",
12801
+ "expected_filters": deepcopy(translated_filters),
12802
+ "expected_buttons": deepcopy(expected_button_summary),
12803
+ "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12804
+ "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12805
+ "expected_pinned": patch.pinned,
12806
+ }
12638
12807
  record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
12639
12808
  except (QingflowApiError, RuntimeError) as error:
12640
12809
  api_error = _coerce_api_error(error)
@@ -12661,17 +12830,20 @@ class AiBuilderFacade:
12661
12830
  patch=patch,
12662
12831
  ordinal=ordinal,
12663
12832
  view_filters=translated_filters,
12664
- current_fields_by_name=current_fields_by_name,
12833
+ hierarchy_filter_groups=hierarchy_root_filter_groups,
12834
+ fields_by_que_id=fields_by_que_id,
12665
12835
  auth_override=view_auth_override,
12666
12836
  explicit_button_dtos=fallback_button_dtos,
12667
12837
  query_condition_payload=query_condition_payload_for_apply,
12668
12838
  associated_resources_payload=associated_resources_payload_for_apply,
12669
12839
  )
12670
- self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
12840
+ self._apply_view_update(profile=profile, view_key=target_key, payload=fallback_payload)
12671
12841
  system_view_sync: dict[str, Any] | None = None
12672
12842
  fallback_system_view_list_type = (
12673
12843
  _resolve_system_view_list_type(view_key=target_key, view_name=patch.name)
12674
- if patch.type.value == "table" and target_key
12844
+ if patch.type.value == "table"
12845
+ and target_key
12846
+ and os.environ.get("QINGFLOW_VIEWS_APPLY_SKIP_SYSTEM_VIEW_SYNC", "").strip().lower() not in {"1", "true", "yes", "on"}
12675
12847
  else None
12676
12848
  )
12677
12849
  if fallback_system_view_list_type is not None:
@@ -12683,7 +12855,9 @@ class AiBuilderFacade:
12683
12855
  payload=fallback_payload,
12684
12856
  list_type=fallback_system_view_list_type,
12685
12857
  schema=schema,
12686
- visible_field_names=apply_columns,
12858
+ visible_field_names=[
12859
+ str((fields_by_que_id.get(que) or {}).get("name") or "") for que in apply_columns if que in fields_by_que_id
12860
+ ],
12687
12861
  )
12688
12862
  if not bool(system_view_sync.get("verified")):
12689
12863
  failure_entry = {
@@ -12708,44 +12882,42 @@ class AiBuilderFacade:
12708
12882
  "apply_columns": apply_columns,
12709
12883
  },
12710
12884
  }
12711
- failed_views.append(failure_entry)
12712
- view_results.append(failure_entry)
12885
+ failure_result = failure_entry
12886
+ view_result = failure_entry
12713
12887
  continue
12714
12888
  if existing_key:
12715
12889
  updated.append(patch.name)
12716
- view_results.append(
12717
- {
12718
- "name": patch.name,
12719
- "view_key": existing_key,
12720
- "type": patch.type.value,
12721
- "status": "updated",
12722
- "fallback_applied": True,
12723
- "expected_filters": deepcopy(translated_filters),
12724
- "expected_buttons": deepcopy(expected_button_summary),
12725
- "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12726
- "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12727
- "system_view_sync": system_view_sync,
12728
- "apply_columns": deepcopy(apply_columns),
12729
- }
12730
- )
12890
+ view_result = {
12891
+ "name": patch.name,
12892
+ "view_key": existing_key,
12893
+ "type": patch.type.value,
12894
+ "status": "updated",
12895
+ "fallback_applied": True,
12896
+ "expected_filters": deepcopy(translated_filters),
12897
+ "expected_buttons": deepcopy(expected_button_summary),
12898
+ "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12899
+ "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12900
+ "expected_pinned": patch.pinned,
12901
+ "system_view_sync": system_view_sync,
12902
+ "apply_columns": deepcopy(apply_columns),
12903
+ }
12731
12904
  record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
12732
12905
  else:
12733
12906
  created.append(patch.name)
12734
- view_results.append(
12735
- {
12736
- "name": patch.name,
12737
- "view_key": created_key,
12738
- "type": patch.type.value,
12739
- "status": "created",
12740
- "fallback_applied": True,
12741
- "expected_filters": deepcopy(translated_filters),
12742
- "expected_buttons": deepcopy(expected_button_summary),
12743
- "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12744
- "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12745
- "system_view_sync": system_view_sync,
12746
- "apply_columns": deepcopy(apply_columns),
12747
- }
12748
- )
12907
+ view_result = {
12908
+ "name": patch.name,
12909
+ "view_key": created_key,
12910
+ "type": patch.type.value,
12911
+ "status": "created",
12912
+ "fallback_applied": True,
12913
+ "expected_filters": deepcopy(translated_filters),
12914
+ "expected_buttons": deepcopy(expected_button_summary),
12915
+ "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12916
+ "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12917
+ "expected_pinned": patch.pinned,
12918
+ "system_view_sync": system_view_sync,
12919
+ "apply_columns": deepcopy(apply_columns),
12920
+ }
12749
12921
  record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
12750
12922
  continue
12751
12923
  fallback_payload = _build_minimal_view_payload(
@@ -12754,16 +12926,16 @@ class AiBuilderFacade:
12754
12926
  patch=patch,
12755
12927
  ordinal=ordinal,
12756
12928
  view_filters=translated_filters,
12757
- current_fields_by_name=current_fields_by_name,
12929
+ hierarchy_filter_groups=hierarchy_root_filter_groups,
12930
+ fields_by_que_id=fields_by_que_id,
12758
12931
  auth_override=view_auth_override,
12759
12932
  explicit_button_dtos=explicit_button_dtos,
12760
12933
  query_condition_payload=query_condition_payload_for_apply,
12761
12934
  associated_resources_payload=associated_resources_payload_for_apply,
12762
12935
  )
12763
- self.views.view_create(profile=profile, payload=fallback_payload)
12936
+ self._apply_view_create(profile=profile, payload=fallback_payload)
12764
12937
  created.append(patch.name)
12765
- view_results.append(
12766
- {
12938
+ view_result = {
12767
12939
  "name": patch.name,
12768
12940
  "view_key": created_key,
12769
12941
  "type": patch.type.value,
@@ -12773,7 +12945,6 @@ class AiBuilderFacade:
12773
12945
  "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12774
12946
  "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12775
12947
  }
12776
- )
12777
12948
  record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
12778
12949
  continue
12779
12950
  except (QingflowApiError, RuntimeError) as fallback_error:
@@ -12799,11 +12970,16 @@ class AiBuilderFacade:
12799
12970
  "view_name": patch.name,
12800
12971
  "view_type": patch.type.value,
12801
12972
  "columns": patch.columns,
12802
- "group_by": patch.group_by,
12803
- "filters": [item.model_dump(mode="json") for item in patch.filters],
12804
- "start_field": patch.start_field,
12805
- "end_field": patch.end_field,
12806
- "title_field": patch.title_field,
12973
+ "group_que_id": patch.group_que_id,
12974
+ "filters": [
12975
+ (item.model_dump(mode="json") if hasattr(item, "model_dump") else item)
12976
+ if not isinstance(item, list)
12977
+ else [sub.model_dump(mode="json") if hasattr(sub, "model_dump") else sub for sub in item]
12978
+ for item in patch.filters
12979
+ ],
12980
+ "start_que_id": patch.start_que_id,
12981
+ "end_que_id": patch.end_que_id,
12982
+ "title_que_id": patch.title_que_id,
12807
12983
  "operation": "sync_default_view" if operation_phase == "default_view_apply_config_sync" else ("update" if existing_key or created_key else "create"),
12808
12984
  "list_type": system_view_list_type,
12809
12985
  "transport_error": {
@@ -12813,24 +12989,69 @@ class AiBuilderFacade:
12813
12989
  },
12814
12990
  **_view_apply_failure_diagnostics(
12815
12991
  patch=patch,
12816
- current_fields_by_name=current_fields_by_name,
12992
+ fields_by_que_id=fields_by_que_id,
12817
12993
  backend_code=api_error.backend_code,
12818
12994
  operation_phase=operation_phase,
12819
12995
  ),
12820
12996
  },
12821
12997
  }
12822
- failed_views.append(failure_entry)
12823
- view_results.append(failure_entry)
12998
+ failure_result = failure_entry
12999
+ view_result = failure_entry
12824
13000
  continue
12825
13001
  view_mutation_ms += _duration_ms(view_mutation_started_at)
13002
+ for item in ((view_result,) if view_result is not None else ()):
13003
+ requested_pinned = item.get("expected_pinned")
13004
+ view_key = str(item.get("view_key") or "").strip()
13005
+ if (
13006
+ str(item.get("status") or "") not in {"created", "updated"}
13007
+ or not isinstance(requested_pinned, bool)
13008
+ or not view_key
13009
+ ):
13010
+ continue
13011
+ try:
13012
+ pin_result = self._set_view_pinned(
13013
+ profile=profile,
13014
+ app_key=app_key,
13015
+ view_key=view_key,
13016
+ pinned=requested_pinned,
13017
+ )
13018
+ item["pinned_state"] = pin_result
13019
+ if not bool(pin_result.get("verified")):
13020
+ failure_result = {
13021
+ "name": item.get("name"),
13022
+ "view_key": view_key,
13023
+ "type": item.get("type"),
13024
+ "status": "failed",
13025
+ "error_code": "VIEW_PIN_STATE_READBACK_MISMATCH",
13026
+ "message": "view configuration was saved, but its pinned state did not verify",
13027
+ "operation": "set_pinned",
13028
+ "details": pin_result,
13029
+ }
13030
+ except (QingflowApiError, RuntimeError, ValueError) as error:
13031
+ api_error = _coerce_api_error(error)
13032
+ pin_failure = {
13033
+ "name": item.get("name"),
13034
+ "view_key": view_key,
13035
+ "type": item.get("type"),
13036
+ "status": "failed",
13037
+ "error_code": "VIEW_PIN_STATE_APPLY_FAILED",
13038
+ "message": "view configuration was saved, but updating its pinned state failed",
13039
+ "request_id": api_error.request_id,
13040
+ "backend_code": api_error.backend_code,
13041
+ "http_status": api_error.http_status,
13042
+ "operation": "set_pinned",
13043
+ "details": {"requested_pinned": requested_pinned},
13044
+ }
13045
+ item["pinned_state"] = {"requested_pinned": requested_pinned, "verified": False}
13046
+ failure_result = pin_failure
12826
13047
  successful_action_view_keys = {
12827
13048
  str(item.get("view_key") or "").strip()
12828
- for item in view_results
13049
+ for item in ((view_result,) if view_result is not None else ())
12829
13050
  if str(item.get("status") or "") in {"created", "updated"} and str(item.get("view_key") or "").strip()
12830
13051
  }
12831
13052
  successful_action_view_names = {
12832
13053
  str(item.get("name") or "").strip()
12833
- for item in view_results
13054
+ for item in ((view_result,) if view_result is not None else ())
12834
13055
  if str(item.get("status") or "") in {"created", "updated"} and str(item.get("name") or "").strip()
12835
13056
  }
12836
13057
  skipped_action_button_intents = [
@@ -12881,12 +13102,12 @@ class AiBuilderFacade:
12881
13102
  action_buttons_failed = bool(action_button_intents) and not (action_buttons_verified and view_action_button_bindings_verified)
12882
13103
  action_buttons_retry_payload = action_buttons_result.get("retry_payload") if isinstance(action_buttons_result.get("retry_payload"), dict) else None
12883
13104
  needs_view_list_readback = bool(created or updated)
12884
- verified_view_result: list[dict[str, Any]] | None = []
13105
+ verified_views: list[dict[str, Any]] | None = []
12885
13106
  verified_views_unavailable = False
12886
13107
  if needs_view_list_readback:
12887
13108
  try:
12888
13109
  view_readback_started_at = time.perf_counter()
12889
- verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
13110
+ verified_views, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
12890
13111
  except (QingflowApiError, RuntimeError) as error:
12891
13112
  view_readback_ms += _duration_ms(view_readback_started_at)
12892
13113
  api_error = _coerce_api_error(error)
@@ -12900,23 +13121,23 @@ class AiBuilderFacade:
12900
13121
  view_readback_ms += _duration_ms(view_readback_started_at)
12901
13122
  verified_names = {
12902
13123
  _extract_view_name(item)
12903
- for item in (verified_view_result or [])
13124
+ for item in (verified_views or [])
12904
13125
  if isinstance(item, dict) and _extract_view_name(item)
12905
13126
  }
12906
13127
  verified_by_key = {
12907
13128
  _extract_view_key(item): item
12908
- for item in (verified_view_result or [])
13129
+ for item in (verified_views or [])
12909
13130
  if isinstance(item, dict) and _extract_view_key(item)
12910
13131
  }
12911
13132
  verified_view_keys_by_name: dict[str, list[str]] = {}
12912
- for item in (verified_view_result or []):
13133
+ for item in (verified_views or []):
12913
13134
  if not isinstance(item, dict):
12914
13135
  continue
12915
13136
  name = _extract_view_name(item)
12916
13137
  key = _extract_view_key(item)
12917
13138
  if name and key:
12918
13139
  verified_view_keys_by_name.setdefault(name, []).append(key)
12919
- verification_by_view: list[dict[str, Any]] = []
13140
+ verification_result: dict[str, Any] | None = None
12920
13141
  filter_readback_pending = False
12921
13142
  filter_mismatches: list[dict[str, Any]] = []
12922
13143
  query_condition_readback_pending = False
@@ -12943,7 +13164,7 @@ class AiBuilderFacade:
12943
13164
  return cached
12944
13165
 
12945
13166
  view_verification_started_at = time.perf_counter()
12946
- for item in view_results:
13167
+ for item in ((view_result,) if view_result is not None else ()):
12947
13168
  status = str(item.get("status") or "")
12948
13169
  name = str(item.get("name") or "")
12949
13170
  item_view_key = str(item.get("view_key") or "").strip()
@@ -12986,7 +13207,7 @@ class AiBuilderFacade:
12986
13207
  verification_entry["readback_ambiguous"] = True
12987
13208
  verification_entry["matching_view_keys"] = matched_keys
12988
13209
  filter_readback_pending = True
12989
- verification_by_view.append(verification_entry)
13210
+ verification_result = verification_entry
12990
13211
  continue
12991
13212
  try:
12992
13213
  config_result, config_error = read_view_config_for_verification(verification_key)
@@ -13004,11 +13225,11 @@ class AiBuilderFacade:
13004
13225
  filters_verified = filters_semantically_equivalent and actual_data_scope == expected_data_scope
13005
13226
  public_expected_filters = _public_view_filter_groups_from_match_rules(
13006
13227
  expected_filter_summary,
13007
- current_fields_by_name=current_fields_by_name,
13228
+ fields_by_que_id=fields_by_que_id,
13008
13229
  )
13009
13230
  public_actual_filters = _public_view_filter_groups_from_match_rules(
13010
13231
  semantic_actual_filters,
13011
- current_fields_by_name=current_fields_by_name,
13232
+ fields_by_que_id=fields_by_que_id,
13012
13233
  )
13013
13234
  verification_entry["filters_verified"] = filters_verified
13014
13235
  verification_entry["view_key"] = verification_key
@@ -13058,7 +13279,7 @@ class AiBuilderFacade:
13058
13279
  verification_entry["readback_ambiguous"] = True
13059
13280
  verification_entry["matching_view_keys"] = matched_keys
13060
13281
  query_condition_readback_pending = True
13061
- verification_by_view.append(verification_entry)
13282
+ verification_result = verification_entry
13062
13283
  continue
13063
13284
  try:
13064
13285
  config_result, config_error = read_view_config_for_verification(verification_key)
@@ -13107,7 +13328,7 @@ class AiBuilderFacade:
13107
13328
  verification_entry["readback_ambiguous"] = True
13108
13329
  verification_entry["matching_view_keys"] = matched_keys
13109
13330
  associated_resource_readback_pending = True
13110
- verification_by_view.append(verification_entry)
13331
+ verification_result = verification_entry
13111
13332
  continue
13112
13333
  try:
13113
13334
  config_result, config_error = read_view_config_for_verification(verification_key)
@@ -13162,7 +13383,7 @@ class AiBuilderFacade:
13162
13383
  verification_entry["readback_ambiguous"] = True
13163
13384
  verification_entry["matching_view_keys"] = matched_keys
13164
13385
  button_readback_pending = True
13165
- verification_by_view.append(verification_entry)
13386
+ verification_result = verification_entry
13166
13387
  continue
13167
13388
  try:
13168
13389
  config_result, config_error = read_view_config_for_verification(verification_key)
@@ -13210,51 +13431,45 @@ class AiBuilderFacade:
13210
13431
  "category": api_error.category,
13211
13432
  }
13212
13433
  button_readback_pending = True
13213
- verification_by_view.append(verification_entry)
13434
+ verification_result = verification_entry
13214
13435
  elif status == "removed":
13215
- verification_by_view.append(
13216
- {
13217
- "name": name,
13218
- "view_key": item.get("view_key"),
13219
- "type": item.get("type"),
13220
- "status": "removed",
13221
- "present_in_readback": False,
13222
- "removed_verified": True,
13223
- "delete_executed": bool(item.get("delete_executed")),
13224
- "readback_status": item.get("readback_status") or "deleted",
13225
- "safe_to_retry_delete": False,
13226
- }
13227
- )
13436
+ verification_result = {
13437
+ "name": name,
13438
+ "view_key": item.get("view_key"),
13439
+ "type": item.get("type"),
13440
+ "status": "removed",
13441
+ "present_in_readback": False,
13442
+ "removed_verified": True,
13443
+ "delete_executed": bool(item.get("delete_executed")),
13444
+ "readback_status": item.get("readback_status") or "deleted",
13445
+ "safe_to_retry_delete": False,
13446
+ }
13228
13447
  elif status == "readback_pending" and item.get("operation") == "delete":
13229
13448
  readback_status = str(item.get("readback_status") or "unavailable")
13230
- verification_by_view.append(
13231
- {
13232
- "name": name,
13233
- "view_key": item.get("view_key"),
13234
- "type": item.get("type"),
13235
- "status": "readback_pending",
13236
- "present_in_readback": True if readback_status == "still_exists" else None,
13237
- "removed_verified": False,
13238
- "delete_executed": bool(item.get("delete_executed")),
13239
- "readback_status": readback_status,
13240
- "safe_to_retry_delete": False,
13241
- "error_code": item.get("error_code"),
13242
- }
13243
- )
13449
+ verification_result = {
13450
+ "name": name,
13451
+ "view_key": item.get("view_key"),
13452
+ "type": item.get("type"),
13453
+ "status": "readback_pending",
13454
+ "present_in_readback": True if readback_status == "still_exists" else None,
13455
+ "removed_verified": False,
13456
+ "delete_executed": bool(item.get("delete_executed")),
13457
+ "readback_status": readback_status,
13458
+ "safe_to_retry_delete": False,
13459
+ "error_code": item.get("error_code"),
13460
+ }
13244
13461
  else:
13245
- verification_by_view.append(
13246
- {
13247
- "name": item.get("name"),
13248
- "type": item.get("type"),
13249
- "status": "failed",
13250
- "present_in_readback": None,
13251
- "error_code": item.get("error_code"),
13252
- }
13253
- )
13462
+ verification_result = {
13463
+ "name": item.get("name"),
13464
+ "type": item.get("type"),
13465
+ "status": "failed",
13466
+ "present_in_readback": None,
13467
+ "error_code": item.get("error_code"),
13468
+ }
13254
13469
  view_verification_ms += _duration_ms(view_verification_started_at)
13255
13470
  removed_delete_results = [
13256
13471
  item
13257
- for item in view_results
13472
+ for item in ((view_result,) if view_result is not None else ())
13258
13473
  if item.get("operation") == "delete" and bool(item.get("delete_executed"))
13259
13474
  ]
13260
13475
  removed_verified = all(str(item.get("readback_status") or "") == "deleted" for item in removed_delete_results)
@@ -13269,19 +13484,19 @@ class AiBuilderFacade:
13269
13484
  view_associated_resources_verified = verified and not associated_resource_readback_pending and not associated_resource_mismatches
13270
13485
  view_buttons_verified = verified and not button_readback_pending and not button_mismatches and not custom_button_readback_pending
13271
13486
  noop = not created and not updated and not removed and not action_button_write_executed
13272
- if failed_views:
13487
+ if failure_result is not None:
13273
13488
  successful_changes = bool(created or updated or removed or action_button_write_succeeded)
13274
- first_failure = failed_views[0]
13489
+ first_failure = failure_result
13275
13490
  response = {
13276
13491
  "status": "partial_success" if successful_changes else "failed",
13277
13492
  "error_code": "VIEW_APPLY_PARTIAL" if successful_changes else "VIEW_APPLY_FAILED",
13278
13493
  "recoverable": True,
13279
- "message": "applied some view patches; at least one view failed" if successful_changes else "one or more view patches failed",
13494
+ "message": "view operation completed with a verification failure" if successful_changes else "view operation failed",
13280
13495
  "normalized_args": normalized_args,
13281
13496
  "missing_fields": [],
13282
13497
  "allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
13283
13498
  "details": {
13284
- "per_view_results": view_results,
13499
+ "view_result": deepcopy(view_result) if view_result is not None else None,
13285
13500
  "filter_mismatches": filter_mismatches,
13286
13501
  "query_condition_mismatches": query_condition_mismatches,
13287
13502
  "associated_resource_mismatches": associated_resource_mismatches,
@@ -13339,13 +13554,14 @@ class AiBuilderFacade:
13339
13554
  "action_buttons_verified": action_buttons_verified,
13340
13555
  "view_button_bindings_verified": view_action_button_bindings_verified,
13341
13556
  "views_read_unavailable": verified_views_unavailable,
13342
- "by_view": verification_by_view,
13557
+ "view": deepcopy(verification_result) if verification_result is not None else None,
13343
13558
  "custom_button_readback_pending": custom_button_readback_pending,
13344
13559
  "custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
13345
13560
  },
13346
13561
  "app_key": app_key,
13347
13562
  "app_name": app_name,
13348
- "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
13563
+ "view_result": deepcopy(view_result) if view_result is not None else None,
13564
+ "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": [failure_result]},
13349
13565
  "verified": verified and view_filters_verified and view_query_conditions_verified and view_associated_resources_verified and view_buttons_verified and action_buttons_verified and view_action_button_bindings_verified,
13350
13566
  "write_executed": bool(created or updated or removed or action_button_write_executed),
13351
13567
  "write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
@@ -13414,6 +13630,7 @@ class AiBuilderFacade:
13414
13630
  **({"query_condition_mismatches": query_condition_mismatches} if query_condition_mismatches else {}),
13415
13631
  **({"associated_resource_mismatches": associated_resource_mismatches} if associated_resource_mismatches else {}),
13416
13632
  **({"button_mismatches": button_mismatches} if button_mismatches else {}),
13633
+ "view_result": deepcopy(view_result) if view_result is not None else None,
13417
13634
  **({"action_buttons_result": action_buttons_result} if action_button_intents else {}),
13418
13635
  **(
13419
13636
  {"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
@@ -13441,10 +13658,11 @@ class AiBuilderFacade:
13441
13658
  "delete_readback_pending": delete_readback_pending,
13442
13659
  "custom_button_readback_pending": custom_button_readback_pending,
13443
13660
  "custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
13444
- "by_view": verification_by_view,
13661
+ "view": deepcopy(verification_result) if verification_result is not None else None,
13445
13662
  },
13446
13663
  "app_key": app_key,
13447
13664
  "app_name": app_name,
13665
+ "view_result": deepcopy(view_result) if view_result is not None else None,
13448
13666
  "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
13449
13667
  "verified": all_verified,
13450
13668
  "write_executed": bool(created or updated or removed or action_button_write_executed),
@@ -13453,6 +13671,305 @@ class AiBuilderFacade:
13453
13671
  }
13454
13672
  return finish_with_publish(response)
13455
13673
 
13674
+ def app_views_schema(
13675
+ self,
13676
+ *,
13677
+ schema_version: str | None = None,
13678
+ schema_kind: str | None = None,
13679
+ ) -> JSONObject:
13680
+ """读取版本化视图声明 schema;detail kind 用于获取专项配置定义。"""
13681
+ normalized_args = {"schema_version": schema_version, "schema_kind": schema_kind}
13682
+ try:
13683
+ payload = views_schema_registry().schema(schema_version=schema_version, schema_kind=schema_kind)
13684
+ except ViewsContractError as error:
13685
+ return _failed(
13686
+ error.error_code,
13687
+ str(error.message),
13688
+ normalized_args=normalized_args,
13689
+ details={"reason_path": error.path or "", **(error.details or {})},
13690
+ suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "summary"}},
13691
+ )
13692
+ return {
13693
+ "status": "success",
13694
+ "error_code": None,
13695
+ "recoverable": False,
13696
+ "message": "read views declaration schema",
13697
+ "normalized_args": normalized_args,
13698
+ "missing_fields": [],
13699
+ "allowed_values": {"schema_kind": payload["supportedSchemaKinds"]},
13700
+ "details": {},
13701
+ "request_id": None,
13702
+ "suggested_next_call": None,
13703
+ "warnings": [],
13704
+ "verification": {"schema_loaded": True},
13705
+ **payload,
13706
+ }
13707
+
13708
+ def app_views_get(
13709
+ self,
13710
+ *,
13711
+ profile: str,
13712
+ app_key: str,
13713
+ schema_version: str | None = None,
13714
+ schema_kind: str = "summary",
13715
+ ) -> JSONObject:
13716
+ """读取应用的 canonical 视图声明数组(summary 级投影或 complete 反向编译)。"""
13717
+ normalized_args = {"app_key": app_key, "schema_version": schema_version, "schema_kind": schema_kind}
13718
+ try:
13719
+ context = views_schema_registry().resolve_schema_version(schema_version)
13720
+ except ViewsContractError as error:
13721
+ return _failed(
13722
+ error.error_code,
13723
+ str(error.message),
13724
+ normalized_args=normalized_args,
13725
+ details={"reason_path": error.path or "", **(error.details or {})},
13726
+ suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "summary"}},
13727
+ )
13728
+ try:
13729
+ views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
13730
+ except (QingflowApiError, RuntimeError) as error:
13731
+ api_error = _coerce_api_error(error)
13732
+ return _failed_from_api_error(
13733
+ "VIEWS_READ_FAILED",
13734
+ api_error,
13735
+ normalized_args=normalized_args,
13736
+ details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
13737
+ suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
13738
+ )
13739
+ schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
13740
+ parsed_schema = _parse_schema(schema)
13741
+ # 读面 summary 面向 agent 展示字段名:从原始 schema 递归收集(同名不丢);
13742
+ # 系统列(queId<=5)用公开名映射,未知系统列以 queId 展示兜底
13743
+ name_by_que_id: dict[int, str] = {
13744
+ **{system_id: name for system_id, name in _SYSTEM_VIEW_QUE_NAMES.items()},
13745
+ **_collect_field_id_to_name(schema),
13746
+ }
13747
+ normalized_kind = str(schema_kind or "summary").strip().lower()
13748
+ if normalized_kind == "complete":
13749
+ return self._app_views_get_complete(
13750
+ profile=profile,
13751
+ app_key=app_key,
13752
+ views=views,
13753
+ parsed_schema=parsed_schema,
13754
+ normalized_args=normalized_args,
13755
+ schema_version=schema_version,
13756
+ )
13757
+ projected: list[dict[str, Any]] = []
13758
+ scope_map = {"ALL": "all", "ONESELF": "oneself", "DEPT": "dept", "SUBORDINATE": "subordinate", "CUSTOM": "custom"}
13759
+ for view in views if isinstance(views, list) else []:
13760
+ if not isinstance(view, dict):
13761
+ continue
13762
+ entry: dict[str, Any] = {
13763
+ "view_key": _extract_view_key(view),
13764
+ "name": _extract_view_name(view),
13765
+ "type": _normalize_view_type_name(view.get("viewgraphType") or view.get("type")),
13766
+ }
13767
+ columns = [name_by_que_id.get(que_id, str(que_id)) for que_id in view.get("viewgraphQueIds") or [] if isinstance(que_id, int)]
13768
+ if columns:
13769
+ entry["columns"] = columns
13770
+ group_que_id = _coerce_positive_int(view.get("groupQueId"))
13771
+ if group_que_id is not None and group_que_id in name_by_que_id:
13772
+ entry["group_by"] = name_by_que_id[group_que_id]
13773
+ gantt_config = view.get("viewgraphGanttConfigVO")
13774
+ if isinstance(gantt_config, dict):
13775
+ for source, target in (
13776
+ ("startTimeQueId", "start_field"),
13777
+ ("endTimeQueId", "end_field"),
13778
+ ("titleQueId", "title_field"),
13779
+ ):
13780
+ gantt_que_id = _coerce_positive_int(gantt_config.get(source))
13781
+ if gantt_que_id is not None and gantt_que_id in name_by_que_id:
13782
+ entry[target] = name_by_que_id[gantt_que_id]
13783
+ title_que_id = _coerce_positive_int(view.get("titleQue"))
13784
+ if title_que_id is not None and title_que_id in name_by_que_id:
13785
+ entry.setdefault("title_field", name_by_que_id[title_que_id])
13786
+ data_permission_type = view.get("dataPermissionType")
13787
+ if data_permission_type:
13788
+ entry["data_permission"] = {
13789
+ "type": "config" if str(data_permission_type).upper() == "CONFIG" else "custom",
13790
+ "scope": scope_map.get(str(view.get("dataScope") or "").upper(), "all"),
13791
+ }
13792
+ projected.append(entry)
13793
+ return {
13794
+ "status": "success",
13795
+ "error_code": None,
13796
+ "recoverable": False,
13797
+ "message": "read canonical views declarations",
13798
+ "normalized_args": normalized_args,
13799
+ "missing_fields": [],
13800
+ "allowed_values": {"view_types": [member.value for member in PublicViewType]},
13801
+ "details": {},
13802
+ "request_id": None,
13803
+ "suggested_next_call": None,
13804
+ "warnings": [],
13805
+ "verification": {"views_read": len(projected)},
13806
+ "schemaVersion": context.schema_version,
13807
+ "apiVersion": context.api_version,
13808
+ "latestVersion": views_schema_registry().latest_version,
13809
+ "app_key": app_key,
13810
+ "views": projected,
13811
+ }
13812
+
13813
+ def _app_views_get_complete(
13814
+ self,
13815
+ *,
13816
+ profile: str,
13817
+ app_key: str,
13818
+ views: Any,
13819
+ parsed_schema: dict[str, Any],
13820
+ normalized_args: JSONObject,
13821
+ schema_version: str | None,
13822
+ ) -> JSONObject:
13823
+ """complete 档 canonical:逐视图 GET /view/{key}/viewConfig → 反向编译。"""
13824
+ from .views_extract import extract_complete_view_declaration, summarize_gaps
13825
+
13826
+ fields_by_que_id = _collect_fields_by_que_id(parsed_schema["fields"])
13827
+ projected: list[dict[str, Any]] = []
13828
+ read_errors: list[dict[str, Any]] = []
13829
+ all_gaps: list[dict[str, Any]] = []
13830
+ for view in views if isinstance(views, list) else []:
13831
+ if not isinstance(view, dict):
13832
+ continue
13833
+ view_key = _extract_view_key(view)
13834
+ view_name = _extract_view_name(view)
13835
+ if not view_key:
13836
+ continue
13837
+ if _resolve_system_view_list_type(view_key=view_key, view_name=view_name) is not None and not _normalize_view_type_name(view.get("viewgraphType") or view.get("type")):
13838
+ # 系统视图(列表/申请页等,viewgraphKey 为固定数字 id):不在公开声明语义内,直接跳过。
13839
+ continue
13840
+ try:
13841
+ config = self.views.view_get_config(profile=profile, viewgraph_key=view_key).get("result") or {}
13842
+ except (QingflowApiError, RuntimeError) as error:
13843
+ api_error = _coerce_api_error(error)
13844
+ read_errors.append(
13845
+ {
13846
+ "view_key": view_key,
13847
+ "name": view_name,
13848
+ "error_code": "VIEW_CONFIG_READ_FAILED",
13849
+ "http_status": api_error.http_status,
13850
+ "message": str(api_error.message or error),
13851
+ }
13852
+ )
13853
+ continue
13854
+ if not isinstance(config, dict) or not config:
13855
+ read_errors.append({"view_key": view_key, "name": view_name, "error_code": "VIEW_CONFIG_EMPTY", "message": "viewConfig read returned no data"})
13856
+ continue
13857
+ declaration, gaps = extract_complete_view_declaration(config, fields_by_que_id=fields_by_que_id)
13858
+ if not declaration:
13859
+ read_errors.append(
13860
+ {
13861
+ "view_key": view_key,
13862
+ "name": view_name,
13863
+ "error_code": "VIEW_NOT_EXTRACTABLE",
13864
+ "message": "view configuration cannot be represented completely by the public contract; see gaps",
13865
+ "gaps": gaps,
13866
+ }
13867
+ )
13868
+ continue
13869
+ if gaps:
13870
+ all_gaps.append({"view_key": view_key, "gaps": gaps})
13871
+ projected.append(declaration)
13872
+ gap_summary: dict[str, int] = {}
13873
+ for item in all_gaps:
13874
+ for kind, count in summarize_gaps(item["gaps"]).items():
13875
+ gap_summary[kind] = gap_summary.get(kind, 0) + count
13876
+ context = views_schema_registry().resolve_schema_version(schema_version)
13877
+ return {
13878
+ "status": "success",
13879
+ "error_code": None,
13880
+ "recoverable": False,
13881
+ "message": "read canonical views declarations (complete)",
13882
+ "normalized_args": normalized_args,
13883
+ "missing_fields": [],
13884
+ "allowed_values": {"view_types": [member.value for member in PublicViewType]},
13885
+ "details": {
13886
+ "extraction_gap_summary": gap_summary,
13887
+ "extraction_gaps": all_gaps[:50],
13888
+ "config_read_errors": read_errors[:50],
13889
+ "config_read_error_count": len(read_errors),
13890
+ },
13891
+ "request_id": None,
13892
+ "suggested_next_call": None,
13893
+ "warnings": [],
13894
+ "verification": {"views_read": len(projected), "views_with_gaps": len(all_gaps), "read_errors": len(read_errors)},
13895
+ "schemaVersion": context.schema_version,
13896
+ "apiVersion": context.api_version,
13897
+ "latestVersion": views_schema_registry().latest_version,
13898
+ "app_key": app_key,
13899
+ "views": projected,
13900
+ }
13901
+
13902
+ def app_views_validate(
13903
+ self,
13904
+ *,
13905
+ declaration: JSONObject,
13906
+ schema_version: str | None = None,
13907
+ ) -> JSONObject:
13908
+ """纯离线校验一条视图声明;不访问后端。"""
13909
+ normalized_args = {"schema_version": schema_version}
13910
+ resolved_version = str(schema_version or (declaration.get("schema_version") if isinstance(declaration, dict) else None) or "").strip() or None
13911
+ try:
13912
+ context = views_schema_registry().resolve_schema_version(resolved_version)
13913
+ except ViewsContractError as error:
13914
+ return _failed(
13915
+ error.error_code,
13916
+ str(error.message),
13917
+ normalized_args=normalized_args,
13918
+ details={"reason_path": error.path or "", **(error.details or {})},
13919
+ suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "complete"}},
13920
+ )
13921
+ issues: list[dict[str, Any]] = []
13922
+ batch_keys = ("views", "upsert_views", "upsertViews", "apps")
13923
+ if not isinstance(declaration, dict):
13924
+ issues.append({"path": "/", "code": "INVALID_SPEC", "message": "views declaration must be a JSON object"})
13925
+ elif any(key in declaration for key in batch_keys):
13926
+ issues.append(
13927
+ {
13928
+ "path": "/",
13929
+ "code": "VIEW_BATCH_CREATE_UNSUPPORTED",
13930
+ "message": "views schema validates one view declaration; batch envelopes are unsupported",
13931
+ }
13932
+ )
13933
+ else:
13934
+ declaration_item = {
13935
+ key: value
13936
+ for key, value in declaration.items()
13937
+ if key not in {"schema_version", "schemaVersion", "apiVersion"}
13938
+ }
13939
+ try:
13940
+ parsed = ViewUpsertPatch.model_validate(declaration_item)
13941
+ context.validate_declaration(compact_none_values(public_view_upsert_payload(parsed)))
13942
+ except ValidationError as exc:
13943
+ first_error = exc.errors()[0]
13944
+ issues.append(
13945
+ {
13946
+ "path": f"/{'/'.join(str(part) for part in first_error.get('loc', ()))}".rstrip("/"),
13947
+ "code": "INVALID_SPEC",
13948
+ "message": str(first_error.get("msg") or exc),
13949
+ }
13950
+ )
13951
+ except ViewsContractError as error:
13952
+ issues.append({"path": error.path or "/", "code": error.error_code, "message": str(error.message)})
13953
+ ok = not issues
13954
+ return {
13955
+ "status": "success" if ok else "failed",
13956
+ "error_code": None if ok else issues[0]["code"],
13957
+ "recoverable": True,
13958
+ "message": "views declaration is valid" if ok else "views declaration failed validation",
13959
+ "normalized_args": normalized_args,
13960
+ "missing_fields": [],
13961
+ "allowed_values": {},
13962
+ "details": {"issues": issues},
13963
+ "request_id": None,
13964
+ "suggested_next_call": None,
13965
+ "warnings": [],
13966
+ "verification": {"validated_entries": 1 if ok else 0},
13967
+ "ok": ok,
13968
+ "schemaVersion": context.schema_version,
13969
+ "apiVersion": context.api_version,
13970
+ "issues": issues,
13971
+ }
13972
+
13456
13973
  def app_publish_verify(
13457
13974
  self,
13458
13975
  *,
@@ -15429,6 +15946,76 @@ class AiBuilderFacade:
15429
15946
  result = legacy_normalized or normalized_views
15430
15947
  return (result, False, None) if include_error else (result, False)
15431
15948
 
15949
+ def _set_view_pinned(
15950
+ self,
15951
+ *,
15952
+ profile: str,
15953
+ app_key: str,
15954
+ view_key: str,
15955
+ pinned: bool,
15956
+ ) -> dict[str, Any]:
15957
+ """Move one view between the application-level fixed and ordinary lists."""
15958
+ target_key = str(view_key or "").strip()
15959
+ if not target_key:
15960
+ raise ValueError("view_key is required to update the pinned state")
15961
+ response = self.views.view_list(profile=profile, app_key=app_key)
15962
+ raw_groups = response.get("result") if isinstance(response, dict) else None
15963
+ if not isinstance(raw_groups, list):
15964
+ raise ValueError("view list does not expose ordinal groups")
15965
+
15966
+ group_keys: dict[str, list[str]] = {
15967
+ "FIXED_VIEW_LIST": [],
15968
+ "VIEW_LIST": [],
15969
+ }
15970
+ for group in raw_groups:
15971
+ if not isinstance(group, dict):
15972
+ continue
15973
+ ordinal_type = str(group.get("ordinalType") or "").strip()
15974
+ if ordinal_type not in group_keys:
15975
+ continue
15976
+ view_list = group.get("viewList")
15977
+ if not isinstance(view_list, list):
15978
+ continue
15979
+ group_keys[ordinal_type] = [
15980
+ key
15981
+ for item in view_list
15982
+ if isinstance(item, dict)
15983
+ and (key := _extract_view_key(item))
15984
+ and key != target_key
15985
+ ]
15986
+
15987
+ target_group = "FIXED_VIEW_LIST" if pinned else "VIEW_LIST"
15988
+ group_keys[target_group].append(target_key)
15989
+ self.views.view_reorder(
15990
+ profile=profile,
15991
+ app_key=app_key,
15992
+ payload=[
15993
+ {"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": group_keys["FIXED_VIEW_LIST"]},
15994
+ {"ordinalType": "VIEW_LIST", "viewKeyList": group_keys["VIEW_LIST"]},
15995
+ ],
15996
+ )
15997
+
15998
+ readback = self.views.view_list(profile=profile, app_key=app_key)
15999
+ readback_groups = readback.get("result") if isinstance(readback, dict) else None
16000
+ actual_group: str | None = None
16001
+ if isinstance(readback_groups, list):
16002
+ for group in readback_groups:
16003
+ if not isinstance(group, dict):
16004
+ continue
16005
+ ordinal_type = str(group.get("ordinalType") or "").strip()
16006
+ for item in group.get("viewList") or []:
16007
+ if isinstance(item, dict) and _extract_view_key(item) == target_key:
16008
+ actual_group = ordinal_type
16009
+ break
16010
+ if actual_group is not None:
16011
+ break
16012
+ return {
16013
+ "requested_pinned": pinned,
16014
+ "actual_pinned": actual_group == "FIXED_VIEW_LIST" if actual_group is not None else None,
16015
+ "verified": actual_group == target_group,
16016
+ "actual_ordinal_type": actual_group,
16017
+ }
16018
+
15432
16019
  def _load_workflow_result(
15433
16020
  self,
15434
16021
  *,
@@ -19226,40 +19813,44 @@ def _chart_apply_batching_context(
19226
19813
  def _view_apply_failure_diagnostics(
19227
19814
  *,
19228
19815
  patch: ViewUpsertPatch,
19229
- current_fields_by_name: dict[str, dict[str, Any]],
19816
+ fields_by_que_id: dict[int, dict[str, Any]],
19230
19817
  backend_code: Any,
19231
19818
  operation_phase: str,
19232
19819
  ) -> dict[str, Any]:
19233
- references: list[tuple[str, str]] = []
19820
+ references: list[tuple[int, str]] = []
19234
19821
 
19235
- def add_reference(name: Any, source: str) -> None:
19236
- text = str(name or "").strip()
19237
- if text:
19238
- references.append((text, source))
19822
+ def add_reference(value: Any, source: str) -> None:
19823
+ que_id = _coerce_positive_int(value)
19824
+ if que_id is not None:
19825
+ references.append((que_id, source))
19239
19826
 
19240
19827
  for column in patch.columns:
19241
19828
  add_reference(column, "columns")
19242
- add_reference(patch.group_by, "group_by")
19243
- add_reference(patch.start_field, "start_field")
19244
- add_reference(patch.end_field, "end_field")
19245
- add_reference(patch.title_field, "title_field")
19246
- for rule in patch.filters:
19247
- add_reference(rule.field_name, "filters")
19829
+ add_reference(patch.group_que_id, "group_que_id")
19830
+ add_reference(patch.start_que_id, "start_que_id")
19831
+ add_reference(patch.end_que_id, "end_que_id")
19832
+ add_reference(patch.title_que_id, "title_que_id")
19833
+ for item in patch.filters:
19834
+ if isinstance(item, list):
19835
+ for rule in item:
19836
+ add_reference(getattr(rule, "que_id", None), "filters")
19837
+ else:
19838
+ add_reference(getattr(item, "que_id", None), "filters")
19248
19839
 
19249
- seen: set[str] = set()
19840
+ seen: set[tuple[int, str]] = set()
19250
19841
  field_entries: list[dict[str, Any]] = []
19251
- for field_name, source in references:
19252
- key = f"{field_name}\0{source}"
19842
+ for que_id, source in references:
19843
+ key = (que_id, source)
19253
19844
  if key in seen:
19254
19845
  continue
19255
19846
  seen.add(key)
19256
- field = current_fields_by_name.get(field_name) or {}
19847
+ field = fields_by_que_id.get(que_id) or {}
19257
19848
  field_entries.append(
19258
19849
  {
19259
- "field_name": field_name,
19850
+ "field_name": str(field.get("name") or que_id),
19260
19851
  "source": source,
19261
19852
  "field_id": field.get("field_id"),
19262
- "que_id": field.get("que_id"),
19853
+ "que_id": field.get("que_id") or que_id,
19263
19854
  "field_type": field.get("type"),
19264
19855
  "que_type": field.get("que_type"),
19265
19856
  "exists_in_schema": bool(field),
@@ -19277,7 +19868,7 @@ def _view_apply_failure_diagnostics(
19277
19868
  "recommended_minimal_retry": {
19278
19869
  "name": patch.name,
19279
19870
  "type": patch.type.value,
19280
- "columns": [field_entries[0]["field_name"]] if field_entries else [],
19871
+ "columns": [field_entries[0]["que_id"]] if field_entries else [],
19281
19872
  **({"filters": [rule.model_dump(mode="json", exclude_none=True) for rule in patch.filters]} if patch.filters else {}),
19282
19873
  },
19283
19874
  }
@@ -20356,6 +20947,8 @@ def _normalize_portal_view_type(view: dict[str, Any]) -> str:
20356
20947
  "ganttView": "ganttView",
20357
20948
  "hierarchy": "hierarchyView",
20358
20949
  "hierarchyView": "hierarchyView",
20950
+ "calendar": "calendarView",
20951
+ "calendarView": "calendarView",
20359
20952
  }
20360
20953
  return mapping.get(raw, "tableView")
20361
20954
 
@@ -21186,8 +21779,10 @@ def _infer_field_type(question: dict[str, Any]) -> str:
21186
21779
 
21187
21780
 
21188
21781
  def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None) -> dict[str, Any]:
21189
- name = str(question.get("queTitle") or "").strip()
21190
21782
  que_id = _coerce_positive_int(question.get("queId"))
21783
+ # 无标题字段用「字段{que_id}」兜底命名,与提取器(eval_replay/views get)对称,
21784
+ # 否则视图列引用无标题字段时 UNKNOWN_VIEW_FIELD
21785
+ name = str(question.get("queTitle") or "").strip() or (f"字段{que_id}" if que_id else "")
21191
21786
  que_type = _coerce_positive_int(question.get("queType"))
21192
21787
  field_type = _infer_field_type(question)
21193
21788
  field_id = field_id_hint or f"field_{que_id or _slugify(name, default='x')}"
@@ -21357,6 +21952,17 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
21357
21952
  for question in inner_row or []:
21358
21953
  if not isinstance(question, dict):
21359
21954
  continue
21955
+ if _coerce_positive_int(question.get("queType")) == 24:
21956
+ # 嵌套段落:不注册段落本身,递归解析其 innerQuestions
21957
+ for nested_row in question.get("innerQuestions", []) or []:
21958
+ for nested_question in (nested_row if isinstance(nested_row, list) else [nested_row]):
21959
+ if not isinstance(nested_question, dict):
21960
+ continue
21961
+ nested_field = _parse_field(nested_question)
21962
+ if str(nested_field["name"] or "").strip():
21963
+ fields_by_name.setdefault(nested_field["name"], nested_field)
21964
+ labels.append(nested_field["name"])
21965
+ continue
21360
21966
  field = _parse_field(question)
21361
21967
  fields_by_name.setdefault(field["name"], field)
21362
21968
  labels.append(field["name"])
@@ -21382,8 +21988,16 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
21382
21988
  for question in row:
21383
21989
  if not isinstance(question, dict):
21384
21990
  continue
21991
+ if _coerce_positive_int(question.get("queType")) == 24:
21992
+ # 同行混入段落容器:不注册段落本身(它的子字段在自己 row),跳过避免同名占位挡住真实字段
21993
+ continue
21385
21994
  field = _parse_field(question)
21386
- fields_by_name.setdefault(field["name"], field)
21995
+ existing = fields_by_name.get(field["name"])
21996
+ # 同名冲突:保留信息更丰富的那个(表格字段带 subfields,被同名普通字段覆盖会丢子字段引用面);
21997
+ # 但 queId 索引必须收录全部同名字段(_collect_fields_by_que_id 按 queId 消费,同名不同 id 各自有效)
21998
+ if existing is None or (not existing.get("subfields") and field.get("subfields")):
21999
+ fields_by_name[field["name"]] = field
22000
+
21387
22001
  labels.append(field["name"])
21388
22002
  if labels:
21389
22003
  root_rows.append(labels)
@@ -26066,7 +26680,11 @@ def _merge_view_summary_with_config(
26066
26680
  return summary
26067
26681
  if isinstance(config.get("auth"), dict):
26068
26682
  summary["visibility_summary"] = _visibility_summary(_public_visibility_from_member_auth(config.get("auth")))
26069
- legacy_columns = [str(value) for value in (summary.get("columns") or []) if str(value or "").strip()]
26683
+ legacy_columns = [
26684
+ _SYSTEM_VIEW_QUE_NAMES.get(value if isinstance(value, int) else None, str(value))
26685
+ for value in (summary.get("columns") or [])
26686
+ if str(value or "").strip()
26687
+ ]
26070
26688
  question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
26071
26689
  canonical_question_entries = _extract_view_question_entries(question_list)
26072
26690
  question_entries_by_id = {
@@ -26167,7 +26785,7 @@ def _merge_view_summary_with_config(
26167
26785
  and str(entry.get("name") or "").strip()
26168
26786
  and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
26169
26787
  ]
26170
- summary["columns"] = canonical_display_columns
26788
+ summary["columns"] = canonical_display_column_ids
26171
26789
  summary["display_columns"] = canonical_display_columns
26172
26790
  summary["display_column_ids"] = canonical_display_column_ids
26173
26791
  summary["column_details"] = canonical_display_entries
@@ -26212,7 +26830,7 @@ def _merge_view_summary_with_config(
26212
26830
  )
26213
26831
  summary["filters"] = _public_view_filter_groups_from_match_rules(
26214
26832
  config.get("viewgraphLimit"),
26215
- current_fields_by_name=field_lookup,
26833
+ fields_by_que_id=field_lookup,
26216
26834
  )
26217
26835
  config_enriched = True
26218
26836
  if any(key in config for key in ("asosChartVisible", "asosChartConfig", "asosChartIdList", "limitType")):
@@ -26455,12 +27073,16 @@ def _normalize_print_tpls(value: Any) -> list[dict[str, Any]]:
26455
27073
  items: list[dict[str, Any]] = []
26456
27074
  for item in value:
26457
27075
  if isinstance(item, dict):
26458
- tpl_id = item.get("printTplId")
27076
+ tpl_id = item.get("printKey")
27077
+ if tpl_id is None:
27078
+ tpl_id = item.get("printTplId")
26459
27079
  if tpl_id is None:
26460
27080
  tpl_id = item.get("templateId")
26461
27081
  if tpl_id is None:
26462
27082
  tpl_id = item.get("id")
26463
- tpl_name = item.get("printTplName")
27083
+ tpl_name = item.get("printName")
27084
+ if tpl_name is None:
27085
+ tpl_name = item.get("printTplName")
26464
27086
  if tpl_name is None:
26465
27087
  tpl_name = item.get("templateName")
26466
27088
  if tpl_name is None:
@@ -26904,7 +27526,7 @@ def _build_grouped_view_button_config(button_config_dtos: list[dict[str, Any]])
26904
27526
 
26905
27527
  def _build_view_button_dtos(
26906
27528
  *,
26907
- current_fields_by_name: dict[str, dict[str, Any]],
27529
+ fields_by_que_id: dict[str, dict[str, Any]],
26908
27530
  bindings: list[ViewButtonBindingPatch],
26909
27531
  valid_custom_button_ids: set[int],
26910
27532
  ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
@@ -26913,7 +27535,7 @@ def _build_view_button_dtos(
26913
27535
  for binding in bindings:
26914
27536
  dto, binding_issues = _serialize_view_button_binding(
26915
27537
  binding=binding,
26916
- current_fields_by_name=current_fields_by_name,
27538
+ fields_by_que_id=fields_by_que_id,
26917
27539
  valid_custom_button_ids=valid_custom_button_ids,
26918
27540
  )
26919
27541
  if binding_issues:
@@ -26926,7 +27548,7 @@ def _build_view_button_dtos(
26926
27548
  def _serialize_view_button_binding(
26927
27549
  *,
26928
27550
  binding: ViewButtonBindingPatch,
26929
- current_fields_by_name: dict[str, dict[str, Any]],
27551
+ fields_by_que_id: dict[str, dict[str, Any]],
26930
27552
  valid_custom_button_ids: set[int],
26931
27553
  allow_unverified_custom_button_id: bool = False,
26932
27554
  ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
@@ -26944,7 +27566,7 @@ def _serialize_view_button_binding(
26944
27566
  }
26945
27567
  ]
26946
27568
  translated_limits, limit_issues = _build_view_button_limit_groups(
26947
- current_fields_by_name=current_fields_by_name,
27569
+ fields_by_que_id=fields_by_que_id,
26948
27570
  groups=binding.button_limit,
26949
27571
  )
26950
27572
  if limit_issues:
@@ -26970,7 +27592,7 @@ def _serialize_view_button_binding(
26970
27592
 
26971
27593
  def _build_view_button_limit_groups(
26972
27594
  *,
26973
- current_fields_by_name: dict[str, dict[str, Any]],
27595
+ fields_by_que_id: dict[str, dict[str, Any]],
26974
27596
  groups: list[list[ViewFilterRulePatch]],
26975
27597
  ) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
26976
27598
  translated_groups: list[list[dict[str, Any]]] = []
@@ -26984,14 +27606,14 @@ def _build_view_button_limit_groups(
26984
27606
  raw_rule = raw_rule.model_dump(mode="json")
26985
27607
  if not isinstance(raw_rule, dict):
26986
27608
  continue
26987
- field_name = str(raw_rule.get("field_name") or "").strip()
26988
- field = current_fields_by_name.get(field_name)
27609
+ rule_que_id = _coerce_positive_int(raw_rule.get("que_id"))
27610
+ field = fields_by_que_id.get(rule_que_id) if rule_que_id is not None else None
26989
27611
  if field is None:
26990
27612
  issues.append(
26991
27613
  {
26992
27614
  "error_code": "UNKNOWN_VIEW_FIELD",
26993
- "missing_fields": [field_name] if field_name else [],
26994
- "reason_path": "buttons[].button_limit[].field_name",
27615
+ "missing_fields": [str(raw_rule.get("que_id") or "")],
27616
+ "reason_path": "buttons[].button_limit[].que_id",
26995
27617
  }
26996
27618
  )
26997
27619
  continue
@@ -27162,7 +27784,7 @@ def _custom_button_view_configs_from_view_summaries(views: list[dict[str, Any]])
27162
27784
  placement = _public_view_button_placement(entry.get("config_type")) or entry.get("placement")
27163
27785
  raw_button_limit = deepcopy(entry.get("button_limit") or [])
27164
27786
  public_button_limit = (
27165
- _public_view_filter_groups_from_match_rules(raw_button_limit, current_fields_by_name=field_lookup)
27787
+ _public_view_filter_groups_from_match_rules(raw_button_limit, fields_by_que_id=field_lookup)
27166
27788
  if raw_button_limit
27167
27789
  else []
27168
27790
  )
@@ -27360,6 +27982,11 @@ def _field_id_for_name(fields: list[dict[str, Any]], name: str) -> str:
27360
27982
  for field in fields:
27361
27983
  if field["name"] == name:
27362
27984
  return field["field_id"]
27985
+ # 表格子字段是视图列/筛选的合法引用面:顶层未命中时递归 subfields
27986
+ for field in fields:
27987
+ for sub in field.get("subfields") or []:
27988
+ if isinstance(sub, dict) and sub.get("name") == name:
27989
+ return str(sub.get("field_id") or f"field_{sub.get('que_id') or ''}")
27363
27990
  raise KeyError(name)
27364
27991
 
27365
27992
 
@@ -27459,7 +28086,7 @@ def _build_public_workflow_spec(*, nodes: list[dict[str, Any]], transitions: lis
27459
28086
 
27460
28087
  def _build_flow_condition_matrix(
27461
28088
  *,
27462
- current_fields_by_name: dict[str, dict[str, Any]],
28089
+ fields_by_que_id: dict[str, dict[str, Any]],
27463
28090
  node: dict[str, Any],
27464
28091
  ) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
27465
28092
  if str(node.get("type") or "") != "condition":
@@ -27477,7 +28104,7 @@ def _build_flow_condition_matrix(
27477
28104
  if not isinstance(raw_rule, dict):
27478
28105
  continue
27479
28106
  field_name = str(raw_rule.get("field_name") or "").strip()
27480
- field = current_fields_by_name.get(field_name)
28107
+ field = fields_by_que_id.get(field_name)
27481
28108
  if field is None:
27482
28109
  issues.append(
27483
28110
  {
@@ -27524,7 +28151,9 @@ def _translate_flow_condition_rule(*, field: dict[str, Any], rule: dict[str, Any
27524
28151
  base["judgeType"] = JUDGE_LESS_OR_EQUAL
27525
28152
  base["judgeValues"] = [_stringify_condition_value(values[0])]
27526
28153
  elif operator == "is_empty":
27527
- base["judgeType"] = JUDGE_EQUAL
28154
+ # 选项/成员/部门类字段的空值,后端历史形态是 EQUAL_ANY(9)+空值(该类字段用
28155
+ # ANY 语义选值,"没选任何值"也是 9 判型);文本/数字类才是 EQUAL(0)+空值
28156
+ base["judgeType"] = JUDGE_EQUAL_ANY if field_type in INCLUDE_ANY_FLOW_FIELD_TYPES or field_type in {FieldType.single_select.value} else JUDGE_EQUAL
27528
28157
  base["judgeValues"] = []
27529
28158
  elif operator == "not_empty":
27530
28159
  base["judgeType"] = JUDGE_UNEQUAL
@@ -27553,7 +28182,7 @@ def _empty_view_query_conditions_payload() -> dict[str, Any]:
27553
28182
 
27554
28183
  def _build_view_query_conditions_payload(
27555
28184
  *,
27556
- current_fields_by_name: dict[str, dict[str, Any]],
28185
+ fields_by_que_id: dict[str, dict[str, Any]],
27557
28186
  query_conditions: Any,
27558
28187
  ) -> tuple[dict[str, Any] | None, dict[str, Any] | None, list[dict[str, Any]]]:
27559
28188
  if query_conditions is None:
@@ -27593,7 +28222,7 @@ def _build_view_query_conditions_payload(
27593
28222
  compiled_row: list[int] = []
27594
28223
  for column_index, selector in enumerate(row):
27595
28224
  field, issue = _resolve_query_condition_field(
27596
- current_fields_by_name=current_fields_by_name,
28225
+ fields_by_que_id=fields_by_que_id,
27597
28226
  selector=selector,
27598
28227
  reason_path=f"query_conditions.rows[{row_index}][{column_index}]",
27599
28228
  )
@@ -27611,14 +28240,16 @@ def _build_view_query_conditions_payload(
27611
28240
  "queryConditionStatus": enabled,
27612
28241
  "queryConditionExact": exact,
27613
28242
  "hideBeforeQueryCondition": hide_before_query,
27614
- "queryCondition": rows if enabled else [],
28243
+ # 后端 ViewgraphInfo.queryCondition 是 List<ViewgraphQueryConditionConfig>({title?,queIds}),
28244
+ # 不能发裸 [[queId]](fastjson expect {, actual [ → 发现 #7)
28245
+ "queryCondition": [{"queIds": row} for row in rows] if enabled else [],
27615
28246
  }
27616
28247
  return payload, _normalize_view_query_conditions_for_compare(payload), []
27617
28248
 
27618
28249
 
27619
28250
  def _resolve_query_condition_field(
27620
28251
  *,
27621
- current_fields_by_name: dict[str, dict[str, Any]],
28252
+ fields_by_que_id: dict[str, dict[str, Any]],
27622
28253
  selector: Any,
27623
28254
  reason_path: str,
27624
28255
  ) -> tuple[dict[str, Any], dict[str, Any] | None]:
@@ -27630,8 +28261,8 @@ def _resolve_query_condition_field(
27630
28261
  "missing_fields": [],
27631
28262
  "message": "query condition field selector cannot be empty",
27632
28263
  }
27633
- fields = [field for field in current_fields_by_name.values() if isinstance(field, dict)]
27634
- field = current_fields_by_name.get(raw)
28264
+ fields = [field for field in fields_by_que_id.values() if isinstance(field, dict)]
28265
+ field = fields_by_que_id.get(raw)
27635
28266
  if field is None:
27636
28267
  for candidate in fields:
27637
28268
  if raw == str(candidate.get("field_id") or "").strip():
@@ -27642,7 +28273,7 @@ def _resolve_query_condition_field(
27642
28273
  field = candidate
27643
28274
  break
27644
28275
  if field is None:
27645
- subfield_parent = _find_subtable_subfield_parent(current_fields_by_name=current_fields_by_name, selector=raw)
28276
+ subfield_parent = _find_subtable_subfield_parent(fields_by_que_id=fields_by_que_id, selector=raw)
27646
28277
  if subfield_parent is not None:
27647
28278
  return {}, {
27648
28279
  "error_code": "INVALID_QUERY_CONDITION_FIELD",
@@ -27690,10 +28321,10 @@ def _resolve_query_condition_field(
27690
28321
 
27691
28322
  def _find_subtable_subfield_parent(
27692
28323
  *,
27693
- current_fields_by_name: dict[str, dict[str, Any]],
28324
+ fields_by_que_id: dict[str, dict[str, Any]],
27694
28325
  selector: str,
27695
28326
  ) -> dict[str, Any] | None:
27696
- for field in current_fields_by_name.values():
28327
+ for field in fields_by_que_id.values():
27697
28328
  if not isinstance(field, dict) or str(field.get("type") or "") != FieldType.subtable.value:
27698
28329
  continue
27699
28330
  for subfield in field.get("subfields") or []:
@@ -27754,6 +28385,9 @@ def _normalize_view_query_conditions_for_compare(config: Any) -> dict[str, Any]:
27754
28385
  rows: list[list[int]] = []
27755
28386
  if isinstance(raw_rows, list):
27756
28387
  for raw_row in raw_rows:
28388
+ if isinstance(raw_row, dict):
28389
+ # ViewgraphQueryConditionConfig 形态({title?,queIds})
28390
+ raw_row = raw_row.get("queIds") or []
27757
28391
  if not isinstance(raw_row, list):
27758
28392
  continue
27759
28393
  compiled_row = [
@@ -27816,11 +28450,18 @@ def _build_view_associated_resources_payload(
27816
28450
  ]
27817
28451
  available_by_id = _associated_resource_index(available_resources)
27818
28452
  if limit_type == "all":
28453
+ # limitType=ALL 时后端保留原 idList 不清(语义忽略但存储保留);
28454
+ # 透传声明的 id 列表,避免 apply 清空→回读 idList 不匹配→READBACK_MISMATCH
28455
+ all_ids = [
28456
+ item_id
28457
+ for item_id in (_coerce_positive_int(raw) for raw in raw_item_ids)
28458
+ if item_id is not None
28459
+ ]
27819
28460
  payload = {
27820
28461
  "asosChartVisible": True,
27821
28462
  "asosChartConfig": {
27822
28463
  "limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_ALL,
27823
- "asosChartIdList": [],
28464
+ "asosChartIdList": all_ids,
27824
28465
  },
27825
28466
  }
27826
28467
  expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
@@ -27841,26 +28482,19 @@ def _build_view_associated_resources_payload(
27841
28482
  ]
27842
28483
  item_ids.append(item_id)
27843
28484
  missing_ids = [item_id for item_id in item_ids if item_id not in available_by_id]
27844
- if missing_ids:
27845
- return None, None, [
27846
- {
27847
- "error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
27848
- "reason_path": "associated_resources.associated_item_ids",
27849
- "missing_fields": [],
27850
- "invalid_associated_item_ids": missing_ids,
27851
- "available_associated_item_ids": sorted(available_by_id),
27852
- "message": "associated_resource references ids that are not in the app-level associated resource pool",
27853
- "next_action": "prefer app_associated_resources_apply for associated report/view display; it can resolve associated_item_id, chart_id/chart_key, or view_key",
27854
- }
27855
- ]
28485
+ # 兼容存量脏引用:关联资源 id 指向闭包外/已下架的图表或视图时,剔除后继续写而非整视图失败;
28486
+ # 剔除明细记入 expected 供回读校验与响应展示
28487
+ resolvable_ids = [item_id for item_id in item_ids if item_id in available_by_id]
27856
28488
  payload = {
27857
28489
  "asosChartVisible": True,
27858
28490
  "asosChartConfig": {
27859
28491
  "limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_SELECT,
27860
- "asosChartIdList": item_ids,
28492
+ "asosChartIdList": resolvable_ids,
27861
28493
  },
27862
28494
  }
27863
28495
  expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
28496
+ if isinstance(expected, dict) and missing_ids:
28497
+ expected["dropped_unresolvable_associated_item_ids"] = missing_ids
27864
28498
  return payload, expected, []
27865
28499
 
27866
28500
 
@@ -28597,6 +29231,10 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
28597
29231
  "view_key": "view_key",
28598
29232
  "viewKey": "view_key",
28599
29233
  "viewgraphKey": "view_key",
29234
+ "pinned": "pinned",
29235
+ "fixed": "pinned",
29236
+ "is_pinned": "pinned",
29237
+ "isPinned": "pinned",
28600
29238
  "type": "type",
28601
29239
  "view_type": "type",
28602
29240
  "viewType": "type",
@@ -28604,17 +29242,25 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
28604
29242
  "column_names": "columns",
28605
29243
  "columnNames": "columns",
28606
29244
  "fields": "columns",
28607
- "group_by": "group_by",
28608
- "groupBy": "group_by",
29245
+ "group_que_id": "group_que_id",
29246
+ "groupQueId": "group_que_id",
29247
+ "group_by": "group_que_id",
29248
+ "groupBy": "group_que_id",
28609
29249
  "filters": "filters",
28610
29250
  "filter_rules": "filters",
28611
29251
  "filterRules": "filters",
28612
- "start_field": "start_field",
28613
- "startField": "start_field",
28614
- "end_field": "end_field",
28615
- "endField": "end_field",
28616
- "title_field": "title_field",
28617
- "titleField": "title_field",
29252
+ "start_que_id": "start_que_id",
29253
+ "startQueId": "start_que_id",
29254
+ "start_field": "start_que_id",
29255
+ "startField": "start_que_id",
29256
+ "end_que_id": "end_que_id",
29257
+ "endQueId": "end_que_id",
29258
+ "end_field": "end_que_id",
29259
+ "endField": "end_que_id",
29260
+ "title_que_id": "title_que_id",
29261
+ "titleQueId": "title_que_id",
29262
+ "title_field": "title_que_id",
29263
+ "titleField": "title_que_id",
28618
29264
  "buttons": "buttons",
28619
29265
  "visibility": "visibility",
28620
29266
  "query_conditions": "query_conditions",
@@ -28701,13 +29347,14 @@ def _normalize_view_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[
28701
29347
 
28702
29348
  _VIEW_PARTIAL_SET_KEYS = {
28703
29349
  "name",
29350
+ "pinned",
28704
29351
  "type",
28705
29352
  "columns",
28706
- "group_by",
29353
+ "group_que_id",
28707
29354
  "filters",
28708
- "start_field",
28709
- "end_field",
28710
- "title_field",
29355
+ "start_que_id",
29356
+ "end_que_id",
29357
+ "title_que_id",
28711
29358
  "buttons",
28712
29359
  "visibility",
28713
29360
  "query_conditions",
@@ -28733,6 +29380,108 @@ def _field_name_by_id(field_names_by_id: dict[int, str], field_id: Any) -> str |
28733
29380
  return name or None
28734
29381
 
28735
29382
 
29383
+ def _collect_fields_by_que_id(fields: list[dict[str, Any]], *, schema: dict[str, Any] | None = None) -> dict[int, dict[str, Any]]:
29384
+ """构建 queId→field 索引。
29385
+
29386
+ 传 schema 时直接从原始 formQues/baseQues 递归收集(同名冲突不丢字段——名字索引
29387
+ 只保留第一个,但同名不同 queId 的字段对 queId 引用各自有效);
29388
+ 不传 schema 时从解析后的 fields 列表递归 subfields 构建。
29389
+ """
29390
+ index: dict[int, dict[str, Any]] = {}
29391
+
29392
+ type_by_question_type = {v: k for k, v in QUESTION_TYPE_TO_FIELD_TYPE.items()}
29393
+
29394
+ def collect_raw(question: dict[str, Any]) -> None:
29395
+ que_id = _coerce_positive_int(question.get("queId"))
29396
+ if que_id is not None and que_id not in index:
29397
+ que_type = _coerce_positive_int(question.get("queType"))
29398
+ # 用 _parse_field 解析(含 options/option_details,与 _parse_schema 对齐);
29399
+ # 直收原始 dict 的 queTitle/queType 不够——选项字段需 options 才能解析值→id
29400
+ parsed = _parse_field(question)
29401
+ index[que_id] = parsed
29402
+ for key in ("subQuestions", "innerQuestions"):
29403
+ for child in question.get(key) or []:
29404
+ if isinstance(child, list):
29405
+ for inner in child:
29406
+ if isinstance(inner, dict):
29407
+ collect_raw(inner)
29408
+ elif isinstance(child, dict):
29409
+ collect_raw(child)
29410
+
29411
+ if schema is not None:
29412
+ for base_question in schema.get("baseQues", []) or []:
29413
+ if isinstance(base_question, dict):
29414
+ collect_raw(base_question)
29415
+ for line in schema.get("formQues", []) or []:
29416
+ for question in (line if isinstance(line, list) else [line]):
29417
+ if isinstance(question, dict):
29418
+ collect_raw(question)
29419
+ return index
29420
+
29421
+ def collect(field: dict[str, Any]) -> None:
29422
+ que_id = _coerce_positive_int(field.get("que_id"))
29423
+ if que_id is not None and que_id not in index:
29424
+ index[que_id] = field
29425
+ for sub in field.get("subfields") or []:
29426
+ if isinstance(sub, dict):
29427
+ collect(sub)
29428
+
29429
+ for field in fields:
29430
+ if isinstance(field, dict):
29431
+ collect(field)
29432
+ return index
29433
+
29434
+
29435
+ def _collect_field_id_to_name(schema: dict[str, Any]) -> dict[int, str]:
29436
+ """从 form schema 收集 queId→queTitle(含子字段/段落内字段;同名时各 id 独立映射)。"""
29437
+ id_to_name: dict[int, str] = {}
29438
+
29439
+ def collect(question: dict[str, Any]) -> None:
29440
+ que_id = _coerce_positive_int(question.get("queId"))
29441
+ if que_id is not None and que_id not in id_to_name:
29442
+ title = str(question.get("queTitle") or "").strip()
29443
+ if title:
29444
+ id_to_name[que_id] = title
29445
+ for key in ("subQuestions", "innerQuestions"):
29446
+ for child in question.get(key) or []:
29447
+ if isinstance(child, list):
29448
+ for inner in child:
29449
+ if isinstance(inner, dict):
29450
+ collect(inner)
29451
+ elif isinstance(child, dict):
29452
+ collect(child)
29453
+
29454
+ for base_question in schema.get("baseQues", []) or []:
29455
+ if isinstance(base_question, dict):
29456
+ collect(base_question)
29457
+ for line in schema.get("formQues", []) or []:
29458
+ for question in (line if isinstance(line, list) else [line]):
29459
+ if isinstance(question, dict):
29460
+ collect(question)
29461
+ return id_to_name
29462
+
29463
+
29464
+ def _resolve_field_ref_preserving(
29465
+ config: dict[str, Any],
29466
+ config_key: str,
29467
+ name: str | None,
29468
+ field_map: dict[str, int],
29469
+ id_to_name: dict[int, str],
29470
+ ) -> int | None:
29471
+ """同名字段歧义消解(字段引用类:card title/cover、board group 等)。
29472
+
29473
+ 契约按字段名引用,同 form 内同名字段时按名解析会取到错误的一个。
29474
+ UPDATE 场景读面原 queId 可用:若原 queId 的字段名与声明名一致,保留原 queId(保真);
29475
+ 只有声明名与原值名字不同(用户真改了字段)才按名重新解析。
29476
+ """
29477
+ if not name:
29478
+ return None
29479
+ original = _coerce_positive_int(config.get(config_key))
29480
+ if original is not None and id_to_name.get(original) == name:
29481
+ return original
29482
+ return field_map.get(name)
29483
+
29484
+
28736
29485
  def _view_upsert_payload_from_existing_view(
28737
29486
  *,
28738
29487
  config: dict[str, Any],
@@ -28742,25 +29491,28 @@ def _view_upsert_payload_from_existing_view(
28742
29491
  ) -> dict[str, Any]:
28743
29492
  view_type = _normalize_view_type_name(config.get("viewgraphType") or summary.get("type"))
28744
29493
  configured_columns = [
28745
- name
28746
- for name in (_field_name_by_id(field_names_by_id, item) for item in (config.get("viewgraphQueIds") or []))
28747
- if name and name not in _KNOWN_SYSTEM_VIEW_COLUMNS
29494
+ que_id
29495
+ for item in (config.get("viewgraphQueIds") or [])
29496
+ if (que_id := _coerce_positive_int(item)) is not None
29497
+ and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_COLUMNS
28748
29498
  ]
28749
29499
  columns = [
28750
- str(name or "").strip()
28751
- for name in (summary.get("apply_columns") or summary.get("columns") or [])
28752
- if str(name or "").strip() and str(name or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
29500
+ que_id
29501
+ for item in (summary.get("apply_column_ids") or summary.get("columns") or [])
29502
+ if (que_id := _coerce_positive_int(item)) is not None
29503
+ and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_COLUMNS
28753
29504
  ]
28754
29505
  if configured_columns and (not columns or len(configured_columns) >= len(columns)):
28755
29506
  columns = configured_columns
28756
29507
  display_config = summary.get("display_config") if isinstance(summary.get("display_config"), dict) else {}
28757
- group_by = str(summary.get("group_by") or display_config.get("group_by") or "").strip() or _field_name_by_id(field_names_by_id, config.get("groupQueId"))
28758
- title_field = str(display_config.get("title_field") or "").strip() or _field_name_by_id(field_names_by_id, config.get("titleQue"))
29508
+ group_que_id = _coerce_positive_int(config.get("groupQueId") or summary.get("group_que_id") or display_config.get("group_que_id"))
29509
+ title_que_id = _coerce_positive_int(config.get("titleQue") or display_config.get("title_que_id"))
28759
29510
  gantt_config = config.get("viewgraphGanttConfigVO") if isinstance(config.get("viewgraphGanttConfigVO"), dict) else {}
28760
- start_field = _field_name_by_id(field_names_by_id, gantt_config.get("startTimeQueId")) if isinstance(gantt_config, dict) else None
28761
- end_field = _field_name_by_id(field_names_by_id, gantt_config.get("endTimeQueId")) if isinstance(gantt_config, dict) else None
28762
- gantt_title = _field_name_by_id(field_names_by_id, gantt_config.get("titleQueId")) if isinstance(gantt_config, dict) else None
29511
+ start_que_id = _coerce_positive_int(gantt_config.get("startTimeQueId"))
29512
+ end_que_id = _coerce_positive_int(gantt_config.get("endTimeQueId"))
29513
+ gantt_title_que_id = _coerce_positive_int(gantt_config.get("titleQueId"))
28763
29514
  query_conditions = _extract_view_query_conditions_config(config)
29515
+ query_conditions["rows"] = query_conditions.get("row_field_ids") or []
28764
29516
  query_conditions.pop("row_field_ids", None)
28765
29517
  associated_resources = _extract_view_associated_resources_config(config)
28766
29518
  payload: dict[str, Any] = {
@@ -28768,7 +29520,6 @@ def _view_upsert_payload_from_existing_view(
28768
29520
  "view_key": view_key,
28769
29521
  "type": view_type or "table",
28770
29522
  "columns": columns,
28771
- "group_by": group_by,
28772
29523
  "filters": [],
28773
29524
  "query_conditions": query_conditions,
28774
29525
  "associated_resources": {
@@ -28777,15 +29528,17 @@ def _view_upsert_payload_from_existing_view(
28777
29528
  "associated_item_ids": list(associated_resources.get("configured_associated_item_ids") or []),
28778
29529
  },
28779
29530
  }
28780
- if title_field:
28781
- payload["title_field"] = title_field
29531
+ if group_que_id is not None:
29532
+ payload["group_que_id"] = group_que_id
29533
+ if title_que_id is not None:
29534
+ payload["title_que_id"] = title_que_id
28782
29535
  if view_type == "gantt":
28783
- if start_field:
28784
- payload["start_field"] = start_field
28785
- if end_field:
28786
- payload["end_field"] = end_field
28787
- if gantt_title:
28788
- payload["title_field"] = gantt_title
29536
+ if start_que_id is not None:
29537
+ payload["start_que_id"] = start_que_id
29538
+ if end_que_id is not None:
29539
+ payload["end_que_id"] = end_que_id
29540
+ if gantt_title_que_id is not None:
29541
+ payload["title_que_id"] = gantt_title_que_id
28789
29542
  return payload
28790
29543
 
28791
29544
 
@@ -28796,7 +29549,7 @@ def _first_present(mapping: dict[str, Any], *keys: str) -> Any:
28796
29549
  return None
28797
29550
 
28798
29551
 
28799
- def _build_view_create_payload(
29552
+ def _build_view_creation_payload(
28800
29553
  *,
28801
29554
  app_key: str,
28802
29555
  base_info: dict[str, Any],
@@ -28804,23 +29557,24 @@ def _build_view_create_payload(
28804
29557
  patch: ViewUpsertPatch,
28805
29558
  ordinal: int,
28806
29559
  view_filters: list[list[dict[str, Any]]] | None,
28807
- current_fields_by_name: dict[str, dict[str, Any]],
29560
+ fields_by_que_id: dict[str, dict[str, Any]],
28808
29561
  auth_override: dict[str, Any] | None = None,
28809
29562
  explicit_button_dtos: list[dict[str, Any]] | None = None,
28810
29563
  query_condition_payload: dict[str, Any] | None = None,
28811
29564
  associated_resources_payload: dict[str, Any] | None = None,
29565
+ hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
28812
29566
  ) -> JSONObject:
28813
29567
  entity = _entity_spec_from_app(base_info=base_info, schema=schema, views=None)
28814
29568
  parsed_schema = _parse_schema(schema)
28815
- visible_field_names = _resolve_view_visible_field_names(patch)
28816
- field_ids = [_field_id_for_name(parsed_schema["fields"], name) for name in visible_field_names]
29569
+ visible_que_ids = _resolve_view_visible_que_ids(patch)
29570
+ field_ids = [f"field_{que}" for que in visible_que_ids]
28817
29571
  gantt_config = _build_public_gantt_payload(parsed_schema["fields"], extract_field_map(schema), patch)
28818
29572
  view_spec = ViewSpec(
28819
29573
  view_id=_slugify(patch.name, default=f"view_{uuid4().hex[:6]}"),
28820
29574
  name=patch.name,
28821
29575
  type=patch.type.value,
28822
29576
  field_ids=field_ids,
28823
- group_by_field_id=_field_id_for_name(_parse_schema(schema)["fields"], patch.group_by) if patch.group_by else None,
29577
+ group_by_field_id=f"field_{patch.group_que_id}" if patch.group_que_id else None,
28824
29578
  config=gantt_config,
28825
29579
  )
28826
29580
  from ..solution.spec_models import EntitySpec
@@ -28829,7 +29583,6 @@ def _build_view_create_payload(
28829
29583
  compiled = compile_views(EntitySpec.model_validate({**entity, "views": [view_spec.model_dump(mode="json")]}))[0]
28830
29584
  field_map = extract_field_map(schema)
28831
29585
  payload = deepcopy(compiled["create_payload"])
28832
- visible_que_ids = [field_map[field_name] for field_name in visible_field_names if field_name in field_map]
28833
29586
  payload["appKey"] = app_key
28834
29587
  payload["ordinal"] = ordinal
28835
29588
  payload["viewgraphQueIds"] = visible_que_ids
@@ -28841,17 +29594,21 @@ def _build_view_create_payload(
28841
29594
  payload["beingShowTitleQue"] = True
28842
29595
  payload["titleQue"] = gantt_config.get("titleQueId") or (visible_que_ids[0] if visible_que_ids else None)
28843
29596
  if patch.type.value == "board":
28844
- payload["groupQueId"] = field_map.get(patch.group_by or "")
29597
+ payload["groupQueId"] = patch.group_que_id
28845
29598
  return _hydrate_view_backend_payload(
28846
29599
  payload=payload,
28847
29600
  view_type=patch.type.value,
28848
29601
  visible_que_id_values=visible_que_ids,
28849
- group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
29602
+ group_que_id=patch.group_que_id,
28850
29603
  view_filters=view_filters,
28851
29604
  gantt_payload=gantt_config,
28852
29605
  button_config_dtos=explicit_button_dtos,
28853
29606
  query_condition_payload=query_condition_payload,
28854
29607
  associated_resources_payload=associated_resources_payload,
29608
+ patch=patch,
29609
+ field_map=field_map,
29610
+ fields_by_que_id=fields_by_que_id,
29611
+ hierarchy_filter_groups=hierarchy_filter_groups,
28855
29612
  )
28856
29613
 
28857
29614
 
@@ -28989,6 +29746,10 @@ def _normalize_view_type_name(value: Any) -> str:
28989
29746
  return "gantt"
28990
29747
  if "board" in normalized:
28991
29748
  return "board"
29749
+ if "hierarchy" in normalized:
29750
+ return "hierarchy"
29751
+ if "calendar" in normalized:
29752
+ return "calendar"
28992
29753
  if "card" in normalized:
28993
29754
  return "card"
28994
29755
  return "table"
@@ -29002,20 +29763,20 @@ def _build_view_update_payload(
29002
29763
  schema: dict[str, Any],
29003
29764
  patch: ViewUpsertPatch,
29004
29765
  view_filters: list[list[dict[str, Any]]] | None,
29005
- current_fields_by_name: dict[str, dict[str, Any]],
29766
+ fields_by_que_id: dict[str, dict[str, Any]],
29006
29767
  auth_override: dict[str, Any] | None = None,
29007
29768
  explicit_button_dtos: list[dict[str, Any]] | None = None,
29008
29769
  query_condition_payload: dict[str, Any] | None = None,
29009
29770
  associated_resources_payload: dict[str, Any] | None = None,
29771
+ hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
29010
29772
  ) -> JSONObject:
29011
29773
  config_response = views.view_get_config(profile=profile, viewgraph_key=source_viewgraph_key)
29012
29774
  config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
29013
29775
  payload = deepcopy(config)
29014
29776
  parsed_schema = _parse_schema(schema)
29015
29777
  field_map = extract_field_map(schema)
29016
- visible_field_names = _resolve_view_visible_field_names(patch)
29017
- visible_que_ids = [_field_id_for_name(parsed_schema["fields"], name) for name in visible_field_names]
29018
- visible_que_id_values = [field_map[name] for name in visible_field_names if name in field_map]
29778
+ visible_que_ids = _resolve_view_visible_que_ids(patch)
29779
+ visible_que_id_values = list(visible_que_ids)
29019
29780
  gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
29020
29781
 
29021
29782
  for key in (
@@ -29037,8 +29798,25 @@ def _build_view_update_payload(
29037
29798
  payload["auth"] = deepcopy(auth_override if isinstance(auth_override, dict) else payload.get("auth") or default_member_auth())
29038
29799
  if "viewName" in payload:
29039
29800
  payload["viewName"] = patch.name
29040
- payload["viewgraphQueIds"] = visible_que_id_values
29041
- payload["viewgraphQuestions"] = _build_viewgraph_questions(schema, visible_que_id_values)
29801
+ # 保留系统列在业务列之间的位置;业务列顺序和增删仍由声明决定。
29802
+ existing_que_ids = dict.fromkeys(
29803
+ que_id
29804
+ for que_id in (_coerce_any_int(raw) for raw in (config.get("viewgraphQueIds") or []))
29805
+ if que_id is not None
29806
+ )
29807
+ business_que_ids = iter(que_id for que_id in visible_que_id_values if que_id > 5)
29808
+ merged_que_ids: list[int] = []
29809
+ for que_id in existing_que_ids:
29810
+ if que_id <= 5:
29811
+ merged_que_ids.append(que_id)
29812
+ elif que_id in visible_que_id_values:
29813
+ merged_que_ids.append(next(business_que_ids))
29814
+ merged_que_ids.extend(business_que_ids)
29815
+ merged_que_ids.extend(que_id for que_id in visible_que_id_values if que_id <= 5 and que_id not in merged_que_ids)
29816
+ payload["viewgraphQueIds"] = merged_que_ids
29817
+ payload["viewgraphQuestions"] = _build_viewgraph_questions(
29818
+ schema, merged_que_ids, existing_by_que_id=_flatten_viewgraph_questions(config.get("viewgraphQuestions"))
29819
+ )
29042
29820
  payload.setdefault("sortType", "defaultSort")
29043
29821
  payload.setdefault("viewgraphSorts", [{"queId": 0, "beingSortAscend": True, "queType": 8}])
29044
29822
  payload.setdefault("beingPinNavigate", True)
@@ -29068,25 +29846,79 @@ def _build_view_update_payload(
29068
29846
  payload["viewgraphType"] = "boardView"
29069
29847
  payload["beingShowTitleQue"] = True
29070
29848
  payload["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
29071
- payload["groupQueId"] = field_map.get(patch.group_by or "")
29849
+ payload["groupQueId"] = patch.group_que_id
29072
29850
  elif normalized_type == "gantt":
29073
29851
  payload["viewgraphType"] = "ganttView"
29074
29852
  payload["beingShowTitleQue"] = True
29075
29853
  payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
29076
29854
  payload.pop("groupQueId", None)
29855
+ elif normalized_type == "hierarchy":
29856
+ payload["viewgraphType"] = "hierarchyView"
29857
+ payload["beingShowTitleQue"] = True
29858
+ payload.pop("groupQueId", None)
29859
+ elif normalized_type == "calendar":
29860
+ payload["viewgraphType"] = "calendarView"
29861
+ payload["beingShowTitleQue"] = True
29862
+ payload.pop("groupQueId", None)
29077
29863
  return _hydrate_view_backend_payload(
29078
29864
  payload=payload,
29079
29865
  view_type=normalized_type,
29080
29866
  visible_que_id_values=visible_que_id_values,
29081
- group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
29867
+ group_que_id=patch.group_que_id,
29082
29868
  view_filters=view_filters,
29083
29869
  gantt_payload=gantt_payload,
29084
29870
  button_config_dtos=button_config_dtos,
29085
29871
  query_condition_payload=query_condition_payload,
29086
29872
  associated_resources_payload=associated_resources_payload,
29873
+ patch=patch,
29874
+ field_map=field_map,
29875
+ fields_by_que_id=fields_by_que_id,
29876
+ hierarchy_filter_groups=hierarchy_filter_groups,
29877
+ schema=schema,
29087
29878
  )
29088
29879
 
29089
29880
 
29881
+ # 系统列 queId→公开名(后端语义;读面展示与 viewgraphQueIds 透传时的名称解析)
29882
+ _SYSTEM_VIEW_QUE_NAMES = {
29883
+ 0: "编号",
29884
+ 1: "提交人",
29885
+ 2: "提交时间",
29886
+ 3: "更新时间",
29887
+ 4: "流程标题",
29888
+ 5: "当前流程状态",
29889
+ -17: "数据ID",
29890
+ -19: "删除时间",
29891
+ -20: "删除人",
29892
+ -21: "处理节点",
29893
+ -22: "抄送节点",
29894
+ -23: "处理时间",
29895
+ -24: "抄送时间",
29896
+ -25: "接收时间",
29897
+ }
29898
+
29899
+ _SYSTEM_VIEW_QUE_IDS = frozenset(
29900
+ {
29901
+ *_SYSTEM_VIEW_QUE_NAMES,
29902
+ -1,
29903
+ -2,
29904
+ -3,
29905
+ -4,
29906
+ -5,
29907
+ -6,
29908
+ -7,
29909
+ -8,
29910
+ -18,
29911
+ -30,
29912
+ -31,
29913
+ }
29914
+ )
29915
+
29916
+
29917
+ def _is_known_system_view_que_id(value: Any) -> bool:
29918
+ que_id = _coerce_any_int(value)
29919
+ return que_id in _SYSTEM_VIEW_QUE_IDS if que_id is not None else False
29920
+
29921
+
29090
29922
  _KNOWN_SYSTEM_VIEW_COLUMNS = {
29091
29923
  "编号",
29092
29924
  "当前流程状态",
@@ -29110,16 +29942,16 @@ def _build_minimal_view_payload(
29110
29942
  patch: ViewUpsertPatch,
29111
29943
  ordinal: int,
29112
29944
  view_filters: list[list[dict[str, Any]]] | None,
29113
- current_fields_by_name: dict[str, dict[str, Any]],
29945
+ fields_by_que_id: dict[str, dict[str, Any]],
29114
29946
  auth_override: dict[str, Any] | None = None,
29115
29947
  explicit_button_dtos: list[dict[str, Any]] | None = None,
29116
29948
  query_condition_payload: dict[str, Any] | None = None,
29117
29949
  associated_resources_payload: dict[str, Any] | None = None,
29950
+ hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
29118
29951
  ) -> JSONObject:
29119
29952
  field_map = extract_field_map(schema)
29120
29953
  parsed_schema = _parse_schema(schema)
29121
- visible_field_names = _resolve_view_visible_field_names(patch)
29122
- visible_que_id_values = [field_map[name] for name in visible_field_names if name in field_map]
29954
+ visible_que_id_values = _resolve_view_visible_que_ids(patch)
29123
29955
  gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
29124
29956
  payload: JSONObject = {
29125
29957
  "appKey": app_key,
@@ -29129,6 +29961,8 @@ def _build_minimal_view_payload(
29129
29961
  "card": "cardView",
29130
29962
  "board": "boardView",
29131
29963
  "gantt": "ganttView",
29964
+ "hierarchy": "hierarchyView",
29965
+ "calendar": "calendarView",
29132
29966
  }[patch.type.value],
29133
29967
  "ordinal": ordinal,
29134
29968
  "viewgraphQueIds": visible_que_id_values,
@@ -29139,17 +29973,21 @@ def _build_minimal_view_payload(
29139
29973
  payload["beingShowTitleQue"] = True
29140
29974
  payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
29141
29975
  if patch.type.value == "board":
29142
- payload["groupQueId"] = field_map.get(patch.group_by or "")
29976
+ payload["groupQueId"] = patch.group_que_id
29143
29977
  return _hydrate_view_backend_payload(
29144
29978
  payload=payload,
29145
29979
  view_type=patch.type.value,
29146
29980
  visible_que_id_values=visible_que_id_values,
29147
- group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
29981
+ group_que_id=patch.group_que_id,
29148
29982
  view_filters=view_filters,
29149
29983
  gantt_payload=gantt_payload,
29150
29984
  button_config_dtos=explicit_button_dtos,
29151
29985
  query_condition_payload=query_condition_payload,
29152
29986
  associated_resources_payload=associated_resources_payload,
29987
+ patch=patch,
29988
+ field_map=field_map,
29989
+ fields_by_que_id=fields_by_que_id,
29990
+ hierarchy_filter_groups=hierarchy_filter_groups,
29153
29991
  )
29154
29992
 
29155
29993
 
@@ -29164,6 +30002,11 @@ def _hydrate_view_backend_payload(
29164
30002
  button_config_dtos: list[dict[str, Any]] | None = None,
29165
30003
  query_condition_payload: dict[str, Any] | None = None,
29166
30004
  associated_resources_payload: dict[str, Any] | None = None,
30005
+ patch: ViewUpsertPatch | None = None,
30006
+ field_map: dict[str, int] | None = None,
30007
+ fields_by_que_id: dict[str, dict[str, Any]] | None = None,
30008
+ hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
30009
+ schema: dict[str, Any] | None = None,
29167
30010
  ) -> JSONObject:
29168
30011
  data = deepcopy(payload)
29169
30012
  data.setdefault("beingPinNavigate", True)
@@ -29233,26 +30076,212 @@ def _hydrate_view_backend_payload(
29233
30076
  data["titleQue"] = (gantt_payload or {}).get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
29234
30077
  data["groupQueId"] = None
29235
30078
  data["viewgraphGanttConfigVO"] = deepcopy(gantt_payload) if gantt_payload else None
30079
+ elif view_type == "hierarchy":
30080
+ data["viewgraphType"] = "hierarchyView"
30081
+ data["beingShowTitleQue"] = True
30082
+ data["groupQueId"] = None
30083
+ data["viewgraphGanttConfigVO"] = None
30084
+ elif view_type == "calendar":
30085
+ data["viewgraphType"] = "calendarView"
30086
+ data["beingShowTitleQue"] = True
30087
+ data["groupQueId"] = None
30088
+ data["viewgraphGanttConfigVO"] = None
29236
30089
  if view_filters is not None:
29237
30090
  data["viewgraphLimit"] = deepcopy(view_filters)
29238
30091
  data["viewgraphLimitType"] = 1
29239
30092
  data["viewgraphLimitFormula"] = ""
29240
30093
  data["dataPermissionType"] = "CUSTOM"
29241
30094
  data["dataScope"] = "CUSTOM" if view_filters else "ALL"
30095
+ if patch is not None:
30096
+ _apply_view_config_groups(
30097
+ data,
30098
+ patch=patch,
30099
+ view_type=view_type,
30100
+ visible_que_id_values=visible_que_id_values,
30101
+ field_map=field_map or {},
30102
+ fields_by_que_id=fields_by_que_id or {},
30103
+ hierarchy_filter_groups=hierarchy_filter_groups,
30104
+ config=payload if isinstance(payload, dict) else None,
30105
+ id_to_name=_collect_field_id_to_name(schema) if isinstance(schema, dict) else None,
30106
+ )
29242
30107
  _apply_view_query_conditions_payload(data, query_condition_payload)
29243
30108
  _apply_view_associated_resources_payload(data, associated_resources_payload)
29244
30109
  return data
29245
30110
 
29246
30111
 
29247
- def _resolve_view_visible_field_names(patch: ViewUpsertPatch) -> list[str]:
29248
- ordered: list[str] = []
29249
- for value in [*patch.columns, patch.title_field, patch.start_field, patch.end_field, patch.group_by]:
29250
- name = str(value or "").strip()
29251
- if name and name not in _KNOWN_SYSTEM_VIEW_COLUMNS and name not in ordered:
29252
- ordered.append(name)
30112
+ _DATA_PERMISSION_SCOPE_MAP = {
30113
+ "all": "ALL",
30114
+ "oneself": "ONESELF",
30115
+ "dept": "DEPT",
30116
+ "subordinate": "SUBORDINATE",
30117
+ "custom": "CUSTOM",
30118
+ }
30119
+
30120
+
30121
+ def _apply_view_config_groups(
30122
+ data: JSONObject,
30123
+ *,
30124
+ patch: ViewUpsertPatch,
30125
+ view_type: str,
30126
+ visible_que_id_values: list[int],
30127
+ field_map: dict[str, int],
30128
+ fields_by_que_id: dict[str, dict[str, Any]],
30129
+ hierarchy_filter_groups: list[list[dict[str, Any]]] | None,
30130
+ config: dict[str, Any] | None = None,
30131
+ id_to_name: dict[int, str] | None = None,
30132
+ ) -> None:
30133
+ """编译 complete 档新字段组:data_permission/sort/card/hierarchy/display/record/share。"""
30134
+ if patch.data_permission is not None:
30135
+ data["dataPermissionType"] = "CONFIG" if patch.data_permission.type == "config" else "CUSTOM"
30136
+ data["dataScope"] = _DATA_PERMISSION_SCOPE_MAP.get(patch.data_permission.scope, "ALL")
30137
+ if patch.sort is not None:
30138
+ if patch.sort:
30139
+ sorts: list[dict[str, Any]] = []
30140
+ for rule in patch.sort:
30141
+ field = fields_by_que_id.get(rule.que_id) or {}
30142
+ que_type = _coerce_positive_int(field.get("que_type")) or FIELD_TYPE_TO_QUESTION_TYPE.get(
30143
+ str(field.get("type") or FieldType.text.value), 2
30144
+ )
30145
+ sorts.append(
30146
+ {
30147
+ "queId": rule.que_id,
30148
+ "beingSortAscend": bool(rule.ascending),
30149
+ "queType": que_type,
30150
+ }
30151
+ )
30152
+ data["sortType"] = "customSort"
30153
+ data["viewgraphSorts"] = sorts
30154
+ else:
30155
+ data["sortType"] = "defaultSort"
30156
+ data["viewgraphSorts"] = [{"queId": 0, "beingSortAscend": True, "queType": 8}]
30157
+ card = patch.card
30158
+ config = config or {}
30159
+ id_to_name = id_to_name or {}
30160
+ if card is not None:
30161
+ if card.title_que_id is not None:
30162
+ data["titleQue"] = card.title_que_id
30163
+ if card.cover_que_id is not None:
30164
+ data["frontCoverQueId"] = card.cover_que_id
30165
+ if card.clipping_mode is not None:
30166
+ data["clippingMode"] = card.clipping_mode
30167
+ if card.image_adaption is not None:
30168
+ data["beingImageAdaption"] = card.image_adaption
30169
+ if card.show_field_title is not None:
30170
+ data["beingShowQueTitle"] = card.show_field_title
30171
+ if card.show_title is not None:
30172
+ data["beingShowTitleQue"] = card.show_title
30173
+ if card.show_cover is not None:
30174
+ data["beingShowCover"] = card.show_cover
30175
+ if card.group_que_id is not None:
30176
+ data["groupQueId"] = card.group_que_id
30177
+ if card.group_color is not None:
30178
+ data["beingGroupColor"] = card.group_color
30179
+ if patch.hierarchy is not None and view_type == "hierarchy":
30180
+ data["viewgraphHierarchyConfigVO"] = {
30181
+ "titleQueId": patch.hierarchy.title_que_id,
30182
+ "childrenQueId": patch.hierarchy.children_que_id,
30183
+ "rootNodeFilter": deepcopy(hierarchy_filter_groups) if hierarchy_filter_groups is not None else [],
30184
+ }
30185
+ data["titleQue"] = patch.hierarchy.title_que_id
30186
+ if patch.calendar is not None and view_type == "calendar":
30187
+ data["viewgraphCalendarConfigVO"] = {
30188
+ "titleQueId": patch.calendar.title_que_id,
30189
+ "dateQueId": patch.calendar.date_que_id,
30190
+ "colorQueId": patch.calendar.color_que_id,
30191
+ }
30192
+ data["titleQue"] = patch.calendar.title_que_id
30193
+ display = patch.display
30194
+ if display is not None:
30195
+ if display.row_height is not None:
30196
+ data["defaultRowHigh"] = display.row_height
30197
+ if display.pin_navigate is not None:
30198
+ data["beingPinNavigate"] = display.pin_navigate
30199
+ if display.remark is not None:
30200
+ data["remark"] = display.remark
30201
+ record = patch.record
30202
+ if record is not None:
30203
+ if record.audit_visible is not None:
30204
+ data["beingAuditRecordVisible"] = record.audit_visible
30205
+ if record.data_log_visible is not None:
30206
+ data["beingDataLogVisible"] = record.data_log_visible
30207
+ if record.qrobot_record_visible is not None:
30208
+ data["beingQrobotRecordVisible"] = record.qrobot_record_visible
30209
+ if record.workflow_future_visible is not None:
30210
+ data["beingWorkflowNodeFutureListVisible"] = record.workflow_future_visible
30211
+ if record.comment is not None:
30212
+ data["beingCommentStatus"] = record.comment
30213
+ share = patch.share
30214
+ if share is not None:
30215
+ if share.need_pass is not None:
30216
+ data["beingNeedPass"] = share.need_pass
30217
+ data["needPass"] = share.need_pass
30218
+ if share.password is None:
30219
+ # 声明未带密码(提取语义:不回传敏感值)——显式清空 payload 里读面带来的旧密码;
30220
+ # 后端 isNotBlank(空)跳过校验且不更新密码列,原密码保留(Java AbstractViewgraphStrategicService:424)
30221
+ data["viewgraphPass"] = ""
30222
+ data.pop("pass", None)
30223
+ if share.password is not None:
30224
+ data["viewgraphPass"] = share.password
30225
+ if share.force_login is not None:
30226
+ data["beingForceLogin"] = share.force_login
30227
+ if share.print_enabled is not None:
30228
+ data["beingPrintStatus"] = share.print_enabled
30229
+
30230
+
30231
+ def _iter_view_config_group_field_names(patch: ViewUpsertPatch) -> list[tuple[str, list[int]]]:
30232
+ """枚举 complete 档新字段组引用的 queId(用于 UNKNOWN_VIEW_FIELD 校验)。"""
30233
+ groups: list[tuple[str, list[int]]] = []
30234
+ if patch.sort:
30235
+ groups.append(("sort", [rule.que_id for rule in patch.sort]))
30236
+ if patch.card is not None:
30237
+ groups.append(
30238
+ (
30239
+ "card",
30240
+ [
30241
+ que
30242
+ for que in (patch.card.title_que_id, patch.card.cover_que_id, patch.card.group_que_id)
30243
+ if que
30244
+ ],
30245
+ )
30246
+ )
30247
+ if patch.gantt is not None:
30248
+ gantt_ids = [
30249
+ que
30250
+ for que in (patch.gantt.title_que_id, patch.gantt.start_que_id, patch.gantt.end_que_id)
30251
+ if que
30252
+ ]
30253
+ gantt_ids.extend(group.que_id for group in patch.gantt.groups or [])
30254
+ for dep in patch.gantt.dependencies or []:
30255
+ gantt_ids.extend([dep.dependency_que_id, dep.predecessor_que_id])
30256
+ if gantt_ids:
30257
+ groups.append(("gantt", gantt_ids))
30258
+ if patch.hierarchy is not None:
30259
+ groups.append(("hierarchy", [patch.hierarchy.title_que_id, patch.hierarchy.children_que_id]))
30260
+ if patch.calendar is not None:
30261
+ calendar_ids = [patch.calendar.title_que_id, patch.calendar.date_que_id]
30262
+ if patch.calendar.color_que_id is not None:
30263
+ calendar_ids.append(patch.calendar.color_que_id)
30264
+ groups.append(("calendar", calendar_ids))
30265
+ return groups
30266
+
30267
+
30268
+
30269
+ def _resolve_view_visible_que_ids(patch: ViewUpsertPatch) -> list[int]:
30270
+ """收集视图引用的业务字段和已知系统字段 queId。"""
30271
+ ordered: list[int] = []
30272
+ extra_ids: list[int | None] = []
30273
+ if patch.hierarchy is not None:
30274
+ extra_ids.extend([patch.hierarchy.title_que_id, patch.hierarchy.children_que_id])
30275
+ if patch.calendar is not None:
30276
+ extra_ids.extend([patch.calendar.title_que_id, patch.calendar.date_que_id, patch.calendar.color_que_id])
30277
+ for value in [*patch.columns, patch.title_que_id, patch.start_que_id, patch.end_que_id, patch.group_que_id, *extra_ids]:
30278
+ que_id = _coerce_any_int(value)
30279
+ if que_id is not None and (que_id > 0 or _is_known_system_view_que_id(que_id)) and que_id not in ordered:
30280
+ ordered.append(que_id)
29253
30281
  return ordered
29254
30282
 
29255
30283
 
30284
+
29256
30285
  def _build_public_gantt_payload(
29257
30286
  fields: list[dict[str, Any]],
29258
30287
  field_map: dict[str, int],
@@ -29260,22 +30289,35 @@ def _build_public_gantt_payload(
29260
30289
  ) -> dict[str, Any]:
29261
30290
  if patch.type.value != "gantt":
29262
30291
  return {}
29263
- title_field_name = str((patch.title_field or (patch.columns[0] if patch.columns else "")) or "").strip()
29264
- start_field_name = str(patch.start_field or "").strip()
29265
- end_field_name = str(patch.end_field or "").strip()
30292
+ gantt_config = patch.gantt
30293
+ title_que_id = (
30294
+ getattr(gantt_config, "title_que_id", None)
30295
+ or patch.title_que_id
30296
+ or (patch.columns[0] if patch.columns else None)
30297
+ )
30298
+ start_que_id = getattr(gantt_config, "start_que_id", None) or patch.start_que_id
30299
+ end_que_id = getattr(gantt_config, "end_que_id", None) or patch.end_que_id
30300
+ dependency = gantt_config.dependencies[0] if gantt_config is not None and gantt_config.dependencies else None
30301
+ auto_calibration = bool(getattr(gantt_config, "auto_calibration", False) or False)
29266
30302
  return {
29267
- "titleQueId": field_map.get(title_field_name),
29268
- "startTimeQueId": field_map.get(start_field_name),
29269
- "endTimeQueId": field_map.get(end_field_name),
29270
- "defaultTimeDimension": "week",
29271
- "ganttGroupVOList": [],
30303
+ "titleQueId": title_que_id,
30304
+ "startTimeQueId": start_que_id,
30305
+ "endTimeQueId": end_que_id,
30306
+ "defaultTimeDimension": getattr(gantt_config, "default_time_dimension", None) or "week",
30307
+ "ganttGroupVOList": [
30308
+ {
30309
+ "groupQueId": group.que_id,
30310
+ "ordinal": group.ordinal if group.ordinal is not None else index,
30311
+ }
30312
+ for index, group in enumerate((gantt_config.groups if gantt_config is not None else []), start=1)
30313
+ ],
29272
30314
  "ganttDependencyVO": {
29273
- "dependencyQueId": None,
29274
- "predecessorTaskQueId": None,
29275
- "startEndOptionId": None,
29276
- "startStartOptionId": None,
29277
- "endEndOptionId": None,
29278
- "endStartOptionId": None,
30315
+ "dependencyQueId": dependency.dependency_que_id if dependency else None,
30316
+ "predecessorTaskQueId": dependency.predecessor_que_id if dependency else None,
30317
+ "startStartOptionId": dependency.start_start_option_id if dependency else None,
30318
+ "startEndOptionId": dependency.start_end_option_id if dependency else None,
30319
+ "endEndOptionId": dependency.end_end_option_id if dependency else None,
30320
+ "endStartOptionId": dependency.end_start_option_id if dependency else None,
29279
30321
  },
29280
30322
  "ganttAutoCalibrationVO": {
29281
30323
  "autoCalibrationRuleVO": {
@@ -29286,15 +30328,15 @@ def _build_public_gantt_payload(
29286
30328
  "endStartFinish": True,
29287
30329
  "endEndFinish": False,
29288
30330
  },
29289
- "beingAutoCalibration": False,
29290
- "userAutoCalibration": False,
30331
+ "beingAutoCalibration": auto_calibration,
30332
+ "userAutoCalibration": auto_calibration,
29291
30333
  },
29292
30334
  }
29293
30335
 
29294
30336
 
29295
30337
  def _build_view_filter_groups(
29296
30338
  *,
29297
- current_fields_by_name: dict[str, dict[str, Any]],
30339
+ fields_by_que_id: dict[str, dict[str, Any]],
29298
30340
  filters: list[Any],
29299
30341
  ) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
29300
30342
  translated_rules: list[dict[str, Any]] = []
@@ -29304,14 +30346,14 @@ def _build_view_filter_groups(
29304
30346
  raw_rule = raw_rule.model_dump(mode="json")
29305
30347
  if not isinstance(raw_rule, dict):
29306
30348
  continue
29307
- field_name = str(raw_rule.get("field_name") or "").strip()
29308
- field = current_fields_by_name.get(field_name)
30349
+ rule_que_id = _coerce_positive_int(raw_rule.get("que_id"))
30350
+ field = fields_by_que_id.get(rule_que_id) if rule_que_id is not None else None
29309
30351
  if field is None:
29310
30352
  issues.append(
29311
30353
  {
29312
30354
  "error_code": "UNKNOWN_VIEW_FIELD",
29313
- "missing_fields": [field_name] if field_name else [],
29314
- "reason_path": "filters[].field_name",
30355
+ "missing_fields": [str(raw_rule.get("que_id") or "")],
30356
+ "reason_path": "filters[].que_id",
29315
30357
  }
29316
30358
  )
29317
30359
  continue
@@ -29393,7 +30435,8 @@ def _resolve_view_filter_values(
29393
30435
  unresolved_values.append(_stringify_condition_value(value))
29394
30436
  continue
29395
30437
  resolved_details.append({"id": resolved.get("id"), "value": str(resolved.get("value") or resolved.get("id"))})
29396
- if unresolved_values:
30438
+ if unresolved_values and not resolved_details:
30439
+ # 全部值都解析不出(选项已删):返回错误让上层判定(该规则已死语义)
29397
30440
  return (
29398
30441
  [],
29399
30442
  [],
@@ -29408,6 +30451,7 @@ def _resolve_view_filter_values(
29408
30451
  },
29409
30452
  },
29410
30453
  )
30454
+ # 部分值解析不出:剔除已删选项值继续写(兼容存量脏数据,不再整视图失败)
29411
30455
  return (
29412
30456
  [str(detail["id"]) for detail in resolved_details if detail.get("id") is not None],
29413
30457
  [_coerce_view_filter_value_detail(detail) for detail in resolved_details],
@@ -29451,6 +30495,10 @@ def _build_view_filter_value_details(values: list[Any]) -> list[dict[str, Any]]:
29451
30495
  details.append(_coerce_view_filter_value_detail({"id": item_id, "value": item_value if item_value is not None else str(item_id)}))
29452
30496
  elif isinstance(value, int):
29453
30497
  details.append(_coerce_view_filter_value_detail({"id": value, "value": str(value)}))
30498
+ elif isinstance(value, str):
30499
+ # text/number 字段:AnswerValue.id 是 Integer(选项 id),字符串值无 id;
30500
+ # 后端 formula getId()==null → 走 getValue() 分支构建 ""value"" 形式
30501
+ details.append(_coerce_view_filter_value_detail({"id": None, "value": value}))
29454
30502
  return details
29455
30503
 
29456
30504
 
@@ -29617,11 +30665,11 @@ def _view_filter_groups_semantic_readback(expected: Any, actual: Any) -> tuple[b
29617
30665
  def _public_view_filter_groups_from_match_rules(
29618
30666
  groups: Any,
29619
30667
  *,
29620
- current_fields_by_name: dict[str, dict[str, Any]],
30668
+ fields_by_que_id: dict[str, dict[str, Any]],
29621
30669
  ) -> list[list[dict[str, Any]]]:
29622
30670
  fields_by_que_id = {
29623
30671
  _coerce_positive_int(field.get("que_id")): field
29624
- for field in current_fields_by_name.values()
30672
+ for field in fields_by_que_id.values()
29625
30673
  if isinstance(field, dict) and _coerce_positive_int(field.get("que_id")) is not None
29626
30674
  }
29627
30675
  public_groups: list[list[dict[str, Any]]] = []
@@ -29632,7 +30680,7 @@ def _public_view_filter_groups_from_match_rules(
29632
30680
  field = fields_by_que_id.get(que_id) or {}
29633
30681
  values = _public_view_filter_rule_values(rule, field=field)
29634
30682
  public_rule: dict[str, Any] = {
29635
- "field_name": str(field.get("name") or rule.get("queTitle") or que_id),
30683
+ "que_id": que_id,
29636
30684
  "operator": _public_view_filter_operator_from_judge_type(rule.get("judgeType"), values=values),
29637
30685
  }
29638
30686
  if public_rule["operator"] == "in":
@@ -29657,6 +30705,8 @@ def _public_view_filter_operator_from_judge_type(judge_type: Any, *, values: lis
29657
30705
  if normalized in {JUDGE_INCLUDE, JUDGE_FUZZY_MATCH}:
29658
30706
  return "contains"
29659
30707
  if normalized in {JUDGE_EQUAL_ANY, JUDGE_INCLUDE_ANY}:
30708
+ # ANY 判型+空值 = "该字段未选任何值"的合法存量形态(judgeType 保真回写,
30709
+ # 转 is_empty 会丢判型——后端 9 与 0 是不同判定域,实测回读不同)
29660
30710
  return "in"
29661
30711
  if normalized == JUDGE_GREATER_OR_EQUAL:
29662
30712
  return "gte"