@qingflow-tech/qingflow-cli 1.1.51 → 1.1.53

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 (106) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +29 -13
  3. package/package.json +1 -1
  4. package/pyproject.toml +8 -2
  5. package/skills/qingflow-cli/SKILL.md +93 -322
  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 +3 -9
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  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 +20 -19
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
  16. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  17. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +3 -2
  18. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +9 -5
  19. package/skills/qingflow-cli/reference/builder/50-views.md +192 -191
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +2 -0
  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 +76 -95
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  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 +20 -19
  28. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  29. package/skills/qingflow-cli/reference/data/README.md +32 -0
  30. package/skills/qingflow-cli/reference/examples/README.md +1 -1
  31. package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
  32. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
  33. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +50 -0
  34. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  35. package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
  36. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  37. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  38. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  39. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  40. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
  41. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  42. package/skills/qingflow-common/SKILL.md +7 -4
  43. package/src/qingflow_mcp/app_form_compat.py +3 -0
  44. package/src/qingflow_mcp/app_form_validator.py +69 -17
  45. package/src/qingflow_mcp/builder_facade/app_form.py +497 -100
  46. package/src/qingflow_mcp/builder_facade/models.py +227 -17
  47. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -0
  48. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +64 -30
  49. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +4 -8
  50. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +4 -8
  51. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +6 -10
  52. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +6 -10
  53. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +11 -18
  54. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +4 -8
  55. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +4 -8
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +6 -10
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +4 -8
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +4 -8
  59. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +4 -8
  60. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +6 -10
  61. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +4 -8
  62. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +4 -8
  63. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +4 -8
  64. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +6 -10
  65. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +46 -15
  66. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +146 -0
  67. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +4 -8
  68. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +4 -8
  69. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +61 -27
  70. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +4 -8
  71. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +43 -0
  72. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +7 -12
  73. package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +42 -0
  74. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1245 -0
  75. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
  76. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
  77. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +73 -0
  78. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
  79. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
  80. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +319 -0
  81. package/src/qingflow_mcp/builder_facade/service.py +1666 -509
  82. package/src/qingflow_mcp/builder_facade/views_extract.py +592 -0
  83. package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
  84. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  85. package/src/qingflow_mcp/cli/commands/builder.py +76 -55
  86. package/src/qingflow_mcp/cli/commands/data.py +134 -0
  87. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  88. package/src/qingflow_mcp/cli/context.py +3 -0
  89. package/src/qingflow_mcp/cli/formatters.py +97 -1
  90. package/src/qingflow_mcp/public_surface.py +8 -0
  91. package/src/qingflow_mcp/response_trim.py +11 -2
  92. package/src/qingflow_mcp/server.py +2 -0
  93. package/src/qingflow_mcp/server_app_builder.py +34 -10
  94. package/src/qingflow_mcp/server_app_user.py +4 -0
  95. package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
  96. package/src/qingflow_mcp/solution/executor.py +96 -8
  97. package/src/qingflow_mcp/solution/spec_models.py +1 -0
  98. package/src/qingflow_mcp/tools/ai_builder_tools.py +218 -398
  99. package/src/qingflow_mcp/tools/csv_export.py +168 -0
  100. package/src/qingflow_mcp/tools/data_tools.py +284 -0
  101. package/src/qingflow_mcp/tools/record_tools.py +11 -46
  102. package/src/qingflow_mcp/tools/solution_tools.py +2 -0
  103. package/src/qingflow_mcp/tools/view_tools.py +0 -6
  104. package/src/qingflow_mcp/tools/view_writer.py +44 -0
  105. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
  106. 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()
@@ -1979,20 +2009,22 @@ class AiBuilderTools(ToolBase):
1979
2009
  suggested_next_call={"tool_name": "app_get_charts", "arguments": {"profile": profile, "app_key": app_key}},
1980
2010
  )
1981
2011
 
