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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +13 -29
  3. package/package.json +1 -1
  4. package/pyproject.toml +2 -8
  5. package/skills/qingflow-cli/SKILL.md +322 -93
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +9 -3
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +137 -3
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +12 -7
  16. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
  17. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -3
  18. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -9
  19. package/skills/qingflow-cli/reference/builder/50-views.md +191 -192
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +0 -2
  21. package/skills/qingflow-cli/reference/builder/README.md +1 -1
  22. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +6 -6
  23. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +95 -76
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
  26. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  27. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
  28. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
  29. package/skills/qingflow-cli/reference/examples/README.md +1 -1
  30. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +159 -0
  31. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +36 -30
  32. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
  33. package/skills/qingflow-cli/reference/record/analysis/README.md +113 -21
  34. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  35. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  36. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  37. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  38. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +80 -52
  39. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  40. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +42 -0
  41. package/skills/qingflow-common/SKILL.md +4 -7
  42. package/src/qingflow_mcp/__init__.py +1 -1
  43. package/src/qingflow_mcp/app_form_compat.py +0 -3
  44. package/src/qingflow_mcp/app_form_validator.py +17 -69
  45. package/src/qingflow_mcp/builder_facade/app_form.py +100 -497
  46. package/src/qingflow_mcp/builder_facade/models.py +17 -227
  47. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +0 -1
  48. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +30 -64
  49. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -4
  50. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +8 -4
  51. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -6
  52. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
  53. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
  54. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
  55. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
  59. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
  60. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
  61. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
  62. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +8 -4
  63. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
  64. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +10 -6
  65. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -46
  66. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
  67. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
  68. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +27 -61
  69. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
  70. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
  71. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +12 -7
  72. package/src/qingflow_mcp/builder_facade/service.py +539 -1671
  73. package/src/qingflow_mcp/builder_facade/workflow_spec.py +1 -1
  74. package/src/qingflow_mcp/cli/commands/__init__.py +1 -2
  75. package/src/qingflow_mcp/cli/commands/builder.py +55 -76
  76. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  77. package/src/qingflow_mcp/cli/context.py +0 -3
  78. package/src/qingflow_mcp/cli/formatters.py +1 -97
  79. package/src/qingflow_mcp/config.py +1 -0
  80. package/src/qingflow_mcp/public_surface.py +0 -8
  81. package/src/qingflow_mcp/response_trim.py +2 -11
  82. package/src/qingflow_mcp/server.py +0 -2
  83. package/src/qingflow_mcp/server_app_builder.py +10 -34
  84. package/src/qingflow_mcp/server_app_user.py +0 -4
  85. package/src/qingflow_mcp/solution/compiler/view_compiler.py +1 -2
  86. package/src/qingflow_mcp/solution/executor.py +8 -96
  87. package/src/qingflow_mcp/solution/spec_models.py +0 -1
  88. package/src/qingflow_mcp/tools/ai_builder_tools.py +398 -218
  89. package/src/qingflow_mcp/tools/base.py +42 -10
  90. package/src/qingflow_mcp/tools/record_tools.py +46 -11
  91. package/src/qingflow_mcp/tools/solution_tools.py +0 -2
  92. package/src/qingflow_mcp/tools/view_tools.py +6 -0
  93. package/skills/qingflow-cli/reference/data/README.md +0 -32
  94. package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +0 -14
  95. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +0 -50
  96. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
  97. package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +0 -42
  98. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +0 -1245
  99. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +0 -53
  100. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +0 -60
  101. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +0 -73
  102. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +0 -48
  103. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +0 -47
  104. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +0 -319
  105. package/src/qingflow_mcp/builder_facade/views_extract.py +0 -592
  106. package/src/qingflow_mcp/builder_facade/views_schema.py +0 -400
  107. package/src/qingflow_mcp/cli/commands/data.py +0 -134
  108. package/src/qingflow_mcp/tools/csv_export.py +0 -168
  109. package/src/qingflow_mcp/tools/data_tools.py +0 -284
  110. package/src/qingflow_mcp/tools/view_writer.py +0 -44
