@qingflow-tech/qingflow-cli 1.1.50-beta.5 → 1.1.51

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 (110) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +13 -29
  3. package/package.json +1 -1
  4. package/pyproject.toml +2 -8
  5. package/skills/qingflow-cli/SKILL.md +322 -93
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +9 -3
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +137 -3
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +12 -7
  16. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
  17. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -3
  18. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -9
  19. package/skills/qingflow-cli/reference/builder/50-views.md +191 -192
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +0 -2
  21. package/skills/qingflow-cli/reference/builder/README.md +1 -1
  22. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +6 -6
  23. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +95 -76
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
  26. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  27. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
  28. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
  29. package/skills/qingflow-cli/reference/examples/README.md +1 -1
  30. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +159 -0
  31. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +36 -30
  32. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
  33. package/skills/qingflow-cli/reference/record/analysis/README.md +113 -21
  34. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  35. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  36. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  37. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  38. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +80 -52
  39. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  40. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +42 -0
  41. package/skills/qingflow-common/SKILL.md +4 -7
  42. package/src/qingflow_mcp/__init__.py +1 -1
  43. package/src/qingflow_mcp/app_form_compat.py +0 -3
  44. package/src/qingflow_mcp/app_form_validator.py +17 -69
  45. package/src/qingflow_mcp/builder_facade/app_form.py +100 -497
  46. package/src/qingflow_mcp/builder_facade/models.py +17 -227
  47. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +0 -1
  48. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +30 -64
  49. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -4
  50. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +8 -4
  51. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -6
  52. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
  53. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
  54. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
  55. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
  59. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
  60. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
  61. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
  62. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +8 -4
  63. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
  64. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +10 -6
  65. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -46
  66. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
  67. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
  68. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +27 -61
  69. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
  70. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
  71. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +12 -7
  72. package/src/qingflow_mcp/builder_facade/service.py +539 -1671
  73. package/src/qingflow_mcp/builder_facade/workflow_spec.py +1 -1
  74. package/src/qingflow_mcp/cli/commands/__init__.py +1 -2
  75. package/src/qingflow_mcp/cli/commands/builder.py +55 -76
  76. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  77. package/src/qingflow_mcp/cli/context.py +0 -3
  78. package/src/qingflow_mcp/cli/formatters.py +1 -97
  79. package/src/qingflow_mcp/config.py +1 -0
  80. package/src/qingflow_mcp/public_surface.py +0 -8
  81. package/src/qingflow_mcp/response_trim.py +2 -11
  82. package/src/qingflow_mcp/server.py +0 -2
  83. package/src/qingflow_mcp/server_app_builder.py +10 -34
  84. package/src/qingflow_mcp/server_app_user.py +0 -4
  85. package/src/qingflow_mcp/solution/compiler/view_compiler.py +1 -2
  86. package/src/qingflow_mcp/solution/executor.py +8 -96
  87. package/src/qingflow_mcp/solution/spec_models.py +0 -1
  88. package/src/qingflow_mcp/tools/ai_builder_tools.py +398 -218
  89. package/src/qingflow_mcp/tools/base.py +42 -10
  90. package/src/qingflow_mcp/tools/record_tools.py +46 -11
  91. package/src/qingflow_mcp/tools/solution_tools.py +0 -2
  92. package/src/qingflow_mcp/tools/view_tools.py +6 -0
  93. package/skills/qingflow-cli/reference/data/README.md +0 -32
  94. package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +0 -14
  95. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +0 -50
  96. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
  97. package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +0 -42
  98. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +0 -1245
  99. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +0 -53
  100. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +0 -60
  101. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +0 -73
  102. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +0 -48
  103. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +0 -47
  104. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +0 -319
  105. package/src/qingflow_mcp/builder_facade/views_extract.py +0 -592
  106. package/src/qingflow_mcp/builder_facade/views_schema.py +0 -400
  107. package/src/qingflow_mcp/cli/commands/data.py +0 -134
  108. package/src/qingflow_mcp/tools/csv_export.py +0 -168
  109. package/src/qingflow_mcp/tools/data_tools.py +0 -284
  110. package/src/qingflow_mcp/tools/view_writer.py +0 -44
@@ -15,8 +15,6 @@ 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
-
20
18
  from ..backend_client import BackendRequestContext
21
19
  from ..config import get_flow_edit_maintenance_threshold
22
20
  from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error
@@ -28,7 +26,7 @@ from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
28
26
  from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
29
27
  from ..solution.compiler.icon_utils import encode_workspace_icon_with_defaults, workspace_icon_config
30
28
  from ..solution.compiler.view_compiler import VIEW_TYPE_MAP
31
- from ..solution.executor import _build_viewgraph_questions, _compact_dict, _flatten_viewgraph_questions, extract_field_map
29
+ from ..solution.executor import _build_viewgraph_questions, _compact_dict, extract_field_map
32
30
  from ..solution.spec_models import FieldType, FormLayoutRowSpec, FormLayoutSectionSpec, ViewSpec
33
31
  from ..tools.app_tools import AppTools
34
32
  from ..tools.custom_button_tools import CustomButtonTools
@@ -43,8 +41,6 @@ from ..tools.workflow_tools import WorkflowTools
43
41
  from . import flow_validation
44
42
  from . import workflow_spec as workflow_spec_api
45
43
  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
48
44
  from .models import (
49
45
  AppChartsReadResponse,
50
46
  AppFieldsReadResponse,
@@ -163,14 +159,9 @@ FIELD_TYPE_TO_QUESTION_TYPE: dict[str, int] = {
163
159
  FieldType.relation.value: 25,
164
160
  }
165
161
 
166
- # code_block / q_linker 输出目标字段类型。后端写链无类型白名单(FormSvcImpl 原样
167
- # 落库),运行时 DataSvcImpl.getAnswerValueByString:attachment 按拆分 URL 写入、
168
- # subtable 整表组装(均有专门分支),rich_text/data_relation 走 Question.canImport
169
- # 兜底按字符串写入(不报错)——四者为存量合法配置,与 validator 侧白名单同口径。
170
162
  INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
171
163
  FieldType.text.value,
172
164
  FieldType.long_text.value,
173
- "rich_text",
174
165
  FieldType.number.value,
175
166
  FieldType.amount.value,
176
167
  FieldType.date.value,
@@ -178,9 +169,6 @@ INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
178
169
  FieldType.single_select.value,
179
170
  FieldType.multi_select.value,
180
171
  FieldType.boolean.value,
181
- FieldType.attachment.value,
182
- "data_relation",
183
- FieldType.subtable.value,
184
172
  )
185
173
 
186
174
  MATCH_TYPE_ACCURACY = 1
@@ -302,7 +290,6 @@ class AiBuilderFacade:
302
290
  self.buttons = buttons
303
291
  self.packages = packages
304
292
  self.views = views
305
- self._view_writer = ViewWriter(views.sessions, views.backend)
306
293
  self.workflows = workflows
307
294
  self.portals = portals
308
295
  self.charts = charts
@@ -3085,27 +3072,14 @@ class AiBuilderFacade:
3085
3072
  member_ids=visibility.external_selectors.member_ids,
3086
3073
  member_emails=visibility.external_selectors.member_emails,
3087
3074
  )
3088
- # 冗余引用降级:提取器对同一成员同时产出 uid/email/name 三通道(uid 必有且直通解析,
3089
- # 保证成员不丢);email/name 走后端通讯录搜索,受数据/权限边界影响解析可能失败。
3090
- # 这类失败降级为非阻断——只有 uid/dept_id 直通也覆盖不了的引用才真正阻断。
3091
- _SOFT_RESOLUTION_KINDS = {"member_email", "member_name", "department_name", "role", "external_member_email"}
3092
- _NOT_FOUND_RESOLUTION_CODES = {"MEMBER_NOT_FOUND", "AMBIGUOUS_MEMBER", "ROLE_NOT_FOUND", "AMBIGUOUS_ROLE", "DEPARTMENT_NOT_FOUND", "AMBIGUOUS_DEPARTMENT"}
3093
- blocking_issues = [
3094
- issue
3095
- for issue in [
3075
+ self._raise_visibility_resolution_issues(
3076
+ [
3096
3077
  *member_resolution["issues"],
3097
3078
  *role_resolution["issues"],
3098
3079
  *department_resolution["issues"],
3099
3080
  *external_member_resolution["issues"],
3100
3081
  ]
3101
- # 冗余 email/name 引用「找不到」降级为非阻断(uid/id 直通已保主体不丢);
3102
- # 权限拒绝/查询失败类(LOOKUP_FAILED/PERMISSION_DENIED)仍阻断
3103
- if not (
3104
- str(issue.get("kind") or "") in _SOFT_RESOLUTION_KINDS
3105
- and str(issue.get("error_code") or "") in _NOT_FOUND_RESOLUTION_CODES
3106
- )
3107
- ]
3108
- self._raise_visibility_resolution_issues(blocking_issues)
3082
+ )
3109
3083
 
3110
3084
  external_depart_entries: list[dict[str, Any]] = []
3111
3085
  for dept_id in visibility.external_selectors.dept_ids:
@@ -3246,7 +3220,7 @@ class AiBuilderFacade:
3246
3220
  }
3247
3221
  )
3248
3222
  condition_matrix, condition_issues = _build_flow_condition_matrix(
3249
- fields_by_que_id=field_name_to_field,
3223
+ current_fields_by_name=field_name_to_field,
3250
3224
  node=normalized_node,
3251
3225
  )
3252
3226
  issues.extend({**issue, "node_id": node.get("id")} for issue in condition_issues)
@@ -3933,7 +3907,7 @@ class AiBuilderFacade:
3933
3907
  "reason_path": f"patch_buttons[{index}].button_id",
3934
3908
  "button_id": selector_id,
3935
3909
  "message": "button_id does not exist in the current app draft",
3936
- "next_action": "call app_get_buttons with button_text to reuse an existing custom button; create only if absent via app_custom_buttons_apply",
3910
+ "next_action": "call app_get and use custom_buttons[].button_id",
3937
3911
  }
3938
3912
  issues.append(issue)
3939
3913
  results.append({"index": index, "status": "failed", **issue})
@@ -4237,7 +4211,7 @@ class AiBuilderFacade:
4237
4211
  "reason_path": f"upsert_buttons[{index}].button_id",
4238
4212
  "button_id": button_id,
4239
4213
  "message": "button_id does not exist in the current app draft",
4240
- "next_action": "call app_get_buttons with button_text to reuse an existing custom button; create only if absent via app_custom_buttons_apply",
4214
+ "next_action": "call app_get and use custom_buttons[].button_id",
4241
4215
  }
4242
4216
  )
4243
4217
  continue
@@ -4308,7 +4282,7 @@ class AiBuilderFacade:
4308
4282
  "reason_path": f"remove_buttons[{index}].button_id",
4309
4283
  "button_id": selector_id,
4310
4284
  "message": "button_id does not exist in the current app draft",
4311
- "next_action": "call app_get_buttons with button_text to reuse an existing custom button; create only if absent via app_custom_buttons_apply",
4285
+ "next_action": "call app_get and use custom_buttons[].button_id",
4312
4286
  }
4313
4287
  )
4314
4288
  continue
@@ -7181,14 +7155,10 @@ class AiBuilderFacade:
7181
7155
  fields = cast(list[dict[str, Any]], state["parsed"]["fields"])
7182
7156
  return _chart_field_names_by_id_from_public_fields(app_key=app_key, fields=fields), None
7183
7157
 
7184
- def app_get_buttons(self, *, profile: str, app_key: str, button_text: str | None = None) -> JSONObject:
7158
+ def app_get_buttons(self, *, profile: str, app_key: str) -> JSONObject:
7185
7159
  self.apps._require_app_key(app_key)
7186
7160
  warnings: list[dict[str, Any]] = []
7187
7161
  verification: dict[str, Any] = {"buttons_loaded": True, "view_configs_loaded": True}
7188
- normalized_text = str(button_text or "").strip() or None
7189
- normalized_args: dict[str, Any] = {"app_key": app_key}
7190
- if normalized_text is not None:
7191
- normalized_args["button_text"] = normalized_text
7192
7162
  try:
7193
7163
  raw = self.buttons.custom_button_list(profile=profile, app_key=app_key, being_draft=True)
7194
7164
  except (QingflowApiError, RuntimeError) as error:
@@ -7196,18 +7166,11 @@ class AiBuilderFacade:
7196
7166
  return _failed_from_api_error(
7197
7167
  "BUTTONS_READ_FAILED",
7198
7168
  api_error,
7199
- normalized_args=normalized_args,
7169
+ normalized_args={"app_key": app_key},
7200
7170
  details={"app_key": app_key},
7201
7171
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
7202
7172
  )
7203
7173
  buttons = raw.get("items") if isinstance(raw.get("items"), list) else []
7204
- total_count = len(buttons)
7205
- if normalized_text is not None:
7206
- buttons = [
7207
- item
7208
- for item in buttons
7209
- if isinstance(item, dict) and str(item.get("buttonText") or item.get("button_text") or "").strip() == normalized_text
7210
- ]
7211
7174
  view_configs: list[dict[str, Any]] = []
7212
7175
  view_config_read_errors: list[dict[str, Any]] = []
7213
7176
  try:
@@ -7227,7 +7190,7 @@ class AiBuilderFacade:
7227
7190
  if view_config_read_errors:
7228
7191
  verification["view_configs_loaded"] = False
7229
7192
  warnings.append(_warning("BUTTON_VIEW_CONFIGS_PARTIAL", "some view button binding configs could not be read back"))
7230
- response: JSONObject = {
7193
+ return {
7231
7194
  "status": "success",
7232
7195
  "error_code": None,
7233
7196
  "recoverable": False,
@@ -7242,11 +7205,6 @@ class AiBuilderFacade:
7242
7205
  "verification": verification,
7243
7206
  "details": {"view_config_read_errors": view_config_read_errors} if view_config_read_errors else {},
7244
7207
  }
7245
- if normalized_text is not None:
7246
- response["button_text_filter"] = normalized_text
7247
- response["total_count"] = total_count
7248
- verification["button_text_exact_match"] = True
7249
- return response
7250
7208
 
7251
7209
  def app_get_associated_resources(self, *, profile: str, app_key: str) -> JSONObject:
7252
7210
  self.apps._require_app_key(app_key)
@@ -8472,7 +8430,11 @@ class AiBuilderFacade:
8472
8430
  try:
8473
8431
  schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
8474
8432
  parsed_schema = _parse_schema(schema)
8475
- fields_by_que_id = _collect_fields_by_que_id([], 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
+ }
8476
8438
  existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
8477
8439
  except (QingflowApiError, RuntimeError) as error:
8478
8440
  api_error = _coerce_api_error(error)
@@ -8563,7 +8525,7 @@ class AiBuilderFacade:
8563
8525
  view_binding = _custom_button_view_binding_to_view_button_patch(binding=binding, button_id=button_id)
8564
8526
  dto, binding_issues = _serialize_view_button_binding(
8565
8527
  binding=view_binding,
8566
- fields_by_que_id=fields_by_que_id,
8528
+ current_fields_by_name=current_fields_by_name,
8567
8529
  valid_custom_button_ids=valid_custom_button_ids,
8568
8530
  allow_unverified_custom_button_id=allow_unverified_numeric_button_ids,
8569
8531
  )
@@ -9493,37 +9455,49 @@ class AiBuilderFacade:
9493
9455
  return fields_result
9494
9456
  current_fields = fields_result.get("fields", [])
9495
9457
  field_names = {str(field.get("name") or "") for field in current_fields}
9496
- fields_by_que_id = _collect_fields_by_que_id(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
+ }
9497
9463
  upsert_views = [public_view_upsert_payload(view) for view in request.upsert_views]
9498
9464
  patch_views = [public_view_partial_payload(patch) for patch in request.patch_views]
9499
9465
  if request.preset is not None:
9500
9466
  upsert_views = _build_views_preset(request.preset, list(field_names))
9501
9467
  blocking_issues: list[dict[str, Any]] = []
9502
9468
  for patch in upsert_views:
9503
- # queId 形态:columns 是 int 数组,系统列 queId<=0 天然排除
9504
- raw_columns = [que for que in (patch.get("columns") or []) if isinstance(que, int) and que > 0]
9505
- columns = raw_columns
9506
- missing_columns = [que for que in columns if que not in fields_by_que_id]
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]
9507
9481
  if missing_columns:
9508
9482
  blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_columns})
9509
- group_que_id = patch.get("group_que_id")
9510
- if group_que_id and group_que_id not in fields_by_que_id:
9511
- blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": [group_que_id]})
9512
- start_que_id = patch.get("start_que_id")
9513
- end_que_id = patch.get("end_que_id")
9514
- title_que_id = patch.get("title_que_id")
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()
9515
9489
  if patch.get("type") == "gantt":
9516
9490
  missing_required = []
9517
- if not start_que_id:
9518
- missing_required.append("start_que_id")
9519
- if not end_que_id:
9520
- missing_required.append("end_que_id")
9491
+ if not start_field:
9492
+ missing_required.append("start_field")
9493
+ if not end_field:
9494
+ missing_required.append("end_field")
9521
9495
  if missing_required:
9522
9496
  blocking_issues.append({"error_code": "INVALID_GANTT_CONFIG", "view_name": patch.get("name"), "missing_fields": missing_required})
9523
- 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]
9497
+ missing_gantt_fields = [name for name in (start_field, end_field, title_field) if name and name not in field_names]
9524
9498
  if missing_gantt_fields:
9525
9499
  blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_gantt_fields})
9526
- translated_filters, filter_issues = _build_view_filter_groups(fields_by_que_id=fields_by_que_id, filters=patch.get("filters") or [])
9500
+ translated_filters, filter_issues = _build_view_filter_groups(current_fields_by_name=current_fields_by_name, filters=patch.get("filters") or [])
9527
9501
  if filter_issues:
9528
9502
  blocking_issues.extend(
9529
9503
  {
@@ -9531,12 +9505,11 @@ class AiBuilderFacade:
9531
9505
  "view_name": patch.get("name"),
9532
9506
  }
9533
9507
  for issue in filter_issues
9534
- if issue.get("error_code")
9535
9508
  )
9536
9509
  if translated_filters:
9537
9510
  patch["filters"] = [dict(rule) for rule in (patch.get("filters") or [])]
9538
9511
  _, _, query_condition_issues = _build_view_query_conditions_payload(
9539
- fields_by_que_id=fields_by_que_id,
9512
+ current_fields_by_name=current_fields_by_name,
9540
9513
  query_conditions=patch.get("query_conditions"),
9541
9514
  )
9542
9515
  if query_condition_issues:
@@ -11823,26 +11796,6 @@ class AiBuilderFacade:
11823
11796
  result["retry_payload"] = self._view_action_buttons_retry_payload(profile=profile, app_key=app_key, intents=retry_intents) if retry_intents else None
11824
11797
  return result
11825
11798
 
11826
- def _apply_view_create(self, *, profile: str, payload: JSONObject) -> str | None:
11827
- """创建一个视图并从后端响应中提取 view key。"""
11828
- create_result = self._view_writer.create_view(profile=profile, payload=payload)
11829
- raw_created = create_result.get("result")
11830
- if isinstance(raw_created, dict):
11831
- raw_view_key = raw_created.get("viewgraphKey") or raw_created.get("viewKey")
11832
- return str(raw_view_key).strip() if raw_view_key is not None and str(raw_view_key).strip() else None
11833
- if isinstance(raw_created, str):
11834
- return raw_created.strip() or None
11835
- return None
11836
-
11837
- def _apply_view_update(self, *, profile: str, view_key: str, payload: JSONObject) -> None:
11838
- """更新一个已有视图。"""
11839
- self.views.view_update(profile=profile, viewgraph_key=view_key, payload=payload)
11840
-
11841
- def _apply_view_remove(self, *, profile: str, view_key: str) -> JSONObject:
11842
- """删除一个视图并执行单 view key 读回验证。"""
11843
- self.views.view_delete(profile=profile, viewgraph_key=view_key)
11844
- return self._verify_view_deleted_by_key(profile=profile, view_key=view_key)
11845
-
11846
11799
  def app_views_apply(
11847
11800
  self,
11848
11801
  *,
@@ -11852,91 +11805,15 @@ class AiBuilderFacade:
11852
11805
  patch_views: list[ViewPartialPatch] | None = None,
11853
11806
  remove_views: list[str],
11854
11807
  publish: bool = True,
11855
- schema_version: str | None = None,
11856
11808
  ) -> JSONObject:
11857
11809
  patch_views = patch_views or []
