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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +7 -2
  4. package/skills/qingflow-cli/SKILL.md +74 -0
  5. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
  6. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +1 -0
  7. package/skills/qingflow-cli/reference/builder/50-views.md +180 -191
  8. package/skills/qingflow-cli/reference/builder/README.md +1 -1
  9. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
  10. package/skills/qingflow-cli/reference/examples/README.md +1 -1
  11. package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
  12. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
  13. package/src/qingflow_mcp/builder_facade/models.py +227 -17
  14. package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +42 -0
  15. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1245 -0
  16. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
  17. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
  18. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +73 -0
  19. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
  20. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
  21. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +319 -0
  22. package/src/qingflow_mcp/builder_facade/service.py +1460 -410
  23. package/src/qingflow_mcp/builder_facade/views_extract.py +592 -0
  24. package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
  25. package/src/qingflow_mcp/cli/commands/builder.py +72 -53
  26. package/src/qingflow_mcp/public_surface.py +3 -0
  27. package/src/qingflow_mcp/response_trim.py +11 -2
  28. package/src/qingflow_mcp/server_app_builder.py +32 -8
  29. package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
  30. package/src/qingflow_mcp/solution/executor.py +96 -8
  31. package/src/qingflow_mcp/solution/spec_models.py +1 -0
  32. package/src/qingflow_mcp/tools/ai_builder_tools.py +211 -393
  33. package/src/qingflow_mcp/tools/solution_tools.py +2 -0
  34. package/src/qingflow_mcp/tools/view_tools.py +0 -6
  35. package/src/qingflow_mcp/tools/view_writer.py +44 -0
  36. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
  37. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +0 -42
@@ -115,6 +115,15 @@ def _elapsed_ms(started_at: float) -> int:
115
115
  return max(0, int((time.perf_counter() - started_at) * 1000))
116
116
 
117
117
 
118
+ def _transport_error_payload(error: QingflowApiError) -> JSONObject:
119
+ return {
120
+ "http_status": error.http_status,
121
+ "backend_code": error.backend_code,
122
+ "category": error.category,
123
+ "request_id": error.request_id,
124
+ }
125
+
126
+
118
127
  def _merge_duration_breakdown(payload: JSONObject, **items: int | None) -> None:
119
128
  breakdown = payload.get("duration_breakdown_ms")
120
129
  if not isinstance(breakdown, dict):
@@ -151,7 +160,6 @@ BUILDER_APPLY_TOOL_NAMES = {
151
160
  "app_publish_verify",
152
161
  }
153
162
  MULTI_APP_SCHEMA_APPLY_PARALLEL_LIMIT = 10
154
- VIEW_APPLY_PARALLEL_LIMIT = 20
155
163
  PORTAL_INLINE_CHART_MAX_WORKERS = 10
156
164
 
157
165
 
@@ -434,6 +442,30 @@ class AiBuilderTools(ToolBase):
434
442
  ) -> JSONObject:
435
443
  return self.app_form_apply(profile=profile, spec=spec or {})
436
444
 
445
+ @mcp.tool()
446
+ def app_views_schema(
447
+ schema_version: str | None = None,
448
+ schema_kind: str | None = None,
449
+ ) -> JSONObject:
450
+ """Get a complete views schema or a focused detail schema such as display or button-config."""
451
+ return self.app_views_schema(schema_version=schema_version, schema_kind=schema_kind)
452
+
453
+ @mcp.tool()
454
+ def app_views_get(
455
+ profile: str = DEFAULT_PROFILE,
456
+ app_key: str = "",
457
+ schema_version: str | None = None,
458
+ schema_kind: str | None = None,
459
+ ) -> JSONObject:
460
+ return self.app_views_get(profile=profile, app_key=app_key, schema_version=schema_version, schema_kind=schema_kind)
461
+
462
+ @mcp.tool()
463
+ def app_views_validate(
464
+ declaration: JSONObject | None = None,
465
+ schema_version: str | None = None,
466
+ ) -> JSONObject:
467
+ return self.app_views_validate(declaration=declaration or {}, schema_version=schema_version)
468
+
437
469
  @mcp.tool(description=self._high_risk_tool_description(operation="delete", target="app configuration"))
438
470
  def app_delete(
439
471
  profile: str = DEFAULT_PROFILE,
@@ -763,19 +795,17 @@ class AiBuilderTools(ToolBase):
763
795
  profile: str = DEFAULT_PROFILE,
764
796
  app_key: str = "",
765
797
  publish: bool = True,
766
- upsert_views: list[JSONObject] | None = None,
798
+ upsert_view: JSONObject | None = None,
767
799
  patch_views: list[JSONObject] | None = None,
768
800
  remove_views: list[str] | None = None,
769
- apps: list[JSONObject] | None = None,
770
801
  ) -> JSONObject:
771
802
  return self.app_views_apply(
772
803
  profile=profile,
773
804
  app_key=app_key,
774
805
  publish=publish,
775
- upsert_views=upsert_views or [],
806
+ upsert_view=upsert_view,
776
807
  patch_views=patch_views or [],
777
808
  remove_views=remove_views or [],
778
- apps=apps,
779
809
  )
780
810
 
781
811
  @mcp.tool()
@@ -2251,17 +2281,34 @@ class AiBuilderTools(ToolBase):
2251
2281
  *,
2252
2282
  profile: str,
2253
2283
  app_key: str,
2284
+ upsert_view: JSONObject | None = None,
2254
2285
  upsert_views: list[JSONObject] | None = None,
2255
2286
  patch_views: list[JSONObject] | None = None,
2256
2287
  remove_views: list[str] | None = None,
2257
2288
  preset: str | None = None,
2258
2289
  ) -> JSONObject:
2259
- """执行应用相关逻辑。"""
2290
+ """Plan one new or fully updated view, plus optional existing-view changes."""
2291
+ if upsert_view is not None and upsert_views:
2292
+ return _config_failure(
2293
+ tool_name="app_views_plan",
2294
+ message="pass either upsert_view or upsert_views, not both.",
2295
+ fix_hint="Use upsert_view for one new or full view declaration.",
2296
+ details={"provided": ["upsert_view", "upsert_views"]},
2297
+ )
2298
+ normalized_upsert_views = [upsert_view] if upsert_view is not None else list(upsert_views or [])
2299
+ if len(normalized_upsert_views) > 1:
2300
+ return _config_failure(
2301
+ tool_name="app_views_plan",
2302
+ message="app_views_plan supports planning one new or fully updated view per call.",
2303
+ fix_hint="Call app_views_plan once for each view, using upsert_view.",
2304
+ details={"max_upsert_views": 1, "received_upsert_views": len(normalized_upsert_views)},
2305
+ error_code="VIEW_BATCH_CREATE_UNSUPPORTED",
2306
+ )
2260
2307
  reserved_failure = _reserved_system_view_name_failure(
2261
2308
  tool_name="app_views_plan",
2262
2309
  profile=profile,
2263
2310
  app_key=app_key,
2264
- upsert_views=upsert_views,
2311
+ upsert_views=normalized_upsert_views,
2265
2312
  remove_views=remove_views,
2266
2313
  )
2267
2314
  if reserved_failure is not None:
@@ -2270,7 +2317,7 @@ class AiBuilderTools(ToolBase):
2270
2317
  request = ViewsPlanRequest.model_validate(
2271
2318
  {
2272
2319
  "app_key": app_key,
2273
- "upsert_views": upsert_views or [],
2320
+ "upsert_views": normalized_upsert_views,
2274
2321
  "patch_views": patch_views or [],
2275
2322
  "remove_views": remove_views or [],
2276
2323
  "preset": preset,
@@ -2287,7 +2334,7 @@ class AiBuilderTools(ToolBase):
2287
2334
  "profile": profile,
2288
2335
  "app_key": app_key,
2289
2336
  "preset": "default_table",
2290
- "upsert_views": [],
2337
+ "upsert_view": None,
2291
2338
  "patch_views": [],
2292
2339
  "remove_views": [],
2293
2340
  },
@@ -2298,7 +2345,7 @@ class AiBuilderTools(ToolBase):
2298
2345
  error_code="VIEWS_PLAN_FAILED",
2299
2346
  normalized_args={
2300
2347
  "app_key": request.app_key,
2301
- "upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
2348
+ "upsert_view": public_view_upsert_payload(request.upsert_views[0]) if request.upsert_views else None,
2302
2349
  "patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
2303
2350
  "remove_views": list(request.remove_views),
2304
2351
  "preset": request.preset.value if request.preset is not None else None,
@@ -2308,7 +2355,7 @@ class AiBuilderTools(ToolBase):
2308
2355
  "arguments": {
2309
2356
  "profile": profile,
2310
2357
  "app_key": request.app_key,
2311
- "upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
2358
+ "upsert_view": public_view_upsert_payload(request.upsert_views[0]) if request.upsert_views else None,
2312
2359
  "patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
2313
2360
  "remove_views": list(request.remove_views),
2314
2361
  "preset": request.preset.value if request.preset is not None else None,
@@ -3931,41 +3978,47 @@ class AiBuilderTools(ToolBase):
3931
3978
  profile: str,
3932
3979
  app_key: str,
3933
3980
  publish: bool = True,
3934
- upsert_views: list[JSONObject],
3981
+ upsert_view: JSONObject | None = None,
3935
3982
  patch_views: list[JSONObject] | None = None,
3936
- remove_views: list[str],
3937
- apps: list[JSONObject] | None = None,
3938
- views: list[JSONObject] | None = None,
3983
+ remove_views: list[str] | None = None,
3984
+ schema_version: str | None = None,
3985
+ upsert_views: list[JSONObject] | None = None,
3939
3986
  ) -> JSONObject:
3940
- """执行应用相关逻辑。"""
3941
- if views is not None:
3942
- return self._app_views_apply_view_batch(
3943
- profile=profile,
3944
- publish=publish,
3945
- views=views,
3987
+ """Apply one new or full view declaration for one application.
3988
+
3989
+ ``upsert_views`` remains an internal compatibility alias while callers
3990
+ migrate to ``upsert_view``. It accepts at most one item.
3991
+ """
3992
+ if upsert_view is not None and upsert_views:
3993
+ return _attach_builder_apply_envelope(
3994
+ "app_views_apply",
3995
+ _config_failure(
3996
+ tool_name="app_views_apply",
3997
+ message="pass either upsert_view or upsert_views, not both.",
3998
+ fix_hint="Use upsert_view for one new or full view declaration.",
3999
+ details={"provided": ["upsert_view", "upsert_views"]},
4000
+ ),
3946
4001
  )
3947
- if apps is not None:
3948
- return self._apply_app_batch(
3949
- tool_name="app_views_apply",
3950
- profile=profile,
3951
- apps=apps,
3952
- apply_one=lambda _index, item, item_app_key: self.app_views_apply(
3953
- profile=profile,
3954
- app_key=item_app_key,
3955
- publish=_payload_bool(item, "publish", default=publish),
3956
- upsert_views=_payload_list(item, "upsert_views", "upsertViews", "views", default=upsert_views or []),
3957
- patch_views=_payload_list(item, "patch_views", "patchViews", default=patch_views or []),
3958
- remove_views=_payload_list(item, "remove_views", "removeViews", default=remove_views or []),
3959
- apps=None,
4002
+ normalized_upsert_views = [upsert_view] if upsert_view is not None else list(upsert_views or [])
4003
+ if len(normalized_upsert_views) > 1:
4004
+ return _attach_builder_apply_envelope(
4005
+ "app_views_apply",
4006
+ _config_failure(
4007
+ tool_name="app_views_apply",
4008
+ message="app_views_apply supports creating or fully updating one view per call.",
4009
+ fix_hint="Call app_views_apply once for each view, using upsert_view.",
4010
+ details={"max_upsert_views": 1, "received_upsert_views": len(normalized_upsert_views)},
4011
+ error_code="VIEW_BATCH_CREATE_UNSUPPORTED",
3960
4012
  ),
3961
4013
  )
3962
4014
  result = self._app_views_apply_once(
3963
4015
  profile=profile,
3964
4016
  app_key=app_key,
3965
4017
  publish=publish,
3966
- upsert_views=upsert_views,
4018
+ upsert_views=normalized_upsert_views,
3967
4019
  patch_views=patch_views or [],
3968
- remove_views=remove_views,
4020
+ remove_views=remove_views or [],
4021
+ schema_version=schema_version,
3969
4022
  )
3970
4023
  result = self._retry_after_self_lock_release(
3971
4024
  profile=profile,
@@ -3974,255 +4027,14 @@ class AiBuilderTools(ToolBase):
3974
4027
  profile=profile,
3975
4028
  app_key=app_key,
3976
4029
  publish=publish,
3977
- remove_views=remove_views,
3978
- upsert_views=upsert_views,
4030
+ remove_views=remove_views or [],
4031
+ upsert_views=normalized_upsert_views,
3979
4032
  patch_views=patch_views or [],
4033
+ schema_version=schema_version,
3980
4034
  ),
3981
4035
  )
3982
4036
  return self._attach_apply_envelope(profile, "app_views_apply", result)
3983
4037
 
3984
- def _app_views_apply_view_batch(
3985
- self,
3986
- *,
3987
- profile: str,
3988
- publish: bool,
3989
- views: list[JSONObject],
3990
- ) -> JSONObject:
3991
- started_at = time.perf_counter()
3992
- if not isinstance(views, list) or not views:
3993
- return self._attach_apply_envelope(profile,
3994
- "app_views_apply",
3995
- _config_failure(
3996
- tool_name="app_views_apply",
3997
- message="app_views_apply views[] batch mode requires a non-empty views array.",
3998
- fix_hint="Pass views as a JSON array; each item must include app_key and operation.",
3999
- details={"expected_shape": {"views": [{"operation": "upsert", "app_key": "APP_KEY", "name": "业务视图", "type": "table", "columns": ["字段A"]}]}},
4000
- ),
4001
- )
4002
-
4003
- def _infer_operation(item: JSONObject) -> str:
4004
- raw = str(item.get("operation") or item.get("op") or "").strip().lower()
4005
- aliases = {
4006
- "create": "upsert",
4007
- "update": "upsert",
4008
- "replace": "upsert",
4009
- "delete": "remove",
4010
- "remove": "remove",
4011
- "patch": "patch",
4012
- "upsert": "upsert",
4013
- }
4014
- if raw:
4015
- return aliases.get(raw, raw)
4016
- if "set" in item or "unset" in item or "update" in item or "values" in item:
4017
- return "patch"
4018
- if "view_key" in item or "viewKey" in item:
4019
- return "patch" if ("name" in item or "type" in item or "columns" in item or "fields" in item) else "remove"
4020
- return "upsert"
4021
-
4022
- def _view_job_payload(item: JSONObject) -> JSONObject:
4023
- return {
4024
- key: deepcopy(value)
4025
- for key, value in item.items()
4026
- if key
4027
- not in {
4028
- "operation",
4029
- "op",
4030
- "app_key",
4031
- "appKey",
4032
- "publish",
4033
- }
4034
- }
4035
-
4036
- def _view_selector(payload: JSONObject) -> str:
4037
- return str(payload.get("view_key") or payload.get("viewKey") or payload.get("viewgraphKey") or payload.get("viewGraphKey") or payload.get("name") or "").strip()
4038
-
4039
- normalized_jobs: list[JSONObject] = []
4040
- job_results: list[JSONObject | None] = [None for _ in views]
4041
- errors: list[JSONObject] = []
4042
- seen_patch_remove: dict[tuple[str, str], int] = {}
4043
- seen_upsert_names: dict[tuple[str, str], int] = {}
4044
-
4045
- def _add_validation_error(index: int, error_code: str, message: str, *, app_key: str = "", operation: str = "", reason_path: str = "") -> None:
4046
- error = {
4047
- "index": index,
4048
- "app_key": app_key,
4049
- "operation": operation,
4050
- "status": "failed",
4051
- "error_code": error_code,
4052
- "message": message,
4053
- **({"reason_path": reason_path} if reason_path else {}),
4054
- }
4055
- errors.append(error)
4056
- job_results[index] = error
4057
-
4058
- for index, item in enumerate(views):
4059
- if not isinstance(item, dict):
4060
- _add_validation_error(index, "VIEWS_FILE_ITEM_INVALID", "views[] item must be an object", reason_path=f"views[{index}]")
4061
- continue
4062
- app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
4063
- operation = _infer_operation(item)
4064
- payload = _view_job_payload(item)
4065
- if not app_key:
4066
- _add_validation_error(index, "VIEWS_FILE_APP_KEY_REQUIRED", "views[] item requires app_key", operation=operation, reason_path=f"views[{index}].app_key")
4067
- continue
4068
- if operation not in {"upsert", "patch", "remove"}:
4069
- _add_validation_error(index, "VIEWS_FILE_OPERATION_INVALID", "views[].operation must be upsert, patch, or remove", app_key=app_key, operation=operation, reason_path=f"views[{index}].operation")
4070
- continue
4071
- selector = _view_selector(payload)
4072
- if operation in {"patch", "remove"} and not selector:
4073
- _add_validation_error(index, "VIEWS_FILE_VIEW_SELECTOR_REQUIRED", f"{operation} view job requires view_key or name", app_key=app_key, operation=operation, reason_path=f"views[{index}].view_key")
4074
- continue
4075
- if operation in {"patch", "remove"} and selector:
4076
- conflict_key = (app_key, selector)
4077
- first_index = seen_patch_remove.get(conflict_key)
4078
- if first_index is not None:
4079
- _add_validation_error(index, "DUPLICATE_VIEW_JOB_CONFLICT", f"views[{index}] conflicts with views[{first_index}] for the same app/view selector", app_key=app_key, operation=operation, reason_path=f"views[{index}]")
4080
- continue
4081
- seen_patch_remove[conflict_key] = index
4082
- if operation == "upsert":
4083
- name = str(payload.get("name") or "").strip()
4084
- if name:
4085
- conflict_key = (app_key, name)
4086
- first_index = seen_upsert_names.get(conflict_key)
4087
- if first_index is not None:
4088
- _add_validation_error(index, "DUPLICATE_VIEW_NAME_IN_BATCH", f"views[{index}] duplicates upsert view name from views[{first_index}] in the same app", app_key=app_key, operation=operation, reason_path=f"views[{index}].name")
4089
- continue
4090
- seen_upsert_names[conflict_key] = index
4091
- normalized_jobs.append(
4092
- {
4093
- "index": index,
4094
- "app_key": app_key,
4095
- "operation": operation,
4096
- "publish": _payload_bool(item, "publish", default=publish),
4097
- "payload": payload,
4098
- }
4099
- )
4100
-
4101
- def _extract_view_identity(result: JSONObject, fallback_payload: JSONObject, fallback_operation: str) -> tuple[str | None, str | None, str]:
4102
- diff = result.get("views_diff") if isinstance(result.get("views_diff"), dict) else {}
4103
- for key, operation_name in (("created", "created"), ("updated", "updated"), ("removed", "removed"), ("failed", "failed")):
4104
- entries = diff.get(key)
4105
- if isinstance(entries, list) and entries:
4106
- entry = entries[0]
4107
- if isinstance(entry, dict):
4108
- return (
4109
- str(entry.get("name") or entry.get("view_name") or entry.get("viewName") or fallback_payload.get("name") or "").strip() or None,
4110
- str(entry.get("view_key") or entry.get("viewKey") or fallback_payload.get("view_key") or fallback_payload.get("viewKey") or "").strip() or None,
4111
- operation_name,
4112
- )
4113
- if entry not in (None, ""):
4114
- return (str(entry), None, operation_name)
4115
- fallback_name = str(fallback_payload.get("name") or "").strip() or None
4116
- fallback_view_key = str(fallback_payload.get("view_key") or fallback_payload.get("viewKey") or "").strip() or None
4117
- return fallback_name, fallback_view_key, "removed" if fallback_operation == "remove" else "updated"
4118
-
4119
- def _run_view_job(job: JSONObject) -> JSONObject:
4120
- payload = deepcopy(job["payload"])
4121
- operation = str(job["operation"])
4122
- app_key = str(job["app_key"])
4123
- job_publish = bool(job.get("publish"))
4124
- if operation == "upsert":
4125
- call_kwargs = {"upsert_views": [payload], "patch_views": [], "remove_views": []}
4126
- elif operation == "patch":
4127
- call_kwargs = {"upsert_views": [], "patch_views": [payload], "remove_views": []}
4128
- else:
4129
- call_kwargs = {"upsert_views": [], "patch_views": [], "remove_views": [_view_selector(payload)]}
4130
- result = self._app_views_apply_once(
4131
- profile=profile,
4132
- app_key=app_key,
4133
- publish=job_publish,
4134
- **call_kwargs,
4135
- )
4136
- result = self._retry_after_self_lock_release(
4137
- profile=profile,
4138
- result=result,
4139
- retry_call=lambda: self._app_views_apply_once(
4140
- profile=profile,
4141
- app_key=app_key,
4142
- publish=job_publish,
4143
- **call_kwargs,
4144
- ),
4145
- )
4146
- status = str(result.get("status") or "success") if isinstance(result, dict) else "failed"
4147
- name, view_key, actual_operation = _extract_view_identity(result if isinstance(result, dict) else {}, payload, operation)
4148
- item: JSONObject = {
4149
- "index": job["index"],
4150
- "app_key": app_key,
4151
- "operation": actual_operation if status != "failed" else operation,
4152
- "requested_operation": operation,
4153
- "status": status,
4154
- **({"name": name} if name else {}),
4155
- **({"view_key": view_key} if view_key else {}),
4156
- "result": result if isinstance(result, dict) else {"status": "failed", "message": str(result)},
4157
- }
4158
- if status == "failed":
4159
- item["error_code"] = item["result"].get("error_code")
4160
- item["message"] = item["result"].get("message")
4161
- return item
4162
-
4163
- view_write_started_at = time.perf_counter()
4164
- view_workers = min(VIEW_APPLY_PARALLEL_LIMIT, len(normalized_jobs)) if normalized_jobs else 0
4165
- if normalized_jobs:
4166
- with ThreadPoolExecutor(max_workers=view_workers) as executor:
4167
- future_map = {executor.submit(_run_view_job, job): job for job in normalized_jobs}
4168
- for future in as_completed(future_map):
4169
- job = future_map[future]
4170
- try:
4171
- item_result = future.result()
4172
- except (QingflowApiError, RuntimeError) as error:
4173
- api_error = _coerce_api_error(error)
4174
- item_result = {
4175
- "index": job["index"],
4176
- "app_key": job["app_key"],
4177
- "operation": job["operation"],
4178
- "requested_operation": job["operation"],
4179
- "status": "failed",
4180
- "error_code": "VIEW_BATCH_JOB_FAILED",
4181
- "message": _public_error_message("VIEW_BATCH_JOB_FAILED", api_error),
4182
- "details": {"transport_error": _transport_error_payload(api_error)},
4183
- "request_id": api_error.request_id,
4184
- "backend_code": api_error.backend_code,
4185
- "http_status": None if api_error.http_status == 404 else api_error.http_status,
4186
- }
4187
- job_results[int(job["index"])] = item_result
4188
- view_write_parallel_wall_ms = _elapsed_ms(view_write_started_at)
4189
-
4190
- final_views = [item if isinstance(item, dict) else {"index": index, "status": "failed", "error_code": "VIEW_BATCH_JOB_MISSING", "message": "view batch job did not produce a result"} for index, item in enumerate(job_results)]
4191
- errors.extend(item for item in final_views if str(item.get("status") or "") == "failed" and item not in errors)
4192
- partial = sum(1 for item in final_views if str(item.get("status") or "") == "partial_success")
4193
- succeeded = sum(1 for item in final_views if str(item.get("status") or "") in {"success", "partial_success"})
4194
- failed = len(final_views) - succeeded
4195
- status = "failed" if succeeded == 0 else "partial_success" if failed > 0 or partial > 0 else "success"
4196
- write_executed = any(
4197
- isinstance(item.get("result"), dict) and bool(item["result"].get("write_executed"))
4198
- for item in final_views
4199
- )
4200
- payload: JSONObject = {
4201
- "status": status,
4202
- "error_code": None if status == "success" else "APP_VIEWS_APPLY_BATCH_PARTIAL" if succeeded else "APP_VIEWS_APPLY_BATCH_FAILED",
4203
- "recoverable": failed > 0 or partial > 0,
4204
- "message": f"applied app_views_apply to {succeeded}/{len(final_views)} views" if status != "success" else f"applied app_views_apply to {succeeded} views",
4205
- "normalized_args": {"views": views},
4206
- "views": final_views,
4207
- "errors": errors,
4208
- "parallelism": {"view_workers": view_workers, "same_app_parallel": True},
4209
- "verification": {
4210
- "batch_verified": failed == 0 and partial == 0,
4211
- "succeeded": succeeded,
4212
- "partial": partial,
4213
- "failed": failed,
4214
- },
4215
- "write_executed": write_executed,
4216
- "write_succeeded": write_executed and failed == 0 and partial == 0,
4217
- "safe_to_retry": not write_executed,
4218
- }
4219
- _merge_duration_breakdown(
4220
- payload,
4221
- view_write_parallel_wall_ms=view_write_parallel_wall_ms,
4222
- total_ms=_elapsed_ms(started_at),
4223
- )
4224
- return self._attach_apply_envelope(profile, "app_views_apply", payload)
4225
-
4226
4038
  def _app_views_apply_once(
4227
4039
  self,
4228
4040
  *,
@@ -4232,6 +4044,7 @@ class AiBuilderTools(ToolBase):
4232
4044
  upsert_views: list[JSONObject],
4233
4045
  patch_views: list[JSONObject],
4234
4046
  remove_views: list[str],
4047
+ schema_version: str | None = None,
4235
4048
  ) -> JSONObject:
4236
4049
  """执行内部辅助逻辑。"""
4237
4050
  reserved_failure = _reserved_system_view_name_failure(
@@ -4259,7 +4072,7 @@ class AiBuilderTools(ToolBase):
4259
4072
  "profile": profile,
4260
4073
  "app_key": app_key,
4261
4074
  "publish": publish,
4262
- "upsert_views": upsert_views or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
4075
+ "upsert_view": (upsert_views[0] if upsert_views else {"name": "业务台账视图", "type": "table", "columns": ["字段A"]}),
4263
4076
  "patch_views": patch_views or [],
4264
4077
  "remove_views": remove_views or [],
4265
4078
  },
@@ -4268,9 +4081,10 @@ class AiBuilderTools(ToolBase):
4268
4081
  normalized_args = {
4269
4082
  "app_key": app_key,
4270
4083
  "publish": publish,
4271
- "upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
4084
+ "upsert_view": public_view_upsert_payload(parsed_views[0]) if parsed_views else None,
4272
4085
  "patch_views": [public_view_partial_payload(patch) for patch in parsed_patch_views],
4273
4086
  "remove_views": list(remove_views or []),
4087
+ "schema_version": schema_version,
4274
4088
  }
4275
4089
  return _safe_tool_call(
4276
4090
  lambda: self._facade.app_views_apply(
@@ -4280,12 +4094,44 @@ class AiBuilderTools(ToolBase):
4280
4094
  upsert_views=parsed_views,
4281
4095
  patch_views=parsed_patch_views,
4282
4096
  remove_views=list(remove_views or []),
4097
+ schema_version=schema_version,
4283
4098
  ),
4284
4099
  error_code="VIEWS_APPLY_FAILED",
4285
4100
  normalized_args=normalized_args,
4286
4101
  suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
4287
4102
  )
4288
4103
 
4104
+ def app_views_schema(
4105
+ self,
4106
+ *,
4107
+ schema_version: str | None = None,
4108
+ schema_kind: str | None = None,
4109
+ ) -> JSONObject:
4110
+ """Return a versioned views schema (summary/complete or a focused detail kind)."""
4111
+ return self._facade.app_views_schema(schema_version=schema_version, schema_kind=schema_kind)
4112
+
4113
+ def app_views_get(
4114
+ self,
4115
+ *,
4116
+ profile: str,
4117
+ app_key: str,
4118
+ schema_version: str | None = None,
4119
+ schema_kind: str | None = None,
4120
+ ) -> JSONObject:
4121
+ """Read canonical views declarations (summary projection or complete reverse-compilation)."""
4122
+ return self._facade.app_views_get(
4123
+ profile=profile, app_key=app_key, schema_version=schema_version, schema_kind=schema_kind or "summary"
4124
+ )
4125
+
4126
+ def app_views_validate(
4127
+ self,
4128
+ *,
4129
+ declaration: JSONObject,
4130
+ schema_version: str | None = None,
4131
+ ) -> JSONObject:
4132
+ """Offline-validate a views declaration without touching the backend."""
4133
+ return self._facade.app_views_validate(declaration=declaration or {}, schema_version=schema_version)
4134
+
4289
4135
  def chart_apply(
4290
4136
  self,
4291
4137
  *,
@@ -5668,12 +5514,10 @@ def _reserved_system_view_name_failure(
5668
5514
  "arguments": {
5669
5515
  "profile": profile,
5670
5516
  "app_key": app_key,
5671
- "upsert_views": [
5672
- {
5673
- **item,
5674
- "name": "业务台账视图",
5675
- }
5676
- ],
5517
+ "upsert_view": {
5518
+ **item,
5519
+ "name": "业务台账视图",
5520
+ },
5677
5521
  "patch_views": patch_views or [],
5678
5522
  "remove_views": remove_views or [],
5679
5523
  },
@@ -5683,7 +5527,7 @@ def _reserved_system_view_name_failure(
5683
5527
  return _config_failure(
5684
5528
  tool_name=tool_name,
5685
5529
  error_code="RESERVED_SYSTEM_VIEW_NAME",
5686
- message=f"upsert_views[{index}].name uses built-in system view name '{name}' without view_key",
5530
+ message=f"upsert_view.name uses built-in system view name '{name}' without view_key",
5687
5531
  fix_hint="Do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送. Use a business-specific view name for new views; to modify a built-in view, pass its existing raw view_key or use patch_views.",
5688
5532
  details={
5689
5533
  "reserved_view_names": sorted(_RESERVED_SYSTEM_VIEW_NAMES),
@@ -8147,20 +7991,20 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8147
7991
  "app_views_plan": {
8148
7992
  "allowed_keys": [
8149
7993
  "app_key",
8150
- "upsert_views",
7994
+ "upsert_view",
8151
7995
  "patch_views",
8152
7996
  "remove_views",
8153
7997
  "preset",
8154
- "upsert_views[].view_key",
8155
- "upsert_views[].name",
8156
- "upsert_views[].type",
8157
- "upsert_views[].columns",
8158
- "upsert_views[].filters",
8159
- "upsert_views[].buttons",
8160
- "upsert_views[].action_buttons",
8161
- "upsert_views[].action_buttons_mode",
8162
- "upsert_views[].visibility",
8163
- "upsert_views[].query_conditions",
7998
+ "upsert_view.view_key",
7999
+ "upsert_view.name",
8000
+ "upsert_view.type",
8001
+ "upsert_view.columns",
8002
+ "upsert_view.filters",
8003
+ "upsert_view.buttons",
8004
+ "upsert_view.action_buttons",
8005
+ "upsert_view.action_buttons_mode",
8006
+ "upsert_view.visibility",
8007
+ "upsert_view.query_conditions",
8164
8008
  "patch_views[].view_key",
8165
8009
  "patch_views[].name",
8166
8010
  "patch_views[].set",
@@ -8181,6 +8025,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8181
8025
  "queryConditions": "query_conditions",
8182
8026
  "query_condition": "query_conditions",
8183
8027
  "queryCondition": "query_conditions",
8028
+ "upsertView": "upsert_view",
8184
8029
  "patchViews": "patch_views",
8185
8030
  "startField": "start_field",
8186
8031
  "endField": "end_field",
@@ -8215,18 +8060,19 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8215
8060
  },
8216
8061
  "execution_notes": [
8217
8062
  "creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
8218
- "upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
8063
+ "upsert_view accepts one new or full view declaration; plan additional views in separate calls",
8064
+ "upsert_view.visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
8219
8065
  "filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
8220
- "upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
8066
+ "upsert_view.query_conditions.rows is a layout matrix of query-panel supported field names, field IDs, or queIds; queIds are preferred and all forms are compiled to backend queryCondition queIds",
8221
8067
  "do not put relation/attachment/subtable/code_block/q_linker/address fields in query_conditions; use filters for fixed filters or app_associated_resources_apply.match_mappings for current-record relation/report matching",
8222
8068
  "use patch_views for partial parameter replacement on existing views; the tool reads current config, merges patch_views[].set/unset, then submits the backend full-save payload internally",
8223
8069
  "business buttons should be declared with action_buttons on the target view; app_views_apply writes the view first, then compiles action_buttons through the custom button writer and binds them to the resulting view_key",
8224
8070
  "action_buttons_mode defaults to merge; replace rewrites this view's custom button bindings, and replace plus action_buttons=[] clears this view's custom button bindings without deleting button bodies",
8225
- "legacy upsert_views[].buttons is a compatibility path for raw full replacement of existing view button config; do not use it as the normal agent path",
8071
+ "legacy upsert_view.buttons is a compatibility path for raw full replacement of existing view button config; do not use it as the normal agent path",
8226
8072
  "remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
8227
8073
  "deleted views return verification.by_view[].delete_executed, readback_status, and safe_to_retry_delete=false; if readback is pending, do not blindly repeat the delete",
8228
8074
  "do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送; these are built-in system/default views. Use business-specific names for new views, and pass the existing raw view_key or patch_views when changing a built-in view",
8229
- "new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_views explicitly changes associated_resources",
8075
+ "new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_view explicitly changes associated_resources",
8230
8076
  "associated report/view resource pool and per-view selected resources are configured through app_associated_resources_apply; app_views_apply only keeps legacy associated_resources input compatible",
8231
8077
  "for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
8232
8078
  *_VISIBILITY_EXECUTION_NOTES,
@@ -8234,7 +8080,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8234
8080
  "minimal_example": {
8235
8081
  "profile": "default",
8236
8082
  "app_key": "APP_KEY",
8237
- "upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
8083
+ "upsert_view": {"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)},
8238
8084
  "remove_views": [],
8239
8085
  },
8240
8086
  "query_conditions_example": {
@@ -8256,41 +8102,37 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8256
8102
  "action_buttons_example": {
8257
8103
  "profile": "default",
8258
8104
  "app_key": "APP_KEY",
8259
- "upsert_views": [
8260
- {
8261
- "name": "生产工单执行视图",
8262
- "type": "table",
8263
- "columns": ["工单编号", "产品", "状态", "负责人"],
8264
- "action_buttons": [
8265
- {
8266
- "text": "开始生产",
8267
- "action": "link",
8268
- "url": "https://example.com/start",
8269
- "placement": "list",
8270
- "visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
8271
- }
8272
- ],
8273
- }
8274
- ],
8105
+ "upsert_view": {
8106
+ "name": "生产工单执行视图",
8107
+ "type": "table",
8108
+ "columns": ["工单编号", "产品", "状态", "负责人"],
8109
+ "action_buttons": [
8110
+ {
8111
+ "text": "开始生产",
8112
+ "action": "link",
8113
+ "url": "https://example.com/start",
8114
+ "placement": "list",
8115
+ "visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
8116
+ }
8117
+ ],
8118
+ },
8275
8119
  "remove_views": [],
8276
8120
  },
8277
8121
  "full_upsert_query_conditions_example": {
8278
8122
  "profile": "default",
8279
8123
  "app_key": "APP_KEY",
8280
- "upsert_views": [
8281
- {
8282
- "name": "客户查询视图",
8283
- "type": "table",
8284
- "columns": ["客户名称", "负责人", "客户状态", "创建时间"],
8285
- "filters": [{"field_name": "客户状态", "operator": "eq", "value": "有效"}],
8286
- "query_conditions": {
8287
- "enabled": True,
8288
- "exact": False,
8289
- "hide_before_query": False,
8290
- "rows": [["客户名称", "负责人"], ["创建时间"]],
8291
- },
8292
- }
8293
- ],
8124
+ "upsert_view": {
8125
+ "name": "客户查询视图",
8126
+ "type": "table",
8127
+ "columns": ["客户名称", "负责人", "客户状态", "创建时间"],
8128
+ "filters": [{"field_name": "客户状态", "operator": "eq", "value": "有效"}],
8129
+ "query_conditions": {
8130
+ "enabled": True,
8131
+ "exact": False,
8132
+ "hide_before_query": False,
8133
+ "rows": [["客户名称", "负责人"], ["创建时间"]],
8134
+ },
8135
+ },
8294
8136
  "remove_views": [],
8295
8137
  },
8296
8138
  "query_conditions_field_rules": {
@@ -8304,48 +8146,35 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8304
8146
  "gantt_example": {
8305
8147
  "profile": "default",
8306
8148
  "app_key": "APP_KEY",
8307
- "upsert_views": [
8308
- {
8309
- "name": "项目甘特图",
8310
- "type": "gantt",
8311
- "columns": ["项目名称", "开始日期", "结束日期"],
8312
- "start_field": "开始日期",
8313
- "end_field": "结束日期",
8314
- "title_field": "项目名称",
8315
- "filters": [{"field_name": "状态", "operator": "eq", "value": "进行中"}],
8316
- }
8317
- ],
8149
+ "upsert_view": {
8150
+ "name": "项目甘特图",
8151
+ "type": "gantt",
8152
+ "columns": ["项目名称", "开始日期", "结束日期"],
8153
+ "start_field": "开始日期",
8154
+ "end_field": "结束日期",
8155
+ "title_field": "项目名称",
8156
+ "filters": [{"field_name": "状态", "operator": "eq", "value": "进行中"}],
8157
+ },
8318
8158
  "remove_views": [],
8319
8159
  },
8320
8160
  },
8321
8161
  "app_views_apply": {
8322
8162
  "allowed_keys": [
8323
- "views",
8324
- "views[].operation",
8325
- "views[].app_key",
8326
- "views[].view_key",
8327
- "views[].name",
8328
- "views[].type",
8329
- "views[].columns",
8330
- "views[].filters",
8331
- "views[].action_buttons",
8332
- "views[].set",
8333
- "views[].unset",
8334
8163
  "app_key",
8335
8164
  "publish",
8336
- "upsert_views",
8165
+ "upsert_view",
8337
8166
  "patch_views",
8338
8167
  "remove_views",
8339
- "upsert_views[].view_key",
8340
- "upsert_views[].name",
8341
- "upsert_views[].type",
8342
- "upsert_views[].columns",
8343
- "upsert_views[].filters",
8344
- "upsert_views[].buttons",
8345
- "upsert_views[].action_buttons",
8346
- "upsert_views[].action_buttons_mode",
8347
- "upsert_views[].visibility",
8348
- "upsert_views[].query_conditions",
8168
+ "upsert_view.view_key",
8169
+ "upsert_view.name",
8170
+ "upsert_view.type",
8171
+ "upsert_view.columns",
8172
+ "upsert_view.filters",
8173
+ "upsert_view.buttons",
8174
+ "upsert_view.action_buttons",
8175
+ "upsert_view.action_buttons_mode",
8176
+ "upsert_view.visibility",
8177
+ "upsert_view.query_conditions",
8349
8178
  "patch_views[].view_key",
8350
8179
  "patch_views[].name",
8351
8180
  "patch_views[].set",
@@ -8366,10 +8195,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8366
8195
  "queryConditions": "query_conditions",
8367
8196
  "query_condition": "query_conditions",
8368
8197
  "queryCondition": "query_conditions",
8369
- "views[].op": "views[].operation",
8370
- "views[].fields": "views[].columns",
8371
- "views[].viewKey": "views[].view_key",
8372
- "views[].actionButtons": "views[].action_buttons",
8198
+ "upsertView": "upsert_view",
8373
8199
  "patchViews": "patch_views",
8374
8200
  "startField": "start_field",
8375
8201
  "endField": "end_field",
@@ -8392,7 +8218,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8392
8218
  "action_buttons[].defaultValues": "action_buttons[].default_values",
8393
8219
  },
8394
8220
  "allowed_values": {
8395
- "views[].operation": ["upsert", "patch", "remove"],
8396
8221
  "view.type": [member.value for member in PublicViewType],
8397
8222
  "view.filter.operator": [member.value for member in ViewFilterOperator],
8398
8223
  "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
@@ -8403,9 +8228,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8403
8228
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
8404
8229
  },
8405
8230
  "execution_notes": [
8406
- "preferred batch input is views[]; each item has app_key and operation=upsert/patch/remove, and the same batch may mix same-app and cross-app views",
8407
- f"views[] jobs run concurrently with an internal limit of {VIEW_APPLY_PARALLEL_LIMIT}; results are returned in input order",
8408
- "apply may return partial_success when some views land and others fail",
8231
+ "upsert_view accepts one new or full view declaration for the app_key in this call; submit additional views in separate calls",
8409
8232
  "creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
8410
8233
  "when duplicate view names exist, supply view_key to target the exact view",
8411
8234
  "read back app_get after any failed or partial view apply",
@@ -8414,24 +8237,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8414
8237
  "business buttons should be declared with action_buttons on the target view; app_views_apply writes the view first, then compiles action_buttons through the custom button writer and binds them to the resulting view_key",
8415
8238
  "patch_views[].set.action_buttons can add buttons to an existing view without rewriting columns/filters; action_buttons_mode defaults to merge, replace rewrites this view's custom button bindings, and replace plus action_buttons=[] clears bindings without deleting button bodies",
8416
8239
  "publish=false with action_buttons is blocked as VIEW_ACTION_BUTTONS_REQUIRE_PUBLISH because the underlying custom button writer may publish after successful button writes",
8417
- "legacy upsert_views[].buttons is a compatibility path for raw full replacement of existing view button config; do not use it as the normal agent path",
8418
- "upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
8240
+ "upsert_view.buttons is a compatibility path for raw full replacement of existing view button config; do not use it as the normal agent path",
8241
+ "upsert_view.visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
8419
8242
  "filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
8420
- "upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
8243
+ "upsert_view.query_conditions.rows is a layout matrix of query-panel supported field names, field IDs, or queIds; queIds are preferred and all forms are compiled to backend queryCondition queIds",
8421
8244
  "do not put relation/attachment/subtable/code_block/q_linker/address fields in query_conditions; use filters for fixed filters or app_associated_resources_apply.match_mappings for current-record relation/report matching",
8422
8245
  "use patch_views for partial parameter replacement on existing views; the public update mode is patch even though the backend save is still a full view payload",
8423
8246
  "remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
8424
8247
  "deleted views return verification.by_view[].delete_executed, readback_status, and safe_to_retry_delete=false; if readback is pending, do not blindly repeat the delete",
8425
8248
  "do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送; these are built-in system/default views. Use business-specific names for new views, and pass the existing raw view_key or patch_views when changing a built-in view",
8426
- "new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_views explicitly changes associated_resources",
8249
+ "new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_view explicitly changes associated_resources",
8427
8250
  "associated report/view resource pool and per-view selected resources are configured through app_associated_resources_apply; app_views_apply keeps legacy associated_resources input compatible but it is no longer the recommended public contract",
8428
8251
  "for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
8429
8252
  *_VISIBILITY_EXECUTION_NOTES,
8430
8253
  ],
8431
8254
  "minimal_example": {
8432
8255
  "profile": "default",
8256
+ "app_key": "APP_KEY",
8433
8257
  "publish": True,
8434
- "views": [{"operation": "upsert", "app_key": "APP_KEY", "name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
8258
+ "upsert_view": {"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)},
8435
8259
  },
8436
8260
  "query_conditions_example": {
8437
8261
  "profile": "default",
@@ -8454,8 +8278,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8454
8278
  "profile": "default",
8455
8279
  "app_key": "APP_KEY",
8456
8280
  "publish": True,
8457
- "upsert_views": [
8458
- {
8281
+ "upsert_view": {
8459
8282
  "name": "生产工单执行视图",
8460
8283
  "type": "table",
8461
8284
  "columns": ["工单编号", "产品", "状态", "负责人"],
@@ -8468,8 +8291,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8468
8291
  "visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
8469
8292
  }
8470
8293
  ],
8471
- }
8472
- ],
8294
+ },
8473
8295
  "remove_views": [],
8474
8296
  },
8475
8297
  "action_buttons_patch_example": {
@@ -8499,8 +8321,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8499
8321
  "profile": "default",
8500
8322
  "app_key": "APP_KEY",
8501
8323
  "publish": True,
8502
- "upsert_views": [
8503
- {
8324
+ "upsert_view": {
8504
8325
  "name": "客户查询视图",
8505
8326
  "type": "table",
8506
8327
  "columns": ["客户名称", "负责人", "客户状态", "创建时间"],
@@ -8511,8 +8332,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8511
8332
  "hide_before_query": False,
8512
8333
  "rows": [["客户名称", "负责人"], ["创建时间"]],
8513
8334
  },
8514
- }
8515
- ],
8335
+ },
8516
8336
  "remove_views": [],
8517
8337
  },
8518
8338
  "query_conditions_field_rules": {
@@ -8527,8 +8347,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8527
8347
  "profile": "default",
8528
8348
  "app_key": "APP_KEY",
8529
8349
  "publish": True,
8530
- "upsert_views": [
8531
- {
8350
+ "upsert_view": {
8532
8351
  "name": "项目甘特图",
8533
8352
  "type": "gantt",
8534
8353
  "columns": ["项目名称", "开始日期", "结束日期"],
@@ -8536,8 +8355,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8536
8355
  "end_field": "结束日期",
8537
8356
  "title_field": "项目名称",
8538
8357
  "filters": [{"field_name": "状态", "operator": "eq", "value": "进行中"}],
8539
- }
8540
- ],
8358
+ },
8541
8359
  "remove_views": [],
8542
8360
  },
8543
8361
  },