@@ -115,15 +115,6 @@ 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
-
127
118
  def _merge_duration_breakdown(payload: JSONObject, **items: int | None) -> None:
128
119
  breakdown = payload.get("duration_breakdown_ms")
129
120
  if not isinstance(breakdown, dict):
@@ -160,6 +151,7 @@ BUILDER_APPLY_TOOL_NAMES = {
160
151
  "app_publish_verify",
161
152
  }
162
153
  MULTI_APP_SCHEMA_APPLY_PARALLEL_LIMIT = 10
154
+ VIEW_APPLY_PARALLEL_LIMIT = 20
163
155
  PORTAL_INLINE_CHART_MAX_WORKERS = 10
164
156
 
165
157
 
@@ -442,30 +434,6 @@ class AiBuilderTools(ToolBase):
442
434
  ) -> JSONObject:
443
435
  return self.app_form_apply(profile=profile, spec=spec or {})
444
436
 
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
-
469
437
  @mcp.tool(description=self._high_risk_tool_description(operation="delete", target="app configuration"))
470
438
  def app_delete(
471
439
  profile: str = DEFAULT_PROFILE,
@@ -795,17 +763,19 @@ class AiBuilderTools(ToolBase):
795
763
  profile: str = DEFAULT_PROFILE,
796
764
  app_key: str = "",
797
765
  publish: bool = True,
798
- upsert_view: JSONObject | None = None,
766
+ upsert_views: list[JSONObject] | None = None,
799
767
  patch_views: list[JSONObject] | None = None,
800
768
  remove_views: list[str] | None = None,
769
+ apps: list[JSONObject] | None = None,
801
770
  ) -> JSONObject:
802
771
  return self.app_views_apply(
803
772
  profile=profile,
804
773
  app_key=app_key,
805
774
  publish=publish,
806
- upsert_view=upsert_view,
775
+ upsert_views=upsert_views or [],
807
776
  patch_views=patch_views or [],
808
777
  remove_views=remove_views or [],
778
+ apps=apps,
809
779
  )
810
780
 
811
781
  @mcp.tool()
@@ -2009,22 +1979,20 @@ class AiBuilderTools(ToolBase):
2009
1979
  suggested_next_call={"tool_name": "app_get_charts", "arguments": {"profile": profile, "app_key": app_key}},
2010
1980
  )
2011
1981
 
2012
- def app_get_buttons(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None, button_text: str | None = None) -> JSONObject:
1982
+ def app_get_buttons(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
2013
1983
  if app_keys is not None:
2014
1984
  return self._read_app_batch(
2015
1985
  tool_name="app_get_buttons",
2016
1986
  profile=profile,
2017
1987
  app_keys=app_keys,
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),
1988
+ read_one=lambda item_app_key: self.app_get_buttons(profile=profile, app_key=item_app_key, app_keys=None),
2019
1989
  )
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()
1990
+ normalized_args = {"app_key": app_key}
2023
1991
  return _safe_tool_call(
2024
- lambda: self._facade.app_get_buttons(profile=profile, app_key=app_key, button_text=button_text),
1992
+ lambda: self._facade.app_get_buttons(profile=profile, app_key=app_key),
2025
1993
  error_code="APP_GET_BUTTONS_FAILED",
2026
1994
  normalized_args=normalized_args,
2027
- suggested_next_call={"tool_name": "app_get_buttons", "arguments": {"profile": profile, **normalized_args}},
1995
+ suggested_next_call={"tool_name": "app_get_buttons", "arguments": {"profile": profile, "app_key": app_key}},
2028
1996
  )
2029
1997
 
