@qingflow-tech/qingflow-cli 1.1.50 → 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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/pyproject.toml +8 -2
- package/skills/qingflow-cli/SKILL.md +115 -3
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +3 -2
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +9 -5
- package/skills/qingflow-cli/reference/builder/50-views.md +192 -191
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +2 -0
- package/skills/qingflow-cli/reference/builder/README.md +1 -1
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +6 -6
- package/skills/qingflow-cli/reference/data/README.md +1 -1
- package/skills/qingflow-cli/reference/examples/README.md +1 -1
- package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
- package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
- package/src/qingflow_mcp/app_form_compat.py +3 -0
- package/src/qingflow_mcp/app_form_validator.py +69 -17
- package/src/qingflow_mcp/builder_facade/app_form.py +497 -100
- package/src/qingflow_mcp/builder_facade/models.py +227 -17
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +64 -30
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +11 -18
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +46 -15
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +61 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +43 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +7 -12
- package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +42 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1245 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +73 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +319 -0
- package/src/qingflow_mcp/builder_facade/service.py +1666 -509
- package/src/qingflow_mcp/builder_facade/views_extract.py +592 -0
- package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
- package/src/qingflow_mcp/cli/commands/builder.py +76 -55
- package/src/qingflow_mcp/config.py +1 -0
- package/src/qingflow_mcp/public_surface.py +3 -0
- package/src/qingflow_mcp/response_trim.py +11 -2
- package/src/qingflow_mcp/server_app_builder.py +34 -10
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
- package/src/qingflow_mcp/solution/executor.py +96 -8
- package/src/qingflow_mcp/solution/spec_models.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +218 -398
- package/src/qingflow_mcp/tools/base.py +42 -10
- package/src/qingflow_mcp/tools/solution_tools.py +2 -0
- package/src/qingflow_mcp/tools/view_tools.py +0 -6
- package/src/qingflow_mcp/tools/view_writer.py +44 -0
- package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
- 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
|
-
|
|
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
|
-
|
|
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,
|
|
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=
|
|
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":
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
3983
|
+
upsert_view: JSONObject | None = None,
|
|
3935
3984
|
patch_views: list[JSONObject] | None = None,
|
|
3936
|
-
remove_views: list[str],
|
|
3937
|
-
|
|
3938
|
-
|
|
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
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
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
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
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=
|
|
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=
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
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"
|
|
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
|
-
"
|
|
7996
|
+
"upsert_view",
|
|
8151
7997
|
"patch_views",
|
|
8152
7998
|
"remove_views",
|
|
8153
7999
|
"preset",
|
|
8154
|
-
"
|
|
8155
|
-
"
|
|
8156
|
-
"
|
|
8157
|
-
"
|
|
8158
|
-
"
|
|
8159
|
-
"
|
|
8160
|
-
"
|
|
8161
|
-
"
|
|
8162
|
-
"
|
|
8163
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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/
|
|
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
|
-
"
|
|
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
|
-
"
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
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
|
-
"
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
"
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
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
|
-
"
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
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
|
-
"
|
|
8167
|
+
"upsert_view",
|
|
8337
8168
|
"patch_views",
|
|
8338
8169
|
"remove_views",
|
|
8339
|
-
"
|
|
8340
|
-
"
|
|
8341
|
-
"
|
|
8342
|
-
"
|
|
8343
|
-
"
|
|
8344
|
-
"
|
|
8345
|
-
"
|
|
8346
|
-
"
|
|
8347
|
-
"
|
|
8348
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
8418
|
-
"
|
|
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
|
-
"
|
|
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/
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
},
|