11858
- operation_values = (
11859
- ("upsert", "upsert_views", upsert_views, "VIEW_BATCH_CREATE_UNSUPPORTED"),
11860
- ("patch", "patch_views", patch_views, "VIEW_BATCH_OPERATION_UNSUPPORTED"),
11861
- ("remove", "remove_views", remove_views, "VIEW_BATCH_OPERATION_UNSUPPORTED"),
11862
- )
11863
- selected_operation_types = [operation for operation, _field_name, values, _error_code in operation_values if values]
11864
- selected_operations = [field_name for _operation, field_name, values, _error_code in operation_values if values]
11865
11810
  normalized_args = {
11866
11811
  "app_key": app_key,
11867
- "upsert_view": public_view_upsert_payload(upsert_views[0]) if upsert_views else None,
11812
+ "upsert_views": [public_view_upsert_payload(patch) for patch in upsert_views],
11868
11813
  "patch_views": [public_view_partial_payload(patch) for patch in patch_views],
11869
11814
  "remove_views": list(remove_views),
11870
11815
  "publish": publish,
11871
- "schema_version": schema_version,
11872
11816
  }
11873
- if len(selected_operations) > 1:
11874
- return _failed(
11875
- "VIEW_SINGLE_OPERATION_UNSUPPORTED",
11876
- "app_views_apply supports one view operation per call; choose upsert_view, patch_views, or remove_views",
11877
- normalized_args=normalized_args,
11878
- details={"app_key": app_key, "operations": selected_operations},
11879
- suggested_next_call=None,
11880
- )
11881
- operation = selected_operation_types[0] if selected_operation_types else None
11882
- for operation_name, _field_name, values, error_code in operation_values:
11883
- if len(values) <= 1:
11884
- continue
11885
- first_value = values[0]
11886
- if operation_name == "upsert":
11887
- suggested_value: Any = public_view_upsert_payload(first_value)
11888
- elif operation_name == "patch":
11889
- suggested_value = [public_view_partial_payload(first_value)]
11890
- else:
11891
- suggested_value = [str(first_value)]
11892
- argument_name = "upsert_view" if operation_name == "upsert" else f"{operation_name}_views"
11893
- suggested_arguments: JSONObject = {
11894
- "profile": profile,
11895
- "app_key": app_key,
11896
- argument_name: suggested_value,
11897
- "publish": publish,
11898
- }
11899
- if schema_version is not None:
11900
- suggested_arguments["schema_version"] = schema_version
11901
- return _failed(
11902
- error_code,
11903
- (
11904
- "app_views_apply supports creating or fully updating one view per call"
11905
- if operation_name == "upsert"
11906
- else f"app_views_apply supports at most one {operation_name} per call"
11907
- ),
11908
- normalized_args=normalized_args,
11909
- details={
11910
- "app_key": app_key,
11911
- f"max_{operation_name}_views": 1,
11912
- f"received_{operation_name}_views": len(values),
11913
- },
11914
- suggested_next_call={
11915
- "tool_name": "app_views_apply",
11916
- "arguments": suggested_arguments,
11917
- },
11918
- )
11919
- schema_context = None
11920
- if schema_version is not None:
11921
- try:
11922
- schema_context = views_schema_registry().resolve_schema_version(schema_version)
11923
- for upsert in upsert_views:
11924
- schema_context.validate_upsert(
11925
- compact_none_values(public_view_upsert_payload(upsert))
11926
- )
11927
- except ViewsContractError as error:
11928
- return _failed(
11929
- error.error_code,
11930
- str(error.message),
11931
- normalized_args=normalized_args,
11932
- details={
11933
- "app_key": app_key,
11934
- "schema_version": schema_version,
11935
- "reason_path": error.path or "",
11936
- **(error.details or {}),
11937
- },
11938
- suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_version": schema_version, "schema_kind": "complete"}},
11939
- )
11940
11817
  apply_started_at = time.perf_counter()
11941
11818
  permission_ms = 0
11942
11819
  view_state_read_ms = 0
@@ -11951,8 +11828,6 @@ class AiBuilderFacade:
11951
11828
  permission_outcomes: list[PermissionCheckOutcome] = []
11952
11829
 
11953
11830
  def finalize(response: JSONObject) -> JSONObject:
11954
- if schema_context is not None:
11955
- response["schemaVersion"] = schema_context.schema_version
11956
11831
  return _apply_permission_outcomes(response, *permission_outcomes)
11957
11832
 
11958
11833
  def attach_duration(response: JSONObject) -> JSONObject:
@@ -12169,25 +12044,7 @@ class AiBuilderFacade:
12169
12044
  return finish(create_permission_outcome.block)
12170
12045
  permission_outcomes.append(create_permission_outcome)
12171
12046
  parsed_schema = _parse_schema(schema)
12172
-
12173
- def _register_subfield_names(field: dict[str, Any], index: dict[str, dict[str, Any]]) -> None:
12174
- # 表格字段(queType 18)的子字段是视图列/筛选/排序的合法引用面(viewgraph_question 有独立行);
12175
- # _parse_schema 只把顶层字段入列,这里递归并入引用索引(不改 fields 布局面)
12176
- for sub in field.get("subfields") or []:
12177
- if isinstance(sub, dict) and str(sub.get("name") or "").strip():
12178
- index.setdefault(str(sub["name"]), sub)
12179
- _register_subfield_names(sub, index)
12180
-
12181
- field_name_to_field: dict[str, dict[str, Any]] = {}
12182
- for field in parsed_schema["fields"]:
12183
- if isinstance(field, dict) and str(field.get("name") or "").strip():
12184
- field_name_to_field[str(field["name"])] = field
12185
- _register_subfield_names(field, field_name_to_field)
12186
- field_names = set(field_name_to_field)
12187
- # views 契约全 queId 化后,列/筛选/排序/配置引用的是 queId(int);
12188
- # fields_by_que_id 必须以 que_id 为键,与 _build_view_filter_groups 等消费方对齐。
12189
- # 递归收集(顶层+表格子字段;段落内字段经 _parse_schema 已展平到 fields)。
12190
- fields_by_que_id = _collect_fields_by_que_id([], schema=schema)
12047
+ field_names = {field["name"] for field in parsed_schema["fields"]}
12191
12048
  if patch_views:
12192
12049
  expanded_views, patch_issues, patch_results = self._expand_view_partial_patches(
12193
12050
  profile=profile,
@@ -12207,18 +12064,14 @@ class AiBuilderFacade:
12207
12064
  suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": patch_issues[0].get("view_key") or "VIEW_KEY"}},
12208
12065
  )
12209
12066
  )
12210
- if len(expanded_views) > 1:
12211
- return finalize(_failed(
12212
- "VIEW_BATCH_OPERATION_UNSUPPORTED",
12213
- "app_views_apply supports hydrating one patch view per call",
12214
- normalized_args=normalized_args,
12215
- details={"app_key": app_key, "received_patch_views": len(expanded_views)},
12216
- suggested_next_call=None,
12217
- ))
12218
- upsert_views = expanded_views
12219
- operation = "upsert" if expanded_views else "patch"
12220
- normalized_args["upsert_view"] = public_view_upsert_payload(expanded_views[0]) if expanded_views else None
12067
+ upsert_views = [*upsert_views, *expanded_views]
12068
+ normalized_args["upsert_views"] = [public_view_upsert_payload(patch) for patch in upsert_views]
12221
12069
  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
+ }
12222
12075
  requires_custom_button_validation = any(
12223
12076
  any(binding.button_type == PublicViewButtonType.custom for binding in (patch.buttons or []))
12224
12077
  for patch in upsert_views
@@ -12314,8 +12167,8 @@ class AiBuilderFacade:
12314
12167
  associated_resource_inventory_ms += _duration_ms(associated_resource_inventory_started_at)
12315
12168
  removed: list[str] = []
12316
12169
  removed_keys: set[str] = set()
12317
- view_result: dict[str, Any] | None = None
12318
- failure_result: dict[str, Any] | None = None
12170
+ view_results: list[dict[str, Any]] = []
12171
+ failed_views: list[dict[str, Any]] = []
12319
12172
  action_button_intents: list[dict[str, Any]] = list(patch_action_button_intents)
12320
12173
 
12321
12174
  def record_action_button_intent(*, patch: ViewUpsertPatch, view_key: str | None, index: int) -> None:
@@ -12333,17 +12186,10 @@ class AiBuilderFacade:
12333
12186
  )
12334
12187
 
12335
12188
  view_mutation_started_at = time.perf_counter()
12336
- if operation == "remove" and remove_views:
12337
- selector_text = str(remove_views[0] or "").strip()
12189
+ for selector in remove_views:
12190
+ selector_text = str(selector or "").strip()
12338
12191
  if not selector_text:
12339
- view_mutation_ms += _duration_ms(view_mutation_started_at)
12340
- return finish(_failed(
12341
- "VIEW_NOT_FOUND",
12342
- "remove_views item did not match an existing view name or view_key",
12343
- normalized_args=normalized_args,
12344
- details={"app_key": app_key},
12345
- suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
12346
- ))
12192
+ continue
12347
12193
  key_match = existing_by_key.get(selector_text)
12348
12194
  matches = [key_match] if isinstance(key_match, dict) else existing_by_name.get(selector_text, [])
12349
12195
  if len(matches) > 1:
@@ -12370,40 +12216,44 @@ class AiBuilderFacade:
12370
12216
  "error_code": "VIEW_NOT_FOUND",
12371
12217
  "message": "remove_views item did not match an existing view name or view_key",
12372
12218
  }
12373
- failure_result = failed_view
12374
- view_result = deepcopy(failed_view)
12375
- else:
12219
+ failed_views.append(failed_view)
12220
+ view_results.append(deepcopy(failed_view))
12221
+ continue
12222
+ if len(matches) == 1:
12376
12223
  key = _extract_view_key(matches[0])
12377
12224
  removed_name = _extract_view_name(matches[0]) or selector_text
12378
12225
  try:
12379
- delete_readback = self._apply_view_remove(profile=profile, view_key=key)
12226
+ self.views.view_delete(profile=profile, viewgraph_key=key)
12227
+ delete_readback = self._verify_view_deleted_by_key(profile=profile, view_key=key)
12380
12228
  removed.append(removed_name)
12381
12229
  if key:
12382
12230
  removed_keys.add(key)
12383
12231
  existing_by_key.pop(key, None)
12384
12232
  existing_by_name.pop(removed_name, None)
12385
- view_result = {
12386
- "name": removed_name,
12387
- "view_key": key,
12388
- "type": None,
12389
- "status": delete_readback.get("status") or "readback_pending",
12390
- "operation": "delete",
12391
- "delete_executed": True,
12392
- "readback_status": delete_readback.get("readback_status"),
12393
- "safe_to_retry_delete": False,
12394
- **(
12395
- {
12396
- "error_code": delete_readback.get("error_code"),
12397
- "message": delete_readback.get("message"),
12398
- "request_id": delete_readback.get("request_id"),
12399
- "backend_code": delete_readback.get("backend_code"),
12400
- "http_status": delete_readback.get("http_status"),
12401
- "transport_error": delete_readback.get("transport_error"),
12402
- }
12403
- if delete_readback.get("readback_status") != "deleted"
12404
- else {}
12405
- ),
12406
- }
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
+ )
12407
12257
  except (QingflowApiError, RuntimeError) as error:
12408
12258
  api_error = _coerce_api_error(error)
12409
12259
  failed_view = {
@@ -12420,8 +12270,8 @@ class AiBuilderFacade:
12420
12270
  "backend_code": api_error.backend_code,
12421
12271
  "http_status": None if api_error.http_status == 404 else api_error.http_status,
12422
12272
  }
12423
- failure_result = failed_view
12424
- view_result = deepcopy(failed_view)
12273
+ failed_views.append(failed_view)
12274
+ view_results.append(deepcopy(failed_view))
12425
12275
  created: list[str] = []
12426
12276
  updated: list[str] = []
12427
12277
  existing_view_list = [
@@ -12431,129 +12281,95 @@ class AiBuilderFacade:
12431
12281
  and _extract_view_name(view) not in removed
12432
12282
  and _extract_view_key(view) not in removed_keys
12433
12283
  ]
12434
- stale_warnings: list[dict[str, Any]] = []
12435
- for ordinal, patch in ((1, upsert_views[0]),) if operation == "upsert" and upsert_views else ():
12436
- apply_columns = _resolve_view_visible_que_ids(patch)
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
+ ]
12437
12291
  if patch.type in {PublicViewType.table, PublicViewType.card} and patch.columns and not apply_columns:
12438
12292
  return _failed(
12439
12293
  "VALIDATION_ERROR",
12440
12294
  "view columns must include at least one real app field; system columns cannot be applied directly",
12441
12295
  normalized_args=normalized_args,
12442
- details={"app_key": app_key, "view_name": patch.name},
12296
+ details={
12297
+ "app_key": app_key,
12298
+ "view_name": patch.name,
12299
+ "ignored_system_columns": ignored_system_columns,
12300
+ },
12443
12301
  )
12444
- # 死引用(字段已从表单删除,后端保存视图时原样透传)降级为警告透传,不再整视图拒绝
12445
- missing_columns = [
12446
- que for que in apply_columns
12447
- if not _is_known_system_view_que_id(que) and que not in fields_by_que_id
12448
- ]
12302
+ missing_columns = [name for name in apply_columns if name not in field_names]
12449
12303
  if missing_columns:
12450
- stale_warnings.append(
12451
- _stale_view_reference_warning(scope="columns", view_name=patch.name, field_ids=missing_columns)
12452
- )
12453
- if patch.group_que_id and patch.group_que_id not in fields_by_que_id:
12454
- stale_warnings.append(
12455
- _stale_view_reference_warning(scope="group_que_id", view_name=patch.name, field_ids=[patch.group_que_id])
12304
+ return _failed(
12305
+ "UNKNOWN_VIEW_FIELD",
12306
+ "view columns reference unknown fields",
12307
+ normalized_args=normalized_args,
12308
+ details={
12309
+ "app_key": app_key,
12310
+ "view_name": patch.name,
12311
+ "missing_fields": missing_columns,
12312
+ "ignored_system_columns": ignored_system_columns,
12313
+ },
12314
+ missing_fields=missing_columns,
12315
+ suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
12456
12316
  )
12457
- for ref_scope, ref_que_id in (
12458
- ("gantt.start_que_id", patch.start_que_id),
12459
- ("gantt.end_que_id", patch.end_que_id),
12460
- ("title_que_id", patch.title_que_id),
12461
- ):
12462
- # 系统字段(提交人/提交时间等)可作标题/时间轴;业务字段死引用仅警告透传
12463
- if ref_que_id and not _is_known_system_view_que_id(ref_que_id) and ref_que_id not in fields_by_que_id:
12464
- stale_warnings.append(
12465
- _stale_view_reference_warning(scope=ref_scope, view_name=patch.name, field_ids=[ref_que_id])
12466
- )
12467
- for group_name, config_que_ids in _iter_view_config_group_field_names(patch):
12468
- missing_config_fields = [
12469
- que for que in config_que_ids
12470
- if que not in fields_by_que_id and not _is_known_system_view_que_id(que)
12471
- ]
12472
- if missing_config_fields:
12473
- stale_warnings.append(
12474
- _stale_view_reference_warning(scope=group_name, view_name=patch.name, field_ids=missing_config_fields)
12475
- )
12476
- hierarchy_root_filter_groups: list[list[dict[str, Any]]] | None = None
12477
- if patch.hierarchy is not None and patch.hierarchy.root_filter:
12478
- hierarchy_root_filter_groups = []
12479
- for root_filter_group in patch.hierarchy.root_filter:
12480
- translated_group, hierarchy_filter_issues = _build_view_filter_groups(
12481
- fields_by_que_id=fields_by_que_id,
12482
- filters=root_filter_group,
12483
- )
12484
- for issue in hierarchy_filter_issues:
12485
- if issue.get("warning_code") == "STALE_OPTION_REFERENCE":
12486
- stale_warnings.append(
12487
- _stale_view_reference_warning(
12488
- scope="hierarchy.root_filter",
12489
- view_name=patch.name,
12490
- values=issue.get("details", {}).get("unresolved_values") or [],
12491
- option=True,
12492
- )
12493
- )
12494
- hierarchy_filter_errors = [issue for issue in hierarchy_filter_issues if issue.get("error_code")]
12495
- if hierarchy_filter_errors:
12496
- first_issue = hierarchy_filter_errors[0]
12497
- return _failed(
12498
- str(first_issue.get("error_code") or "UNKNOWN_VIEW_FIELD"),
12499
- "view hierarchy root_filter references invalid fields or values",
12500
- normalized_args=normalized_args,
12501
- details={
12502
- "app_key": app_key,
12503
- "view_name": patch.name,
12504
- **first_issue,
12505
- },
12506
- missing_fields=list(first_issue.get("missing_fields") or []),
12507
- suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
12508
- )
12509
- hierarchy_root_filter_groups.extend(translated_group)
12510
- translated_filters: list[list[dict[str, Any]]] | None
12511
- filter_issues: list[dict[str, Any]]
12512
- if patch.preserve_filters:
12513
- translated_filters = None
12514
- filter_issues = []
12515
- elif patch.filters and isinstance(patch.filters[0], list):
12516
- # 嵌套形态 [[rule,...],[rule,...]]:组间 OR、组内 AND(viewgraphLimit 多组),
12517
- # 复用 button_limit 的多组编译;后端简易模式会从多组规则自动派生公式
12518
- translated_filters, filter_issues = _build_view_button_limit_groups(
12519
- fields_by_que_id=fields_by_que_id, groups=patch.filters
12317
+ if patch.group_by and patch.group_by not in field_names:
12318
+ return _failed(
12319
+ "UNKNOWN_VIEW_FIELD",
12320
+ f"group_by references unknown field '{patch.group_by}'",
12321
+ normalized_args=normalized_args,
12322
+ details={
12323
+ "app_key": app_key,
12324
+ "view_name": patch.name,
12325
+ "missing_fields": [patch.group_by],
12326
+ },
12327
+ missing_fields=[patch.group_by],
12328
+ suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
12520
12329
  )
12521
- else:
12522
- translated_filters, filter_issues = _build_view_filter_groups(fields_by_que_id=fields_by_que_id, filters=patch.filters)
12523
- if filter_issues:
12524
- for issue in filter_issues:
12525
- if issue.get("warning_code") == "STALE_OPTION_REFERENCE":
12526
- stale_warnings.append(
12527
- _stale_view_reference_warning(
12528
- scope="filters",
12529
- view_name=patch.name,
12530
- values=issue.get("details", {}).get("unresolved_values") or [],
12531
- option=True,
12532
- )
12533
- )
12534
- filter_errors = [issue for issue in filter_issues if issue.get("error_code")]
12535
- if filter_errors:
12536
- first_issue = filter_errors[0]
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:
12537
12332
  return _failed(
12538
- str(first_issue.get("error_code") or "UNKNOWN_VIEW_FIELD"),
12539
- "view filters reference invalid fields or values",
12333
+ "UNKNOWN_VIEW_FIELD",
12334
+ f"gantt configuration references unknown field '{gantt_field_name}'",
12540
12335
  normalized_args=normalized_args,
12541
12336
  details={
12542
12337
  "app_key": app_key,
12543
12338
  "view_name": patch.name,
12544
- **first_issue,
12339
+ "missing_fields": [gantt_field_name],
12545
12340
  },
12546
- missing_fields=list(first_issue.get("missing_fields") or []),
12547
- allowed_values=first_issue.get("allowed_values") or {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
12341
+ missing_fields=[gantt_field_name],
12548
12342
  suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
12549
12343
  )
12344
+ translated_filters: list[list[dict[str, Any]]] | None
12345
+ filter_issues: list[dict[str, Any]]
12346
+ if patch.preserve_filters:
12347
+ translated_filters = None
12348
+ filter_issues = []
12349
+ else:
12350
+ translated_filters, filter_issues = _build_view_filter_groups(current_fields_by_name=current_fields_by_name, filters=patch.filters)
12351
+ if filter_issues:
12352
+ first_issue = filter_issues[0]
12353
+ return _failed(
12354
+ str(first_issue.get("error_code") or "UNKNOWN_VIEW_FIELD"),
12355
+ "view filters reference invalid fields or values",
12356
+ normalized_args=normalized_args,
12357
+ details={
12358
+ "app_key": app_key,
12359
+ "view_name": patch.name,
12360
+ **first_issue,
12361
+ },
12362
+ missing_fields=list(first_issue.get("missing_fields") or []),
12363
+ allowed_values=first_issue.get("allowed_values") or {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
12364
+ suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
12365
+ )
12550
12366
  if patch.preserve_query_conditions:
12551
12367
  query_condition_payload = None
12552
12368
  expected_query_conditions = None
12553
12369
  query_condition_issues = []
12554
12370
  else:
12555
12371
  query_condition_payload, expected_query_conditions, query_condition_issues = _build_view_query_conditions_payload(
12556
- fields_by_que_id=fields_by_que_id,
12372
+ current_fields_by_name=current_fields_by_name,
12557
12373
  query_conditions=patch.query_conditions,
12558
12374
  )
12559
12375
  if query_condition_issues:
@@ -12610,7 +12426,7 @@ class AiBuilderFacade:
12610
12426
  expected_button_summary = None
12611
12427
  elif patch.buttons is not None:
12612
12428
  explicit_button_dtos, button_issues = _build_view_button_dtos(
12613
- fields_by_que_id=fields_by_que_id,
12429
+ current_fields_by_name=current_fields_by_name,
12614
12430
  bindings=patch.buttons,
12615
12431
  valid_custom_button_ids=valid_custom_button_ids,
12616
12432
  )
@@ -12627,10 +12443,7 @@ class AiBuilderFacade:
12627
12443
  },
12628
12444
  missing_fields=list(first_issue.get("missing_fields") or []),
12629
12445
  allowed_values=first_issue.get("allowed_values") or {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
12630
- suggested_next_call={
12631
- "tool_name": "app_get_buttons",
12632
- "arguments": {"profile": profile, "app_key": app_key},
12633
- },
12446
+ suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
12634
12447
  )
12635
12448
  expected_button_summary = _normalize_expected_view_buttons_for_compare(
12636
12449
  explicit_button_dtos or [],
@@ -12671,10 +12484,6 @@ class AiBuilderFacade:
12671
12484
  existing_key = _extract_view_key(matched_existing_view)
12672
12485
  created_key: str | None = None
12673
12486
  system_view_list_type = _resolve_system_view_list_type(view_key=existing_key, view_name=patch.name) if existing_key else None
12674
- 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"}:
12675
- # 环境受限开关:默认视图列序同步依赖 app apply/baseInfo 回读,在快照/脱敏环境上该读面可能不可用;
12676
- # 视图写主体(view_update)不受影响,仅跳过同步与尾部校验。
12677
- system_view_list_type = None
12678
12487
  operation_phase = "view_update" if existing_key else "view_create"
12679
12488
  query_condition_payload_for_apply = deepcopy(query_condition_payload)
12680
12489
  expected_query_conditions_for_verify = deepcopy(expected_query_conditions)
@@ -12708,17 +12517,16 @@ class AiBuilderFacade:
12708
12517
  views=self.views,
12709
12518
  profile=profile,
12710
12519
  source_viewgraph_key=existing_key,
12711
- hierarchy_filter_groups=hierarchy_root_filter_groups,
12712
12520
  schema=schema,
12713
12521
  patch=patch,
12714
12522
  view_filters=translated_filters,
12715
- fields_by_que_id=fields_by_que_id,
12523
+ current_fields_by_name=current_fields_by_name,
12716
12524
  auth_override=view_auth_override,
12717
12525
  explicit_button_dtos=explicit_button_dtos,
12718
12526
  query_condition_payload=query_condition_payload_for_apply,
12719
12527
  associated_resources_payload=associated_resources_payload_for_apply,
12720
12528
  )
12721
- self._apply_view_update(profile=profile, view_key=existing_key, payload=payload)
12529
+ self.views.view_update(profile=profile, viewgraph_key=existing_key, payload=payload)
12722
12530
  system_view_sync: dict[str, Any] | None = None
12723
12531
  if system_view_list_type is not None and patch.type.value == "table":
12724
12532
  operation_phase = "default_view_apply_config_sync"
@@ -12729,9 +12537,7 @@ class AiBuilderFacade:
12729
12537
  payload=payload,
12730
12538
  list_type=system_view_list_type,
12731
12539
  schema=schema,
12732
- visible_field_names=[
12733
- str((fields_by_que_id.get(que) or {}).get("name") or "") for que in apply_columns if que in fields_by_que_id
12734
- ],
12540
+ visible_field_names=apply_columns,
12735
12541
  )
12736
12542
  if not bool(system_view_sync.get("verified")):
12737
12543
  failure_entry = {
@@ -12756,23 +12562,24 @@ class AiBuilderFacade:
12756
12562
  "apply_columns": apply_columns,
12757
12563
  },
12758
12564
  }
12759
- failure_result = failure_entry
12760
- view_result = failure_entry
12565
+ failed_views.append(failure_entry)
12566
+ view_results.append(failure_entry)
12761
12567
  continue
12762
12568
  updated.append(patch.name)
12763
- view_result = {
12764
- "name": patch.name,
12765
- "view_key": existing_key,
12766
- "type": patch.type.value,
12767
- "status": "updated",
12768
- "expected_filters": deepcopy(translated_filters),
12769
- "expected_buttons": deepcopy(expected_button_summary),
12770
- "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12771
- "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12772
- "expected_pinned": patch.pinned,
12773
- "system_view_sync": system_view_sync,
12774
- "apply_columns": deepcopy(apply_columns),
12775
- }
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
+ )
12776
12583
  record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