2030
1998
  def app_get_associated_resources(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
@@ -2283,34 +2251,17 @@ class AiBuilderTools(ToolBase):
2283
2251
  *,
2284
2252
  profile: str,
2285
2253
  app_key: str,
2286
- upsert_view: JSONObject | None = None,
2287
2254
  upsert_views: list[JSONObject] | None = None,
2288
2255
  patch_views: list[JSONObject] | None = None,
2289
2256
  remove_views: list[str] | None = None,
2290
2257
  preset: str | None = None,
2291
2258
  ) -> JSONObject:
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
- )
2259
+ """执行应用相关逻辑。"""
2309
2260
  reserved_failure = _reserved_system_view_name_failure(
2310
2261
  tool_name="app_views_plan",
2311
2262
  profile=profile,
2312
2263
  app_key=app_key,
2313
- upsert_views=normalized_upsert_views,
2264
+ upsert_views=upsert_views,
2314
2265
  remove_views=remove_views,
2315
2266
  )
2316
2267
  if reserved_failure is not None:
@@ -2319,7 +2270,7 @@ class AiBuilderTools(ToolBase):
2319
2270
  request = ViewsPlanRequest.model_validate(
2320
2271
  {
2321
2272
  "app_key": app_key,
2322
- "upsert_views": normalized_upsert_views,
2273
+ "upsert_views": upsert_views or [],
2323
2274
  "patch_views": patch_views or [],
2324
2275
  "remove_views": remove_views or [],
2325
2276
  "preset": preset,
@@ -2336,7 +2287,7 @@ class AiBuilderTools(ToolBase):
2336
2287
  "profile": profile,
2337
2288
  "app_key": app_key,
2338
2289
  "preset": "default_table",
2339
- "upsert_view": None,
2290
+ "upsert_views": [],
2340
2291
  "patch_views": [],
2341
2292
  "remove_views": [],
2342
2293
  },
@@ -2347,7 +2298,7 @@ class AiBuilderTools(ToolBase):
2347
2298
  error_code="VIEWS_PLAN_FAILED",
2348
2299
  normalized_args={
2349
2300
  "app_key": request.app_key,
2350
- "upsert_view": public_view_upsert_payload(request.upsert_views[0]) if request.upsert_views else None,
2301
+ "upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
2351
2302
  "patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
2352
2303
  "remove_views": list(request.remove_views),
2353
2304
  "preset": request.preset.value if request.preset is not None else None,
@@ -2357,7 +2308,7 @@ class AiBuilderTools(ToolBase):
2357
2308
  "arguments": {
2358
2309
  "profile": profile,
2359
2310
  "app_key": request.app_key,
2360
- "upsert_view": public_view_upsert_payload(request.upsert_views[0]) if request.upsert_views else None,
2311
+ "upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
2361
2312
  "patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
2362
2313
  "remove_views": list(request.remove_views),
2363
2314
  "preset": request.preset.value if request.preset is not None else None,
@@ -3980,47 +3931,41 @@ class AiBuilderTools(ToolBase):
3980
3931
  profile: str,
3981
3932
  app_key: str,
3982
3933
  publish: bool = True,
3983
- upsert_view: JSONObject | None = None,
3934
+ upsert_views: list[JSONObject],
3984
3935
  patch_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,
3936
+ remove_views: list[str],
3937
+ apps: list[JSONObject] | None = None,
3938
+ views: list[JSONObject] | None = None,
3988
3939
  ) -> JSONObject:
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
- ),
3940
+ """执行应用相关逻辑。"""
3941
+ if views is not None:
3942
+ return self._app_views_apply_view_batch(
3943
+ profile=profile,
3944
+ publish=publish,
3945
+ views=views,
4003
3946
  )
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",
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,
4014
3960
  ),
4015
3961
  )
4016
3962
  result = self._app_views_apply_once(
4017
3963
  profile=profile,
4018
3964
  app_key=app_key,
4019
3965
  publish=publish,
4020
- upsert_views=normalized_upsert_views,
3966
+ upsert_views=upsert_views,
4021
3967
  patch_views=patch_views or [],
4022
- remove_views=remove_views or [],
4023
- schema_version=schema_version,
3968
+ remove_views=remove_views,
4024
3969
  )