1982
- def app_get_buttons(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
2012
+ def app_get_buttons(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None, button_text: str | None = None) -> JSONObject:
1983
2013
  if app_keys is not None:
1984
2014
  return self._read_app_batch(
1985
2015
  tool_name="app_get_buttons",
1986
2016
  profile=profile,
1987
2017
  app_keys=app_keys,
1988
- read_one=lambda item_app_key: self.app_get_buttons(profile=profile, app_key=item_app_key, app_keys=None),
2018
+ read_one=lambda item_app_key: self.app_get_buttons(profile=profile, app_key=item_app_key, app_keys=None, button_text=button_text),
1989
2019
  )
1990
- normalized_args = {"app_key": app_key}
2020
+ normalized_args: JSONObject = {"app_key": app_key}
2021
+ if str(button_text or "").strip():
2022
+ normalized_args["button_text"] = str(button_text or "").strip()
1991
2023
  return _safe_tool_call(
1992
- lambda: self._facade.app_get_buttons(profile=profile, app_key=app_key),
2024
+ lambda: self._facade.app_get_buttons(profile=profile, app_key=app_key, button_text=button_text),
1993
2025
  error_code="APP_GET_BUTTONS_FAILED",
1994
2026
  normalized_args=normalized_args,
1995
- suggested_next_call={"tool_name": "app_get_buttons", "arguments": {"profile": profile, "app_key": app_key}},
2027
+ suggested_next_call={"tool_name": "app_get_buttons", "arguments": {"profile": profile, **normalized_args}},
1996
2028
  )
1997
2029
 
1998
2030
  def app_get_associated_resources(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
@@ -2251,17 +2283,34 @@ class AiBuilderTools(ToolBase):
2251
2283
  *,
2252
2284
  profile: str,
2253
2285
  app_key: str,
2286
+ upsert_view: JSONObject | None = None,
2254
2287
  upsert_views: list[JSONObject] | None = None,
2255
2288
  patch_views: list[JSONObject] | None = None,
2256
2289
  remove_views: list[str] | None = None,
2257
2290
  preset: str | None = None,
2258
2291
  ) -> JSONObject:
2259
- """执行应用相关逻辑。"""
2292
+ """Plan one new or fully updated view, plus optional existing-view changes."""
2293
+ if upsert_view is not None and upsert_views:
2294
+ return _config_failure(
2295
+ tool_name="app_views_plan",
2296
+ message="pass either upsert_view or upsert_views, not both.",
2297
+ fix_hint="Use upsert_view for one new or full view declaration.",
2298
+ details={"provided": ["upsert_view", "upsert_views"]},
2299
+ )
2300
+ normalized_upsert_views = [upsert_view] if upsert_view is not None else list(upsert_views or [])
2301
+ if len(normalized_upsert_views) > 1:
2302
+ return _config_failure(
2303
+ tool_name="app_views_plan",
2304
+ message="app_views_plan supports planning one new or fully updated view per call.",
2305
+ fix_hint="Call app_views_plan once for each view, using upsert_view.",
2306
+ details={"max_upsert_views": 1, "received_upsert_views": len(normalized_upsert_views)},
2307
+ error_code="VIEW_BATCH_CREATE_UNSUPPORTED",
2308
+ )
2260
2309
  reserved_failure = _reserved_system_view_name_failure(
2261
2310
  tool_name="app_views_plan",
2262
2311
  profile=profile,
2263
2312
  app_key=app_key,
2264
- upsert_views=upsert_views,
2313
+ upsert_views=normalized_upsert_views,
2265
2314
  remove_views=remove_views,
2266
2315
  )
2267
2316
  if reserved_failure is not None:
@@ -2270,7 +2319,7 @@ class AiBuilderTools(ToolBase):
2270
2319
  request = ViewsPlanRequest.model_validate(
2271
2320
  {
2272
2321
  "app_key": app_key,
2273
- "upsert_views": upsert_views or [],
2322
+ "upsert_views": normalized_upsert_views,
2274
2323
  "patch_views": patch_views or [],
2275
2324
  "remove_views": remove_views or [],
2276
2325
  "preset": preset,
@@ -2287,7 +2336,7 @@ class AiBuilderTools(ToolBase):
2287
2336
  "profile": profile,
2288
2337
  "app_key": app_key,
2289
2338
  "preset": "default_table",
2290
- "upsert_views": [],
2339
+ "upsert_view": None,
2291
2340
  "patch_views": [],
2292
2341
  "remove_views": [],
2293
2342
  },
@@ -2298,7 +2347,7 @@ class AiBuilderTools(ToolBase):
2298
2347
  error_code="VIEWS_PLAN_FAILED",
2299
2348
  normalized_args={
2300
2349
  "app_key": request.app_key,
2301
- "upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
2350
+ "upsert_view": public_view_upsert_payload(request.upsert_views[0]) if request.upsert_views else None,
2302
2351
  "patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
2303
2352
  "remove_views": list(request.remove_views),
2304
2353
  "preset": request.preset.value if request.preset is not None else None,
@@ -2308,7 +2357,7 @@ class AiBuilderTools(ToolBase):
2308
2357
  "arguments": {
2309
2358
  "profile": profile,
2310
2359
  "app_key": request.app_key,
2311
- "upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
2360
+ "upsert_view": public_view_upsert_payload(request.upsert_views[0]) if request.upsert_views else None,
2312
2361
  "patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
2313
2362
  "remove_views": list(request.remove_views),
2314
2363
  "preset": request.preset.value if request.preset is not None else None,
@@ -3931,41 +3980,47 @@ class AiBuilderTools(ToolBase):
3931
3980
  profile: str,
3932
3981
  app_key: str,
3933
3982
  publish: bool = True,
3934
- upsert_views: list[JSONObject],
3983
+ upsert_view: JSONObject | None = None,
3935
3984
  patch_views: list[JSONObject] | None = None,
3936
- remove_views: list[str],
3937
- apps: list[JSONObject] | None = None,
3938
- views: list[JSONObject] | None = None,
3985
+ remove_views: list[str] | None = None,
3986
+ schema_version: str | None = None,
3987
+ upsert_views: list[JSONObject] | None = None,
3939
3988
  ) -> 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,
3989
+ """Apply one new or full view declaration for one application.
3990
+
3991
+ ``upsert_views`` remains an internal compatibility alias while callers
3992
+ migrate to ``upsert_view``. It accepts at most one item.
3993
+ """
3994
+ if upsert_view is not None and upsert_views:
3995
+ return _attach_builder_apply_envelope(
3996
+ "app_views_apply",
3997
+ _config_failure(
3998
+ tool_name="app_views_apply",
3999
+ message="pass either upsert_view or upsert_views, not both.",
4000
+ fix_hint="Use upsert_view for one new or full view declaration.",
4001
+ details={"provided": ["upsert_view", "upsert_views"]},
4002
+ ),
3946
4003
  )
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,
4004
+ normalized_upsert_views = [upsert_view] if upsert_view is not None else list(upsert_views or [])
4005
+ if len(normalized_upsert_views) > 1:
4006
+ return _attach_builder_apply_envelope(
4007
+ "app_views_apply",
4008
+ _config_failure(
4009
+ tool_name="app_views_apply",
4010
+ message="app_views_apply supports creating or fully updating one view per call.",
4011
+ fix_hint="Call app_views_apply once for each view, using upsert_view.",
4012
+ details={"max_upsert_views": 1, "received_upsert_views": len(normalized_upsert_views)},
4013
+ error_code="VIEW_BATCH_CREATE_UNSUPPORTED",
3960
4014
  ),
3961
4015
  )
3962
4016
  result = self._app_views_apply_once(
3963
4017
  profile=profile,
3964
4018
  app_key=app_key,
3965
4019
  publish=publish,
3966
- upsert_views=upsert_views,
4020
+ upsert_views=normalized_upsert_views,
3967
4021
  patch_views=patch_views or [],
3968
- remove_views=remove_views,
4022
+ remove_views=remove_views or [],
4023
+ schema_version=schema_version,
3969
4024
  )
3970
4025
  result = self._retry_after_self_lock_release(
3971
4026
  profile=profile,
@@ -3974,255 +4029,14 @@ class AiBuilderTools(ToolBase):
3974
4029
  profile=profile,
3975
4030
  app_key=app_key,
3976
4031
  publish=publish,
3977
- remove_views=remove_views,
3978
- upsert_views=upsert_views,
4032
+ remove_views=remove_views or [],
4033
+ upsert_views=normalized_upsert_views,
3979
4034
  patch_views=patch_views or [],
4035
+ schema_version=schema_version,
3980
4036
  ),
3981
4037
  )
3982
4038
  return self._attach_apply_envelope(profile, "app_views_apply", result)
3983
4039
 
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
4040
  def _app_views_apply_once(
4227
4041
  self,
4228
4042
  *,
@@ -4232,6 +4046,7 @@ class AiBuilderTools(ToolBase):
4232
4046
  upsert_views: list[JSONObject],
4233
4047
  patch_views: list[JSONObject],
4234
4048
  remove_views: list[str],
4049
+ schema_version: str | None = None,
4235
4050
  ) -> JSONObject:
4236
4051
  """执行内部辅助逻辑。"""
4237
4052
  reserved_failure = _reserved_system_view_name_failure(
@@ -4259,7 +4074,7 @@ class AiBuilderTools(ToolBase):
4259
4074
  "profile": profile,
4260
4075
  "app_key": app_key,
4261
4076
  "publish": publish,
4262
- "upsert_views": upsert_views or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
4077
+ "upsert_view": (upsert_views[0] if upsert_views else {"name": "业务台账视图", "type": "table", "columns": ["字段A"]}),
4263
4078
  "patch_views": patch_views or [],
4264
4079
  "remove_views": remove_views or [],
4265
4080
  },
@@ -4268,9 +4083,10 @@ class AiBuilderTools(ToolBase):
4268
4083
  normalized_args = {
4269
4084
  "app_key": app_key,
4270
4085
  "publish": publish,
4271
- "upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
4086
+ "upsert_view": public_view_upsert_payload(parsed_views[0]) if parsed_views else None,
4272
4087
  "patch_views": [public_view_partial_payload(patch) for patch in parsed_patch_views],
4273
4088
  "remove_views": list(remove_views or []),
4089
+ "schema_version": schema_version,
4274
4090
  }
4275
4091
  return _safe_tool_call(
4276
4092
  lambda: self._facade.app_views_apply(
@@ -4280,12 +4096,44 @@ class AiBuilderTools(ToolBase):
4280
4096
  upsert_views=parsed_views,
4281
4097
  patch_views=parsed_patch_views,
4282
4098
  remove_views=list(remove_views or []),
4099
+ schema_version=schema_version,
4283
4100
  ),
4284
4101
  error_code="VIEWS_APPLY_FAILED",
4285
4102
  normalized_args=normalized_args,
4286
4103
  suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
4287
4104
  )
4288
4105
 
4106
+ def app_views_schema(
4107
+ self,
4108
+ *,
4109
+ schema_version: str | None = None,
4110
+ schema_kind: str | None = None,
4111
+ ) -> JSONObject:
4112
+ """Return a versioned views schema (summary/complete or a focused detail kind)."""
4113
+ return self._facade.app_views_schema(schema_version=schema_version, schema_kind=schema_kind)
4114
+
4115
+ def app_views_get(
4116
+ self,
4117
+ *,
4118
+ profile: str,
4119
+ app_key: str,
4120
+ schema_version: str | None = None,
4121
+ schema_kind: str | None = None,
4122
+ ) -> JSONObject:
4123
+ """Read canonical views declarations (summary projection or complete reverse-compilation)."""
4124
+ return self._facade.app_views_get(
4125
+ profile=profile, app_key=app_key, schema_version=schema_version, schema_kind=schema_kind or "summary"
4126
+ )
4127
+
4128
+ def app_views_validate(
4129
+ self,
4130
+ *,
4131
+ declaration: JSONObject,
4132
+ schema_version: str | None = None,
4133
+ ) -> JSONObject:
4134
+ """Offline-validate a views declaration without touching the backend."""
4135
+ return self._facade.app_views_validate(declaration=declaration or {}, schema_version=schema_version)
4136
+
4289
4137
  def chart_apply(
4290
4138
  self,
4291
4139
  *,
@@ -5668,12 +5516,10 @@ def _reserved_system_view_name_failure(
5668
5516
  "arguments": {
5669
5517
  "profile": profile,
5670
5518
  "app_key": app_key,
5671
- "upsert_views": [
5672
- {
5673
- **item,
5674
- "name": "业务台账视图",
5675
- }
5676
- ],
5519
+ "upsert_view": {
5520
+ **item,
5521
+ "name": "业务台账视图",
5522
+ },
5677
5523
  "patch_views": patch_views or [],
5678
5524
  "remove_views": remove_views or [],
5679
5525
  },
@@ -5683,7 +5529,7 @@ def _reserved_system_view_name_failure(
5683
5529
  return _config_failure(
5684
5530
  tool_name=tool_name,
5685
5531
  error_code="RESERVED_SYSTEM_VIEW_NAME",
5686
- message=f"upsert_views[{index}].name uses built-in system view name '{name}' without view_key",
5532
+ message=f"upsert_view.name uses built-in system view name '{name}' without view_key",
5687
5533
  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
5534
  details={
5689
5535
  "reserved_view_names": sorted(_RESERVED_SYSTEM_VIEW_NAMES),
@@ -8147,20 +7993,20 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8147
7993
  "app_views_plan": {
8148
7994
  "allowed_keys": [
8149
7995
  "app_key",
8150
- "upsert_views",
7996
+ "upsert_view",
8151
7997
  "patch_views",
8152
7998
  "remove_views",
8153
7999
  "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",
8000
+ "upsert_view.view_key",
8001
+ "upsert_view.name",
8002
+ "upsert_view.type",
8003
+ "upsert_view.columns",
8004
+ "upsert_view.filters",
8005
+ "upsert_view.buttons",
8006
+ "upsert_view.action_buttons",
8007
+ "upsert_view.action_buttons_mode",
8008
+ "upsert_view.visibility",
8009
+ "upsert_view.query_conditions",
8164
8010
  "patch_views[].view_key",
8165
8011
  "patch_views[].name",
8166
8012
  "patch_views[].set",
@@ -8181,6 +8027,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8181
8027
  "queryConditions": "query_conditions",
8182
8028
  "query_condition": "query_conditions",
8183
8029
  "queryCondition": "query_conditions",
8030
+ "upsertView": "upsert_view",
8184
8031
  "patchViews": "patch_views",
8185
8032
  "startField": "start_field",
8186
8033
  "endField": "end_field",
@@ -8215,18 +8062,19 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8215
8062
  },
8216
8063
  "execution_notes": [
8217
8064
  "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",
8065
+ "upsert_view accepts one new or full view declaration; plan additional views in separate calls",
8066
+ "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
8067
  "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",
8068
+ "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
8069
  "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
8070
  "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
8071
  "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
8072
  "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",
8073
+ "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
8074
  "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
8075
  "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
8076
  "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",
8077
+ "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
8078
  "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
8079
  "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
8080
  *_VISIBILITY_EXECUTION_NOTES,
@@ -8234,7 +8082,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8234
8082
  "minimal_example": {
8235
8083
  "profile": "default",
8236
8084
  "app_key": "APP_KEY",
8237
- "upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
8085
+ "upsert_view": {"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)},
8238
8086
  "remove_views": [],
8239
8087
  },
8240
8088
  "query_conditions_example": {
@@ -8256,41 +8104,37 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8256
8104
  "action_buttons_example": {
8257
8105
  "profile": "default",
8258
8106
  "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
- ],
8107
+ "upsert_view": {
8108
+ "name": "生产工单执行视图",
8109
+ "type": "table",
8110
+ "columns": ["工单编号", "产品", "状态", "负责人"],
8111
+ "action_buttons": [
8112
+ {
8113
+ "text": "开始生产",
8114
+ "action": "link",
8115
+ "url": "https://example.com/start",
8116
+ "placement": "list",
8117
+ "visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
8118
+ }
8119
+ ],
8120
+ },
8275
8121
  "remove_views": [],
8276
8122
  },
8277
8123
  "full_upsert_query_conditions_example": {
8278
8124
  "profile": "default",
8279
8125
  "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
- ],
8126
+ "upsert_view": {
8127
+ "name": "客户查询视图",
8128
+ "type": "table",
8129
+ "columns": ["客户名称", "负责人", "客户状态", "创建时间"],
8130
+ "filters": [{"field_name": "客户状态", "operator": "eq", "value": "有效"}],
8131
+ "query_conditions": {
8132
+ "enabled": True,
8133
+ "exact": False,
8134
+ "hide_before_query": False,
8135
+ "rows": [["客户名称", "负责人"], ["创建时间"]],
8136
+ },
8137
+ },
8294
8138
  "remove_views": [],
8295
8139
  },
8296
8140
  "query_conditions_field_rules": {
@@ -8304,48 +8148,35 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8304
8148
  "gantt_example": {
8305
8149
  "profile": "default",
8306
8150
  "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
- ],
8151
+ "upsert_view": {
8152
+ "name": "项目甘特图",
8153
+ "type": "gantt",
8154
+ "columns": ["项目名称", "开始日期", "结束日期"],
8155
+ "start_field": "开始日期",
8156
+ "end_field": "结束日期",
8157
+ "title_field": "项目名称",
8158
+ "filters": [{"field_name": "状态", "operator": "eq", "value": "进行中"}],
8159
+ },
8318
8160
  "remove_views": [],
8319
8161
  },
8320
8162
  },
8321
8163
  "app_views_apply": {
8322
8164
  "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
8165
  "app_key",
8335
8166
  "publish",
8336
- "upsert_views",
8167
+ "upsert_view",
8337
8168
  "patch_views",
8338
8169
  "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",
8170
+ "upsert_view.view_key",
8171
+ "upsert_view.name",
8172
+ "upsert_view.type",
8173
+ "upsert_view.columns",
8174
+ "upsert_view.filters",
8175
+ "upsert_view.buttons",
8176
+ "upsert_view.action_buttons",
8177
+ "upsert_view.action_buttons_mode",
8178
+ "upsert_view.visibility",
8179
+ "upsert_view.query_conditions",
8349
8180
  "patch_views[].view_key",
8350
8181
  "patch_views[].name",
8351
8182
  "patch_views[].set",
@@ -8366,10 +8197,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8366
8197
  "queryConditions": "query_conditions",
8367
8198
  "query_condition": "query_conditions",
8368
8199
  "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",
8200
+ "upsertView": "upsert_view",
8373
8201
  "patchViews": "patch_views",
8374
8202
  "startField": "start_field",
8375
8203
  "endField": "end_field",
@@ -8392,7 +8220,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8392
8220
  "action_buttons[].defaultValues": "action_buttons[].default_values",
8393
8221
  },
8394
8222
  "allowed_values": {
8395
- "views[].operation": ["upsert", "patch", "remove"],
8396
8223
  "view.type": [member.value for member in PublicViewType],
8397
8224
  "view.filter.operator": [member.value for member in ViewFilterOperator],
8398
8225
  "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
@@ -8403,9 +8230,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8403
8230
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
8404
8231
  },
8405
8232
  "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",
8233
+ "upsert_view accepts one new or full view declaration for the app_key in this call; submit additional views in separate calls",
8409
8234
  "creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
8410
8235
  "when duplicate view names exist, supply view_key to target the exact view",
8411
8236
  "read back app_get after any failed or partial view apply",
@@ -8414,24 +8239,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8414
8239
  "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
8240
  "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
8241
  "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",
8242
+ "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",
8243
+ "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
8244
  "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",
8245
+ "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
8246
  "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
8247
  "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
8248
  "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
8249
  "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
8250
  "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",
8251
+ "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
8252
  "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
8253
  "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
8254
  *_VISIBILITY_EXECUTION_NOTES,
8430
8255
  ],
8431
8256
  "minimal_example": {
8432
8257
  "profile": "default",
8258
+ "app_key": "APP_KEY",
8433
8259
  "publish": True,
8434
- "views": [{"operation": "upsert", "app_key": "APP_KEY", "name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
8260
+ "upsert_view": {"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)},
8435
8261
  },
8436
8262
  "query_conditions_example": {
8437
8263
  "profile": "default",
@@ -8454,8 +8280,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8454
8280
  "profile": "default",
8455
8281
  "app_key": "APP_KEY",
8456
8282
  "publish": True,
8457
- "upsert_views": [
8458
- {
8283
+ "upsert_view": {
8459
8284
  "name": "生产工单执行视图",
8460
8285
  "type": "table",
8461
8286
  "columns": ["工单编号", "产品", "状态", "负责人"],
@@ -8468,8 +8293,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8468
8293
  "visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
8469
8294
  }
8470
8295
  ],
8471
- }
8472
- ],
8296
+ },
8473
8297
  "remove_views": [],
8474
8298
  },
8475
8299
  "action_buttons_patch_example": {
@@ -8499,8 +8323,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8499
8323
  "profile": "default",
8500
8324
  "app_key": "APP_KEY",
8501
8325
  "publish": True,
8502
- "upsert_views": [
8503
- {
8326
+ "upsert_view": {
8504
8327
  "name": "客户查询视图",
8505
8328
  "type": "table",
8506
8329
  "columns": ["客户名称", "负责人", "客户状态", "创建时间"],
@@ -8511,8 +8334,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8511
8334
  "hide_before_query": False,
8512
8335
  "rows": [["客户名称", "负责人"], ["创建时间"]],
8513
8336
  },
8514
- }
8515
- ],
8337
+ },
8516
8338
  "remove_views": [],
8517
8339
  },
8518
8340
  "query_conditions_field_rules": {
@@ -8527,8 +8349,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8527
8349
  "profile": "default",
8528
8350
  "app_key": "APP_KEY",
8529
8351
  "publish": True,
8530
- "upsert_views": [
8531
- {
8352
+ "upsert_view": {
8532
8353
  "name": "项目甘特图",
8533
8354
  "type": "gantt",
8534
8355
  "columns": ["项目名称", "开始日期", "结束日期"],
@@ -8536,8 +8357,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8536
8357
  "end_field": "结束日期",
8537
8358
  "title_field": "项目名称",
8538
8359
  "filters": [{"field_name": "状态", "operator": "eq", "value": "进行中"}],
8539
- }
8540
- ],
8360
+ },
8541
8361
  "remove_views": [],
8542
8362
  },
8543
8363
  },