12777
12584
  else:
12778
12585
  template_key = _pick_view_template_key(existing_view_list, desired_type=patch.type.value)
@@ -12784,45 +12591,50 @@ class AiBuilderFacade:
12784
12591
  views=self.views,
12785
12592
  profile=profile,
12786
12593
  source_viewgraph_key=created_key,
12787
- hierarchy_filter_groups=hierarchy_root_filter_groups,
12788
12594
  schema=schema,
12789
12595
  patch=patch,
12790
12596
  view_filters=translated_filters,
12791
- fields_by_que_id=fields_by_que_id,
12597
+ current_fields_by_name=current_fields_by_name,
12792
12598
  auth_override=view_auth_override,
12793
12599
  explicit_button_dtos=explicit_button_dtos,
12794
12600
  query_condition_payload=query_condition_payload_for_apply,
12795
12601
  associated_resources_payload=associated_resources_payload_for_apply,
12796
12602
  )
12797
- self._apply_view_update(profile=profile, view_key=created_key, payload=payload)
12603
+ self.views.view_update(profile=profile, viewgraph_key=created_key, payload=payload)
12798
12604
  else:
12799
- payload = _build_view_creation_payload(
12605
+ payload = _build_view_create_payload(
12800
12606
  app_key=app_key,
12801
12607
  base_info=base,
12802
12608
  schema=schema,
12803
12609
  patch=patch,
12804
12610
  ordinal=ordinal,
12805
12611
  view_filters=translated_filters,
12806
- fields_by_que_id=fields_by_que_id,
12612
+ current_fields_by_name=current_fields_by_name,
12807
12613
  auth_override=view_auth_override,
12808
12614
  explicit_button_dtos=explicit_button_dtos,
12809
12615
  query_condition_payload=query_condition_payload_for_apply,
12810
12616
  associated_resources_payload=associated_resources_payload_for_apply,
12811
- hierarchy_filter_groups=hierarchy_root_filter_groups,
12812
12617
  )
12813
- created_key = self._apply_view_create(profile=profile, payload=payload)
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
12814
12625
  created.append(patch.name)
12815
- view_result = {
12816
- "name": patch.name,
12817
- "view_key": created_key,
12818
- "type": patch.type.value,
12819
- "status": "created",
12820
- "expected_filters": deepcopy(translated_filters),
12821
- "expected_buttons": deepcopy(expected_button_summary),
12822
- "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12823
- "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12824
- "expected_pinned": patch.pinned,
12825
- }
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
+ )
12826
12638
  record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
12827
12639
  except (QingflowApiError, RuntimeError) as error:
12828
12640
  api_error = _coerce_api_error(error)
@@ -12849,20 +12661,17 @@ class AiBuilderFacade:
12849
12661
  patch=patch,
12850
12662
  ordinal=ordinal,
12851
12663
  view_filters=translated_filters,
12852
- hierarchy_filter_groups=hierarchy_root_filter_groups,
12853
- fields_by_que_id=fields_by_que_id,
12664
+ current_fields_by_name=current_fields_by_name,
12854
12665
  auth_override=view_auth_override,
12855
12666
  explicit_button_dtos=fallback_button_dtos,
12856
12667
  query_condition_payload=query_condition_payload_for_apply,
12857
12668
  associated_resources_payload=associated_resources_payload_for_apply,
12858
12669
  )
12859
- self._apply_view_update(profile=profile, view_key=target_key, payload=fallback_payload)
12670
+ self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
12860
12671
  system_view_sync: dict[str, Any] | None = None
12861
12672
  fallback_system_view_list_type = (
12862
12673
  _resolve_system_view_list_type(view_key=target_key, view_name=patch.name)
12863
- if patch.type.value == "table"
12864
- and target_key
12865
- and os.environ.get("QINGFLOW_VIEWS_APPLY_SKIP_SYSTEM_VIEW_SYNC", "").strip().lower() not in {"1", "true", "yes", "on"}
12674
+ if patch.type.value == "table" and target_key
12866
12675
  else None
12867
12676
  )
12868
12677
  if fallback_system_view_list_type is not None:
@@ -12874,9 +12683,7 @@ class AiBuilderFacade:
12874
12683
  payload=fallback_payload,
12875
12684
  list_type=fallback_system_view_list_type,
12876
12685
  schema=schema,
12877
- visible_field_names=[
12878
- str((fields_by_que_id.get(que) or {}).get("name") or "") for que in apply_columns if que in fields_by_que_id
12879
- ],
12686
+ visible_field_names=apply_columns,
12880
12687
  )
12881
12688
  if not bool(system_view_sync.get("verified")):
12882
12689
  failure_entry = {
@@ -12901,42 +12708,44 @@ class AiBuilderFacade:
12901
12708
  "apply_columns": apply_columns,
12902
12709
  },
12903
12710
  }
12904
- failure_result = failure_entry
12905
- view_result = failure_entry
12711
+ failed_views.append(failure_entry)
12712
+ view_results.append(failure_entry)
12906
12713
  continue
12907
12714
  if existing_key:
12908
12715
  updated.append(patch.name)
12909
- view_result = {
12910
- "name": patch.name,
12911
- "view_key": existing_key,
12912
- "type": patch.type.value,
12913
- "status": "updated",
12914
- "fallback_applied": True,
12915
- "expected_filters": deepcopy(translated_filters),
12916
- "expected_buttons": deepcopy(expected_button_summary),
12917
- "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12918
- "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12919
- "expected_pinned": patch.pinned,
12920
- "system_view_sync": system_view_sync,
12921
- "apply_columns": deepcopy(apply_columns),
12922
- }
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
+ )
12923
12731
  record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
12924
12732
  else:
12925
12733
  created.append(patch.name)
12926
- view_result = {
12927
- "name": patch.name,
12928
- "view_key": created_key,
12929
- "type": patch.type.value,
12930
- "status": "created",
12931
- "fallback_applied": True,
12932
- "expected_filters": deepcopy(translated_filters),
12933
- "expected_buttons": deepcopy(expected_button_summary),
12934
- "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12935
- "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12936
- "expected_pinned": patch.pinned,
12937
- "system_view_sync": system_view_sync,
12938
- "apply_columns": deepcopy(apply_columns),
12939
- }
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
+ )
12940
12749
  record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
12941
12750
  continue
12942
12751
  fallback_payload = _build_minimal_view_payload(
@@ -12945,16 +12754,16 @@ class AiBuilderFacade:
12945
12754
  patch=patch,
12946
12755
  ordinal=ordinal,
12947
12756
  view_filters=translated_filters,
12948
- hierarchy_filter_groups=hierarchy_root_filter_groups,
12949
- fields_by_que_id=fields_by_que_id,
12757
+ current_fields_by_name=current_fields_by_name,
12950
12758
  auth_override=view_auth_override,
12951
12759
  explicit_button_dtos=explicit_button_dtos,
12952
12760
  query_condition_payload=query_condition_payload_for_apply,
12953
12761
  associated_resources_payload=associated_resources_payload_for_apply,
12954
12762
  )
12955
- self._apply_view_create(profile=profile, payload=fallback_payload)
12763
+ self.views.view_create(profile=profile, payload=fallback_payload)
12956
12764
  created.append(patch.name)
12957
- view_result = {
12765
+ view_results.append(
12766
+ {
12958
12767
  "name": patch.name,
12959
12768
  "view_key": created_key,
12960
12769
  "type": patch.type.value,
@@ -12964,6 +12773,7 @@ class AiBuilderFacade:
12964
12773
  "expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
12965
12774
  "expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
12966
12775
  }
12776
+ )
12967
12777
  record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
12968
12778
  continue
12969
12779
  except (QingflowApiError, RuntimeError) as fallback_error:
@@ -12989,16 +12799,11 @@ class AiBuilderFacade:
12989
12799
  "view_name": patch.name,
12990
12800
  "view_type": patch.type.value,
12991
12801
  "columns": patch.columns,
12992
- "group_que_id": patch.group_que_id,
12993
- "filters": [
12994
- (item.model_dump(mode="json") if hasattr(item, "model_dump") else item)
12995
- if not isinstance(item, list)
12996
- else [sub.model_dump(mode="json") if hasattr(sub, "model_dump") else sub for sub in item]
12997
- for item in patch.filters
12998
- ],
12999
- "start_que_id": patch.start_que_id,
13000
- "end_que_id": patch.end_que_id,
13001
- "title_que_id": patch.title_que_id,
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,
13002
12807
  "operation": "sync_default_view" if operation_phase == "default_view_apply_config_sync" else ("update" if existing_key or created_key else "create"),
13003
12808
  "list_type": system_view_list_type,
13004
12809
  "transport_error": {
@@ -13008,69 +12813,24 @@ class AiBuilderFacade:
13008
12813
  },
13009
12814
  **_view_apply_failure_diagnostics(
13010
12815
  patch=patch,
13011
- fields_by_que_id=fields_by_que_id,
12816
+ current_fields_by_name=current_fields_by_name,
13012
12817
  backend_code=api_error.backend_code,
13013
12818
  operation_phase=operation_phase,
13014
12819
  ),
13015
12820
  },
13016
12821
  }
13017
- failure_result = failure_entry
13018
- view_result = failure_entry
12822
+ failed_views.append(failure_entry)
12823
+ view_results.append(failure_entry)
13019
12824
  continue
13020
12825
  view_mutation_ms += _duration_ms(view_mutation_started_at)
13021
- for item in ((view_result,) if view_result is not None else ()):
13022
- requested_pinned = item.get("expected_pinned")
13023
- view_key = str(item.get("view_key") or "").strip()
13024
- if (
13025
- str(item.get("status") or "") not in {"created", "updated"}
13026
- or not isinstance(requested_pinned, bool)
13027
- or not view_key
13028
- ):
13029
- continue
13030
- try:
13031
- pin_result = self._set_view_pinned(
13032
- profile=profile,
13033
- app_key=app_key,
13034
- view_key=view_key,
13035
- pinned=requested_pinned,
13036
- )
13037
- item["pinned_state"] = pin_result
13038
- if not bool(pin_result.get("verified")):
13039
- failure_result = {
13040
- "name": item.get("name"),
13041
- "view_key": view_key,
13042
- "type": item.get("type"),
13043
- "status": "failed",
13044
- "error_code": "VIEW_PIN_STATE_READBACK_MISMATCH",
13045
- "message": "view configuration was saved, but its pinned state did not verify",
13046
- "operation": "set_pinned",
13047
- "details": pin_result,
13048
- }
13049
- except (QingflowApiError, RuntimeError, ValueError) as error:
13050
- api_error = _coerce_api_error(error)
13051
- pin_failure = {
13052
- "name": item.get("name"),
13053
- "view_key": view_key,
13054
- "type": item.get("type"),
13055
- "status": "failed",
13056
- "error_code": "VIEW_PIN_STATE_APPLY_FAILED",
13057
- "message": "view configuration was saved, but updating its pinned state failed",
13058
- "request_id": api_error.request_id,
13059
- "backend_code": api_error.backend_code,
13060
- "http_status": api_error.http_status,
13061
- "operation": "set_pinned",
13062
- "details": {"requested_pinned": requested_pinned},
13063
- }
13064
- item["pinned_state"] = {"requested_pinned": requested_pinned, "verified": False}
13065
- failure_result = pin_failure
13066
12826
  successful_action_view_keys = {
13067
12827
  str(item.get("view_key") or "").strip()
13068
- for item in ((view_result,) if view_result is not None else ())
12828
+ for item in view_results
13069
12829
  if str(item.get("status") or "") in {"created", "updated"} and str(item.get("view_key") or "").strip()
13070
12830
  }
13071
12831
  successful_action_view_names = {
13072
12832
  str(item.get("name") or "").strip()
13073
- for item in ((view_result,) if view_result is not None else ())
12833
+ for item in view_results
13074
12834
  if str(item.get("status") or "") in {"created", "updated"} and str(item.get("name") or "").strip()
13075
12835
  }
13076
12836
  skipped_action_button_intents = [
@@ -13121,12 +12881,12 @@ class AiBuilderFacade:
13121
12881
  action_buttons_failed = bool(action_button_intents) and not (action_buttons_verified and view_action_button_bindings_verified)
13122
12882
  action_buttons_retry_payload = action_buttons_result.get("retry_payload") if isinstance(action_buttons_result.get("retry_payload"), dict) else None
13123
12883
  needs_view_list_readback = bool(created or updated)
13124
- verified_views: list[dict[str, Any]] | None = []
12884
+ verified_view_result: list[dict[str, Any]] | None = []
13125
12885
  verified_views_unavailable = False
13126
12886
  if needs_view_list_readback:
13127
12887
  try:
13128
12888
  view_readback_started_at = time.perf_counter()
13129
- verified_views, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
12889
+ verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
13130
12890
  except (QingflowApiError, RuntimeError) as error:
13131
12891
  view_readback_ms += _duration_ms(view_readback_started_at)
13132
12892
  api_error = _coerce_api_error(error)
@@ -13140,23 +12900,23 @@ class AiBuilderFacade:
13140
12900
  view_readback_ms += _duration_ms(view_readback_started_at)
13141
12901
  verified_names = {
13142
12902
  _extract_view_name(item)
13143
- for item in (verified_views or [])
12903
+ for item in (verified_view_result or [])
13144
12904
  if isinstance(item, dict) and _extract_view_name(item)
13145
12905
  }
13146
12906
  verified_by_key = {
13147
12907
  _extract_view_key(item): item
13148
- for item in (verified_views or [])
12908
+ for item in (verified_view_result or [])
13149
12909
  if isinstance(item, dict) and _extract_view_key(item)
13150
12910
  }
13151
12911
  verified_view_keys_by_name: dict[str, list[str]] = {}
13152
- for item in (verified_views or []):
12912
+ for item in (verified_view_result or []):
13153
12913
  if not isinstance(item, dict):
13154
12914
  continue
13155
12915
  name = _extract_view_name(item)
13156
12916
  key = _extract_view_key(item)
13157
12917
  if name and key:
13158
12918
  verified_view_keys_by_name.setdefault(name, []).append(key)
13159
- verification_result: dict[str, Any] | None = None
12919
+ verification_by_view: list[dict[str, Any]] = []
13160
12920
  filter_readback_pending = False
13161
12921
  filter_mismatches: list[dict[str, Any]] = []
13162
12922
  query_condition_readback_pending = False
@@ -13183,7 +12943,7 @@ class AiBuilderFacade:
13183
12943
  return cached
13184
12944
 
13185
12945
  view_verification_started_at = time.perf_counter()
13186
- for item in ((view_result,) if view_result is not None else ()):
12946
+ for item in view_results:
13187
12947
  status = str(item.get("status") or "")
13188
12948
  name = str(item.get("name") or "")
13189
12949
  item_view_key = str(item.get("view_key") or "").strip()
@@ -13226,7 +12986,7 @@ class AiBuilderFacade:
13226
12986
  verification_entry["readback_ambiguous"] = True
13227
12987
  verification_entry["matching_view_keys"] = matched_keys
13228
12988
  filter_readback_pending = True
13229
- verification_result = verification_entry
12989
+ verification_by_view.append(verification_entry)
13230
12990
  continue
13231
12991
  try:
13232
12992
  config_result, config_error = read_view_config_for_verification(verification_key)
@@ -13244,11 +13004,11 @@ class AiBuilderFacade:
13244
13004
  filters_verified = filters_semantically_equivalent and actual_data_scope == expected_data_scope
13245
13005
  public_expected_filters = _public_view_filter_groups_from_match_rules(
13246
13006
  expected_filter_summary,
13247
- fields_by_que_id=fields_by_que_id,
13007
+ current_fields_by_name=current_fields_by_name,
13248
13008
  )
13249
13009
  public_actual_filters = _public_view_filter_groups_from_match_rules(
13250
13010
  semantic_actual_filters,
13251
- fields_by_que_id=fields_by_que_id,
13011
+ current_fields_by_name=current_fields_by_name,
13252
13012
  )
13253
13013
  verification_entry["filters_verified"] = filters_verified
13254
13014
  verification_entry["view_key"] = verification_key
@@ -13298,7 +13058,7 @@ class AiBuilderFacade:
13298
13058
  verification_entry["readback_ambiguous"] = True
13299
13059
  verification_entry["matching_view_keys"] = matched_keys
13300
13060
  query_condition_readback_pending = True
13301
- verification_result = verification_entry
13061
+ verification_by_view.append(verification_entry)
13302
13062
  continue
13303
13063
  try:
13304
13064
  config_result, config_error = read_view_config_for_verification(verification_key)
@@ -13347,7 +13107,7 @@ class AiBuilderFacade:
13347
13107
  verification_entry["readback_ambiguous"] = True
13348
13108
  verification_entry["matching_view_keys"] = matched_keys
13349
13109
  associated_resource_readback_pending = True
13350
- verification_result = verification_entry
13110
+ verification_by_view.append(verification_entry)
13351
13111
  continue
13352
13112
  try:
13353
13113
  config_result, config_error = read_view_config_for_verification(verification_key)
@@ -13402,7 +13162,7 @@ class AiBuilderFacade:
13402
13162
  verification_entry["readback_ambiguous"] = True
13403
13163
  verification_entry["matching_view_keys"] = matched_keys
13404
13164
  button_readback_pending = True
13405
- verification_result = verification_entry
13165
+ verification_by_view.append(verification_entry)
13406
13166
  continue
13407
13167
  try:
13408
13168
  config_result, config_error = read_view_config_for_verification(verification_key)
@@ -13450,45 +13210,51 @@ class AiBuilderFacade:
13450
13210
  "category": api_error.category,
13451
13211
  }