4025
3970
  result = self._retry_after_self_lock_release(
4026
3971
  profile=profile,
@@ -4029,14 +3974,255 @@ class AiBuilderTools(ToolBase):
4029
3974
  profile=profile,
4030
3975
  app_key=app_key,
4031
3976
  publish=publish,
4032
- remove_views=remove_views or [],
4033
- upsert_views=normalized_upsert_views,
3977
+ remove_views=remove_views,
3978
+ upsert_views=upsert_views,
4034
3979
  patch_views=patch_views or [],
4035
- schema_version=schema_version,
4036
3980
  ),
4037
3981
  )
4038
3982
  return self._attach_apply_envelope(profile, "app_views_apply", result)
4039
3983
 
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
+
4040
4226
  def _app_views_apply_once(
4041
4227
  self,
4042
4228
  *,
@@ -4046,7 +4232,6 @@ class AiBuilderTools(ToolBase):
4046
4232
  upsert_views: list[JSONObject],
4047
4233
  patch_views: list[JSONObject],
4048
4234
  remove_views: list[str],
4049
- schema_version: str | None = None,
4050
4235
  ) -> JSONObject:
4051
4236
  """执行内部辅助逻辑。"""
4052
4237
  reserved_failure = _reserved_system_view_name_failure(
@@ -4074,7 +4259,7 @@ class AiBuilderTools(ToolBase):
4074
4259
  "profile": profile,
4075
4260
  "app_key": app_key,
4076
4261
  "publish": publish,
4077
- "upsert_view": (upsert_views[0] if upsert_views else {"name": "业务台账视图", "type": "table", "columns": ["字段A"]}),
4262
+ "upsert_views": upsert_views or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
4078
4263
  "patch_views": patch_views or [],
4079
4264
  "remove_views": remove_views or [],
4080
4265
  },
@@ -4083,10 +4268,9 @@ class AiBuilderTools(ToolBase):
4083
4268
  normalized_args = {
4084
4269
  "app_key": app_key,
4085
4270
  "publish": publish,
4086
- "upsert_view": public_view_upsert_payload(parsed_views[0]) if parsed_views else None,
4271
+ "upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
4087
4272
  "patch_views": [public_view_partial_payload(patch) for patch in parsed_patch_views],
4088
4273
  "remove_views": list(remove_views or []),
4089
- "schema_version": schema_version,
4090
4274
  }
4091
4275
  return _safe_tool_call(
4092
4276
  lambda: self._facade.app_views_apply(
@@ -4096,44 +4280,12 @@ class AiBuilderTools(ToolBase):
4096
4280
  upsert_views=parsed_views,
4097
4281
  patch_views=parsed_patch_views,
4098
4282
  remove_views=list(remove_views or []),
4099
- schema_version=schema_version,
4100
4283
  ),
4101
4284
  error_code="VIEWS_APPLY_FAILED",
4102
4285
  normalized_args=normalized_args,
4103
4286
  suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
4104
4287
  )