13452
13212
  button_readback_pending = True
13453
- verification_result = verification_entry
13213
+ verification_by_view.append(verification_entry)
13454
13214
  elif status == "removed":
13455
- verification_result = {
13456
- "name": name,
13457
- "view_key": item.get("view_key"),
13458
- "type": item.get("type"),
13459
- "status": "removed",
13460
- "present_in_readback": False,
13461
- "removed_verified": True,
13462
- "delete_executed": bool(item.get("delete_executed")),
13463
- "readback_status": item.get("readback_status") or "deleted",
13464
- "safe_to_retry_delete": False,
13465
- }
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
+ )
13466
13228
  elif status == "readback_pending" and item.get("operation") == "delete":
13467
13229
  readback_status = str(item.get("readback_status") or "unavailable")
13468
- verification_result = {
13469
- "name": name,
13470
- "view_key": item.get("view_key"),
13471
- "type": item.get("type"),
13472
- "status": "readback_pending",
13473
- "present_in_readback": True if readback_status == "still_exists" else None,
13474
- "removed_verified": False,
13475
- "delete_executed": bool(item.get("delete_executed")),
13476
- "readback_status": readback_status,
13477
- "safe_to_retry_delete": False,
13478
- "error_code": item.get("error_code"),
13479
- }
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
+ )
13480
13244
  else:
13481
- verification_result = {
13482
- "name": item.get("name"),
13483
- "type": item.get("type"),
13484
- "status": "failed",
13485
- "present_in_readback": None,
13486
- "error_code": item.get("error_code"),
13487
- }
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
+ )
13488
13254
  view_verification_ms += _duration_ms(view_verification_started_at)
13489
13255
  removed_delete_results = [
13490
13256
  item
13491
- for item in ((view_result,) if view_result is not None else ())
13257
+ for item in view_results
13492
13258
  if item.get("operation") == "delete" and bool(item.get("delete_executed"))
13493
13259
  ]
13494
13260
  removed_verified = all(str(item.get("readback_status") or "") == "deleted" for item in removed_delete_results)
@@ -13503,19 +13269,19 @@ class AiBuilderFacade:
13503
13269
  view_associated_resources_verified = verified and not associated_resource_readback_pending and not associated_resource_mismatches
13504
13270
  view_buttons_verified = verified and not button_readback_pending and not button_mismatches and not custom_button_readback_pending
13505
13271
  noop = not created and not updated and not removed and not action_button_write_executed
13506
- if failure_result is not None:
13272
+ if failed_views:
13507
13273
  successful_changes = bool(created or updated or removed or action_button_write_succeeded)
13508
- first_failure = failure_result
13274
+ first_failure = failed_views[0]
13509
13275
  response = {
13510
13276
  "status": "partial_success" if successful_changes else "failed",
13511
13277
  "error_code": "VIEW_APPLY_PARTIAL" if successful_changes else "VIEW_APPLY_FAILED",
13512
13278
  "recoverable": True,
13513
- "message": "view operation completed with a verification failure" if successful_changes else "view operation failed",
13279
+ "message": "applied some view patches; at least one view failed" if successful_changes else "one or more view patches failed",
13514
13280
  "normalized_args": normalized_args,
13515
13281
  "missing_fields": [],
13516
13282
  "allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
13517
13283
  "details": {
13518
- "view_result": deepcopy(view_result) if view_result is not None else None,
13284
+ "per_view_results": view_results,
13519
13285
  "filter_mismatches": filter_mismatches,
13520
13286
  "query_condition_mismatches": query_condition_mismatches,
13521
13287
  "associated_resource_mismatches": associated_resource_mismatches,
@@ -13533,8 +13299,7 @@ class AiBuilderFacade:
13533
13299
  "http_status": first_failure.get("http_status"),
13534
13300
  "noop": noop,
13535
13301
  "warnings": (
13536
- list(stale_warnings)
13537
- + (
13302
+ (
13538
13303
  [_warning("VIEW_FILTERS_UNVERIFIED", "view definitions may exist, but saved filter behavior is not fully verified")]
13539
13304
  if (filter_readback_pending or filter_mismatches)
13540
13305
  else []
@@ -13574,14 +13339,13 @@ class AiBuilderFacade:
13574
13339
  "action_buttons_verified": action_buttons_verified,
13575
13340
  "view_button_bindings_verified": view_action_button_bindings_verified,
13576
13341
  "views_read_unavailable": verified_views_unavailable,
13577
- "view": deepcopy(verification_result) if verification_result is not None else None,
13342
+ "by_view": verification_by_view,
13578
13343
  "custom_button_readback_pending": custom_button_readback_pending,
13579
13344
  "custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
13580
13345
  },
13581
13346
  "app_key": app_key,
13582
13347
  "app_name": app_name,
13583
- "view_result": deepcopy(view_result) if view_result is not None else None,
13584
- "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": [failure_result]},
13348
+ "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
13585
13349
  "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,
13586
13350
  "write_executed": bool(created or updated or removed or action_button_write_executed),
13587
13351
  "write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
@@ -13589,7 +13353,6 @@ class AiBuilderFacade:
13589
13353
  }
13590
13354
  return finish_with_publish(response)
13591
13355
  warnings: list[dict[str, Any]] = []
13592
- warnings.extend(stale_warnings)
13593
13356
  if filter_readback_pending or filter_mismatches:
13594
13357
  warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
13595
13358
  if query_condition_readback_pending or query_condition_mismatches:
@@ -13608,388 +13371,87 @@ class AiBuilderFacade:
13608
13371
  )
13609
13372
  )
13610
13373
  if delete_readback_pending:
13611
- warnings.append(
13612
- _warning(
13613
- "VIEW_DELETE_READBACK_PENDING",
13614
- "view delete was sent, but deletion readback is not fully verified; do not blindly repeat delete",
13615
- )
13616
- )
13617
- all_verified = (
13618
- verified
13619
- and view_filters_verified
13620
- and view_query_conditions_verified
13621
- and view_associated_resources_verified
13622
- and view_buttons_verified
13623
- and action_buttons_verified
13624
- and view_action_button_bindings_verified
13625
- )
13626
- action_buttons_error_code = str(action_buttons_result.get("error_code") or "VIEW_ACTION_BUTTONS_APPLY_FAILED") if action_buttons_failed else None
13627
- response = {
13628
- "status": "success" if all_verified else "partial_success",
13629
- "error_code": None if all_verified else (action_buttons_error_code if action_buttons_failed else "VIEW_BUTTON_READBACK_MISMATCH" if button_mismatches else "VIEW_ASSOCIATED_RESOURCE_READBACK_MISMATCH" if associated_resource_mismatches else "VIEW_QUERY_CONDITION_READBACK_MISMATCH" if query_condition_mismatches else "VIEW_FILTER_READBACK_MISMATCH" if filter_mismatches else "VIEWS_READBACK_PENDING"),
13630
- "recoverable": not all_verified,
13631
- "message": (
13632
- "applied view patch"
13633
- if all_verified
13634
- else "applied view patch; inline action buttons did not fully verify"
13635
- if action_buttons_failed
13636
- else "applied view patch; buttons did not fully verify"
13637
- if button_mismatches
13638
- else "applied view patch; associated resources did not fully verify"
13639
- if associated_resource_mismatches
13640
- else "applied view patch; query conditions did not fully verify"
13641
- if query_condition_mismatches
13642
- else "applied view patch; filters did not fully verify"
13643
- if filter_mismatches
13644
- else "applied view patch; views readback pending"
13645
- ),
13646
- "normalized_args": normalized_args,
13647
- "missing_fields": [],
13648
- "allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator], "view.associated_resources.limit_type": ["all", "select"]},
13649
- "details": {
13650
- **({"filter_mismatches": filter_mismatches} if filter_mismatches else {}),
13651
- **({"query_condition_mismatches": query_condition_mismatches} if query_condition_mismatches else {}),
13652
- **({"associated_resource_mismatches": associated_resource_mismatches} if associated_resource_mismatches else {}),
13653
- **({"button_mismatches": button_mismatches} if button_mismatches else {}),
13654
- "view_result": deepcopy(view_result) if view_result is not None else None,
13655
- **({"action_buttons_result": action_buttons_result} if action_button_intents else {}),
13656
- **(
13657
- {"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
13658
- if custom_button_readback_pending_entries
13659
- else {}
13660
- ),
13661
- },
13662
- "request_id": None,
13663
- "suggested_next_call": None if all_verified else (action_buttons_retry_payload if action_buttons_failed and action_buttons_retry_payload else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}}),
13664
- "noop": noop,
13665
- "warnings": warnings,
13666
- "verification": {
13667
- "views_verified": verified,
13668
- "view_filters_verified": view_filters_verified,
13669
- "view_query_conditions_verified": view_query_conditions_verified,
13670
- "view_associated_resources_verified": view_associated_resources_verified,
13671
- "view_buttons_verified": view_buttons_verified,
13672
- "action_buttons_verified": action_buttons_verified,
13673
- "view_button_bindings_verified": view_action_button_bindings_verified,
13674
- "views_read_unavailable": verified_views_unavailable,
13675
- "filter_readback_pending": filter_readback_pending,
13676
- "query_condition_readback_pending": query_condition_readback_pending,
13677
- "associated_resource_readback_pending": associated_resource_readback_pending,
13678
- "button_readback_pending": button_readback_pending,
13679
- "delete_readback_pending": delete_readback_pending,
13680
- "custom_button_readback_pending": custom_button_readback_pending,
13681
- "custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
13682
- "view": deepcopy(verification_result) if verification_result is not None else None,
13683
- },
13684
- "app_key": app_key,
13685
- "app_name": app_name,
13686
- "view_result": deepcopy(view_result) if view_result is not None else None,
13687
- "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
13688
- "verified": all_verified,
13689
- "write_executed": bool(created or updated or removed or action_button_write_executed),
13690
- "write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
13691
- "safe_to_retry": not bool(created or updated or removed or action_button_write_executed),
13692
- }
13693
- return finish_with_publish(response)
13694
-
13695
- def app_views_schema(
13696
- self,
13697
- *,
13698
- schema_version: str | None = None,
13699
- schema_kind: str | None = None,
13700
- ) -> JSONObject:
13701
- """读取版本化视图声明 schema;detail kind 用于获取专项配置定义。"""
13702
- normalized_args = {"schema_version": schema_version, "schema_kind": schema_kind}
13703
- try:
13704
- payload = views_schema_registry().schema(schema_version=schema_version, schema_kind=schema_kind)
13705
- except ViewsContractError as error:
13706
- return _failed(
13707
- error.error_code,
13708
- str(error.message),
13709
- normalized_args=normalized_args,
13710
- details={"reason_path": error.path or "", **(error.details or {})},
13711
- suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "summary"}},
13712
- )
13713
- return {
13714
- "status": "success",
13715
- "error_code": None,
13716
- "recoverable": False,
13717
- "message": "read views declaration schema",
13718
- "normalized_args": normalized_args,
13719
- "missing_fields": [],
13720
- "allowed_values": {"schema_kind": payload["supportedSchemaKinds"]},
13721
- "details": {},
13722
- "request_id": None,
13723
- "suggested_next_call": None,
13724
- "warnings": [],
13725
- "verification": {"schema_loaded": True},
13726
- **payload,
13727
- }
13728
-
13729
- def app_views_get(
13730
- self,
13731
- *,
13732
- profile: str,
13733
- app_key: str,
13734
- schema_version: str | None = None,
13735
- schema_kind: str = "summary",
13736
- ) -> JSONObject:
13737
- """读取应用的 canonical 视图声明数组(summary 级投影或 complete 反向编译)。"""
13738
- normalized_args = {"app_key": app_key, "schema_version": schema_version, "schema_kind": schema_kind}
13739
- try:
13740
- context = views_schema_registry().resolve_schema_version(schema_version)
13741
- except ViewsContractError as error:
13742
- return _failed(
13743
- error.error_code,
13744
- str(error.message),
13745
- normalized_args=normalized_args,
13746
- details={"reason_path": error.path or "", **(error.details or {})},
13747
- suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "summary"}},
13748
- )
13749
- try:
13750
- views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
13751
- except (QingflowApiError, RuntimeError) as error:
13752
- api_error = _coerce_api_error(error)
13753
- return _failed_from_api_error(
13754
- "VIEWS_READ_FAILED",
13755
- api_error,
13756
- normalized_args=normalized_args,
13757
- details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
13758
- suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
13759
- )
13760
- schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
13761
- parsed_schema = _parse_schema(schema)
13762
- # 读面 summary 面向 agent 展示字段名:从原始 schema 递归收集(同名不丢);
13763
- # 系统列(queId<=5)用公开名映射,未知系统列以 queId 展示兜底
13764
- name_by_que_id: dict[int, str] = {
13765
- **{system_id: name for system_id, name in _SYSTEM_VIEW_QUE_NAMES.items()},
13766
- **_collect_field_id_to_name(schema),
13767
- }
13768
- normalized_kind = str(schema_kind or "summary").strip().lower()
13769
- if normalized_kind == "complete":
13770
- return self._app_views_get_complete(
13771
- profile=profile,
13772
- app_key=app_key,
13773
- views=views,
13774
- parsed_schema=parsed_schema,
13775
- normalized_args=normalized_args,
13776
- schema_version=schema_version,
13777
- )
13778
- projected: list[dict[str, Any]] = []
13779
- scope_map = {"ALL": "all", "ONESELF": "oneself", "DEPT": "dept", "SUBORDINATE": "subordinate", "CUSTOM": "custom"}
13780
- for view in views if isinstance(views, list) else []:
13781
- if not isinstance(view, dict):
13782
- continue
13783
- entry: dict[str, Any] = {
13784
- "view_key": _extract_view_key(view),
13785
- "name": _extract_view_name(view),
13786
- "type": _normalize_view_type_name(view.get("viewgraphType") or view.get("type")),
13787
- }
13788
- columns = [name_by_que_id.get(que_id, str(que_id)) for que_id in view.get("viewgraphQueIds") or [] if isinstance(que_id, int)]
13789
- if columns:
13790
- entry["columns"] = columns
13791
- group_que_id = _coerce_positive_int(view.get("groupQueId"))
13792
- if group_que_id is not None and group_que_id in name_by_que_id:
13793
- entry["group_by"] = name_by_que_id[group_que_id]
13794
- gantt_config = view.get("viewgraphGanttConfigVO")
13795
- if isinstance(gantt_config, dict):
13796
- for source, target in (
13797
- ("startTimeQueId", "start_field"),
13798
- ("endTimeQueId", "end_field"),
13799
- ("titleQueId", "title_field"),
13800
- ):
13801
- gantt_que_id = _coerce_positive_int(gantt_config.get(source))
13802
- if gantt_que_id is not None and gantt_que_id in name_by_que_id:
13803
- entry[target] = name_by_que_id[gantt_que_id]
13804
- title_que_id = _coerce_positive_int(view.get("titleQue"))
13805
- if title_que_id is not None and title_que_id in name_by_que_id:
13806
- entry.setdefault("title_field", name_by_que_id[title_que_id])
13807
- data_permission_type = view.get("dataPermissionType")
13808
- if data_permission_type:
13809
- entry["data_permission"] = {
13810
- "type": "config" if str(data_permission_type).upper() == "CONFIG" else "custom",
13811
- "scope": scope_map.get(str(view.get("dataScope") or "").upper(), "all"),
13812
- }
13813
- projected.append(entry)
13814
- return {
13815
- "status": "success",
13816
- "error_code": None,
13817
- "recoverable": False,
13818
- "message": "read canonical views declarations",
13819
- "normalized_args": normalized_args,
13820
- "missing_fields": [],
13821
- "allowed_values": {"view_types": [member.value for member in PublicViewType]},
13822
- "details": {},
13823
- "request_id": None,
13824
- "suggested_next_call": None,
13825
- "warnings": [],
13826
- "verification": {"views_read": len(projected)},
13827
- "schemaVersion": context.schema_version,
13828
- "apiVersion": context.api_version,
13829
- "latestVersion": views_schema_registry().latest_version,
13830
- "app_key": app_key,
13831
- "views": projected,
13832
- }
13833
-
13834
- def _app_views_get_complete(
13835
- self,
13836
- *,
13837
- profile: str,
13838
- app_key: str,
13839
- views: Any,
13840
- parsed_schema: dict[str, Any],
13841
- normalized_args: JSONObject,
13842
- schema_version: str | None,
13843
- ) -> JSONObject:
13844
- """complete 档 canonical:逐视图 GET /view/{key}/viewConfig → 反向编译。"""
13845
- from .views_extract import extract_complete_view_declaration, summarize_gaps
13846
-
13847
- fields_by_que_id = _collect_fields_by_que_id(parsed_schema["fields"])
13848
- projected: list[dict[str, Any]] = []
13849
- read_errors: list[dict[str, Any]] = []
13850
- all_gaps: list[dict[str, Any]] = []
13851
- for view in views if isinstance(views, list) else []:
13852
- if not isinstance(view, dict):
13853
- continue
13854
- view_key = _extract_view_key(view)
13855
- view_name = _extract_view_name(view)
13856
- if not view_key:
13857
- continue
13858
- 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")):
13859
- # 系统视图(列表/申请页等,viewgraphKey 为固定数字 id):不在公开声明语义内,直接跳过。
13860
- continue
13861
- try:
13862
- config = self.views.view_get_config(profile=profile, viewgraph_key=view_key).get("result") or {}
13863
- except (QingflowApiError, RuntimeError) as error:
13864
- api_error = _coerce_api_error(error)
13865
- read_errors.append(
13866
- {
13867
- "view_key": view_key,
13868
- "name": view_name,
13869
- "error_code": "VIEW_CONFIG_READ_FAILED",
13870
- "http_status": api_error.http_status,
13871
- "message": str(api_error.message or error),
13872
- }
13873
- )
13874
- continue
13875
- if not isinstance(config, dict) or not config:
13876
- read_errors.append({"view_key": view_key, "name": view_name, "error_code": "VIEW_CONFIG_EMPTY", "message": "viewConfig read returned no data"})
13877
- continue
13878
- declaration, gaps = extract_complete_view_declaration(config, fields_by_que_id=fields_by_que_id)
13879
- if not declaration:
13880
- read_errors.append(
13881
- {
13882
- "view_key": view_key,
13883
- "name": view_name,
13884
- "error_code": "VIEW_NOT_EXTRACTABLE",
13885
- "message": "view configuration cannot be represented completely by the public contract; see gaps",
13886
- "gaps": gaps,
13887
- }
13374
+ warnings.append(
13375
+ _warning(
13376
+ "VIEW_DELETE_READBACK_PENDING",
13377
+ "view delete was sent, but deletion readback is not fully verified; do not blindly repeat delete",
13888
13378
  )
13889
- continue
13890
- if gaps:
13891
- all_gaps.append({"view_key": view_key, "gaps": gaps})
13892
- projected.append(declaration)
13893
- gap_summary: dict[str, int] = {}
13894
- for item in all_gaps:
13895
- for kind, count in summarize_gaps(item["gaps"]).items():
13896
- gap_summary[kind] = gap_summary.get(kind, 0) + count
13897
- context = views_schema_registry().resolve_schema_version(schema_version)
13898
- return {
13899
- "status": "success",
13900
- "error_code": None,
13901
- "recoverable": False,
13902
- "message": "read canonical views declarations (complete)",
13379
+ )
13380
+ all_verified = (
13381
+ verified
13382
+ and view_filters_verified
13383
+ and view_query_conditions_verified
13384
+ and view_associated_resources_verified
13385
+ and view_buttons_verified
13386
+ and action_buttons_verified
13387
+ and view_action_button_bindings_verified
13388
+ )
13389
+ action_buttons_error_code = str(action_buttons_result.get("error_code") or "VIEW_ACTION_BUTTONS_APPLY_FAILED") if action_buttons_failed else None
13390
+ response = {
13391
+ "status": "success" if all_verified else "partial_success",
13392
+ "error_code": None if all_verified else (action_buttons_error_code if action_buttons_failed else "VIEW_BUTTON_READBACK_MISMATCH" if button_mismatches else "VIEW_ASSOCIATED_RESOURCE_READBACK_MISMATCH" if associated_resource_mismatches else "VIEW_QUERY_CONDITION_READBACK_MISMATCH" if query_condition_mismatches else "VIEW_FILTER_READBACK_MISMATCH" if filter_mismatches else "VIEWS_READBACK_PENDING"),
13393
+ "recoverable": not all_verified,
13394
+ "message": (
13395
+ "applied view patch"
13396
+ if all_verified
13397
+ else "applied view patch; inline action buttons did not fully verify"
13398
+ if action_buttons_failed
13399
+ else "applied view patch; buttons did not fully verify"
13400
+ if button_mismatches
13401
+ else "applied view patch; associated resources did not fully verify"
13402
+ if associated_resource_mismatches
13403
+ else "applied view patch; query conditions did not fully verify"
13404
+ if query_condition_mismatches
13405
+ else "applied view patch; filters did not fully verify"
13406
+ if filter_mismatches
13407
+ else "applied view patch; views readback pending"
13408
+ ),
13903
13409
  "normalized_args": normalized_args,
13904
13410
  "missing_fields": [],
13905
- "allowed_values": {"view_types": [member.value for member in PublicViewType]},
13411
+ "allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator], "view.associated_resources.limit_type": ["all", "select"]},
13906
13412
  "details": {
13907
- "extraction_gap_summary": gap_summary,
13908
- "extraction_gaps": all_gaps[:50],
13909
- "config_read_errors": read_errors[:50],
13910
- "config_read_error_count": len(read_errors),
13413
+ **({"filter_mismatches": filter_mismatches} if filter_mismatches else {}),
13414
+ **({"query_condition_mismatches": query_condition_mismatches} if query_condition_mismatches else {}),
13415
+ **({"associated_resource_mismatches": associated_resource_mismatches} if associated_resource_mismatches else {}),
13416
+ **({"button_mismatches": button_mismatches} if button_mismatches else {}),
13417
+ **({"action_buttons_result": action_buttons_result} if action_button_intents else {}),
13418
+ **(
13419
+ {"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
13420
+ if custom_button_readback_pending_entries
13421
+ else {}
13422
+ ),
13911
13423
  },
13912
13424
  "request_id": None,
13913
- "suggested_next_call": None,
13914
- "warnings": [],
13915
- "verification": {"views_read": len(projected), "views_with_gaps": len(all_gaps), "read_errors": len(read_errors)},
13916
- "schemaVersion": context.schema_version,
13917
- "apiVersion": context.api_version,
13918
- "latestVersion": views_schema_registry().latest_version,
13425
+ "suggested_next_call": None if all_verified else (action_buttons_retry_payload if action_buttons_failed and action_buttons_retry_payload else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}}),
13426
+ "noop": noop,
13427
+ "warnings": warnings,
13428
+ "verification": {
13429
+ "views_verified": verified,
13430
+ "view_filters_verified": view_filters_verified,
13431
+ "view_query_conditions_verified": view_query_conditions_verified,
13432
+ "view_associated_resources_verified": view_associated_resources_verified,
13433
+ "view_buttons_verified": view_buttons_verified,
13434
+ "action_buttons_verified": action_buttons_verified,
13435
+ "view_button_bindings_verified": view_action_button_bindings_verified,
13436
+ "views_read_unavailable": verified_views_unavailable,
13437
+ "filter_readback_pending": filter_readback_pending,
13438
+ "query_condition_readback_pending": query_condition_readback_pending,
13439
+ "associated_resource_readback_pending": associated_resource_readback_pending,
13440
+ "button_readback_pending": button_readback_pending,
13441
+ "delete_readback_pending": delete_readback_pending,
13442
+ "custom_button_readback_pending": custom_button_readback_pending,
13443
+ "custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
13444
+ "by_view": verification_by_view,
13445
+ },
13919
13446
  "app_key": app_key,
13920
- "views": projected,
13921
- }
13922
-
13923
- def app_views_validate(
13924
- self,
13925
- *,
13926
- declaration: JSONObject,
13927
- schema_version: str | None = None,
13928
- ) -> JSONObject:
13929
- """纯离线校验一条视图声明;不访问后端。"""
13930
- normalized_args = {"schema_version": schema_version}
13931
- resolved_version = str(schema_version or (declaration.get("schema_version") if isinstance(declaration, dict) else None) or "").strip() or None
13932
- try:
13933
- context = views_schema_registry().resolve_schema_version(resolved_version)
13934
- except ViewsContractError as error:
13935
- return _failed(
13936
- error.error_code,
13937
- str(error.message),
13938
- normalized_args=normalized_args,
13939
- details={"reason_path": error.path or "", **(error.details or {})},
13940
- suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "complete"}},
13941
- )
13942
- issues: list[dict[str, Any]] = []
13943
- batch_keys = ("views", "upsert_views", "upsertViews", "apps")
13944
- if not isinstance(declaration, dict):
13945
- issues.append({"path": "/", "code": "INVALID_SPEC", "message": "views declaration must be a JSON object"})
13946
- elif any(key in declaration for key in batch_keys):
13947
- issues.append(
13948
- {
13949
- "path": "/",
13950
- "code": "VIEW_BATCH_CREATE_UNSUPPORTED",
13951
- "message": "views schema validates one view declaration; batch envelopes are unsupported",
13952
- }
13953
- )
13954
- else:
13955
- declaration_item = {
13956
- key: value
13957
- for key, value in declaration.items()
13958
- if key not in {"schema_version", "schemaVersion", "apiVersion"}
13959
- }
13960
- try:
13961
- parsed = ViewUpsertPatch.model_validate(declaration_item)
13962
- context.validate_declaration(compact_none_values(public_view_upsert_payload(parsed)))
13963
- except ValidationError as exc:
13964
- first_error = exc.errors()[0]
13965
- issues.append(
13966
- {
13967
- "path": f"/{'/'.join(str(part) for part in first_error.get('loc', ()))}".rstrip("/"),
13968
- "code": "INVALID_SPEC",
13969
- "message": str(first_error.get("msg") or exc),
13970
- }
13971
- )
13972
- except ViewsContractError as error:
13973
- issues.append({"path": error.path or "/", "code": error.error_code, "message": str(error.message)})
13974
- ok = not issues
13975
- return {
13976
- "status": "success" if ok else "failed",
13977
- "error_code": None if ok else issues[0]["code"],
13978
- "recoverable": True,
13979
- "message": "views declaration is valid" if ok else "views declaration failed validation",
13980
- "normalized_args": normalized_args,
13981
- "missing_fields": [],
13982
- "allowed_values": {},
13983
- "details": {"issues": issues},
13984
- "request_id": None,
13985
- "suggested_next_call": None,
13986
- "warnings": [],
13987
- "verification": {"validated_entries": 1 if ok else 0},
13988
- "ok": ok,
13989
- "schemaVersion": context.schema_version,
13990
- "apiVersion": context.api_version,
13991
- "issues": issues,
13447
+ "app_name": app_name,
13448
+ "views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
13449
+ "verified": all_verified,
13450
+ "write_executed": bool(created or updated or removed or action_button_write_executed),
13451
+ "write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
13452
+ "safe_to_retry": not bool(created or updated or removed or action_button_write_executed),
13992
13453
  }
13454
+ return finish_with_publish(response)
13993
13455
 
13994
13456
  def app_publish_verify(
13995
13457
  self,
@@ -15967,76 +15429,6 @@ class AiBuilderFacade:
15967
15429
  result = legacy_normalized or normalized_views
15968
15430
  return (result, False, None) if include_error else (result, False)
15969
15431
 
15970
- def _set_view_pinned(
15971
- self,
15972
- *,
15973
- profile: str,
15974
- app_key: str,
15975
- view_key: str,
15976
- pinned: bool,
15977
- ) -> dict[str, Any]:
15978
- """Move one view between the application-level fixed and ordinary lists."""
15979
- target_key = str(view_key or "").strip()
15980
- if not target_key:
15981
- raise ValueError("view_key is required to update the pinned state")
15982
- response = self.views.view_list(profile=profile, app_key=app_key)
15983
- raw_groups = response.get("result") if isinstance(response, dict) else None
15984
- if not isinstance(raw_groups, list):
15985
- raise ValueError("view list does not expose ordinal groups")
15986
-
15987
- group_keys: dict[str, list[str]] = {
15988
- "FIXED_VIEW_LIST": [],
15989
- "VIEW_LIST": [],
15990
- }
15991
- for group in raw_groups:
15992
- if not isinstance(group, dict):
15993
- continue
15994
- ordinal_type = str(group.get("ordinalType") or "").strip()
15995
- if ordinal_type not in group_keys:
15996
- continue
15997
- view_list = group.get("viewList")
15998
- if not isinstance(view_list, list):
15999
- continue
16000
- group_keys[ordinal_type] = [
16001
- key
16002
- for item in view_list
16003
- if isinstance(item, dict)
16004
- and (key := _extract_view_key(item))
16005
- and key != target_key
16006
- ]
16007
-
16008
- target_group = "FIXED_VIEW_LIST" if pinned else "VIEW_LIST"
16009
- group_keys[target_group].append(target_key)
16010
- self.views.view_reorder(
16011
- profile=profile,
16012
- app_key=app_key,
16013
- payload=[
16014
- {"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": group_keys["FIXED_VIEW_LIST"]},
16015
- {"ordinalType": "VIEW_LIST", "viewKeyList": group_keys["VIEW_LIST"]},
16016
- ],
16017
- )
16018
-
16019
- readback = self.views.view_list(profile=profile, app_key=app_key)
16020
- readback_groups = readback.get("result") if isinstance(readback, dict) else None
16021
- actual_group: str | None = None
16022
- if isinstance(readback_groups, list):
16023
- for group in readback_groups:
16024
- if not isinstance(group, dict):
16025
- continue
16026
- ordinal_type = str(group.get("ordinalType") or "").strip()
16027
- for item in group.get("viewList") or []:
16028
- if isinstance(item, dict) and _extract_view_key(item) == target_key:
16029
- actual_group = ordinal_type
16030
- break
16031
- if actual_group is not None:
16032
- break
16033
- return {
16034
- "requested_pinned": pinned,
16035
- "actual_pinned": actual_group == "FIXED_VIEW_LIST" if actual_group is not None else None,
16036
- "verified": actual_group == target_group,
16037
- "actual_ordinal_type": actual_group,
16038
- }
16039
-
16040
15432
  def _load_workflow_result(
16041
15433
  self,
16042
15434
  *,
@@ -17464,7 +16856,7 @@ def _resolve_custom_button_view_button_ref(
17464
16856
  "reason_path": reason_path,
17465
16857
  "button_ref": button_ref,
17466
16858
  "message": "button_ref id does not exist in the current app draft or was removed in this apply",
17467
- "next_action": "call app_get_buttons with button_text to find an existing button_id, or use a client_key from upsert_buttons",
16859
+ "next_action": "call app_get and use custom_buttons[].button_id, or use a client_key from upsert_buttons",
17468
16860
  }
17469
16861
  ref_text = str(button_ref or "").strip()
17470
16862
  if ref_text in client_key_map:
@@ -19834,44 +19226,40 @@ def _chart_apply_batching_context(
19834
19226
  def _view_apply_failure_diagnostics(
19835
19227
  *,
19836
19228
  patch: ViewUpsertPatch,
19837
- fields_by_que_id: dict[int, dict[str, Any]],
19229
+ current_fields_by_name: dict[str, dict[str, Any]],
19838
19230
  backend_code: Any,
19839
19231
  operation_phase: str,
19840
19232
  ) -> dict[str, Any]:
19841
- references: list[tuple[int, str]] = []
19233
+ references: list[tuple[str, str]] = []
19842
19234
 
19843
- def add_reference(value: Any, source: str) -> None:
19844
- que_id = _coerce_positive_int(value)
19845
- if que_id is not None:
19846
- references.append((que_id, source))
19235
+ def add_reference(name: Any, source: str) -> None:
19236
+ text = str(name or "").strip()
19237
+ if text:
19238
+ references.append((text, source))
19847
19239
 
19848
19240
  for column in patch.columns:
19849
19241
  add_reference(column, "columns")
19850
- add_reference(patch.group_que_id, "group_que_id")
19851
- add_reference(patch.start_que_id, "start_que_id")
19852
- add_reference(patch.end_que_id, "end_que_id")
19853
- add_reference(patch.title_que_id, "title_que_id")
19854
- for item in patch.filters:
19855
- if isinstance(item, list):
19856
- for rule in item:
19857
- add_reference(getattr(rule, "que_id", None), "filters")
19858
- else:
19859
- add_reference(getattr(item, "que_id", None), "filters")
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")
19860
19248
 
19861
- seen: set[tuple[int, str]] = set()
19249
+ seen: set[str] = set()
19862
19250
  field_entries: list[dict[str, Any]] = []
19863
- for que_id, source in references:
19864
- key = (que_id, source)
19251
+ for field_name, source in references:
19252
+ key = f"{field_name}\0{source}"
19865
19253
  if key in seen:
19866
19254
  continue
19867
19255
  seen.add(key)
19868
- field = fields_by_que_id.get(que_id) or {}
19256
+ field = current_fields_by_name.get(field_name) or {}
19869
19257
  field_entries.append(
19870
19258
  {
19871
- "field_name": str(field.get("name") or que_id),
19259
+ "field_name": field_name,
19872
19260
  "source": source,
19873
19261
  "field_id": field.get("field_id"),
19874
- "que_id": field.get("que_id") or que_id,
19262
+ "que_id": field.get("que_id"),
19875
19263
  "field_type": field.get("type"),
19876
19264
  "que_type": field.get("que_type"),
19877
19265
  "exists_in_schema": bool(field),
@@ -19889,7 +19277,7 @@ def _view_apply_failure_diagnostics(
19889
19277
  "recommended_minimal_retry": {
19890
19278
  "name": patch.name,
19891
19279
  "type": patch.type.value,
19892
- "columns": [field_entries[0]["que_id"]] if field_entries else [],
19280
+ "columns": [field_entries[0]["field_name"]] if field_entries else [],
19893
19281
  **({"filters": [rule.model_dump(mode="json", exclude_none=True) for rule in patch.filters]} if patch.filters else {}),
19894
19282
  },
19895
19283
  }
@@ -20968,8 +20356,6 @@ def _normalize_portal_view_type(view: dict[str, Any]) -> str:
20968
20356
  "ganttView": "ganttView",
20969
20357
  "hierarchy": "hierarchyView",
20970
20358
  "hierarchyView": "hierarchyView",
20971
- "calendar": "calendarView",
20972
- "calendarView": "calendarView",
20973
20359
  }
20974
20360
  return mapping.get(raw, "tableView")
20975
20361
 
@@ -21800,10 +21186,8 @@ def _infer_field_type(question: dict[str, Any]) -> str:
21800
21186
 
21801
21187
 
21802
21188
  def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None) -> dict[str, Any]:
21189
+ name = str(question.get("queTitle") or "").strip()
21803
21190
  que_id = _coerce_positive_int(question.get("queId"))
21804
- # 无标题字段用「字段{que_id}」兜底命名,与提取器(eval_replay/views get)对称,
21805
- # 否则视图列引用无标题字段时 UNKNOWN_VIEW_FIELD
21806
- name = str(question.get("queTitle") or "").strip() or (f"字段{que_id}" if que_id else "")
21807
21191
  que_type = _coerce_positive_int(question.get("queType"))
21808
21192
  field_type = _infer_field_type(question)
21809
21193
  field_id = field_id_hint or f"field_{que_id or _slugify(name, default='x')}"
@@ -21973,17 +21357,6 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
21973
21357
  for question in inner_row or []:
21974
21358
  if not isinstance(question, dict):
21975
21359
  continue
21976
- if _coerce_positive_int(question.get("queType")) == 24:
21977
- # 嵌套段落:不注册段落本身,递归解析其 innerQuestions
21978
- for nested_row in question.get("innerQuestions", []) or []:
21979
- for nested_question in (nested_row if isinstance(nested_row, list) else [nested_row]):
21980
- if not isinstance(nested_question, dict):
21981
- continue
21982
- nested_field = _parse_field(nested_question)
21983
- if str(nested_field["name"] or "").strip():
21984
- fields_by_name.setdefault(nested_field["name"], nested_field)
21985
- labels.append(nested_field["name"])
21986
- continue
21987
21360
  field = _parse_field(question)
21988
21361
  fields_by_name.setdefault(field["name"], field)
21989
21362
  labels.append(field["name"])
@@ -22009,16 +21382,8 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
22009
21382
  for question in row:
22010
21383
  if not isinstance(question, dict):
22011
21384
  continue
22012
- if _coerce_positive_int(question.get("queType")) == 24:
22013
- # 同行混入段落容器:不注册段落本身(它的子字段在自己 row),跳过避免同名占位挡住真实字段
22014
- continue
22015
21385
  field = _parse_field(question)
22016
- existing = fields_by_name.get(field["name"])
22017
- # 同名冲突:保留信息更丰富的那个(表格字段带 subfields,被同名普通字段覆盖会丢子字段引用面);
22018
- # 但 queId 索引必须收录全部同名字段(_collect_fields_by_que_id 按 queId 消费,同名不同 id 各自有效)
22019
- if existing is None or (not existing.get("subfields") and field.get("subfields")):
22020
- fields_by_name[field["name"]] = field
22021
-
21386
+ fields_by_name.setdefault(field["name"], field)
22022
21387
  labels.append(field["name"])
22023
21388
  if labels:
22024
21389
  root_rows.append(labels)
@@ -23243,23 +22608,6 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
23243
22608
  field.pop("_question_rebuild_required", None)
23244
22609
 
23245
22610
 
23246
- class AmbiguousFieldSelector(ValueError):
23247
- """name-only selector 命中多个同名字段。
23248
-
23249
- 方案 A(duplicate-field-name):后端以 queId 为字段身份、允许同父级同名;
23250
- name 命中多个字段时禁止静默取第一个,必须显式失败并给出候选 queId。
23251
- compiler 包装层(app_form)把它转写为 AMBIGUOUS_FIELD_SELECTOR 错误码。
23252
- """
23253
-
23254
- def __init__(self, location: str, candidates: list[int]) -> None:
23255
- self.location = location
23256
- self.candidates = candidates
23257
- super().__init__(
23258
- f"{location} matches multiple fields with the same name; "
23259
- f"use queId to disambiguate (candidate queIds: {sorted(candidates)})"
23260
- )
23261
-
23262
-
23263
22611
  def _resolve_field_selector_with_uniqueness(
23264
22612
  *,
23265
22613
  fields: list[dict[str, Any]],
@@ -23275,12 +22623,7 @@ def _resolve_field_selector_with_uniqueness(
23275
22623
  name = str(selector.name or "").strip()
23276
22624
  matched = [field for field in fields if str(field.get("name") or "").strip() == name]
23277
22625
  if len(matched) > 1:
23278
- candidate_ids = [
23279
- ref
23280
- for candidate in matched
23281
- if (ref := _coerce_any_int(candidate.get("que_id"))) is not None
23282
- ]
23283
- raise AmbiguousFieldSelector(location, candidate_ids)
22626
+ raise ValueError(f"{location} matched multiple fields with the same name; use field_id or que_id")
23284
22627
  else:
23285
22628
  matched = []
23286
22629
  if len(matched) != 1:
@@ -26723,11 +26066,7 @@ def _merge_view_summary_with_config(
26723
26066
  return summary
26724
26067
  if isinstance(config.get("auth"), dict):
26725
26068
  summary["visibility_summary"] = _visibility_summary(_public_visibility_from_member_auth(config.get("auth")))
26726
- legacy_columns = [
26727
- _SYSTEM_VIEW_QUE_NAMES.get(value if isinstance(value, int) else None, str(value))
26728
- for value in (summary.get("columns") or [])
26729
- if str(value or "").strip()
26730
- ]
26069
+ legacy_columns = [str(value) for value in (summary.get("columns") or []) if str(value or "").strip()]
26731
26070
  question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
26732
26071
  canonical_question_entries = _extract_view_question_entries(question_list)
26733
26072
  question_entries_by_id = {
@@ -26873,7 +26212,7 @@ def _merge_view_summary_with_config(
26873
26212
  )
26874
26213
  summary["filters"] = _public_view_filter_groups_from_match_rules(
26875
26214
  config.get("viewgraphLimit"),
26876
- fields_by_que_id=field_lookup,
26215
+ current_fields_by_name=field_lookup,
26877
26216
  )
26878
26217
  config_enriched = True
26879
26218
  if any(key in config for key in ("asosChartVisible", "asosChartConfig", "asosChartIdList", "limitType")):
@@ -27116,16 +26455,12 @@ def _normalize_print_tpls(value: Any) -> list[dict[str, Any]]:
27116
26455
  items: list[dict[str, Any]] = []
27117
26456
  for item in value:
27118
26457
  if isinstance(item, dict):
27119
- tpl_id = item.get("printKey")
27120
- if tpl_id is None:
27121
- tpl_id = item.get("printTplId")
26458
+ tpl_id = item.get("printTplId")
27122
26459
  if tpl_id is None:
27123
26460
  tpl_id = item.get("templateId")
27124
26461
  if tpl_id is None:
27125
26462
  tpl_id = item.get("id")
27126
- tpl_name = item.get("printName")
27127
- if tpl_name is None:
27128
- tpl_name = item.get("printTplName")
26463
+ tpl_name = item.get("printTplName")
27129
26464
  if tpl_name is None:
27130
26465
  tpl_name = item.get("templateName")
27131
26466
  if tpl_name is None:
@@ -27569,7 +26904,7 @@ def _build_grouped_view_button_config(button_config_dtos: list[dict[str, Any]])
27569
26904
 
27570
26905
  def _build_view_button_dtos(
27571
26906
  *,
27572
- fields_by_que_id: dict[str, dict[str, Any]],
26907
+ current_fields_by_name: dict[str, dict[str, Any]],
27573
26908
  bindings: list[ViewButtonBindingPatch],
27574
26909
  valid_custom_button_ids: set[int],
27575
26910
  ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
@@ -27578,7 +26913,7 @@ def _build_view_button_dtos(
27578
26913
  for binding in bindings:
27579
26914
  dto, binding_issues = _serialize_view_button_binding(
27580
26915
  binding=binding,
27581
- fields_by_que_id=fields_by_que_id,
26916
+ current_fields_by_name=current_fields_by_name,
27582
26917
  valid_custom_button_ids=valid_custom_button_ids,
27583
26918
  )
27584
26919
  if binding_issues:
@@ -27591,7 +26926,7 @@ def _build_view_button_dtos(
27591
26926
  def _serialize_view_button_binding(
27592
26927
  *,
27593
26928
  binding: ViewButtonBindingPatch,
27594
- fields_by_que_id: dict[str, dict[str, Any]],
26929
+ current_fields_by_name: dict[str, dict[str, Any]],
27595
26930
  valid_custom_button_ids: set[int],
27596
26931
  allow_unverified_custom_button_id: bool = False,
27597
26932
  ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
@@ -27606,11 +26941,10 @@ def _serialize_view_button_binding(
27606
26941
  "reason_path": "buttons[].button_id",
27607
26942
  "missing_fields": [],
27608
26943
  "details": {"button_id": binding.button_id},
27609
- "next_action": "call app_get_buttons with the intended button_text to reuse an existing custom button; create only if absent via app_custom_buttons_apply",
27610
26944
  }
27611
26945
  ]
27612
26946
  translated_limits, limit_issues = _build_view_button_limit_groups(
27613
- fields_by_que_id=fields_by_que_id,
26947
+ current_fields_by_name=current_fields_by_name,
27614
26948
  groups=binding.button_limit,
27615
26949
  )
27616
26950
  if limit_issues:
@@ -27636,7 +26970,7 @@ def _serialize_view_button_binding(
27636
26970
 
27637
26971
  def _build_view_button_limit_groups(
27638
26972
  *,
27639
- fields_by_que_id: dict[str, dict[str, Any]],
26973
+ current_fields_by_name: dict[str, dict[str, Any]],
27640
26974
  groups: list[list[ViewFilterRulePatch]],
27641
26975
  ) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
27642
26976
  translated_groups: list[list[dict[str, Any]]] = []
@@ -27650,14 +26984,14 @@ def _build_view_button_limit_groups(
27650
26984
  raw_rule = raw_rule.model_dump(mode="json")
27651
26985
  if not isinstance(raw_rule, dict):
27652
26986
  continue
27653
- rule_que_id = _coerce_positive_int(raw_rule.get("que_id"))
27654
- field = fields_by_que_id.get(rule_que_id) if rule_que_id is not None else None
26987
+ field_name = str(raw_rule.get("field_name") or "").strip()
26988
+ field = current_fields_by_name.get(field_name)
27655
26989
  if field is None:
27656
26990
  issues.append(
27657
26991
  {
27658
26992
  "error_code": "UNKNOWN_VIEW_FIELD",
27659
- "missing_fields": [str(raw_rule.get("que_id") or "")],
27660
- "reason_path": "buttons[].button_limit[].que_id",
26993
+ "missing_fields": [field_name] if field_name else [],
26994
+ "reason_path": "buttons[].button_limit[].field_name",
27661
26995
  }
27662
26996
  )
27663
26997
  continue
@@ -27667,11 +27001,6 @@ def _build_view_button_limit_groups(
27667
27001
  issue["reason_path"] = "buttons[].button_limit[].values"
27668
27002
  issues.append(issue)
27669
27003
  continue
27670
- stale_warning = translated_rule.pop("_stale_warning", None)
27671
- if stale_warning is not None:
27672
- stale_warning = deepcopy(stale_warning)
27673
- stale_warning["reason_path"] = "buttons[].button_limit[].values"
27674
- issues.append(stale_warning)
27675
27004
  translated_rules.append(translated_rule)
27676
27005
  if translated_rules:
27677
27006
  translated_groups.append(translated_rules)
@@ -27833,7 +27162,7 @@ def _custom_button_view_configs_from_view_summaries(views: list[dict[str, Any]])
27833
27162
  placement = _public_view_button_placement(entry.get("config_type")) or entry.get("placement")
27834
27163
  raw_button_limit = deepcopy(entry.get("button_limit") or [])
27835
27164
  public_button_limit = (
27836
- _public_view_filter_groups_from_match_rules(raw_button_limit, fields_by_que_id=field_lookup)
27165
+ _public_view_filter_groups_from_match_rules(raw_button_limit, current_fields_by_name=field_lookup)
27837
27166
  if raw_button_limit
27838
27167
  else []
27839
27168
  )
@@ -28031,11 +27360,6 @@ def _field_id_for_name(fields: list[dict[str, Any]], name: str) -> str:
28031
27360
  for field in fields:
28032
27361
  if field["name"] == name:
28033
27362
  return field["field_id"]
28034
- # 表格子字段是视图列/筛选的合法引用面:顶层未命中时递归 subfields
28035
- for field in fields:
28036
- for sub in field.get("subfields") or []:
28037
- if isinstance(sub, dict) and sub.get("name") == name:
28038
- return str(sub.get("field_id") or f"field_{sub.get('que_id') or ''}")
28039
27363
  raise KeyError(name)
28040
27364
 
28041
27365
 
@@ -28135,7 +27459,7 @@ def _build_public_workflow_spec(*, nodes: list[dict[str, Any]], transitions: lis
28135
27459
 
28136
27460
  def _build_flow_condition_matrix(
28137
27461
  *,
28138
- fields_by_que_id: dict[str, dict[str, Any]],
27462
+ current_fields_by_name: dict[str, dict[str, Any]],
28139
27463
  node: dict[str, Any],
28140
27464
  ) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
28141
27465
  if str(node.get("type") or "") != "condition":
@@ -28153,7 +27477,7 @@ def _build_flow_condition_matrix(
28153
27477
  if not isinstance(raw_rule, dict):
28154
27478
  continue
28155
27479
  field_name = str(raw_rule.get("field_name") or "").strip()
28156
- field = fields_by_que_id.get(field_name)
27480
+ field = current_fields_by_name.get(field_name)
28157
27481
  if field is None:
28158
27482
  issues.append(
28159
27483
  {
@@ -28200,9 +27524,7 @@ def _translate_flow_condition_rule(*, field: dict[str, Any], rule: dict[str, Any
28200
27524
  base["judgeType"] = JUDGE_LESS_OR_EQUAL
28201
27525
  base["judgeValues"] = [_stringify_condition_value(values[0])]
28202
27526
  elif operator == "is_empty":
28203
- # 选项/成员/部门类字段的空值,后端历史形态是 EQUAL_ANY(9)+空值(该类字段用
28204
- # ANY 语义选值,"没选任何值"也是 9 判型);文本/数字类才是 EQUAL(0)+空值
28205
- 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
27527
+ base["judgeType"] = JUDGE_EQUAL
28206
27528
  base["judgeValues"] = []
28207
27529
  elif operator == "not_empty":
28208
27530
  base["judgeType"] = JUDGE_UNEQUAL
@@ -28231,7 +27553,7 @@ def _empty_view_query_conditions_payload() -> dict[str, Any]:
28231
27553
 
28232
27554
  def _build_view_query_conditions_payload(
28233
27555
  *,
28234
- fields_by_que_id: dict[str, dict[str, Any]],
27556
+ current_fields_by_name: dict[str, dict[str, Any]],
28235
27557
  query_conditions: Any,
28236
27558
  ) -> tuple[dict[str, Any] | None, dict[str, Any] | None, list[dict[str, Any]]]:
28237
27559
  if query_conditions is None:
@@ -28271,7 +27593,7 @@ def _build_view_query_conditions_payload(
28271
27593
  compiled_row: list[int] = []
28272
27594
  for column_index, selector in enumerate(row):
28273
27595
  field, issue = _resolve_query_condition_field(
28274
- fields_by_que_id=fields_by_que_id,
27596
+ current_fields_by_name=current_fields_by_name,
28275
27597
  selector=selector,
28276
27598
  reason_path=f"query_conditions.rows[{row_index}][{column_index}]",
28277
27599
  )
@@ -28289,16 +27611,14 @@ def _build_view_query_conditions_payload(
28289
27611
  "queryConditionStatus": enabled,
28290
27612
  "queryConditionExact": exact,
28291
27613
  "hideBeforeQueryCondition": hide_before_query,
28292
- # 后端 ViewgraphInfo.queryCondition 是 List<ViewgraphQueryConditionConfig>({title?,queIds}),
28293
- # 不能发裸 [[queId]](fastjson expect {, actual [ → 发现 #7)
28294
- "queryCondition": [{"queIds": row} for row in rows] if enabled else [],
27614
+ "queryCondition": rows if enabled else [],
28295
27615
  }
28296
27616
  return payload, _normalize_view_query_conditions_for_compare(payload), []
28297
27617
 
28298
27618
 
28299
27619
  def _resolve_query_condition_field(
28300
27620
  *,
28301
- fields_by_que_id: dict[str, dict[str, Any]],
27621
+ current_fields_by_name: dict[str, dict[str, Any]],
28302
27622
  selector: Any,
28303
27623
  reason_path: str,
28304
27624
  ) -> tuple[dict[str, Any], dict[str, Any] | None]:
@@ -28310,8 +27630,8 @@ def _resolve_query_condition_field(
28310
27630
  "missing_fields": [],
28311
27631
  "message": "query condition field selector cannot be empty",
28312
27632
  }
28313
- fields = [field for field in fields_by_que_id.values() if isinstance(field, dict)]
28314
- field = fields_by_que_id.get(raw)
27633
+ fields = [field for field in current_fields_by_name.values() if isinstance(field, dict)]
27634
+ field = current_fields_by_name.get(raw)
28315
27635
  if field is None:
28316
27636
  for candidate in fields:
28317
27637
  if raw == str(candidate.get("field_id") or "").strip():
@@ -28322,7 +27642,7 @@ def _resolve_query_condition_field(
28322
27642
  field = candidate
28323
27643
  break
28324
27644
  if field is None:
28325
- subfield_parent = _find_subtable_subfield_parent(fields_by_que_id=fields_by_que_id, selector=raw)
27645
+ subfield_parent = _find_subtable_subfield_parent(current_fields_by_name=current_fields_by_name, selector=raw)
28326
27646
  if subfield_parent is not None:
28327
27647
  return {}, {
28328
27648
  "error_code": "INVALID_QUERY_CONDITION_FIELD",
@@ -28370,10 +27690,10 @@ def _resolve_query_condition_field(
28370
27690
 
28371
27691
  def _find_subtable_subfield_parent(
28372
27692
  *,
28373
- fields_by_que_id: dict[str, dict[str, Any]],
27693
+ current_fields_by_name: dict[str, dict[str, Any]],
28374
27694
  selector: str,
28375
27695
  ) -> dict[str, Any] | None:
28376
- for field in fields_by_que_id.values():
27696
+ for field in current_fields_by_name.values():
28377
27697
  if not isinstance(field, dict) or str(field.get("type") or "") != FieldType.subtable.value:
28378
27698
  continue
28379
27699
  for subfield in field.get("subfields") or []:
@@ -28434,9 +27754,6 @@ def _normalize_view_query_conditions_for_compare(config: Any) -> dict[str, Any]:
28434
27754
  rows: list[list[int]] = []
28435
27755
  if isinstance(raw_rows, list):
28436
27756
  for raw_row in raw_rows:
28437
- if isinstance(raw_row, dict):
28438
- # ViewgraphQueryConditionConfig 形态({title?,queIds})
28439
- raw_row = raw_row.get("queIds") or []
28440
27757
  if not isinstance(raw_row, list):
28441
27758
  continue
28442
27759
  compiled_row = [
@@ -28499,18 +27816,11 @@ def _build_view_associated_resources_payload(
28499
27816
  ]
28500
27817
  available_by_id = _associated_resource_index(available_resources)
28501
27818
  if limit_type == "all":
28502
- # limitType=ALL 时后端保留原 idList 不清(语义忽略但存储保留);
28503
- # 透传声明的 id 列表,避免 apply 清空→回读 idList 不匹配→READBACK_MISMATCH
28504
- all_ids = [
28505
- item_id
28506
- for item_id in (_coerce_positive_int(raw) for raw in raw_item_ids)
28507
- if item_id is not None
28508
- ]
28509
27819
  payload = {
28510
27820
  "asosChartVisible": True,
28511
27821
  "asosChartConfig": {
28512
27822
  "limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_ALL,
28513
- "asosChartIdList": all_ids,
27823
+ "asosChartIdList": [],
28514
27824
  },
28515
27825
  }
28516
27826
  expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
@@ -28531,19 +27841,26 @@ def _build_view_associated_resources_payload(
28531
27841
  ]
28532
27842
  item_ids.append(item_id)
28533
27843
  missing_ids = [item_id for item_id in item_ids if item_id not in available_by_id]
28534
- # 兼容存量脏引用:关联资源 id 指向闭包外/已下架的图表或视图时,剔除后继续写而非整视图失败;
28535
- # 剔除明细记入 expected 供回读校验与响应展示
28536
- resolvable_ids = [item_id for item_id in item_ids if item_id 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
+ ]
28537
27856
  payload = {
28538
27857
  "asosChartVisible": True,
28539
27858
  "asosChartConfig": {
28540
27859
  "limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_SELECT,
28541
- "asosChartIdList": resolvable_ids,
27860
+ "asosChartIdList": item_ids,
28542
27861
  },
28543
27862
  }
28544
27863
  expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
28545
- if isinstance(expected, dict) and missing_ids:
28546
- expected["dropped_unresolvable_associated_item_ids"] = missing_ids
28547
27864
  return payload, expected, []
28548
27865
 
28549
27866
 
@@ -29280,10 +28597,6 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
29280
28597
  "view_key": "view_key",
29281
28598
  "viewKey": "view_key",
29282
28599
  "viewgraphKey": "view_key",
29283
- "pinned": "pinned",
29284
- "fixed": "pinned",
29285
- "is_pinned": "pinned",
29286
- "isPinned": "pinned",
29287
28600
  "type": "type",
29288
28601
  "view_type": "type",
29289
28602
  "viewType": "type",
@@ -29291,25 +28604,17 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
29291
28604
  "column_names": "columns",
29292
28605
  "columnNames": "columns",
29293
28606
  "fields": "columns",
29294
- "group_que_id": "group_que_id",
29295
- "groupQueId": "group_que_id",
29296
- "group_by": "group_que_id",
29297
- "groupBy": "group_que_id",
28607
+ "group_by": "group_by",
28608
+ "groupBy": "group_by",
29298
28609
  "filters": "filters",
29299
28610
  "filter_rules": "filters",
29300
28611
  "filterRules": "filters",
29301
- "start_que_id": "start_que_id",
29302
- "startQueId": "start_que_id",
29303
- "start_field": "start_que_id",
29304
- "startField": "start_que_id",
29305
- "end_que_id": "end_que_id",
29306
- "endQueId": "end_que_id",
29307
- "end_field": "end_que_id",
29308
- "endField": "end_que_id",
29309
- "title_que_id": "title_que_id",
29310
- "titleQueId": "title_que_id",
29311
- "title_field": "title_que_id",
29312
- "titleField": "title_que_id",
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",
29313
28618
  "buttons": "buttons",
29314
28619
  "visibility": "visibility",
29315
28620
  "query_conditions": "query_conditions",
@@ -29396,14 +28701,13 @@ def _normalize_view_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[
29396
28701
 
29397
28702
  _VIEW_PARTIAL_SET_KEYS = {
29398
28703
  "name",
29399
- "pinned",
29400
28704
  "type",
29401
28705
  "columns",
29402
- "group_que_id",
28706
+ "group_by",
29403
28707
  "filters",
29404
- "start_que_id",
29405
- "end_que_id",
29406
- "title_que_id",
28708
+ "start_field",
28709
+ "end_field",
28710
+ "title_field",
29407
28711
  "buttons",
29408
28712
  "visibility",
29409
28713
  "query_conditions",
@@ -29429,108 +28733,6 @@ def _field_name_by_id(field_names_by_id: dict[int, str], field_id: Any) -> str |
29429
28733
  return name or None
29430
28734
 
29431
28735
 
29432
- def _collect_fields_by_que_id(fields: list[dict[str, Any]], *, schema: dict[str, Any] | None = None) -> dict[int, dict[str, Any]]:
29433
- """构建 queId→field 索引。
29434
-
29435
- 传 schema 时直接从原始 formQues/baseQues 递归收集(同名冲突不丢字段——名字索引
29436
- 只保留第一个,但同名不同 queId 的字段对 queId 引用各自有效);
29437
- 不传 schema 时从解析后的 fields 列表递归 subfields 构建。
29438
- """
29439
- index: dict[int, dict[str, Any]] = {}
29440
-
29441
- type_by_question_type = {v: k for k, v in QUESTION_TYPE_TO_FIELD_TYPE.items()}
29442
-
29443
- def collect_raw(question: dict[str, Any]) -> None:
29444
- que_id = _coerce_positive_int(question.get("queId"))
29445
- if que_id is not None and que_id not in index:
29446
- que_type = _coerce_positive_int(question.get("queType"))
29447
- # 用 _parse_field 解析(含 options/option_details,与 _parse_schema 对齐);
29448
- # 直收原始 dict 的 queTitle/queType 不够——选项字段需 options 才能解析值→id
29449
- parsed = _parse_field(question)
29450
- index[que_id] = parsed
29451
- for key in ("subQuestions", "innerQuestions"):
29452
- for child in question.get(key) or []:
29453
- if isinstance(child, list):
29454
- for inner in child:
29455
- if isinstance(inner, dict):
29456
- collect_raw(inner)
29457
- elif isinstance(child, dict):
29458
- collect_raw(child)
29459
-
29460
- if schema is not None:
29461
- for base_question in schema.get("baseQues", []) or []:
29462
- if isinstance(base_question, dict):
29463
- collect_raw(base_question)
29464
- for line in schema.get("formQues", []) or []:
29465
- for question in (line if isinstance(line, list) else [line]):
29466
- if isinstance(question, dict):
29467
- collect_raw(question)
29468
- return index
29469
-
29470
- def collect(field: dict[str, Any]) -> None:
29471
- que_id = _coerce_positive_int(field.get("que_id"))
29472
- if que_id is not None and que_id not in index:
29473
- index[que_id] = field
29474
- for sub in field.get("subfields") or []:
29475
- if isinstance(sub, dict):
29476
- collect(sub)
29477
-
29478
- for field in fields:
29479
- if isinstance(field, dict):
29480
- collect(field)
29481
- return index
29482
-
29483
-
29484
- def _collect_field_id_to_name(schema: dict[str, Any]) -> dict[int, str]:
29485
- """从 form schema 收集 queId→queTitle(含子字段/段落内字段;同名时各 id 独立映射)。"""
29486
- id_to_name: dict[int, str] = {}
29487
-
29488
- def collect(question: dict[str, Any]) -> None:
29489
- que_id = _coerce_positive_int(question.get("queId"))
29490
- if que_id is not None and que_id not in id_to_name:
29491
- title = str(question.get("queTitle") or "").strip()
29492
- if title:
29493
- id_to_name[que_id] = title
29494
- for key in ("subQuestions", "innerQuestions"):
29495
- for child in question.get(key) or []:
29496
- if isinstance(child, list):
29497
- for inner in child:
29498
- if isinstance(inner, dict):
29499
- collect(inner)
29500
- elif isinstance(child, dict):
29501
- collect(child)
29502
-
29503
- for base_question in schema.get("baseQues", []) or []:
29504
- if isinstance(base_question, dict):
29505
- collect(base_question)
29506
- for line in schema.get("formQues", []) or []:
29507
- for question in (line if isinstance(line, list) else [line]):
29508
- if isinstance(question, dict):
29509
- collect(question)
29510
- return id_to_name
29511
-
29512
-
29513
- def _resolve_field_ref_preserving(
29514
- config: dict[str, Any],
29515
- config_key: str,
29516
- name: str | None,
29517
- field_map: dict[str, int],
29518
- id_to_name: dict[int, str],
29519
- ) -> int | None:
29520
- """同名字段歧义消解(字段引用类:card title/cover、board group 等)。
29521
-
29522
- 契约按字段名引用,同 form 内同名字段时按名解析会取到错误的一个。
29523
- UPDATE 场景读面原 queId 可用:若原 queId 的字段名与声明名一致,保留原 queId(保真);
29524
- 只有声明名与原值名字不同(用户真改了字段)才按名重新解析。
29525
- """
29526
- if not name:
29527
- return None
29528
- original = _coerce_positive_int(config.get(config_key))
29529
- if original is not None and id_to_name.get(original) == name:
29530
- return original
29531
- return field_map.get(name)
29532
-
29533
-
29534
28736
  def _view_upsert_payload_from_existing_view(
29535
28737
  *,
29536
28738
  config: dict[str, Any],
@@ -29540,28 +28742,25 @@ def _view_upsert_payload_from_existing_view(
29540
28742
  ) -> dict[str, Any]:
29541
28743
  view_type = _normalize_view_type_name(config.get("viewgraphType") or summary.get("type"))
29542
28744
  configured_columns = [
29543
- que_id
29544
- for item in (config.get("viewgraphQueIds") or [])
29545
- if (que_id := _coerce_positive_int(item)) is not None
29546
- and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_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
29547
28748
  ]
29548
28749
  columns = [
29549
- que_id
29550
- for item in (summary.get("apply_column_ids") or summary.get("columns") or [])
29551
- if (que_id := _coerce_positive_int(item)) is not None
29552
- and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_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
29553
28753
  ]
29554
28754
  if configured_columns and (not columns or len(configured_columns) >= len(columns)):
29555
28755
  columns = configured_columns
29556
28756
  display_config = summary.get("display_config") if isinstance(summary.get("display_config"), dict) else {}
29557
- group_que_id = _coerce_positive_int(config.get("groupQueId") or summary.get("group_que_id") or display_config.get("group_que_id"))
29558
- title_que_id = _coerce_positive_int(config.get("titleQue") or display_config.get("title_que_id"))
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"))
29559
28759
  gantt_config = config.get("viewgraphGanttConfigVO") if isinstance(config.get("viewgraphGanttConfigVO"), dict) else {}
29560
- start_que_id = _coerce_positive_int(gantt_config.get("startTimeQueId"))
29561
- end_que_id = _coerce_positive_int(gantt_config.get("endTimeQueId"))
29562
- gantt_title_que_id = _coerce_positive_int(gantt_config.get("titleQueId"))
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
29563
28763
  query_conditions = _extract_view_query_conditions_config(config)
29564
- query_conditions["rows"] = query_conditions.get("row_field_ids") or []
29565
28764
  query_conditions.pop("row_field_ids", None)
29566
28765
  associated_resources = _extract_view_associated_resources_config(config)
29567
28766
  payload: dict[str, Any] = {
@@ -29569,6 +28768,7 @@ def _view_upsert_payload_from_existing_view(
29569
28768
  "view_key": view_key,
29570
28769
  "type": view_type or "table",
29571
28770
  "columns": columns,
28771
+ "group_by": group_by,
29572
28772
  "filters": [],
29573
28773
  "query_conditions": query_conditions,
29574
28774
  "associated_resources": {
@@ -29577,17 +28777,15 @@ def _view_upsert_payload_from_existing_view(
29577
28777
  "associated_item_ids": list(associated_resources.get("configured_associated_item_ids") or []),
29578
28778
  },
29579
28779
  }
29580
- if group_que_id is not None:
29581
- payload["group_que_id"] = group_que_id
29582
- if title_que_id is not None:
29583
- payload["title_que_id"] = title_que_id
28780
+ if title_field:
28781
+ payload["title_field"] = title_field
29584
28782
  if view_type == "gantt":
29585
- if start_que_id is not None:
29586
- payload["start_que_id"] = start_que_id
29587
- if end_que_id is not None:
29588
- payload["end_que_id"] = end_que_id
29589
- if gantt_title_que_id is not None:
29590
- payload["title_que_id"] = gantt_title_que_id
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
29591
28789
  return payload
29592
28790
 
29593
28791
 
@@ -29598,7 +28796,7 @@ def _first_present(mapping: dict[str, Any], *keys: str) -> Any:
29598
28796
  return None
29599
28797
 
29600
28798
 
29601
- def _build_view_creation_payload(
28799
+ def _build_view_create_payload(
29602
28800
  *,
29603
28801
  app_key: str,
29604
28802
  base_info: dict[str, Any],
@@ -29606,24 +28804,23 @@ def _build_view_creation_payload(
29606
28804
  patch: ViewUpsertPatch,
29607
28805
  ordinal: int,
29608
28806
  view_filters: list[list[dict[str, Any]]] | None,
29609
- fields_by_que_id: dict[str, dict[str, Any]],
28807
+ current_fields_by_name: dict[str, dict[str, Any]],
29610
28808
  auth_override: dict[str, Any] | None = None,
29611
28809
  explicit_button_dtos: list[dict[str, Any]] | None = None,
29612
28810
  query_condition_payload: dict[str, Any] | None = None,
29613
28811
  associated_resources_payload: dict[str, Any] | None = None,
29614
- hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
29615
28812
  ) -> JSONObject:
29616
28813
  entity = _entity_spec_from_app(base_info=base_info, schema=schema, views=None)
29617
28814
  parsed_schema = _parse_schema(schema)
29618
- visible_que_ids = _resolve_view_visible_que_ids(patch)
29619
- field_ids = [f"field_{que}" for que in visible_que_ids]
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]
29620
28817
  gantt_config = _build_public_gantt_payload(parsed_schema["fields"], extract_field_map(schema), patch)
29621
28818
  view_spec = ViewSpec(
29622
28819
  view_id=_slugify(patch.name, default=f"view_{uuid4().hex[:6]}"),
29623
28820
  name=patch.name,
29624
28821
  type=patch.type.value,
29625
28822
  field_ids=field_ids,
29626
- group_by_field_id=f"field_{patch.group_que_id}" if patch.group_que_id else None,
28823
+ group_by_field_id=_field_id_for_name(_parse_schema(schema)["fields"], patch.group_by) if patch.group_by else None,
29627
28824
  config=gantt_config,
29628
28825
  )
29629
28826
  from ..solution.spec_models import EntitySpec
@@ -29632,6 +28829,7 @@ def _build_view_creation_payload(
29632
28829
  compiled = compile_views(EntitySpec.model_validate({**entity, "views": [view_spec.model_dump(mode="json")]}))[0]
29633
28830
  field_map = extract_field_map(schema)
29634
28831
  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]
29635
28833
  payload["appKey"] = app_key
29636
28834
  payload["ordinal"] = ordinal
29637
28835
  payload["viewgraphQueIds"] = visible_que_ids
@@ -29643,21 +28841,17 @@ def _build_view_creation_payload(
29643
28841
  payload["beingShowTitleQue"] = True
29644
28842
  payload["titleQue"] = gantt_config.get("titleQueId") or (visible_que_ids[0] if visible_que_ids else None)
29645
28843
  if patch.type.value == "board":
29646
- payload["groupQueId"] = patch.group_que_id
28844
+ payload["groupQueId"] = field_map.get(patch.group_by or "")
29647
28845
  return _hydrate_view_backend_payload(
29648
28846
  payload=payload,
29649
28847
  view_type=patch.type.value,
29650
28848
  visible_que_id_values=visible_que_ids,
29651
- group_que_id=patch.group_que_id,
28849
+ group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
29652
28850
  view_filters=view_filters,
29653
28851
  gantt_payload=gantt_config,
29654
28852
  button_config_dtos=explicit_button_dtos,
29655
28853
  query_condition_payload=query_condition_payload,
29656
28854
  associated_resources_payload=associated_resources_payload,
29657
- patch=patch,
29658
- field_map=field_map,
29659
- fields_by_que_id=fields_by_que_id,
29660
- hierarchy_filter_groups=hierarchy_filter_groups,
29661
28855
  )
29662
28856
 
29663
28857
 
@@ -29795,10 +28989,6 @@ def _normalize_view_type_name(value: Any) -> str:
29795
28989
  return "gantt"
29796
28990
  if "board" in normalized:
29797
28991
  return "board"
29798
- if "hierarchy" in normalized:
29799
- return "hierarchy"
29800
- if "calendar" in normalized:
29801
- return "calendar"
29802
28992
  if "card" in normalized:
29803
28993
  return "card"
29804
28994
  return "table"
@@ -29812,20 +29002,20 @@ def _build_view_update_payload(
29812
29002
  schema: dict[str, Any],
29813
29003
  patch: ViewUpsertPatch,
29814
29004
  view_filters: list[list[dict[str, Any]]] | None,
29815
- fields_by_que_id: dict[str, dict[str, Any]],
29005
+ current_fields_by_name: dict[str, dict[str, Any]],
29816
29006
  auth_override: dict[str, Any] | None = None,
29817
29007
  explicit_button_dtos: list[dict[str, Any]] | None = None,
29818
29008
  query_condition_payload: dict[str, Any] | None = None,
29819
29009
  associated_resources_payload: dict[str, Any] | None = None,
29820
- hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
29821
29010
  ) -> JSONObject:
29822
29011
  config_response = views.view_get_config(profile=profile, viewgraph_key=source_viewgraph_key)
29823
29012
  config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
29824
29013
  payload = deepcopy(config)
29825
29014
  parsed_schema = _parse_schema(schema)
29826
29015
  field_map = extract_field_map(schema)
29827
- visible_que_ids = _resolve_view_visible_que_ids(patch)
29828
- visible_que_id_values = list(visible_que_ids)
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]
29829
29019
  gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
29830
29020
 
29831
29021
  for key in (
@@ -29847,25 +29037,8 @@ def _build_view_update_payload(
29847
29037
  payload["auth"] = deepcopy(auth_override if isinstance(auth_override, dict) else payload.get("auth") or default_member_auth())
29848
29038
  if "viewName" in payload:
29849
29039
  payload["viewName"] = patch.name
29850
- # 保留系统列在业务列之间的位置;业务列顺序和增删仍由声明决定。
29851
- existing_que_ids = dict.fromkeys(
29852
- que_id
29853
- for que_id in (_coerce_any_int(raw) for raw in (config.get("viewgraphQueIds") or []))
29854
- if que_id is not None
29855
- )
29856
- business_que_ids = iter(que_id for que_id in visible_que_id_values if que_id > 5)
29857
- merged_que_ids: list[int] = []
29858
- for que_id in existing_que_ids:
29859
- if que_id <= 5:
29860
- merged_que_ids.append(que_id)
29861
- elif que_id in visible_que_id_values:
29862
- merged_que_ids.append(next(business_que_ids))
29863
- merged_que_ids.extend(business_que_ids)
29864
- 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)
29865
- payload["viewgraphQueIds"] = merged_que_ids
29866
- payload["viewgraphQuestions"] = _build_viewgraph_questions(
29867
- schema, merged_que_ids, existing_by_que_id=_flatten_viewgraph_questions(config.get("viewgraphQuestions"))
29868
- )
29040
+ payload["viewgraphQueIds"] = visible_que_id_values
29041
+ payload["viewgraphQuestions"] = _build_viewgraph_questions(schema, visible_que_id_values)
29869
29042
  payload.setdefault("sortType", "defaultSort")
29870
29043
  payload.setdefault("viewgraphSorts", [{"queId": 0, "beingSortAscend": True, "queType": 8}])
29871
29044
  payload.setdefault("beingPinNavigate", True)
@@ -29895,79 +29068,25 @@ def _build_view_update_payload(
29895
29068
  payload["viewgraphType"] = "boardView"
29896
29069
  payload["beingShowTitleQue"] = True
29897
29070
  payload["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
29898
- payload["groupQueId"] = patch.group_que_id
29071
+ payload["groupQueId"] = field_map.get(patch.group_by or "")
29899
29072
  elif normalized_type == "gantt":
29900
29073
  payload["viewgraphType"] = "ganttView"
29901
29074
  payload["beingShowTitleQue"] = True
29902
29075
  payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
29903
29076
  payload.pop("groupQueId", None)
29904
- elif normalized_type == "hierarchy":
29905
- payload["viewgraphType"] = "hierarchyView"
29906
- payload["beingShowTitleQue"] = True
29907
- payload.pop("groupQueId", None)
29908
- elif normalized_type == "calendar":
29909
- payload["viewgraphType"] = "calendarView"
29910
- payload["beingShowTitleQue"] = True
29911
- payload.pop("groupQueId", None)
29912
29077
  return _hydrate_view_backend_payload(
29913
29078
  payload=payload,
29914
29079
  view_type=normalized_type,
29915
29080
  visible_que_id_values=visible_que_id_values,
29916
- group_que_id=patch.group_que_id,
29081
+ group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
29917
29082
  view_filters=view_filters,
29918
29083
  gantt_payload=gantt_payload,
29919
29084
  button_config_dtos=button_config_dtos,
29920
29085
  query_condition_payload=query_condition_payload,
29921
29086
  associated_resources_payload=associated_resources_payload,
29922
- patch=patch,
29923
- field_map=field_map,
29924
- fields_by_que_id=fields_by_que_id,
29925
- hierarchy_filter_groups=hierarchy_filter_groups,
29926
- schema=schema,
29927
29087
  )
29928
29088
 
29929
29089
 
29930
- # 系统列 queId→公开名(后端语义;读面展示与 viewgraphQueIds 透传时的名称解析)
29931
- _SYSTEM_VIEW_QUE_NAMES = {
29932
- 0: "编号",
29933
- 1: "提交人",
29934
- 2: "提交时间",
29935
- 3: "更新时间",
29936
- 4: "流程标题",
29937
- 5: "当前流程状态",
29938
- -17: "数据ID",
29939
- -19: "删除时间",
29940
- -20: "删除人",
29941
- -21: "处理节点",
29942
- -22: "抄送节点",
29943
- -23: "处理时间",
29944
- -24: "抄送时间",
29945
- -25: "接收时间",
29946
- }
29947
-
29948
- _SYSTEM_VIEW_QUE_IDS = frozenset(
29949
- {
29950
- *_SYSTEM_VIEW_QUE_NAMES,
29951
- -1,
29952
- -2,
29953
- -3,
29954
- -4,
29955
- -5,
29956
- -6,
29957
- -7,
29958
- -8,
29959
- -18,
29960
- -30,
29961
- -31,
29962
- }
29963
- )
29964
-
29965
-
29966
- def _is_known_system_view_que_id(value: Any) -> bool:
29967
- que_id = _coerce_any_int(value)
29968
- return que_id in _SYSTEM_VIEW_QUE_IDS if que_id is not None else False
29969
-
29970
-
29971
29090
  _KNOWN_SYSTEM_VIEW_COLUMNS = {
29972
29091
  "编号",
29973
29092
  "当前流程状态",
@@ -29991,16 +29110,16 @@ def _build_minimal_view_payload(
29991
29110
  patch: ViewUpsertPatch,
29992
29111
  ordinal: int,
29993
29112
  view_filters: list[list[dict[str, Any]]] | None,
29994
- fields_by_que_id: dict[str, dict[str, Any]],
29113
+ current_fields_by_name: dict[str, dict[str, Any]],
29995
29114
  auth_override: dict[str, Any] | None = None,
29996
29115
  explicit_button_dtos: list[dict[str, Any]] | None = None,
29997
29116
  query_condition_payload: dict[str, Any] | None = None,
29998
29117
  associated_resources_payload: dict[str, Any] | None = None,
29999
- hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
30000
29118
  ) -> JSONObject:
30001
29119
  field_map = extract_field_map(schema)
30002
29120
  parsed_schema = _parse_schema(schema)
30003
- visible_que_id_values = _resolve_view_visible_que_ids(patch)
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]
30004
29123
  gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
30005
29124
  payload: JSONObject = {
30006
29125
  "appKey": app_key,
@@ -30010,8 +29129,6 @@ def _build_minimal_view_payload(
30010
29129
  "card": "cardView",
30011
29130
  "board": "boardView",
30012
29131
  "gantt": "ganttView",
30013
- "hierarchy": "hierarchyView",
30014
- "calendar": "calendarView",
30015
29132
  }[patch.type.value],
30016
29133
  "ordinal": ordinal,
30017
29134
  "viewgraphQueIds": visible_que_id_values,
@@ -30022,21 +29139,17 @@ def _build_minimal_view_payload(
30022
29139
  payload["beingShowTitleQue"] = True
30023
29140
  payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
30024
29141
  if patch.type.value == "board":
30025
- payload["groupQueId"] = patch.group_que_id
29142
+ payload["groupQueId"] = field_map.get(patch.group_by or "")
30026
29143
  return _hydrate_view_backend_payload(
30027
29144
  payload=payload,
30028
29145
  view_type=patch.type.value,
30029
29146
  visible_que_id_values=visible_que_id_values,
30030
- group_que_id=patch.group_que_id,
29147
+ group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
30031
29148
  view_filters=view_filters,
30032
29149
  gantt_payload=gantt_payload,
30033
29150
  button_config_dtos=explicit_button_dtos,
30034
29151
  query_condition_payload=query_condition_payload,
30035
29152
  associated_resources_payload=associated_resources_payload,
30036
- patch=patch,
30037
- field_map=field_map,
30038
- fields_by_que_id=fields_by_que_id,
30039
- hierarchy_filter_groups=hierarchy_filter_groups,
30040
29153
  )
30041
29154
 
30042
29155
 
@@ -30051,11 +29164,6 @@ def _hydrate_view_backend_payload(
30051
29164
  button_config_dtos: list[dict[str, Any]] | None = None,
30052
29165
  query_condition_payload: dict[str, Any] | None = None,
30053
29166
  associated_resources_payload: dict[str, Any] | None = None,
30054
- patch: ViewUpsertPatch | None = None,
30055
- field_map: dict[str, int] | None = None,
30056
- fields_by_que_id: dict[str, dict[str, Any]] | None = None,
30057
- hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
30058
- schema: dict[str, Any] | None = None,
30059
29167
  ) -> JSONObject:
30060
29168
  data = deepcopy(payload)
30061
29169
  data.setdefault("beingPinNavigate", True)
@@ -30125,239 +29233,26 @@ def _hydrate_view_backend_payload(
30125
29233
  data["titleQue"] = (gantt_payload or {}).get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
30126
29234
  data["groupQueId"] = None
30127
29235
  data["viewgraphGanttConfigVO"] = deepcopy(gantt_payload) if gantt_payload else None
30128
- elif view_type == "hierarchy":
30129
- data["viewgraphType"] = "hierarchyView"
30130
- data["beingShowTitleQue"] = True
30131
- data["groupQueId"] = None
30132
- data["viewgraphGanttConfigVO"] = None
30133
- elif view_type == "calendar":
30134
- data["viewgraphType"] = "calendarView"
30135
- data["beingShowTitleQue"] = True
30136
- data["groupQueId"] = None
30137
- data["viewgraphGanttConfigVO"] = None
30138
29236
  if view_filters is not None:
30139
29237
  data["viewgraphLimit"] = deepcopy(view_filters)
30140
29238
  data["viewgraphLimitType"] = 1
30141
29239
  data["viewgraphLimitFormula"] = ""
30142
29240
  data["dataPermissionType"] = "CUSTOM"
30143
29241
  data["dataScope"] = "CUSTOM" if view_filters else "ALL"
30144
- if patch is not None:
30145
- _apply_view_config_groups(
30146
- data,
30147
- patch=patch,
30148
- view_type=view_type,
30149
- visible_que_id_values=visible_que_id_values,
30150
- field_map=field_map or {},
30151
- fields_by_que_id=fields_by_que_id or {},
30152
- hierarchy_filter_groups=hierarchy_filter_groups,
30153
- config=payload if isinstance(payload, dict) else None,
30154
- id_to_name=_collect_field_id_to_name(schema) if isinstance(schema, dict) else None,
30155
- )
30156
29242
  _apply_view_query_conditions_payload(data, query_condition_payload)
30157
29243
  _apply_view_associated_resources_payload(data, associated_resources_payload)
30158
29244
  return data
30159
29245
 
30160
29246
 
30161
- _DATA_PERMISSION_SCOPE_MAP = {
30162
- "all": "ALL",
30163
- "oneself": "ONESELF",
30164
- "dept": "DEPT",
30165
- "subordinate": "SUBORDINATE",
30166
- "custom": "CUSTOM",
30167
- }
30168
-
30169
-
30170
- def _apply_view_config_groups(
30171
- data: JSONObject,
30172
- *,
30173
- patch: ViewUpsertPatch,
30174
- view_type: str,
30175
- visible_que_id_values: list[int],
30176
- field_map: dict[str, int],
30177
- fields_by_que_id: dict[str, dict[str, Any]],
30178
- hierarchy_filter_groups: list[list[dict[str, Any]]] | None,
30179
- config: dict[str, Any] | None = None,
30180
- id_to_name: dict[int, str] | None = None,
30181
- ) -> None:
30182
- """编译 complete 档新字段组:data_permission/sort/card/hierarchy/display/record/share。"""
30183
- if patch.data_permission is not None:
30184
- data["dataPermissionType"] = "CONFIG" if patch.data_permission.type == "config" else "CUSTOM"
30185
- data["dataScope"] = _DATA_PERMISSION_SCOPE_MAP.get(patch.data_permission.scope, "ALL")
30186
- if patch.sort is not None:
30187
- if patch.sort:
30188
- sorts: list[dict[str, Any]] = []
30189
- for rule in patch.sort:
30190
- field = fields_by_que_id.get(rule.que_id) or {}
30191
- que_type = _coerce_positive_int(field.get("que_type")) or FIELD_TYPE_TO_QUESTION_TYPE.get(
30192
- str(field.get("type") or FieldType.text.value), 2
30193
- )
30194
- sorts.append(
30195
- {
30196
- "queId": rule.que_id,
30197
- "beingSortAscend": bool(rule.ascending),
30198
- "queType": que_type,
30199
- }
30200
- )
30201
- data["sortType"] = "customSort"
30202
- data["viewgraphSorts"] = sorts
30203
- else:
30204
- data["sortType"] = "defaultSort"
30205
- data["viewgraphSorts"] = [{"queId": 0, "beingSortAscend": True, "queType": 8}]
30206
- card = patch.card
30207
- config = config or {}
30208
- id_to_name = id_to_name or {}
30209
- if card is not None:
30210
- if card.title_que_id is not None:
30211
- data["titleQue"] = card.title_que_id
30212
- if card.cover_que_id is not None:
30213
- data["frontCoverQueId"] = card.cover_que_id
30214
- if card.clipping_mode is not None:
30215
- data["clippingMode"] = card.clipping_mode
30216
- if card.image_adaption is not None:
30217
- data["beingImageAdaption"] = card.image_adaption
30218
- if card.show_field_title is not None:
30219
- data["beingShowQueTitle"] = card.show_field_title
30220
- if card.show_title is not None:
30221
- data["beingShowTitleQue"] = card.show_title
30222
- if card.show_cover is not None:
30223
- data["beingShowCover"] = card.show_cover
30224
- if card.group_que_id is not None:
30225
- data["groupQueId"] = card.group_que_id
30226
- if card.group_color is not None:
30227
- data["beingGroupColor"] = card.group_color
30228
- if patch.hierarchy is not None and view_type == "hierarchy":
30229
- data["viewgraphHierarchyConfigVO"] = {
30230
- "titleQueId": patch.hierarchy.title_que_id,
30231
- "childrenQueId": patch.hierarchy.children_que_id,
30232
- "rootNodeFilter": deepcopy(hierarchy_filter_groups) if hierarchy_filter_groups is not None else [],
30233
- }
30234
- data["titleQue"] = patch.hierarchy.title_que_id
30235
- if patch.calendar is not None and view_type == "calendar":
30236
- data["viewgraphCalendarConfigVO"] = {
30237
- "titleQueId": patch.calendar.title_que_id,
30238
- "dateQueId": patch.calendar.date_que_id,
30239
- "colorQueId": patch.calendar.color_que_id,
30240
- }
30241
- data["titleQue"] = patch.calendar.title_que_id
30242
- display = patch.display
30243
- if display is not None:
30244
- if display.row_height is not None:
30245
- data["defaultRowHigh"] = display.row_height
30246
- if display.pin_navigate is not None:
30247
- data["beingPinNavigate"] = display.pin_navigate
30248
- if display.remark is not None:
30249
- data["remark"] = display.remark
30250
- record = patch.record
30251
- if record is not None:
30252
- if record.audit_visible is not None:
30253
- data["beingAuditRecordVisible"] = record.audit_visible
30254
- if record.data_log_visible is not None:
30255
- data["beingDataLogVisible"] = record.data_log_visible
30256
- if record.qrobot_record_visible is not None:
30257
- data["beingQrobotRecordVisible"] = record.qrobot_record_visible
30258
- if record.workflow_future_visible is not None:
30259
- data["beingWorkflowNodeFutureListVisible"] = record.workflow_future_visible
30260
- if record.comment is not None:
30261
- data["beingCommentStatus"] = record.comment
30262
- share = patch.share
30263
- if share is not None:
30264
- if share.need_pass is not None:
30265
- data["beingNeedPass"] = share.need_pass
30266
- data["needPass"] = share.need_pass
30267
- if share.password is None:
30268
- # 声明未带密码(提取语义:不回传敏感值)——显式清空 payload 里读面带来的旧密码;
30269
- # 后端 isNotBlank(空)跳过校验且不更新密码列,原密码保留(Java AbstractViewgraphStrategicService:424)
30270
- data["viewgraphPass"] = ""
30271
- data.pop("pass", None)
30272
- if share.password is not None:
30273
- data["viewgraphPass"] = share.password
30274
- if share.force_login is not None:
30275
- data["beingForceLogin"] = share.force_login
30276
- if share.print_enabled is not None:
30277
- data["beingPrintStatus"] = share.print_enabled
30278
-
30279
-
30280
- def _stale_view_reference_warning(
30281
- *,
30282
- scope: str,
30283
- view_name: str,
30284
- field_ids: list[Any] | None = None,
30285
- values: list[Any] | None = None,
30286
- option: bool = False,
30287
- ) -> dict[str, Any]:
30288
- """存量死引用(已删字段/已删选项)透传警告:后端保存视图不校验引用存在性并原样透传,
30289
- CLI 同口径透传写回,仅以警告留痕,不再整视图拒绝(apply(get(S))≡S 重放兼容)。"""
30290
- warning = _warning(
30291
- "STALE_OPTION_REFERENCE" if option else "STALE_FIELD_REFERENCE",
30292
- (
30293
- f"view {scope} references option values not present in the current form; original values passed through (backend tolerates stale references)"
30294
- if option
30295
- else f"view {scope} references fields not present in the current form; ids passed through (backend tolerates stale references)"
30296
- ),
30297
- scope=scope,
30298
- view_name=view_name,
30299
- )
30300
- if field_ids:
30301
- warning["field_ids"] = list(field_ids)
30302
- if values:
30303
- warning["values"] = list(values)
30304
- return warning
30305
-
30306
-
30307
- def _iter_view_config_group_field_names(patch: ViewUpsertPatch) -> list[tuple[str, list[int]]]:
30308
- """枚举 complete 档新字段组引用的 queId(用于 UNKNOWN_VIEW_FIELD 校验)。"""
30309
- groups: list[tuple[str, list[int]]] = []
30310
- if patch.sort:
30311
- groups.append(("sort", [rule.que_id for rule in patch.sort]))
30312
- if patch.card is not None:
30313
- groups.append(
30314
- (
30315
- "card",
30316
- [
30317
- que
30318
- for que in (patch.card.title_que_id, patch.card.cover_que_id, patch.card.group_que_id)
30319
- if que
30320
- ],
30321
- )
30322
- )
30323
- if patch.gantt is not None:
30324
- gantt_ids = [
30325
- que
30326
- for que in (patch.gantt.title_que_id, patch.gantt.start_que_id, patch.gantt.end_que_id)
30327
- if que
30328
- ]
30329
- gantt_ids.extend(group.que_id for group in patch.gantt.groups or [])
30330
- for dep in patch.gantt.dependencies or []:
30331
- gantt_ids.extend([dep.dependency_que_id, dep.predecessor_que_id])
30332
- if gantt_ids:
30333
- groups.append(("gantt", gantt_ids))
30334
- if patch.hierarchy is not None:
30335
- groups.append(("hierarchy", [patch.hierarchy.title_que_id, patch.hierarchy.children_que_id]))
30336
- if patch.calendar is not None:
30337
- calendar_ids = [patch.calendar.title_que_id, patch.calendar.date_que_id]
30338
- if patch.calendar.color_que_id is not None:
30339
- calendar_ids.append(patch.calendar.color_que_id)
30340
- groups.append(("calendar", calendar_ids))
30341
- return groups
30342
-
30343
-
30344
-
30345
- def _resolve_view_visible_que_ids(patch: ViewUpsertPatch) -> list[int]:
30346
- """收集视图引用的业务字段和已知系统字段 queId。"""
30347
- ordered: list[int] = []
30348
- extra_ids: list[int | None] = []
30349
- if patch.hierarchy is not None:
30350
- extra_ids.extend([patch.hierarchy.title_que_id, patch.hierarchy.children_que_id])
30351
- if patch.calendar is not None:
30352
- extra_ids.extend([patch.calendar.title_que_id, patch.calendar.date_que_id, patch.calendar.color_que_id])
30353
- for value in [*patch.columns, patch.title_que_id, patch.start_que_id, patch.end_que_id, patch.group_que_id, *extra_ids]:
30354
- que_id = _coerce_any_int(value)
30355
- 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:
30356
- ordered.append(que_id)
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)
30357
29253
  return ordered
30358
29254
 
30359
29255
 
30360
-
30361
29256
  def _build_public_gantt_payload(
30362
29257
  fields: list[dict[str, Any]],
30363
29258
  field_map: dict[str, int],
@@ -30365,35 +29260,22 @@ def _build_public_gantt_payload(
30365
29260
  ) -> dict[str, Any]:
30366
29261
  if patch.type.value != "gantt":
30367
29262
  return {}
30368
- gantt_config = patch.gantt
30369
- title_que_id = (
30370
- getattr(gantt_config, "title_que_id", None)
30371
- or patch.title_que_id
30372
- or (patch.columns[0] if patch.columns else None)
30373
- )
30374
- start_que_id = getattr(gantt_config, "start_que_id", None) or patch.start_que_id
30375
- end_que_id = getattr(gantt_config, "end_que_id", None) or patch.end_que_id
30376
- dependency = gantt_config.dependencies[0] if gantt_config is not None and gantt_config.dependencies else None
30377
- auto_calibration = bool(getattr(gantt_config, "auto_calibration", False) or False)
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()
30378
29266
  return {
30379
- "titleQueId": title_que_id,
30380
- "startTimeQueId": start_que_id,
30381
- "endTimeQueId": end_que_id,
30382
- "defaultTimeDimension": getattr(gantt_config, "default_time_dimension", None) or "week",
30383
- "ganttGroupVOList": [
30384
- {
30385
- "groupQueId": group.que_id,
30386
- "ordinal": group.ordinal if group.ordinal is not None else index,
30387
- }
30388
- for index, group in enumerate((gantt_config.groups if gantt_config is not None else []), start=1)
30389
- ],
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": [],
30390
29272
  "ganttDependencyVO": {
30391
- "dependencyQueId": dependency.dependency_que_id if dependency else None,
30392
- "predecessorTaskQueId": dependency.predecessor_que_id if dependency else None,
30393
- "startStartOptionId": dependency.start_start_option_id if dependency else None,
30394
- "startEndOptionId": dependency.start_end_option_id if dependency else None,
30395
- "endEndOptionId": dependency.end_end_option_id if dependency else None,
30396
- "endStartOptionId": dependency.end_start_option_id if dependency else None,
29273
+ "dependencyQueId": None,
29274
+ "predecessorTaskQueId": None,
29275
+ "startEndOptionId": None,
29276
+ "startStartOptionId": None,
29277
+ "endEndOptionId": None,
29278
+ "endStartOptionId": None,
30397
29279
  },
30398
29280
  "ganttAutoCalibrationVO": {
30399
29281
  "autoCalibrationRuleVO": {
@@ -30404,15 +29286,15 @@ def _build_public_gantt_payload(
30404
29286
  "endStartFinish": True,
30405
29287
  "endEndFinish": False,
30406
29288
  },
30407
- "beingAutoCalibration": auto_calibration,
30408
- "userAutoCalibration": auto_calibration,
29289
+ "beingAutoCalibration": False,
29290
+ "userAutoCalibration": False,
30409
29291
  },
30410
29292
  }
30411
29293
 
30412
29294
 
30413
29295
  def _build_view_filter_groups(
30414
29296
  *,
30415
- fields_by_que_id: dict[str, dict[str, Any]],
29297
+ current_fields_by_name: dict[str, dict[str, Any]],
30416
29298
  filters: list[Any],
30417
29299
  ) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
30418
29300
  translated_rules: list[dict[str, Any]] = []
@@ -30422,14 +29304,14 @@ def _build_view_filter_groups(
30422
29304
  raw_rule = raw_rule.model_dump(mode="json")
30423
29305
  if not isinstance(raw_rule, dict):
30424
29306
  continue
30425
- rule_que_id = _coerce_positive_int(raw_rule.get("que_id"))
30426
- field = fields_by_que_id.get(rule_que_id) if rule_que_id is not None else None
29307
+ field_name = str(raw_rule.get("field_name") or "").strip()
29308
+ field = current_fields_by_name.get(field_name)
30427
29309
  if field is None:
30428
29310
  issues.append(
30429
29311
  {
30430
29312
  "error_code": "UNKNOWN_VIEW_FIELD",
30431
- "missing_fields": [str(raw_rule.get("que_id") or "")],
30432
- "reason_path": "filters[].que_id",
29313
+ "missing_fields": [field_name] if field_name else [],
29314
+ "reason_path": "filters[].field_name",
30433
29315
  }
30434
29316
  )
30435
29317
  continue
@@ -30437,9 +29319,6 @@ def _build_view_filter_groups(
30437
29319
  if issue:
30438
29320
  issues.append(issue)
30439
29321
  continue
30440
- stale_warning = translated_rule.pop("_stale_warning", None)
30441
- if stale_warning is not None:
30442
- issues.append(stale_warning)
30443
29322
  translated_rules.append(translated_rule)
30444
29323
  return ([translated_rules] if translated_rules else []), issues
30445
29324
 
@@ -30450,9 +29329,9 @@ def _translate_view_filter_rule(*, field: dict[str, Any], rule: dict[str, Any])
30450
29329
  field_type = str(field.get("type") or FieldType.text.value)
30451
29330
  que_id = _coerce_positive_int(field.get("que_id")) or 0
30452
29331
  que_type = _coerce_positive_int(field.get("que_type")) or FIELD_TYPE_TO_QUESTION_TYPE.get(field_type, 2)
30453
- judge_values, judge_value_details, value_issue = _resolve_view_filter_values(field=field, values=values)
30454
- if value_issue and value_issue.get("error_code"):
30455
- return {}, value_issue
29332
+ judge_values, judge_value_details, issue = _resolve_view_filter_values(field=field, values=values)
29333
+ if issue:
29334
+ return {}, issue
30456
29335
  payload: dict[str, Any] = {
30457
29336
  "queId": que_id,
30458
29337
  "queTitle": str(field.get("name") or ""),
@@ -30484,9 +29363,6 @@ def _translate_view_filter_rule(*, field: dict[str, Any], rule: dict[str, Any])
30484
29363
  elif operator == "not_empty":
30485
29364
  payload["judgeType"] = JUDGE_UNEQUAL
30486
29365
  payload["judgeValues"] = []
30487
- if value_issue is not None:
30488
- # 死选项透传:规则照常写入,warning 随 payload 携带由上层收集(_build_view_filter_groups 弹出)
30489
- payload["_stale_warning"] = value_issue
30490
29366
  return payload, None
30491
29367
 
30492
29368
 
@@ -30517,23 +29393,21 @@ def _resolve_view_filter_values(
30517
29393
  unresolved_values.append(_stringify_condition_value(value))
30518
29394
  continue
30519
29395
  resolved_details.append({"id": resolved.get("id"), "value": str(resolved.get("value") or resolved.get("id"))})
30520
- if unresolved_values and not resolved_details:
30521
- # 全部值都解析不出(选项已删):原值透传(后端对存量死选项引用原样保存),
30522
- # 以 STALE_OPTION_REFERENCE 警告留痕,不再整视图失败
29396
+ if unresolved_values:
30523
29397
  return (
30524
- [_stringify_condition_value(value) for value in values],
30525
- _build_view_filter_value_details(values),
29398
+ [],
29399
+ [],
30526
29400
  {
30527
- "warning_code": "STALE_OPTION_REFERENCE",
29401
+ "error_code": "UNKNOWN_VIEW_FILTER_VALUE",
30528
29402
  "reason_path": "filters[].values",
30529
29403
  "missing_fields": [],
29404
+ "allowed_values": {"filter_values": sorted(option_by_value.keys())},
30530
29405
  "details": {
30531
29406
  "field_name": str(field.get("name") or ""),
30532
29407
  "unresolved_values": unresolved_values,
30533
29408
  },
30534
29409
  },
30535
29410
  )
30536
- # 部分值解析不出:剔除已删选项值继续写(兼容存量脏数据,不再整视图失败)
30537
29411
  return (
30538
29412
  [str(detail["id"]) for detail in resolved_details if detail.get("id") is not None],
30539
29413
  [_coerce_view_filter_value_detail(detail) for detail in resolved_details],
@@ -30577,10 +29451,6 @@ def _build_view_filter_value_details(values: list[Any]) -> list[dict[str, Any]]:
30577
29451
  details.append(_coerce_view_filter_value_detail({"id": item_id, "value": item_value if item_value is not None else str(item_id)}))
30578
29452
  elif isinstance(value, int):
30579
29453
  details.append(_coerce_view_filter_value_detail({"id": value, "value": str(value)}))
30580
- elif isinstance(value, str):
30581
- # text/number 字段:AnswerValue.id 是 Integer(选项 id),字符串值无 id;
30582
- # 后端 formula getId()==null → 走 getValue() 分支构建 ""value"" 形式
30583
- details.append(_coerce_view_filter_value_detail({"id": None, "value": value}))
30584
29454
  return details
30585
29455
 
30586
29456
 
@@ -30747,11 +29617,11 @@ def _view_filter_groups_semantic_readback(expected: Any, actual: Any) -> tuple[b
30747
29617
  def _public_view_filter_groups_from_match_rules(
30748
29618
  groups: Any,
30749
29619
  *,
30750
- fields_by_que_id: dict[str, dict[str, Any]],
29620
+ current_fields_by_name: dict[str, dict[str, Any]],
30751
29621
  ) -> list[list[dict[str, Any]]]:
30752
29622
  fields_by_que_id = {
30753
29623
  _coerce_positive_int(field.get("que_id")): field
30754
- for field in fields_by_que_id.values()
29624
+ for field in current_fields_by_name.values()
30755
29625
  if isinstance(field, dict) and _coerce_positive_int(field.get("que_id")) is not None
30756
29626
  }
30757
29627
  public_groups: list[list[dict[str, Any]]] = []
@@ -30762,7 +29632,7 @@ def _public_view_filter_groups_from_match_rules(
30762
29632
  field = fields_by_que_id.get(que_id) or {}
30763
29633
  values = _public_view_filter_rule_values(rule, field=field)
30764
29634
  public_rule: dict[str, Any] = {
30765
- "que_id": que_id,
29635
+ "field_name": str(field.get("name") or rule.get("queTitle") or que_id),
30766
29636
  "operator": _public_view_filter_operator_from_judge_type(rule.get("judgeType"), values=values),
30767
29637
  }
30768
29638
  if public_rule["operator"] == "in":
@@ -30787,8 +29657,6 @@ def _public_view_filter_operator_from_judge_type(judge_type: Any, *, values: lis
30787
29657
  if normalized in {JUDGE_INCLUDE, JUDGE_FUZZY_MATCH}:
30788
29658
  return "contains"
30789
29659
  if normalized in {JUDGE_EQUAL_ANY, JUDGE_INCLUDE_ANY}:
30790
- # ANY 判型+空值 = "该字段未选任何值"的合法存量形态(judgeType 保真回写,
30791
- # 转 is_empty 会丢判型——后端 9 与 0 是不同判定域,实测回读不同)
30792
29660
  return "in"
30793
29661
  if normalized == JUDGE_GREATER_OR_EQUAL:
30794
29662
  return "gte"