4105
4288
 
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
-
4137
4289
  def chart_apply(
4138
4290
  self,
4139
4291
  *,
@@ -5516,10 +5668,12 @@ def _reserved_system_view_name_failure(
5516
5668
  "arguments": {
5517
5669
  "profile": profile,
5518
5670
  "app_key": app_key,
5519
- "upsert_view": {
5520
- **item,
5521
- "name": "业务台账视图",
5522
- },
5671
+ "upsert_views": [
5672
+ {
5673
+ **item,
5674
+ "name": "业务台账视图",
5675
+ }
5676
+ ],
5523
5677
  "patch_views": patch_views or [],
5524
5678
  "remove_views": remove_views or [],
5525
5679
  },
@@ -5529,7 +5683,7 @@ def _reserved_system_view_name_failure(
5529
5683
  return _config_failure(
5530
5684
  tool_name=tool_name,
5531
5685
  error_code="RESERVED_SYSTEM_VIEW_NAME",
5532
- message=f"upsert_view.name uses built-in system view name '{name}' without view_key",
5686
+ message=f"upsert_views[{index}].name uses built-in system view name '{name}' without view_key",
5533
5687
  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.",
5534
5688
  details={
5535
5689
  "reserved_view_names": sorted(_RESERVED_SYSTEM_VIEW_NAMES),
@@ -7993,20 +8147,20 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
7993
8147
  "app_views_plan": {
7994
8148
  "allowed_keys": [
7995
8149
  "app_key",
7996
- "upsert_view",
8150
+ "upsert_views",
7997
8151
  "patch_views",
7998
8152
  "remove_views",
7999
8153
  "preset",
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",
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",
8010
8164
  "patch_views[].view_key",
8011
8165
  "patch_views[].name",
8012
8166
  "patch_views[].set",
@@ -8027,7 +8181,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8027
8181
  "queryConditions": "query_conditions",
8028
8182
  "query_condition": "query_conditions",
8029
8183
  "queryCondition": "query_conditions",
8030
- "upsertView": "upsert_view",
8031
8184
  "patchViews": "patch_views",
8032
8185
  "startField": "start_field",
8033
8186
  "endField": "end_field",
@@ -8062,19 +8215,18 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8062
8215
  },
8063
8216
  "execution_notes": [
8064
8217
  "creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
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",
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",
8067
8219
  "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",
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",
8220
+ "upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
8069
8221
  "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",
8070
8222
  "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",
8071
8223
  "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",
8072
8224
  "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",
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",
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",
8074
8226
  "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",
8075
8227
  "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",
8076
8228
  "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",
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",
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",
8078
8230
  "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",
8079
8231
  "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",
8080
8232
  *_VISIBILITY_EXECUTION_NOTES,
@@ -8082,7 +8234,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8082
8234
  "minimal_example": {
8083
8235
  "profile": "default",
8084
8236
  "app_key": "APP_KEY",
8085
- "upsert_view": {"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)},
8237
+ "upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
8086
8238
  "remove_views": [],
8087
8239
  },
8088
8240
  "query_conditions_example": {
@@ -8104,37 +8256,41 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8104
8256
  "action_buttons_example": {
8105
8257
  "profile": "default",
8106
8258
  "app_key": "APP_KEY",
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
- },
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
+ ],
8121
8275
  "remove_views": [],
8122
8276
  },
8123
8277
  "full_upsert_query_conditions_example": {
8124
8278
  "profile": "default",
8125
8279
  "app_key": "APP_KEY",
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
- },
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
+ ],
8138
8294
  "remove_views": [],
8139
8295
  },
8140
8296
  "query_conditions_field_rules": {
@@ -8148,35 +8304,48 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8148
8304
  "gantt_example": {
8149
8305
  "profile": "default",
8150
8306
  "app_key": "APP_KEY",
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
- },
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
+ ],
8160
8318
  "remove_views": [],
8161
8319
  },
8162
8320
  },
8163
8321
  "app_views_apply": {
8164
8322
  "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",
8165
8334
  "app_key",
8166
8335
  "publish",
8167
- "upsert_view",
8336
+ "upsert_views",
8168
8337
  "patch_views",
8169
8338
  "remove_views",
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",
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",
8180
8349
  "patch_views[].view_key",
8181
8350
  "patch_views[].name",
8182
8351
  "patch_views[].set",
@@ -8197,7 +8366,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8197
8366
  "queryConditions": "query_conditions",
8198
8367
  "query_condition": "query_conditions",
8199
8368
  "queryCondition": "query_conditions",
8200
- "upsertView": "upsert_view",
8369
+ "views[].op": "views[].operation",
8370
+ "views[].fields": "views[].columns",
8371
+ "views[].viewKey": "views[].view_key",
8372
+ "views[].actionButtons": "views[].action_buttons",
8201
8373
  "patchViews": "patch_views",
8202
8374
  "startField": "start_field",
8203
8375
  "endField": "end_field",
@@ -8220,6 +8392,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8220
8392
  "action_buttons[].defaultValues": "action_buttons[].default_values",
8221
8393
  },
8222
8394
  "allowed_values": {
8395
+ "views[].operation": ["upsert", "patch", "remove"],
8223
8396
  "view.type": [member.value for member in PublicViewType],
8224
8397
  "view.filter.operator": [member.value for member in ViewFilterOperator],
8225
8398
  "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
@@ -8230,7 +8403,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8230
8403
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
8231
8404
  },
8232
8405
  "execution_notes": [
8233
- "upsert_view accepts one new or full view declaration for the app_key in this call; submit additional views in separate calls",
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",
8234
8409
  "creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
8235
8410
  "when duplicate view names exist, supply view_key to target the exact view",
8236
8411
  "read back app_get after any failed or partial view apply",
@@ -8239,25 +8414,24 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8239
8414
  "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",
8240
8415
  "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",
8241
8416
  "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",
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",
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",
8244
8419
  "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",
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",
8420
+ "upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
8246
8421
  "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",
8247
8422
  "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",
8248
8423
  "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",
8249
8424
  "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",
8250
8425
  "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",
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",
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",
8252
8427
  "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",
8253
8428
  "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",
8254
8429
  *_VISIBILITY_EXECUTION_NOTES,
8255
8430
  ],
8256
8431
  "minimal_example": {
8257
8432
  "profile": "default",
8258
- "app_key": "APP_KEY",
8259
8433
  "publish": True,
8260
- "upsert_view": {"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)},
8434
+ "views": [{"operation": "upsert", "app_key": "APP_KEY", "name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
8261
8435
  },
8262
8436
  "query_conditions_example": {
8263
8437
  "profile": "default",
@@ -8280,7 +8454,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8280
8454
  "profile": "default",
8281
8455
  "app_key": "APP_KEY",
8282
8456
  "publish": True,
8283
- "upsert_view": {
8457
+ "upsert_views": [
8458
+ {
8284
8459
  "name": "生产工单执行视图",
8285
8460
  "type": "table",
8286
8461
  "columns": ["工单编号", "产品", "状态", "负责人"],
@@ -8293,7 +8468,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8293
8468
  "visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
8294
8469
  }
8295
8470
  ],
8296
- },
8471
+ }
8472
+ ],
8297
8473
  "remove_views": [],
8298
8474
  },
8299
8475
  "action_buttons_patch_example": {
@@ -8323,7 +8499,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8323
8499
  "profile": "default",
8324
8500
  "app_key": "APP_KEY",
8325
8501
  "publish": True,
8326
- "upsert_view": {
8502
+ "upsert_views": [
8503
+ {
8327
8504
  "name": "客户查询视图",
8328
8505
  "type": "table",
8329
8506
  "columns": ["客户名称", "负责人", "客户状态", "创建时间"],
@@ -8334,7 +8511,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8334
8511
  "hide_before_query": False,
8335
8512
  "rows": [["客户名称", "负责人"], ["创建时间"]],
8336
8513
  },
8337
- },
8514
+ }
8515
+ ],
8338
8516
  "remove_views": [],
8339
8517
  },
8340
8518
  "query_conditions_field_rules": {
@@ -8349,7 +8527,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8349
8527
  "profile": "default",
8350
8528
  "app_key": "APP_KEY",
8351
8529
  "publish": True,
8352
- "upsert_view": {
8530
+ "upsert_views": [
8531
+ {
8353
8532
  "name": "项目甘特图",
8354
8533
  "type": "gantt",
8355
8534
  "columns": ["项目名称", "开始日期", "结束日期"],
@@ -8357,7 +8536,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
8357
8536
  "end_field": "结束日期",
8358
8537
  "title_field": "项目名称",
8359
8538
  "filters": [{"field_name": "状态", "operator": "eq", "value": "进行中"}],
8360
- },
8539
+ }
8540
+ ],
8361
8541
  "remove_views": [],
8362
8542
  },
8363
8543
  },