@josephyan/qingflow-cli 1.1.4 → 1.1.6
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 +7 -3
- package/docs/local-agent-install.md +57 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/bin/qingflow.mjs +1 -34
- package/npm/lib/runtime.mjs +21 -101
- package/npm/scripts/postinstall.mjs +1 -10
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +58 -44
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
- package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
- package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
- package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
- package/skills/qingflow-cli/reference/builder/README.md +41 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
- package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
- package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
- package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
- package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
- package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
- package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
- package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
- package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
- package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
- package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
- package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
- package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
- package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
- package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
- package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
- package/skills/qingflow-mcp-setup/SKILL.md +115 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +282 -102
- package/src/qingflow_mcp/builder_facade/service.py +4192 -935
- package/src/qingflow_mcp/cli/commands/builder.py +316 -298
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +101 -27
- package/src/qingflow_mcp/cli/commands/task.py +28 -47
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +784 -16
- package/src/qingflow_mcp/cli/main.py +117 -33
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +26 -17
- package/src/qingflow_mcp/response_trim.py +81 -17
- package/src/qingflow_mcp/server.py +14 -12
- package/src/qingflow_mcp/server_app_builder.py +65 -21
- package/src/qingflow_mcp/server_app_user.py +22 -16
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/executor.py +245 -18
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +197 -35
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +244 -34
- package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
- package/src/qingflow_mcp/tools/file_tools.py +9 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +1141 -356
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +59 -45
- package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
- /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
- /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
|
@@ -15,17 +15,22 @@ from ..builder_facade.button_style_catalog import (
|
|
|
15
15
|
)
|
|
16
16
|
from ..public_surface import public_builder_contract_tool_names
|
|
17
17
|
from ..config import DEFAULT_PROFILE
|
|
18
|
-
from ..errors import QingflowApiError
|
|
18
|
+
from ..errors import QingflowApiError, backend_code_int
|
|
19
19
|
from ..json_types import JSONObject
|
|
20
20
|
from ..builder_facade.models import (
|
|
21
21
|
AssociatedResourcesApplyRequest,
|
|
22
22
|
ChartApplyRequest,
|
|
23
23
|
CustomButtonsApplyRequest,
|
|
24
24
|
CustomButtonPatch,
|
|
25
|
+
FIELD_TYPE_ALIASES,
|
|
25
26
|
FIELD_TYPE_ID_ALIASES,
|
|
26
27
|
FieldPatch,
|
|
27
28
|
FieldRemovePatch,
|
|
28
29
|
FieldUpdatePatch,
|
|
30
|
+
FlowPreset,
|
|
31
|
+
FlowNodePatch,
|
|
32
|
+
FlowPlanRequest,
|
|
33
|
+
FlowTransitionPatch,
|
|
29
34
|
LayoutApplyMode,
|
|
30
35
|
LayoutPlanRequest,
|
|
31
36
|
LayoutPreset,
|
|
@@ -65,6 +70,7 @@ from .qingbi_report_tools import QingbiReportTools
|
|
|
65
70
|
from .role_tools import RoleTools
|
|
66
71
|
from .solution_tools import SolutionTools
|
|
67
72
|
from .view_tools import ViewTools
|
|
73
|
+
from .workflow_tools import WorkflowTools
|
|
68
74
|
|
|
69
75
|
|
|
70
76
|
def _normalize_builder_view_key(value: str) -> str:
|
|
@@ -74,6 +80,32 @@ def _normalize_builder_view_key(value: str) -> str:
|
|
|
74
80
|
return raw
|
|
75
81
|
|
|
76
82
|
|
|
83
|
+
def _payload_get(payload: JSONObject, *keys: str, default: Any = None) -> Any:
|
|
84
|
+
for key in keys:
|
|
85
|
+
if key in payload:
|
|
86
|
+
return payload.get(key)
|
|
87
|
+
return default
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _payload_list(payload: JSONObject, *keys: str, default: list | None = None) -> list:
|
|
91
|
+
value = _payload_get(payload, *keys, default=default if default is not None else [])
|
|
92
|
+
return value if isinstance(value, list) else []
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _payload_bool(payload: JSONObject, *keys: str, default: bool = True) -> bool:
|
|
96
|
+
value = _payload_get(payload, *keys, default=default)
|
|
97
|
+
if isinstance(value, bool):
|
|
98
|
+
return value
|
|
99
|
+
if isinstance(value, str):
|
|
100
|
+
normalized = value.strip().lower()
|
|
101
|
+
if normalized in {"1", "true", "yes", "y", "on"}:
|
|
102
|
+
return True
|
|
103
|
+
if normalized in {"0", "false", "no", "n", "off"}:
|
|
104
|
+
return False
|
|
105
|
+
return bool(value)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
PUBLIC_STABLE_FLOW_NODE_TYPES = ["start", "approve", "fill", "copy", "webhook", "end"]
|
|
77
109
|
BUILDER_APPLY_SCHEMA_VERSION = "builder.apply.v1"
|
|
78
110
|
BUILDER_APPLY_TOOL_NAMES = {
|
|
79
111
|
"package_apply",
|
|
@@ -85,6 +117,7 @@ BUILDER_APPLY_TOOL_NAMES = {
|
|
|
85
117
|
"app_associated_resources_apply",
|
|
86
118
|
"app_charts_apply",
|
|
87
119
|
"portal_apply",
|
|
120
|
+
"portal_delete",
|
|
88
121
|
"app_publish_verify",
|
|
89
122
|
}
|
|
90
123
|
|
|
@@ -107,6 +140,7 @@ class AiBuilderTools(ToolBase):
|
|
|
107
140
|
buttons=CustomButtonTools(sessions, backend),
|
|
108
141
|
packages=PackageTools(sessions, backend),
|
|
109
142
|
views=ViewTools(sessions, backend),
|
|
143
|
+
workflows=WorkflowTools(sessions, backend),
|
|
110
144
|
portals=PortalTools(sessions, backend),
|
|
111
145
|
charts=QingbiReportTools(sessions, backend),
|
|
112
146
|
roles=RoleTools(sessions, backend),
|
|
@@ -114,6 +148,165 @@ class AiBuilderTools(ToolBase):
|
|
|
114
148
|
solutions=SolutionTools(sessions, backend),
|
|
115
149
|
)
|
|
116
150
|
|
|
151
|
+
def _apply_app_batch(self, *, tool_name: str, profile: str, apps: list[JSONObject], apply_one) -> JSONObject:
|
|
152
|
+
if not isinstance(apps, list) or not apps:
|
|
153
|
+
return _attach_builder_apply_envelope(tool_name, _config_failure(
|
|
154
|
+
tool_name=tool_name,
|
|
155
|
+
message=f"{tool_name} batch mode requires non-empty apps[].",
|
|
156
|
+
fix_hint="Pass apps as a JSON array; each item must include app_key plus that resource's normal payload.",
|
|
157
|
+
details={"expected_shape": {"apps": [{"app_key": "APP_KEY"}]}},
|
|
158
|
+
))
|
|
159
|
+
|
|
160
|
+
app_results: list[JSONObject] = []
|
|
161
|
+
errors: list[JSONObject] = []
|
|
162
|
+
for index, item in enumerate(apps):
|
|
163
|
+
if not isinstance(item, dict):
|
|
164
|
+
error = {
|
|
165
|
+
"index": index,
|
|
166
|
+
"status": "failed",
|
|
167
|
+
"error_code": "APPS_FILE_ITEM_INVALID",
|
|
168
|
+
"message": "apps[] item must be an object",
|
|
169
|
+
"reason_path": f"apps[{index}]",
|
|
170
|
+
}
|
|
171
|
+
errors.append(error)
|
|
172
|
+
app_results.append(error)
|
|
173
|
+
continue
|
|
174
|
+
app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
175
|
+
if not app_key:
|
|
176
|
+
error = {
|
|
177
|
+
"index": index,
|
|
178
|
+
"status": "failed",
|
|
179
|
+
"error_code": "APPS_FILE_APP_KEY_REQUIRED",
|
|
180
|
+
"message": "apps[] item requires app_key",
|
|
181
|
+
"reason_path": f"apps[{index}].app_key",
|
|
182
|
+
}
|
|
183
|
+
errors.append(error)
|
|
184
|
+
app_results.append(error)
|
|
185
|
+
continue
|
|
186
|
+
try:
|
|
187
|
+
result = apply_one(index, item, app_key)
|
|
188
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
189
|
+
api_error = _coerce_api_error(error)
|
|
190
|
+
result = {
|
|
191
|
+
"status": "failed",
|
|
192
|
+
"error_code": f"{tool_name.upper()}_BATCH_ITEM_FAILED",
|
|
193
|
+
"recoverable": True,
|
|
194
|
+
"message": _public_error_message(f"{tool_name.upper()}_BATCH_ITEM_FAILED", api_error),
|
|
195
|
+
"app_key": app_key,
|
|
196
|
+
"normalized_args": {"app_key": app_key},
|
|
197
|
+
"details": {"transport_error": _transport_error_payload(api_error)},
|
|
198
|
+
"request_id": api_error.request_id,
|
|
199
|
+
"backend_code": api_error.backend_code,
|
|
200
|
+
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
201
|
+
}
|
|
202
|
+
status = str(result.get("status") or "success") if isinstance(result, dict) else "failed"
|
|
203
|
+
wrapped: JSONObject = {
|
|
204
|
+
"index": index,
|
|
205
|
+
"app_key": app_key,
|
|
206
|
+
"status": status,
|
|
207
|
+
"result": result if isinstance(result, dict) else {"status": "failed", "message": str(result)},
|
|
208
|
+
}
|
|
209
|
+
if status == "failed":
|
|
210
|
+
error = {
|
|
211
|
+
"index": index,
|
|
212
|
+
"app_key": app_key,
|
|
213
|
+
"status": status,
|
|
214
|
+
"error_code": wrapped["result"].get("error_code"),
|
|
215
|
+
"message": wrapped["result"].get("message"),
|
|
216
|
+
}
|
|
217
|
+
errors.append(error)
|
|
218
|
+
wrapped["error"] = error
|
|
219
|
+
app_results.append(wrapped)
|
|
220
|
+
|
|
221
|
+
succeeded = sum(1 for item in app_results if str(item.get("status") or "") in {"success", "partial_success"})
|
|
222
|
+
failed = len(app_results) - succeeded
|
|
223
|
+
status = "success" if failed == 0 else "failed" if succeeded == 0 else "partial_success"
|
|
224
|
+
write_executed = any(
|
|
225
|
+
isinstance(item.get("result"), dict) and bool(item["result"].get("write_executed"))
|
|
226
|
+
for item in app_results
|
|
227
|
+
)
|
|
228
|
+
payload: JSONObject = {
|
|
229
|
+
"status": status,
|
|
230
|
+
"error_code": None if status == "success" else f"{tool_name.upper()}_BATCH_PARTIAL" if succeeded else f"{tool_name.upper()}_BATCH_FAILED",
|
|
231
|
+
"recoverable": failed > 0,
|
|
232
|
+
"message": (
|
|
233
|
+
f"applied {tool_name} to {succeeded}/{len(app_results)} apps"
|
|
234
|
+
if status != "success"
|
|
235
|
+
else f"applied {tool_name} to {succeeded} apps"
|
|
236
|
+
),
|
|
237
|
+
"normalized_args": {"apps": apps},
|
|
238
|
+
"apps": app_results,
|
|
239
|
+
"errors": errors,
|
|
240
|
+
"verification": {
|
|
241
|
+
"batch_verified": failed == 0,
|
|
242
|
+
"succeeded": succeeded,
|
|
243
|
+
"failed": failed,
|
|
244
|
+
},
|
|
245
|
+
"write_executed": write_executed,
|
|
246
|
+
"write_succeeded": write_executed and failed == 0,
|
|
247
|
+
"safe_to_retry": not write_executed,
|
|
248
|
+
}
|
|
249
|
+
return _attach_builder_apply_envelope(tool_name, payload)
|
|
250
|
+
|
|
251
|
+
def _read_app_batch(self, *, tool_name: str, profile: str, app_keys: list[str], read_one) -> JSONObject:
|
|
252
|
+
normalized_keys = [str(item).strip() for item in app_keys if str(item).strip()]
|
|
253
|
+
if not normalized_keys:
|
|
254
|
+
return _config_failure(
|
|
255
|
+
tool_name=tool_name,
|
|
256
|
+
message=f"{tool_name} batch mode requires non-empty app_keys[].",
|
|
257
|
+
fix_hint="Pass app_keys as a non-empty list of app keys.",
|
|
258
|
+
details={"expected_shape": {"app_keys": ["APP_A", "APP_B"]}},
|
|
259
|
+
)
|
|
260
|
+
apps: list[JSONObject] = []
|
|
261
|
+
errors: list[JSONObject] = []
|
|
262
|
+
for index, app_key in enumerate(normalized_keys):
|
|
263
|
+
try:
|
|
264
|
+
result = read_one(app_key)
|
|
265
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
266
|
+
api_error = _coerce_api_error(error)
|
|
267
|
+
result = {
|
|
268
|
+
"status": "failed",
|
|
269
|
+
"error_code": f"{tool_name.upper()}_BATCH_ITEM_FAILED",
|
|
270
|
+
"recoverable": True,
|
|
271
|
+
"message": _public_error_message(f"{tool_name.upper()}_BATCH_ITEM_FAILED", api_error),
|
|
272
|
+
"app_key": app_key,
|
|
273
|
+
"details": {"transport_error": _transport_error_payload(api_error)},
|
|
274
|
+
"request_id": api_error.request_id,
|
|
275
|
+
"backend_code": api_error.backend_code,
|
|
276
|
+
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
277
|
+
}
|
|
278
|
+
status = str(result.get("status") or "success") if isinstance(result, dict) else "failed"
|
|
279
|
+
app_item: JSONObject = {
|
|
280
|
+
"index": index,
|
|
281
|
+
"app_key": app_key,
|
|
282
|
+
"status": status,
|
|
283
|
+
"data": result,
|
|
284
|
+
}
|
|
285
|
+
if status == "failed":
|
|
286
|
+
error = {
|
|
287
|
+
"index": index,
|
|
288
|
+
"app_key": app_key,
|
|
289
|
+
"status": "failed",
|
|
290
|
+
"error_code": result.get("error_code") if isinstance(result, dict) else None,
|
|
291
|
+
"message": result.get("message") if isinstance(result, dict) else str(result),
|
|
292
|
+
}
|
|
293
|
+
app_item["error"] = error
|
|
294
|
+
errors.append(error)
|
|
295
|
+
apps.append(app_item)
|
|
296
|
+
succeeded = sum(1 for item in apps if str(item.get("status") or "") in {"success", "partial_success"})
|
|
297
|
+
failed = len(apps) - succeeded
|
|
298
|
+
return {
|
|
299
|
+
"status": "success" if failed == 0 else "failed" if succeeded == 0 else "partial_success",
|
|
300
|
+
"error_code": None if failed == 0 else f"{tool_name.upper()}_BATCH_PARTIAL" if succeeded else f"{tool_name.upper()}_BATCH_FAILED",
|
|
301
|
+
"recoverable": failed > 0,
|
|
302
|
+
"message": f"read {tool_name} for {succeeded}/{len(apps)} apps" if failed else f"read {tool_name} for {succeeded} apps",
|
|
303
|
+
"normalized_args": {"app_keys": normalized_keys},
|
|
304
|
+
"app_keys": normalized_keys,
|
|
305
|
+
"apps": apps,
|
|
306
|
+
"errors": errors,
|
|
307
|
+
"verification": {"batch_verified": failed == 0, "succeeded": succeeded, "failed": failed},
|
|
308
|
+
}
|
|
309
|
+
|
|
117
310
|
def register(self, mcp) -> None:
|
|
118
311
|
"""注册当前工具到 MCP 服务。"""
|
|
119
312
|
@mcp.tool()
|
|
@@ -138,8 +331,11 @@ class AiBuilderTools(ToolBase):
|
|
|
138
331
|
package_id: int | None = None,
|
|
139
332
|
package_name: str | None = None,
|
|
140
333
|
create_if_missing: bool = False,
|
|
141
|
-
icon: str | None = None,
|
|
334
|
+
icon: str | JSONObject | None = None,
|
|
142
335
|
color: str | None = None,
|
|
336
|
+
icon_name: str | None = None,
|
|
337
|
+
icon_color: str | None = None,
|
|
338
|
+
icon_config: JSONObject | None = None,
|
|
143
339
|
visibility: JSONObject | None = None,
|
|
144
340
|
items: list[dict] | None = None,
|
|
145
341
|
allow_detach: bool = False,
|
|
@@ -151,6 +347,9 @@ class AiBuilderTools(ToolBase):
|
|
|
151
347
|
create_if_missing=create_if_missing,
|
|
152
348
|
icon=icon,
|
|
153
349
|
color=color,
|
|
350
|
+
icon_name=icon_name,
|
|
351
|
+
icon_color=icon_color,
|
|
352
|
+
icon_config=icon_config,
|
|
154
353
|
visibility=visibility,
|
|
155
354
|
items=items or None,
|
|
156
355
|
allow_detach=allow_detach,
|
|
@@ -299,15 +498,11 @@ class AiBuilderTools(ToolBase):
|
|
|
299
498
|
|
|
300
499
|
@mcp.tool()
|
|
301
500
|
def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
302
|
-
|
|
303
|
-
return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get, data_key="summary", tool_name="app_get")
|
|
304
|
-
return self.app_get(profile=profile, app_key=app_key)
|
|
501
|
+
return self.app_get(profile=profile, app_key=app_key, app_keys=app_keys)
|
|
305
502
|
|
|
306
503
|
@mcp.tool()
|
|
307
504
|
def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
308
|
-
|
|
309
|
-
return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_fields, data_key="fields", tool_name="app_get_fields")
|
|
310
|
-
return self.app_get_fields(profile=profile, app_key=app_key)
|
|
505
|
+
return self.app_get_fields(profile=profile, app_key=app_key, app_keys=app_keys)
|
|
311
506
|
|
|
312
507
|
@mcp.tool()
|
|
313
508
|
def app_repair_code_blocks(
|
|
@@ -320,39 +515,27 @@ class AiBuilderTools(ToolBase):
|
|
|
320
515
|
|
|
321
516
|
@mcp.tool()
|
|
322
517
|
def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
323
|
-
|
|
324
|
-
return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_layout, data_key="sections", tool_name="app_get_layout")
|
|
325
|
-
return self.app_get_layout(profile=profile, app_key=app_key)
|
|
518
|
+
return self.app_get_layout(profile=profile, app_key=app_key, app_keys=app_keys)
|
|
326
519
|
|
|
327
520
|
@mcp.tool()
|
|
328
521
|
def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
329
|
-
|
|
330
|
-
return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_views, data_key="views", tool_name="app_get_views")
|
|
331
|
-
return self.app_get_views(profile=profile, app_key=app_key)
|
|
522
|
+
return self.app_get_views(profile=profile, app_key=app_key, app_keys=app_keys)
|
|
332
523
|
|
|
333
524
|
@mcp.tool()
|
|
334
525
|
def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
335
|
-
|
|
336
|
-
return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_flow, data_key="spec", tool_name="app_get_flow")
|
|
337
|
-
return self.app_get_flow(profile=profile, app_key=app_key)
|
|
526
|
+
return self.app_get_flow(profile=profile, app_key=app_key, app_keys=app_keys)
|
|
338
527
|
|
|
339
528
|
@mcp.tool()
|
|
340
529
|
def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
341
|
-
|
|
342
|
-
return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_charts, data_key="charts", tool_name="app_get_charts")
|
|
343
|
-
return self.app_get_charts(profile=profile, app_key=app_key)
|
|
530
|
+
return self.app_get_charts(profile=profile, app_key=app_key, app_keys=app_keys)
|
|
344
531
|
|
|
345
532
|
@mcp.tool()
|
|
346
533
|
def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
347
|
-
|
|
348
|
-
return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_buttons, data_key="buttons", tool_name="app_get_buttons")
|
|
349
|
-
return self._facade.app_get_buttons(profile=profile, app_key=app_key)
|
|
534
|
+
return self.app_get_buttons(profile=profile, app_key=app_key, app_keys=app_keys)
|
|
350
535
|
|
|
351
536
|
@mcp.tool()
|
|
352
537
|
def app_get_associated_resources(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
353
|
-
|
|
354
|
-
return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_associated_resources, data_key="associated_resources", tool_name="app_get_associated_resources")
|
|
355
|
-
return self._facade.app_get_associated_resources(profile=profile, app_key=app_key)
|
|
538
|
+
return self.app_get_associated_resources(profile=profile, app_key=app_key, app_keys=app_keys)
|
|
356
539
|
|
|
357
540
|
@mcp.tool()
|
|
358
541
|
def portal_list(profile: str = DEFAULT_PROFILE) -> JSONObject:
|
|
@@ -384,8 +567,11 @@ class AiBuilderTools(ToolBase):
|
|
|
384
567
|
package_id: int | None = None,
|
|
385
568
|
app_name: str = "",
|
|
386
569
|
app_title: str = "",
|
|
387
|
-
icon: str = "",
|
|
570
|
+
icon: str | JSONObject = "",
|
|
388
571
|
color: str = "",
|
|
572
|
+
icon_name: str | None = None,
|
|
573
|
+
icon_color: str | None = None,
|
|
574
|
+
icon_config: JSONObject | None = None,
|
|
389
575
|
visibility: JSONObject | None = None,
|
|
390
576
|
create_if_missing: bool = False,
|
|
391
577
|
publish: bool = True,
|
|
@@ -394,7 +580,7 @@ class AiBuilderTools(ToolBase):
|
|
|
394
580
|
remove_fields: list[JSONObject] | None = None,
|
|
395
581
|
apps: list[JSONObject] | None = None,
|
|
396
582
|
) -> JSONObject:
|
|
397
|
-
if apps:
|
|
583
|
+
if apps is not None:
|
|
398
584
|
if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
|
|
399
585
|
return _config_failure(
|
|
400
586
|
tool_name="app_schema_apply",
|
|
@@ -443,6 +629,9 @@ class AiBuilderTools(ToolBase):
|
|
|
443
629
|
app_title=app_title,
|
|
444
630
|
icon=icon,
|
|
445
631
|
color=color,
|
|
632
|
+
icon_name=icon_name,
|
|
633
|
+
icon_color=icon_color,
|
|
634
|
+
icon_config=icon_config,
|
|
446
635
|
visibility=visibility,
|
|
447
636
|
create_if_missing=create_if_missing,
|
|
448
637
|
publish=publish,
|
|
@@ -469,7 +658,7 @@ class AiBuilderTools(ToolBase):
|
|
|
469
658
|
app_key: str = "",
|
|
470
659
|
version_id: str = "",
|
|
471
660
|
) -> JSONObject:
|
|
472
|
-
return self.
|
|
661
|
+
return self.app_flow_get(profile=profile, app_key=app_key, version_id=version_id or None)
|
|
473
662
|
|
|
474
663
|
@mcp.tool()
|
|
475
664
|
def app_flow_get_schema(profile: str = DEFAULT_PROFILE, schema_version: str = "") -> JSONObject:
|
|
@@ -479,7 +668,10 @@ class AiBuilderTools(ToolBase):
|
|
|
479
668
|
def app_flow_apply(
|
|
480
669
|
profile: str = DEFAULT_PROFILE,
|
|
481
670
|
app_key: str = "",
|
|
671
|
+
mode: str = "replace",
|
|
482
672
|
publish: bool = True,
|
|
673
|
+
nodes: list[JSONObject] | None = None,
|
|
674
|
+
transitions: list[JSONObject] | None = None,
|
|
483
675
|
spec: JSONObject | None = None,
|
|
484
676
|
idempotency_key: str = "",
|
|
485
677
|
schema_version: str = "",
|
|
@@ -488,8 +680,11 @@ class AiBuilderTools(ToolBase):
|
|
|
488
680
|
return self.app_flow_apply(
|
|
489
681
|
profile=profile,
|
|
490
682
|
app_key=app_key,
|
|
683
|
+
mode=mode,
|
|
491
684
|
publish=publish,
|
|
492
|
-
|
|
685
|
+
nodes=nodes or [],
|
|
686
|
+
transitions=transitions or [],
|
|
687
|
+
spec=spec,
|
|
493
688
|
idempotency_key=idempotency_key or None,
|
|
494
689
|
schema_version=schema_version or None,
|
|
495
690
|
patch_nodes=patch_nodes,
|
|
@@ -595,6 +790,13 @@ class AiBuilderTools(ToolBase):
|
|
|
595
790
|
patch_sections=patch_sections,
|
|
596
791
|
)
|
|
597
792
|
|
|
793
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="delete", target="portal"))
|
|
794
|
+
def portal_delete(
|
|
795
|
+
profile: str = DEFAULT_PROFILE,
|
|
796
|
+
dash_key: str = "",
|
|
797
|
+
) -> JSONObject:
|
|
798
|
+
return self.portal_delete(profile=profile, dash_key=dash_key)
|
|
799
|
+
|
|
598
800
|
@mcp.tool()
|
|
599
801
|
def app_publish_verify(
|
|
600
802
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -602,17 +804,10 @@ class AiBuilderTools(ToolBase):
|
|
|
602
804
|
app_keys: list[str] | None = None,
|
|
603
805
|
expected_package_id: int | None = None,
|
|
604
806
|
) -> JSONObject:
|
|
605
|
-
if app_keys:
|
|
606
|
-
return self._facade._batch_read_app_keys(
|
|
607
|
-
profile=profile,
|
|
608
|
-
app_keys=app_keys,
|
|
609
|
-
single_reader=lambda profile, app_key: self.app_publish_verify(profile=profile, app_key=app_key, expected_package_id=expected_package_id),
|
|
610
|
-
data_key="verification",
|
|
611
|
-
tool_name="app_publish_verify",
|
|
612
|
-
)
|
|
613
807
|
return self.app_publish_verify(
|
|
614
808
|
profile=profile,
|
|
615
809
|
app_key=app_key,
|
|
810
|
+
app_keys=app_keys,
|
|
616
811
|
expected_package_id=expected_package_id,
|
|
617
812
|
)
|
|
618
813
|
|
|
@@ -661,15 +856,16 @@ class AiBuilderTools(ToolBase):
|
|
|
661
856
|
"allowed_values": {"tool_name": public_tool_names},
|
|
662
857
|
"details": {"reason_path": "tool_name"},
|
|
663
858
|
"suggested_next_call": None,
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
859
|
+
"request_id": None,
|
|
860
|
+
"backend_code": None,
|
|
861
|
+
"http_status": None,
|
|
862
|
+
"noop": False,
|
|
863
|
+
"warnings": [],
|
|
864
|
+
"verification": {},
|
|
865
|
+
"verified": False,
|
|
866
|
+
}
|
|
672
867
|
contract = _builder_contract_with_apply_output(lookup_name, contract)
|
|
868
|
+
contract_summary = _builder_tool_contract_summary(lookup_name, contract)
|
|
673
869
|
return {
|
|
674
870
|
"status": "success",
|
|
675
871
|
"error_code": None,
|
|
@@ -688,6 +884,8 @@ class AiBuilderTools(ToolBase):
|
|
|
688
884
|
"verification": {},
|
|
689
885
|
"verified": True,
|
|
690
886
|
"tool_name": requested,
|
|
887
|
+
"summary": contract_summary,
|
|
888
|
+
"json_paths": contract_summary["json_paths"],
|
|
691
889
|
"contract": contract,
|
|
692
890
|
}
|
|
693
891
|
|
|
@@ -710,6 +908,12 @@ class AiBuilderTools(ToolBase):
|
|
|
710
908
|
"color_count": len(catalog["icon_colors"]),
|
|
711
909
|
"warnings": [],
|
|
712
910
|
"verification": {"source": "backend AiBuildConstant ICON_NAMES/ICON_COLORS"},
|
|
911
|
+
"json_paths": {
|
|
912
|
+
"icon_names": "$.icon_names",
|
|
913
|
+
"icon_colors": "$.icon_colors",
|
|
914
|
+
"generic_icon_names": "$.generic_icon_names",
|
|
915
|
+
"common_examples": "$.common_examples",
|
|
916
|
+
},
|
|
713
917
|
}
|
|
714
918
|
|
|
715
919
|
@tool_cn_name("分组创建")
|
|
@@ -776,13 +980,23 @@ class AiBuilderTools(ToolBase):
|
|
|
776
980
|
package_id: int | None = None,
|
|
777
981
|
package_name: str | None = None,
|
|
778
982
|
create_if_missing: bool = False,
|
|
779
|
-
icon: str | None = None,
|
|
983
|
+
icon: str | JSONObject | None = None,
|
|
780
984
|
color: str | None = None,
|
|
985
|
+
icon_name: str | None = None,
|
|
986
|
+
icon_color: str | None = None,
|
|
987
|
+
icon_config: JSONObject | None = None,
|
|
781
988
|
visibility: JSONObject | None = None,
|
|
782
989
|
items: list[dict] | None = None,
|
|
783
990
|
allow_detach: bool = False,
|
|
784
991
|
) -> JSONObject:
|
|
785
992
|
"""执行分组与包相关逻辑。"""
|
|
993
|
+
icon, color = _normalize_builder_icon_args(
|
|
994
|
+
icon=icon,
|
|
995
|
+
color=color,
|
|
996
|
+
icon_name=icon_name,
|
|
997
|
+
icon_color=icon_color,
|
|
998
|
+
icon_config=icon_config,
|
|
999
|
+
)
|
|
786
1000
|
visibility_patch = None
|
|
787
1001
|
if visibility is not None:
|
|
788
1002
|
try:
|
|
@@ -905,16 +1119,23 @@ class AiBuilderTools(ToolBase):
|
|
|
905
1119
|
contain_disable: bool = False,
|
|
906
1120
|
) -> JSONObject:
|
|
907
1121
|
"""执行工具方法逻辑。"""
|
|
1122
|
+
normalized_query = str(query or "").strip()
|
|
908
1123
|
normalized_args = {
|
|
909
|
-
"query":
|
|
1124
|
+
"query": normalized_query,
|
|
910
1125
|
"page_num": page_num,
|
|
911
1126
|
"page_size": page_size,
|
|
912
1127
|
"contain_disable": contain_disable,
|
|
913
1128
|
}
|
|
1129
|
+
if not normalized_query:
|
|
1130
|
+
return _config_failure(
|
|
1131
|
+
tool_name="member_search",
|
|
1132
|
+
message="query is required for member_search; builder member lookup is a contact-directory path, not a record candidate fallback.",
|
|
1133
|
+
fix_hint="For record member/department field ambiguity, use record member-candidates / department-candidates instead.",
|
|
1134
|
+
)
|
|
914
1135
|
return _safe_tool_call(
|
|
915
1136
|
lambda: self._facade.member_search(
|
|
916
1137
|
profile=profile,
|
|
917
|
-
query=
|
|
1138
|
+
query=normalized_query,
|
|
918
1139
|
page_num=page_num,
|
|
919
1140
|
page_size=page_size,
|
|
920
1141
|
contain_disable=contain_disable,
|
|
@@ -927,9 +1148,16 @@ class AiBuilderTools(ToolBase):
|
|
|
927
1148
|
@tool_cn_name("角色检索")
|
|
928
1149
|
def role_search(self, *, profile: str, keyword: str, page_num: int = 1, page_size: int = 20) -> JSONObject:
|
|
929
1150
|
"""执行角色相关逻辑。"""
|
|
930
|
-
|
|
1151
|
+
normalized_keyword = str(keyword or "").strip()
|
|
1152
|
+
normalized_args = {"keyword": normalized_keyword, "page_num": page_num, "page_size": page_size}
|
|
1153
|
+
if not normalized_keyword:
|
|
1154
|
+
return _config_failure(
|
|
1155
|
+
tool_name="role_search",
|
|
1156
|
+
message="keyword is required for role_search; builder role lookup is a contact-management path, not a record candidate fallback.",
|
|
1157
|
+
fix_hint="For record member/department field ambiguity, use record member-candidates / department-candidates instead.",
|
|
1158
|
+
)
|
|
931
1159
|
return _safe_tool_call(
|
|
932
|
-
lambda: self._facade.role_search(profile=profile, keyword=
|
|
1160
|
+
lambda: self._facade.role_search(profile=profile, keyword=normalized_keyword, page_num=page_num, page_size=page_size),
|
|
933
1161
|
error_code="ROLE_SEARCH_FAILED",
|
|
934
1162
|
normalized_args=normalized_args,
|
|
935
1163
|
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, **normalized_args}},
|
|
@@ -1058,19 +1286,20 @@ class AiBuilderTools(ToolBase):
|
|
|
1058
1286
|
apps: list[JSONObject] | None = None,
|
|
1059
1287
|
) -> JSONObject:
|
|
1060
1288
|
"""执行应用按钮 apply 逻辑。"""
|
|
1061
|
-
if apps:
|
|
1062
|
-
return self.
|
|
1289
|
+
if apps is not None:
|
|
1290
|
+
return self._apply_app_batch(
|
|
1291
|
+
tool_name="app_custom_buttons_apply",
|
|
1063
1292
|
profile=profile,
|
|
1064
1293
|
apps=apps,
|
|
1065
|
-
|
|
1294
|
+
apply_one=lambda _index, item, item_app_key: self.app_custom_buttons_apply(
|
|
1066
1295
|
profile=profile,
|
|
1067
|
-
app_key=
|
|
1068
|
-
upsert_buttons=
|
|
1069
|
-
patch_buttons=
|
|
1070
|
-
remove_buttons=
|
|
1071
|
-
view_configs=
|
|
1296
|
+
app_key=item_app_key,
|
|
1297
|
+
upsert_buttons=_payload_list(item, "upsert_buttons", "upsertButtons", "buttons", default=upsert_buttons or []),
|
|
1298
|
+
patch_buttons=_payload_list(item, "patch_buttons", "patchButtons", default=patch_buttons or []),
|
|
1299
|
+
remove_buttons=_payload_list(item, "remove_buttons", "removeButtons", default=remove_buttons or []),
|
|
1300
|
+
view_configs=_payload_list(item, "view_configs", "viewConfigs", default=view_configs or []),
|
|
1301
|
+
apps=None,
|
|
1072
1302
|
),
|
|
1073
|
-
tool_name="app_custom_buttons_apply",
|
|
1074
1303
|
)
|
|
1075
1304
|
raw_request = {
|
|
1076
1305
|
"app_key": app_key,
|
|
@@ -1127,20 +1356,31 @@ class AiBuilderTools(ToolBase):
|
|
|
1127
1356
|
apps: list[JSONObject] | None = None,
|
|
1128
1357
|
) -> JSONObject:
|
|
1129
1358
|
"""执行应用关联资源 apply 逻辑。"""
|
|
1130
|
-
if apps:
|
|
1131
|
-
return self.
|
|
1359
|
+
if apps is not None:
|
|
1360
|
+
return self._apply_app_batch(
|
|
1361
|
+
tool_name="app_associated_resources_apply",
|
|
1132
1362
|
profile=profile,
|
|
1133
1363
|
apps=apps,
|
|
1134
|
-
|
|
1364
|
+
apply_one=lambda _index, item, item_app_key: self.app_associated_resources_apply(
|
|
1135
1365
|
profile=profile,
|
|
1136
|
-
app_key=
|
|
1137
|
-
upsert_resources=
|
|
1138
|
-
patch_resources=
|
|
1139
|
-
remove_associated_item_ids=
|
|
1140
|
-
|
|
1141
|
-
|
|
1366
|
+
app_key=item_app_key,
|
|
1367
|
+
upsert_resources=_payload_list(item, "upsert_resources", "upsertResources", "resources", default=upsert_resources or []),
|
|
1368
|
+
patch_resources=_payload_list(item, "patch_resources", "patchResources", default=patch_resources or []),
|
|
1369
|
+
remove_associated_item_ids=_payload_list(
|
|
1370
|
+
item,
|
|
1371
|
+
"remove_associated_item_ids",
|
|
1372
|
+
"removeAssociatedItemIds",
|
|
1373
|
+
default=remove_associated_item_ids or [],
|
|
1374
|
+
),
|
|
1375
|
+
reorder_associated_item_ids=_payload_list(
|
|
1376
|
+
item,
|
|
1377
|
+
"reorder_associated_item_ids",
|
|
1378
|
+
"reorderAssociatedItemIds",
|
|
1379
|
+
default=reorder_associated_item_ids or [],
|
|
1380
|
+
),
|
|
1381
|
+
view_configs=_payload_list(item, "view_configs", "viewConfigs", default=view_configs or []),
|
|
1382
|
+
apps=None,
|
|
1142
1383
|
),
|
|
1143
|
-
tool_name="app_associated_resources_apply",
|
|
1144
1384
|
)
|
|
1145
1385
|
raw_request = {
|
|
1146
1386
|
"app_key": app_key,
|
|
@@ -1304,11 +1544,14 @@ class AiBuilderTools(ToolBase):
|
|
|
1304
1544
|
))
|
|
1305
1545
|
|
|
1306
1546
|
@tool_cn_name("应用详情查询")
|
|
1307
|
-
def app_get(self, *, profile: str, app_key: str
|
|
1547
|
+
def app_get(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
|
|
1308
1548
|
"""执行应用相关逻辑。"""
|
|
1309
|
-
if app_keys:
|
|
1310
|
-
return
|
|
1311
|
-
|
|
1549
|
+
if app_keys is not None:
|
|
1550
|
+
return self._read_app_batch(
|
|
1551
|
+
tool_name="app_get",
|
|
1552
|
+
profile=profile,
|
|
1553
|
+
app_keys=app_keys,
|
|
1554
|
+
read_one=lambda item_app_key: self.app_get(profile=profile, app_key=item_app_key, app_keys=None),
|
|
1312
1555
|
)
|
|
1313
1556
|
normalized_args = {"app_key": app_key}
|
|
1314
1557
|
return _publicize_package_fields(_safe_tool_call(
|
|
@@ -1330,10 +1573,15 @@ class AiBuilderTools(ToolBase):
|
|
|
1330
1573
|
)
|
|
1331
1574
|
|
|
1332
1575
|
@tool_cn_name("应用字段详情查询")
|
|
1333
|
-
def app_get_fields(self, *, profile: str, app_key: str
|
|
1576
|
+
def app_get_fields(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
|
|
1334
1577
|
"""执行应用相关逻辑。"""
|
|
1335
|
-
if app_keys:
|
|
1336
|
-
return self.
|
|
1578
|
+
if app_keys is not None:
|
|
1579
|
+
return self._read_app_batch(
|
|
1580
|
+
tool_name="app_get_fields",
|
|
1581
|
+
profile=profile,
|
|
1582
|
+
app_keys=app_keys,
|
|
1583
|
+
read_one=lambda item_app_key: self.app_get_fields(profile=profile, app_key=item_app_key, app_keys=None),
|
|
1584
|
+
)
|
|
1337
1585
|
normalized_args = {"app_key": app_key}
|
|
1338
1586
|
return _safe_tool_call(
|
|
1339
1587
|
lambda: self._facade.app_get_fields(profile=profile, app_key=app_key),
|
|
@@ -1372,10 +1620,15 @@ class AiBuilderTools(ToolBase):
|
|
|
1372
1620
|
)
|
|
1373
1621
|
|
|
1374
1622
|
@tool_cn_name("应用布局详情查询")
|
|
1375
|
-
def app_get_layout(self, *, profile: str, app_key: str
|
|
1623
|
+
def app_get_layout(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
|
|
1376
1624
|
"""执行应用相关逻辑。"""
|
|
1377
|
-
if app_keys:
|
|
1378
|
-
return self.
|
|
1625
|
+
if app_keys is not None:
|
|
1626
|
+
return self._read_app_batch(
|
|
1627
|
+
tool_name="app_get_layout",
|
|
1628
|
+
profile=profile,
|
|
1629
|
+
app_keys=app_keys,
|
|
1630
|
+
read_one=lambda item_app_key: self.app_get_layout(profile=profile, app_key=item_app_key, app_keys=None),
|
|
1631
|
+
)
|
|
1379
1632
|
normalized_args = {"app_key": app_key}
|
|
1380
1633
|
return _safe_tool_call(
|
|
1381
1634
|
lambda: self._facade.app_get_layout(profile=profile, app_key=app_key),
|
|
@@ -1396,10 +1649,15 @@ class AiBuilderTools(ToolBase):
|
|
|
1396
1649
|
)
|
|
1397
1650
|
|
|
1398
1651
|
@tool_cn_name("应用视图详情查询")
|
|
1399
|
-
def app_get_views(self, *, profile: str, app_key: str
|
|
1652
|
+
def app_get_views(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
|
|
1400
1653
|
"""执行应用相关逻辑。"""
|
|
1401
|
-
if app_keys:
|
|
1402
|
-
return self.
|
|
1654
|
+
if app_keys is not None:
|
|
1655
|
+
return self._read_app_batch(
|
|
1656
|
+
tool_name="app_get_views",
|
|
1657
|
+
profile=profile,
|
|
1658
|
+
app_keys=app_keys,
|
|
1659
|
+
read_one=lambda item_app_key: self.app_get_views(profile=profile, app_key=item_app_key, app_keys=None),
|
|
1660
|
+
)
|
|
1403
1661
|
normalized_args = {"app_key": app_key}
|
|
1404
1662
|
return _safe_tool_call(
|
|
1405
1663
|
lambda: self._facade.app_get_views(profile=profile, app_key=app_key),
|
|
@@ -1408,27 +1666,53 @@ class AiBuilderTools(ToolBase):
|
|
|
1408
1666
|
suggested_next_call={"tool_name": "app_get_views", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1409
1667
|
)
|
|
1410
1668
|
|
|
1411
|
-
@tool_cn_name("
|
|
1412
|
-
def
|
|
1413
|
-
|
|
1669
|
+
@tool_cn_name("应用流程摘要读取")
|
|
1670
|
+
def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
1671
|
+
"""执行应用相关逻辑。"""
|
|
1672
|
+
normalized_args = {"app_key": app_key}
|
|
1414
1673
|
return _safe_tool_call(
|
|
1415
|
-
lambda: self._facade.
|
|
1416
|
-
error_code="
|
|
1674
|
+
lambda: self._facade.app_read_flow_summary(profile=profile, app_key=app_key),
|
|
1675
|
+
error_code="FLOW_READ_FAILED",
|
|
1417
1676
|
normalized_args=normalized_args,
|
|
1418
|
-
suggested_next_call={"tool_name": "
|
|
1677
|
+
suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1419
1678
|
)
|
|
1420
1679
|
|
|
1421
|
-
@tool_cn_name("
|
|
1422
|
-
def app_get_flow(self, *, profile: str, app_key: str
|
|
1680
|
+
@tool_cn_name("应用流程详情查询")
|
|
1681
|
+
def app_get_flow(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
|
|
1423
1682
|
"""执行应用相关逻辑。"""
|
|
1424
|
-
if app_keys:
|
|
1425
|
-
return self.
|
|
1683
|
+
if app_keys is not None:
|
|
1684
|
+
return self._read_app_batch(
|
|
1685
|
+
tool_name="app_get_flow",
|
|
1686
|
+
profile=profile,
|
|
1687
|
+
app_keys=app_keys,
|
|
1688
|
+
read_one=lambda item_app_key: self.app_get_flow(profile=profile, app_key=item_app_key, app_keys=None),
|
|
1689
|
+
)
|
|
1690
|
+
normalized_args = {"app_key": app_key}
|
|
1691
|
+
return _safe_tool_call(
|
|
1692
|
+
lambda: self._facade.app_get_flow(profile=profile, app_key=app_key),
|
|
1693
|
+
error_code="APP_GET_FLOW_FAILED",
|
|
1694
|
+
normalized_args=normalized_args,
|
|
1695
|
+
suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1696
|
+
)
|
|
1697
|
+
|
|
1698
|
+
@tool_cn_name("应用流程规格查询")
|
|
1699
|
+
def app_flow_get(self, *, profile: str, app_key: str, version_id: str | None = None) -> JSONObject:
|
|
1426
1700
|
normalized_args = {"app_key": app_key, "version_id": version_id}
|
|
1427
1701
|
return _safe_tool_call(
|
|
1428
1702
|
lambda: self._facade.flow_get(profile=profile, app_key=app_key, version_id=version_id),
|
|
1429
|
-
error_code="
|
|
1703
|
+
error_code="APP_FLOW_GET_FAILED",
|
|
1430
1704
|
normalized_args=normalized_args,
|
|
1431
|
-
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile,
|
|
1705
|
+
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, **normalized_args}},
|
|
1706
|
+
)
|
|
1707
|
+
|
|
1708
|
+
@tool_cn_name("应用流程规格 Schema 查询")
|
|
1709
|
+
def app_flow_get_schema(self, *, profile: str, schema_version: str | None = None) -> JSONObject:
|
|
1710
|
+
normalized_args = {"schema_version": schema_version}
|
|
1711
|
+
return _safe_tool_call(
|
|
1712
|
+
lambda: self._facade.flow_get_schema(profile=profile, schema_version=schema_version),
|
|
1713
|
+
error_code="APP_FLOW_SCHEMA_GET_FAILED",
|
|
1714
|
+
normalized_args=normalized_args,
|
|
1715
|
+
suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile, **normalized_args}},
|
|
1432
1716
|
)
|
|
1433
1717
|
|
|
1434
1718
|
@tool_cn_name("应用图表摘要读取")
|
|
@@ -1443,10 +1727,15 @@ class AiBuilderTools(ToolBase):
|
|
|
1443
1727
|
)
|
|
1444
1728
|
|
|
1445
1729
|
@tool_cn_name("应用图表详情查询")
|
|
1446
|
-
def app_get_charts(self, *, profile: str, app_key: str
|
|
1730
|
+
def app_get_charts(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
|
|
1447
1731
|
"""执行应用相关逻辑。"""
|
|
1448
|
-
if app_keys:
|
|
1449
|
-
return self.
|
|
1732
|
+
if app_keys is not None:
|
|
1733
|
+
return self._read_app_batch(
|
|
1734
|
+
tool_name="app_get_charts",
|
|
1735
|
+
profile=profile,
|
|
1736
|
+
app_keys=app_keys,
|
|
1737
|
+
read_one=lambda item_app_key: self.app_get_charts(profile=profile, app_key=item_app_key, app_keys=None),
|
|
1738
|
+
)
|
|
1450
1739
|
normalized_args = {"app_key": app_key}
|
|
1451
1740
|
return _safe_tool_call(
|
|
1452
1741
|
lambda: self._facade.app_get_charts(profile=profile, app_key=app_key),
|
|
@@ -1457,9 +1746,13 @@ class AiBuilderTools(ToolBase):
|
|
|
1457
1746
|
|
|
1458
1747
|
@tool_cn_name("自定义按钮读取")
|
|
1459
1748
|
def app_get_buttons(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1749
|
+
if app_keys is not None:
|
|
1750
|
+
return self._read_app_batch(
|
|
1751
|
+
tool_name="app_get_buttons",
|
|
1752
|
+
profile=profile,
|
|
1753
|
+
app_keys=app_keys,
|
|
1754
|
+
read_one=lambda item_app_key: self.app_get_buttons(profile=profile, app_key=item_app_key, app_keys=None),
|
|
1755
|
+
)
|
|
1463
1756
|
normalized_args = {"app_key": app_key}
|
|
1464
1757
|
return _safe_tool_call(
|
|
1465
1758
|
lambda: self._facade.app_get_buttons(profile=profile, app_key=app_key),
|
|
@@ -1470,9 +1763,13 @@ class AiBuilderTools(ToolBase):
|
|
|
1470
1763
|
|
|
1471
1764
|
@tool_cn_name("关联资源读取")
|
|
1472
1765
|
def app_get_associated_resources(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1766
|
+
if app_keys is not None:
|
|
1767
|
+
return self._read_app_batch(
|
|
1768
|
+
tool_name="app_get_associated_resources",
|
|
1769
|
+
profile=profile,
|
|
1770
|
+
app_keys=app_keys,
|
|
1771
|
+
read_one=lambda item_app_key: self.app_get_associated_resources(profile=profile, app_key=item_app_key, app_keys=None),
|
|
1772
|
+
)
|
|
1476
1773
|
normalized_args = {"app_key": app_key}
|
|
1477
1774
|
return _safe_tool_call(
|
|
1478
1775
|
lambda: self._facade.app_get_associated_resources(profile=profile, app_key=app_key),
|
|
@@ -1558,6 +1855,22 @@ class AiBuilderTools(ToolBase):
|
|
|
1558
1855
|
remove_fields: list[JSONObject],
|
|
1559
1856
|
) -> JSONObject:
|
|
1560
1857
|
"""执行应用相关逻辑。"""
|
|
1858
|
+
system_field_failure = _reserved_system_field_name_failure(
|
|
1859
|
+
tool_name="app_schema_plan",
|
|
1860
|
+
profile=profile,
|
|
1861
|
+
app_key=app_key,
|
|
1862
|
+
package_id=package_id,
|
|
1863
|
+
app_name=app_name,
|
|
1864
|
+
icon=icon,
|
|
1865
|
+
color=color,
|
|
1866
|
+
visibility=visibility,
|
|
1867
|
+
create_if_missing=create_if_missing,
|
|
1868
|
+
add_fields=add_fields,
|
|
1869
|
+
update_fields=update_fields,
|
|
1870
|
+
remove_fields=remove_fields,
|
|
1871
|
+
)
|
|
1872
|
+
if system_field_failure is not None:
|
|
1873
|
+
return system_field_failure
|
|
1561
1874
|
try:
|
|
1562
1875
|
request = SchemaPlanRequest.model_validate(
|
|
1563
1876
|
{
|
|
@@ -1651,6 +1964,52 @@ class AiBuilderTools(ToolBase):
|
|
|
1651
1964
|
suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_request}},
|
|
1652
1965
|
)
|
|
1653
1966
|
|
|
1967
|
+
@tool_cn_name("应用流程规划")
|
|
1968
|
+
def app_flow_plan(
|
|
1969
|
+
self,
|
|
1970
|
+
*,
|
|
1971
|
+
profile: str,
|
|
1972
|
+
app_key: str,
|
|
1973
|
+
mode: str = "replace",
|
|
1974
|
+
nodes: list[JSONObject] | None = None,
|
|
1975
|
+
transitions: list[JSONObject] | None = None,
|
|
1976
|
+
preset: str | None = None,
|
|
1977
|
+
) -> JSONObject:
|
|
1978
|
+
"""执行应用相关逻辑。"""
|
|
1979
|
+
try:
|
|
1980
|
+
request = FlowPlanRequest.model_validate(
|
|
1981
|
+
{
|
|
1982
|
+
"app_key": app_key,
|
|
1983
|
+
"mode": mode,
|
|
1984
|
+
"nodes": nodes or [],
|
|
1985
|
+
"transitions": transitions or [],
|
|
1986
|
+
"preset": preset,
|
|
1987
|
+
}
|
|
1988
|
+
)
|
|
1989
|
+
except ValidationError as exc:
|
|
1990
|
+
return _validation_failure(
|
|
1991
|
+
str(exc),
|
|
1992
|
+
tool_name="app_flow_plan",
|
|
1993
|
+
exc=exc,
|
|
1994
|
+
suggested_next_call={
|
|
1995
|
+
"tool_name": "app_flow_plan",
|
|
1996
|
+
"arguments": {
|
|
1997
|
+
"profile": profile,
|
|
1998
|
+
"app_key": app_key,
|
|
1999
|
+
"mode": "replace",
|
|
2000
|
+
"preset": "basic_approval",
|
|
2001
|
+
"nodes": [],
|
|
2002
|
+
"transitions": [],
|
|
2003
|
+
},
|
|
2004
|
+
},
|
|
2005
|
+
)
|
|
2006
|
+
return _safe_tool_call(
|
|
2007
|
+
lambda: self._facade.app_flow_plan(profile=profile, request=request),
|
|
2008
|
+
error_code="FLOW_PLAN_FAILED",
|
|
2009
|
+
normalized_args=request.model_dump(mode="json"),
|
|
2010
|
+
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
|
|
2011
|
+
)
|
|
2012
|
+
|
|
1654
2013
|
@tool_cn_name("应用视图规划")
|
|
1655
2014
|
def app_views_plan(
|
|
1656
2015
|
self,
|
|
@@ -1662,6 +2021,15 @@ class AiBuilderTools(ToolBase):
|
|
|
1662
2021
|
preset: str | None = None,
|
|
1663
2022
|
) -> JSONObject:
|
|
1664
2023
|
"""执行应用相关逻辑。"""
|
|
2024
|
+
reserved_failure = _reserved_system_view_name_failure(
|
|
2025
|
+
tool_name="app_views_plan",
|
|
2026
|
+
profile=profile,
|
|
2027
|
+
app_key=app_key,
|
|
2028
|
+
upsert_views=upsert_views,
|
|
2029
|
+
remove_views=remove_views,
|
|
2030
|
+
)
|
|
2031
|
+
if reserved_failure is not None:
|
|
2032
|
+
return reserved_failure
|
|
1665
2033
|
try:
|
|
1666
2034
|
request = ViewsPlanRequest.model_validate(
|
|
1667
2035
|
{
|
|
@@ -1703,8 +2071,11 @@ class AiBuilderTools(ToolBase):
|
|
|
1703
2071
|
package_id: int | None = None,
|
|
1704
2072
|
app_name: str = "",
|
|
1705
2073
|
app_title: str = "",
|
|
1706
|
-
icon: str = "",
|
|
2074
|
+
icon: str | JSONObject = "",
|
|
1707
2075
|
color: str = "",
|
|
2076
|
+
icon_name: str | None = None,
|
|
2077
|
+
icon_color: str | None = None,
|
|
2078
|
+
icon_config: JSONObject | None = None,
|
|
1708
2079
|
visibility: JSONObject | None = None,
|
|
1709
2080
|
create_if_missing: bool = False,
|
|
1710
2081
|
publish: bool = True,
|
|
@@ -1714,7 +2085,25 @@ class AiBuilderTools(ToolBase):
|
|
|
1714
2085
|
apps: list[JSONObject] | None = None,
|
|
1715
2086
|
) -> JSONObject:
|
|
1716
2087
|
"""执行应用相关逻辑。"""
|
|
1717
|
-
|
|
2088
|
+
icon, color = _normalize_builder_icon_args(
|
|
2089
|
+
icon=icon,
|
|
2090
|
+
color=color,
|
|
2091
|
+
icon_name=icon_name,
|
|
2092
|
+
icon_color=icon_color,
|
|
2093
|
+
icon_config=icon_config,
|
|
2094
|
+
)
|
|
2095
|
+
if apps is not None:
|
|
2096
|
+
normalized_apps_payload = _normalize_schema_apps_argument(
|
|
2097
|
+
tool_name="app_schema_apply",
|
|
2098
|
+
package_id=package_id,
|
|
2099
|
+
apps=apps,
|
|
2100
|
+
)
|
|
2101
|
+
failure = normalized_apps_payload.get("failure")
|
|
2102
|
+
if isinstance(failure, dict):
|
|
2103
|
+
return _attach_builder_apply_envelope("app_schema_apply", failure)
|
|
2104
|
+
package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
|
|
2105
|
+
apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
|
|
2106
|
+
input_warnings = list(normalized_apps_payload.get("warnings") or [])
|
|
1718
2107
|
result = self._app_schema_apply_multi(
|
|
1719
2108
|
profile=profile,
|
|
1720
2109
|
package_id=package_id,
|
|
@@ -1723,6 +2112,10 @@ class AiBuilderTools(ToolBase):
|
|
|
1723
2112
|
publish=publish,
|
|
1724
2113
|
apps=apps,
|
|
1725
2114
|
)
|
|
2115
|
+
if input_warnings:
|
|
2116
|
+
result_warnings = list(result.get("warnings") or [])
|
|
2117
|
+
result_warnings.extend(deepcopy(input_warnings))
|
|
2118
|
+
result["warnings"] = result_warnings
|
|
1726
2119
|
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
1727
2120
|
result = self._app_schema_apply_once(
|
|
1728
2121
|
profile=profile,
|
|
@@ -1778,6 +2171,17 @@ class AiBuilderTools(ToolBase):
|
|
|
1778
2171
|
}
|
|
1779
2172
|
if visibility is not None:
|
|
1780
2173
|
normalized_args["visibility"] = deepcopy(visibility)
|
|
2174
|
+
system_field_failure = _reserved_system_field_name_failure_for_apps(
|
|
2175
|
+
tool_name="app_schema_apply",
|
|
2176
|
+
profile=profile,
|
|
2177
|
+
package_id=package_id,
|
|
2178
|
+
visibility=visibility,
|
|
2179
|
+
create_if_missing=create_if_missing,
|
|
2180
|
+
publish=publish,
|
|
2181
|
+
apps=apps,
|
|
2182
|
+
)
|
|
2183
|
+
if system_field_failure is not None:
|
|
2184
|
+
return system_field_failure
|
|
1781
2185
|
if package_id is None:
|
|
1782
2186
|
return _config_failure(
|
|
1783
2187
|
tool_name="app_schema_apply",
|
|
@@ -1787,9 +2191,20 @@ class AiBuilderTools(ToolBase):
|
|
|
1787
2191
|
if not apps:
|
|
1788
2192
|
return _config_failure(
|
|
1789
2193
|
tool_name="app_schema_apply",
|
|
2194
|
+
error_code="APPS_FILE_EMPTY",
|
|
1790
2195
|
message="app_schema_apply multi-app mode requires non-empty apps.",
|
|
1791
2196
|
fix_hint="Pass apps as a non-empty list of app schema items.",
|
|
1792
2197
|
)
|
|
2198
|
+
shape_failure = _schema_apps_shape_failure(tool_name="app_schema_apply", apps=apps)
|
|
2199
|
+
if shape_failure is not None:
|
|
2200
|
+
return shape_failure
|
|
2201
|
+
static_validation_failure = _multi_app_static_validation_failure(
|
|
2202
|
+
tool_name="app_schema_apply",
|
|
2203
|
+
apps=apps,
|
|
2204
|
+
create_if_missing=create_if_missing,
|
|
2205
|
+
)
|
|
2206
|
+
if static_validation_failure is not None:
|
|
2207
|
+
return static_validation_failure
|
|
1793
2208
|
icon_errors: list[JSONObject] = []
|
|
1794
2209
|
seen_new_app_icons: dict[str, int] = {}
|
|
1795
2210
|
for index, raw_item in enumerate(apps):
|
|
@@ -1854,9 +2269,13 @@ class AiBuilderTools(ToolBase):
|
|
|
1854
2269
|
)
|
|
1855
2270
|
|
|
1856
2271
|
client_key_to_app_key: dict[str, str] = {}
|
|
2272
|
+
app_name_to_app_key: dict[str, str] = {}
|
|
1857
2273
|
created_app_keys: list[str] = []
|
|
1858
2274
|
results: list[JSONObject] = []
|
|
1859
2275
|
any_write_executed = False
|
|
2276
|
+
any_write_may_have_succeeded = False
|
|
2277
|
+
pending_readback_app_keys: list[str] = []
|
|
2278
|
+
pending_readback_app_names: list[str] = []
|
|
1860
2279
|
client_keys: set[str] = set()
|
|
1861
2280
|
|
|
1862
2281
|
for index, raw_item in enumerate(apps):
|
|
@@ -1895,31 +2314,58 @@ class AiBuilderTools(ToolBase):
|
|
|
1895
2314
|
)
|
|
1896
2315
|
public_shell = _publicize_package_fields(shell)
|
|
1897
2316
|
resolved_key = str(public_shell.get("app_key") or "").strip()
|
|
2317
|
+
shell_write_executed = _schema_apply_result_has_write(public_shell)
|
|
2318
|
+
shell_write_may_have_succeeded = _schema_apply_result_may_have_write(public_shell)
|
|
2319
|
+
if shell_write_executed:
|
|
2320
|
+
any_write_executed = True
|
|
2321
|
+
if shell_write_may_have_succeeded:
|
|
2322
|
+
any_write_may_have_succeeded = True
|
|
1898
2323
|
if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
|
|
2324
|
+
pending_readback = bool(shell_write_may_have_succeeded or shell_write_executed) and (
|
|
2325
|
+
str(public_shell.get("next_action") or "") == "readback_before_retry"
|
|
2326
|
+
or bool((public_shell.get("verification") if isinstance(public_shell.get("verification"), dict) else {}).get("readback_before_retry"))
|
|
2327
|
+
or not resolved_key
|
|
2328
|
+
)
|
|
2329
|
+
item_status = "pending_readback" if pending_readback else "failed"
|
|
2330
|
+
if pending_readback:
|
|
2331
|
+
if resolved_key:
|
|
2332
|
+
pending_readback_app_keys.append(resolved_key)
|
|
2333
|
+
if app_name:
|
|
2334
|
+
pending_readback_app_names.append(app_name)
|
|
1899
2335
|
results.append({
|
|
1900
2336
|
"index": index,
|
|
1901
2337
|
"row_number": index + 1,
|
|
1902
2338
|
"client_key": client_key or None,
|
|
1903
2339
|
"app_name": app_name or None,
|
|
1904
2340
|
"app_key": resolved_key or app_key or None,
|
|
1905
|
-
"status":
|
|
2341
|
+
"status": item_status,
|
|
1906
2342
|
"stage": "resolve_or_create_shell",
|
|
1907
2343
|
"error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
|
|
1908
2344
|
"message": public_shell.get("message") or "app shell resolve/create failed",
|
|
1909
|
-
"
|
|
2345
|
+
"write_executed": shell_write_executed,
|
|
2346
|
+
"write_may_have_succeeded": bool(shell_write_may_have_succeeded or shell_write_executed),
|
|
2347
|
+
"safe_to_retry": False if pending_readback else (not shell_write_executed and not any_write_executed),
|
|
2348
|
+
**({"next_action": "readback_before_retry"} if pending_readback else {}),
|
|
2349
|
+
**({"verification": public_shell.get("verification")} if isinstance(public_shell.get("verification"), dict) else {}),
|
|
2350
|
+
**({"created": True} if pending_readback and create_if_missing and not app_key else {}),
|
|
1910
2351
|
})
|
|
1911
2352
|
continue
|
|
1912
2353
|
if bool(public_shell.get("created")):
|
|
1913
2354
|
created_app_keys.append(resolved_key)
|
|
1914
|
-
if
|
|
1915
|
-
|
|
2355
|
+
if shell_write_may_have_succeeded and str(public_shell.get("next_action") or "") == "readback_before_retry":
|
|
2356
|
+
pending_readback_app_keys.append(resolved_key)
|
|
2357
|
+
if app_name:
|
|
2358
|
+
pending_readback_app_names.append(app_name)
|
|
1916
2359
|
if client_key:
|
|
1917
2360
|
client_key_to_app_key[client_key] = resolved_key
|
|
2361
|
+
resolved_name = str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip()
|
|
2362
|
+
if resolved_name:
|
|
2363
|
+
app_name_to_app_key[resolved_name] = resolved_key
|
|
1918
2364
|
results.append({
|
|
1919
2365
|
"index": index,
|
|
1920
2366
|
"row_number": index + 1,
|
|
1921
2367
|
"client_key": client_key or None,
|
|
1922
|
-
"app_name":
|
|
2368
|
+
"app_name": resolved_name or None,
|
|
1923
2369
|
"app_key": resolved_key,
|
|
1924
2370
|
"status": "shell_ready",
|
|
1925
2371
|
"created": bool(public_shell.get("created")),
|
|
@@ -1939,7 +2385,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1939
2385
|
item = deepcopy(raw_item)
|
|
1940
2386
|
app_key = str(existing.get("app_key") or "").strip()
|
|
1941
2387
|
try:
|
|
1942
|
-
compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key)
|
|
2388
|
+
compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key, app_name_to_app_key)
|
|
1943
2389
|
except ValueError as error:
|
|
1944
2390
|
final_items.append({
|
|
1945
2391
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
@@ -1976,7 +2422,11 @@ class AiBuilderTools(ToolBase):
|
|
|
1976
2422
|
"verified": bool(shell_result.get("verified")),
|
|
1977
2423
|
"error_code": shell_result.get("error_code"),
|
|
1978
2424
|
"message": shell_result.get("message"),
|
|
2425
|
+
"write_executed": _schema_apply_result_has_write(shell_result),
|
|
2426
|
+
"write_may_have_succeeded": _schema_apply_result_may_have_write(shell_result),
|
|
1979
2427
|
"safe_to_retry": False,
|
|
2428
|
+
**({"next_action": shell_result.get("next_action")} if shell_result.get("next_action") else {}),
|
|
2429
|
+
**({"verification": shell_result.get("verification")} if isinstance(shell_result.get("verification"), dict) else {}),
|
|
1980
2430
|
})
|
|
1981
2431
|
continue
|
|
1982
2432
|
|
|
@@ -1998,11 +2448,18 @@ class AiBuilderTools(ToolBase):
|
|
|
1998
2448
|
public_result = _publicize_package_fields(field_result)
|
|
1999
2449
|
if _schema_apply_result_has_write(public_result):
|
|
2000
2450
|
any_write_executed = True
|
|
2451
|
+
if _schema_apply_result_may_have_write(public_result):
|
|
2452
|
+
any_write_may_have_succeeded = True
|
|
2001
2453
|
item_status = public_result.get("status") if public_result.get("status") in {"success", "partial_success"} else "failed"
|
|
2002
2454
|
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
2003
2455
|
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
2004
2456
|
field_diff = _merge_schema_field_diffs(shell_field_diff, public_result.get("field_diff") or {})
|
|
2005
2457
|
field_diff_details = _merge_schema_field_diffs(shell_field_diff_details, public_result.get("field_diff_details") or {})
|
|
2458
|
+
if _schema_apply_result_may_have_write(public_result) and str(public_result.get("next_action") or "") == "readback_before_retry":
|
|
2459
|
+
if app_key:
|
|
2460
|
+
pending_readback_app_keys.append(app_key)
|
|
2461
|
+
if existing.get("app_name"):
|
|
2462
|
+
pending_readback_app_names.append(str(existing.get("app_name")))
|
|
2006
2463
|
final_items.append({
|
|
2007
2464
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2008
2465
|
"status": item_status,
|
|
@@ -2015,55 +2472,33 @@ class AiBuilderTools(ToolBase):
|
|
|
2015
2472
|
"verified": bool(public_result.get("verified")),
|
|
2016
2473
|
"error_code": public_result.get("error_code"),
|
|
2017
2474
|
"message": public_result.get("message"),
|
|
2475
|
+
"write_executed": _schema_apply_result_has_write(public_result),
|
|
2476
|
+
"write_may_have_succeeded": _schema_apply_result_may_have_write(public_result),
|
|
2018
2477
|
"safe_to_retry": False,
|
|
2478
|
+
**({"next_action": public_result.get("next_action")} if public_result.get("next_action") else {}),
|
|
2479
|
+
**({"verification": public_result.get("verification")} if isinstance(public_result.get("verification"), dict) else {}),
|
|
2019
2480
|
})
|
|
2020
2481
|
|
|
2021
|
-
|
|
2022
|
-
for index, raw_item in enumerate(apps):
|
|
2023
|
-
if not isinstance(raw_item, dict):
|
|
2024
|
-
continue
|
|
2025
|
-
layout_spec = raw_item.get("layout")
|
|
2026
|
-
if not isinstance(layout_spec, dict) or not layout_spec:
|
|
2027
|
-
continue
|
|
2028
|
-
final_item = next((it for it in final_items if it.get("index") == index), None)
|
|
2029
|
-
if not final_item or final_item.get("status") not in {"success", "partial_success"}:
|
|
2030
|
-
continue
|
|
2031
|
-
resolved_app_key = str(final_item.get("app_key") or "").strip()
|
|
2032
|
-
if not resolved_app_key:
|
|
2033
|
-
continue
|
|
2034
|
-
layout_mode = str(layout_spec.get("mode") or "merge").strip() or "merge"
|
|
2035
|
-
layout_sections = layout_spec.get("sections") or []
|
|
2036
|
-
try:
|
|
2037
|
-
layout_result = self.app_layout_apply(
|
|
2038
|
-
profile=profile,
|
|
2039
|
-
app_key=resolved_app_key,
|
|
2040
|
-
mode=layout_mode,
|
|
2041
|
-
publish=publish,
|
|
2042
|
-
sections=list(layout_sections),
|
|
2043
|
-
)
|
|
2044
|
-
layout_ok = layout_result.get("status") in {"success", "partial_success"}
|
|
2045
|
-
except Exception as layout_error:
|
|
2046
|
-
layout_ok = False
|
|
2047
|
-
layout_result = {"status": "failed", "message": str(layout_error)}
|
|
2048
|
-
if layout_ok:
|
|
2049
|
-
final_item["layout_applied"] = True
|
|
2050
|
-
final_item["layout_status"] = layout_result.get("status")
|
|
2051
|
-
else:
|
|
2052
|
-
final_item["layout_warning"] = layout_result.get("error_code") or "LAYOUT_APPLY_FAILED"
|
|
2053
|
-
final_item["layout_message"] = layout_result.get("message")
|
|
2054
|
-
|
|
2482
|
+
pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
|
|
2055
2483
|
succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
|
|
2056
|
-
failed =
|
|
2057
|
-
|
|
2058
|
-
|
|
2484
|
+
failed = sum(1 for item in final_items if item.get("status") == "failed")
|
|
2485
|
+
partial = sum(1 for item in final_items if item.get("status") == "partial_success")
|
|
2486
|
+
overall_status = (
|
|
2487
|
+
"success"
|
|
2488
|
+
if failed == 0 and pending_readback == 0 and partial == 0
|
|
2489
|
+
else ("partial_success" if succeeded > 0 or pending_readback > 0 or any_write_executed or any_write_may_have_succeeded else "failed")
|
|
2490
|
+
)
|
|
2491
|
+
uncertain_write = any_write_may_have_succeeded or pending_readback > 0
|
|
2492
|
+
response: JSONObject = {
|
|
2059
2493
|
"status": overall_status,
|
|
2060
2494
|
"mode": "multi_app",
|
|
2061
2495
|
"total": len(apps),
|
|
2062
2496
|
"succeeded": succeeded,
|
|
2063
2497
|
"failed": failed,
|
|
2498
|
+
"pending_readback": pending_readback,
|
|
2064
2499
|
"created_app_keys": created_app_keys,
|
|
2065
2500
|
"write_executed": any_write_executed,
|
|
2066
|
-
"safe_to_retry": not any_write_executed,
|
|
2501
|
+
"safe_to_retry": not (any_write_executed or uncertain_write),
|
|
2067
2502
|
"package_id": package_id,
|
|
2068
2503
|
"publish_requested": publish,
|
|
2069
2504
|
"apps": final_items,
|
|
@@ -2071,12 +2506,23 @@ class AiBuilderTools(ToolBase):
|
|
|
2071
2506
|
"verification": {
|
|
2072
2507
|
"all_apps_succeeded": failed == 0,
|
|
2073
2508
|
"created_app_count": len(created_app_keys),
|
|
2509
|
+
"pending_readback_count": pending_readback,
|
|
2074
2510
|
},
|
|
2075
2511
|
"request_id": None,
|
|
2076
2512
|
"error_code": None if overall_status != "failed" else "MULTI_APP_SCHEMA_APPLY_FAILED",
|
|
2077
2513
|
"recoverable": overall_status != "success",
|
|
2078
|
-
"message":
|
|
2514
|
+
"message": (
|
|
2515
|
+
"multi-app schema apply needs readback before retry"
|
|
2516
|
+
if pending_readback > 0
|
|
2517
|
+
else ("multi-app schema apply completed" if overall_status != "failed" else "multi-app schema apply failed")
|
|
2518
|
+
),
|
|
2079
2519
|
}
|
|
2520
|
+
if uncertain_write:
|
|
2521
|
+
response["write_may_have_succeeded"] = True
|
|
2522
|
+
response["next_action"] = "readback_before_retry"
|
|
2523
|
+
response["pending_readback_app_keys"] = list(dict.fromkeys(pending_readback_app_keys))
|
|
2524
|
+
response["pending_readback_app_names"] = list(dict.fromkeys(pending_readback_app_names))
|
|
2525
|
+
return response
|
|
2080
2526
|
|
|
2081
2527
|
def _app_schema_apply_once(
|
|
2082
2528
|
self,
|
|
@@ -2097,6 +2543,23 @@ class AiBuilderTools(ToolBase):
|
|
|
2097
2543
|
) -> JSONObject:
|
|
2098
2544
|
"""执行内部辅助逻辑。"""
|
|
2099
2545
|
effective_app_name = app_name or app_title
|
|
2546
|
+
system_field_failure = _reserved_system_field_name_failure(
|
|
2547
|
+
tool_name="app_schema_apply",
|
|
2548
|
+
profile=profile,
|
|
2549
|
+
app_key=app_key,
|
|
2550
|
+
package_id=package_id,
|
|
2551
|
+
app_name=effective_app_name,
|
|
2552
|
+
icon=icon,
|
|
2553
|
+
color=color,
|
|
2554
|
+
visibility=visibility,
|
|
2555
|
+
create_if_missing=create_if_missing,
|
|
2556
|
+
publish=publish,
|
|
2557
|
+
add_fields=add_fields,
|
|
2558
|
+
update_fields=update_fields,
|
|
2559
|
+
remove_fields=remove_fields,
|
|
2560
|
+
)
|
|
2561
|
+
if system_field_failure is not None:
|
|
2562
|
+
return system_field_failure
|
|
2100
2563
|
icon_failure = _validate_workspace_icon_for_builder(
|
|
2101
2564
|
tool_name="app_schema_apply",
|
|
2102
2565
|
icon=icon,
|
|
@@ -2191,20 +2654,30 @@ class AiBuilderTools(ToolBase):
|
|
|
2191
2654
|
return _publicize_package_fields(result)
|
|
2192
2655
|
|
|
2193
2656
|
@tool_cn_name("应用布局应用")
|
|
2194
|
-
def app_layout_apply(
|
|
2657
|
+
def app_layout_apply(
|
|
2658
|
+
self,
|
|
2659
|
+
*,
|
|
2660
|
+
profile: str,
|
|
2661
|
+
app_key: str,
|
|
2662
|
+
mode: str = "merge",
|
|
2663
|
+
publish: bool = True,
|
|
2664
|
+
sections: list[JSONObject],
|
|
2665
|
+
apps: list[JSONObject] | None = None,
|
|
2666
|
+
) -> JSONObject:
|
|
2195
2667
|
"""执行应用相关逻辑。"""
|
|
2196
|
-
if apps:
|
|
2197
|
-
return self.
|
|
2668
|
+
if apps is not None:
|
|
2669
|
+
return self._apply_app_batch(
|
|
2670
|
+
tool_name="app_layout_apply",
|
|
2198
2671
|
profile=profile,
|
|
2199
2672
|
apps=apps,
|
|
2200
|
-
|
|
2673
|
+
apply_one=lambda _index, item, item_app_key: self.app_layout_apply(
|
|
2201
2674
|
profile=profile,
|
|
2202
|
-
app_key=
|
|
2203
|
-
mode=
|
|
2204
|
-
publish=
|
|
2205
|
-
sections=
|
|
2675
|
+
app_key=item_app_key,
|
|
2676
|
+
mode=str(_payload_get(item, "mode", default=mode) or mode),
|
|
2677
|
+
publish=_payload_bool(item, "publish", default=publish),
|
|
2678
|
+
sections=_payload_list(item, "sections", default=sections or []),
|
|
2679
|
+
apps=None,
|
|
2206
2680
|
),
|
|
2207
|
-
tool_name="app_layout_apply",
|
|
2208
2681
|
)
|
|
2209
2682
|
result = self._app_layout_apply_once(
|
|
2210
2683
|
profile=profile,
|
|
@@ -2290,15 +2763,25 @@ class AiBuilderTools(ToolBase):
|
|
|
2290
2763
|
*,
|
|
2291
2764
|
profile: str,
|
|
2292
2765
|
app_key: str,
|
|
2293
|
-
|
|
2766
|
+
mode: str = "replace",
|
|
2294
2767
|
publish: bool = True,
|
|
2768
|
+
nodes: list[JSONObject] | None = None,
|
|
2769
|
+
transitions: list[JSONObject] | None = None,
|
|
2770
|
+
spec: JSONObject | None = None,
|
|
2295
2771
|
idempotency_key: str | None = None,
|
|
2296
2772
|
schema_version: str | None = None,
|
|
2297
2773
|
patch_nodes: list[JSONObject] | None = None,
|
|
2298
2774
|
) -> JSONObject:
|
|
2299
2775
|
"""执行应用相关逻辑。"""
|
|
2300
|
-
if patch_nodes:
|
|
2301
|
-
|
|
2776
|
+
if patch_nodes is not None:
|
|
2777
|
+
normalized_args = {
|
|
2778
|
+
"app_key": app_key,
|
|
2779
|
+
"publish": publish,
|
|
2780
|
+
"patch_nodes": patch_nodes,
|
|
2781
|
+
"idempotency_key": idempotency_key,
|
|
2782
|
+
"schema_version": schema_version,
|
|
2783
|
+
}
|
|
2784
|
+
return _attach_builder_apply_envelope("app_flow_apply", _safe_tool_call(
|
|
2302
2785
|
lambda: self._facade.flow_patch_nodes(
|
|
2303
2786
|
profile=profile,
|
|
2304
2787
|
app_key=app_key,
|
|
@@ -2307,50 +2790,132 @@ class AiBuilderTools(ToolBase):
|
|
|
2307
2790
|
idempotency_key=idempotency_key,
|
|
2308
2791
|
schema_version=schema_version,
|
|
2309
2792
|
),
|
|
2310
|
-
error_code="
|
|
2311
|
-
normalized_args=
|
|
2312
|
-
suggested_next_call={"tool_name": "
|
|
2793
|
+
error_code="FLOW_PATCH_FAILED",
|
|
2794
|
+
normalized_args=normalized_args,
|
|
2795
|
+
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2796
|
+
))
|
|
2797
|
+
if spec is not None:
|
|
2798
|
+
normalized_args = {
|
|
2799
|
+
"app_key": app_key,
|
|
2800
|
+
"publish": publish,
|
|
2801
|
+
"spec": spec,
|
|
2802
|
+
"idempotency_key": idempotency_key,
|
|
2803
|
+
"schema_version": schema_version,
|
|
2804
|
+
}
|
|
2805
|
+
return _attach_builder_apply_envelope("app_flow_apply", _safe_tool_call(
|
|
2806
|
+
lambda: self._facade.flow_apply(
|
|
2807
|
+
profile=profile,
|
|
2808
|
+
app_key=app_key,
|
|
2809
|
+
spec=spec,
|
|
2810
|
+
publish=publish,
|
|
2811
|
+
idempotency_key=idempotency_key,
|
|
2812
|
+
schema_version=schema_version,
|
|
2813
|
+
),
|
|
2814
|
+
error_code="FLOW_SPEC_APPLY_FAILED",
|
|
2815
|
+
normalized_args=normalized_args,
|
|
2816
|
+
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2817
|
+
))
|
|
2818
|
+
effective_nodes = nodes or []
|
|
2819
|
+
effective_transitions = transitions or []
|
|
2820
|
+
result = self._app_flow_apply_once(
|
|
2821
|
+
profile=profile,
|
|
2822
|
+
app_key=app_key,
|
|
2823
|
+
mode=mode,
|
|
2824
|
+
publish=publish,
|
|
2825
|
+
nodes=effective_nodes,
|
|
2826
|
+
transitions=effective_transitions,
|
|
2827
|
+
)
|
|
2828
|
+
result = self._retry_after_self_lock_release(
|
|
2829
|
+
profile=profile,
|
|
2830
|
+
result=result,
|
|
2831
|
+
retry_call=lambda: self._app_flow_apply_once(
|
|
2832
|
+
profile=profile,
|
|
2833
|
+
app_key=app_key,
|
|
2834
|
+
mode=mode,
|
|
2835
|
+
publish=publish,
|
|
2836
|
+
nodes=effective_nodes,
|
|
2837
|
+
transitions=effective_transitions,
|
|
2838
|
+
),
|
|
2839
|
+
)
|
|
2840
|
+
return _attach_builder_apply_envelope("app_flow_apply", result)
|
|
2841
|
+
|
|
2842
|
+
def _app_flow_apply_once(
|
|
2843
|
+
self,
|
|
2844
|
+
*,
|
|
2845
|
+
profile: str,
|
|
2846
|
+
app_key: str,
|
|
2847
|
+
mode: str = "replace",
|
|
2848
|
+
publish: bool = True,
|
|
2849
|
+
nodes: list[JSONObject],
|
|
2850
|
+
transitions: list[JSONObject],
|
|
2851
|
+
) -> JSONObject:
|
|
2852
|
+
"""执行内部辅助逻辑。"""
|
|
2853
|
+
plan_result = self._rewrite_plan_result_for_apply(
|
|
2854
|
+
result=self.app_flow_plan(
|
|
2855
|
+
profile=profile,
|
|
2856
|
+
app_key=app_key,
|
|
2857
|
+
mode=mode,
|
|
2858
|
+
nodes=nodes,
|
|
2859
|
+
transitions=transitions,
|
|
2860
|
+
preset=None,
|
|
2861
|
+
),
|
|
2862
|
+
profile=profile,
|
|
2863
|
+
publish=publish,
|
|
2864
|
+
plan_tool_name="app_flow_plan",
|
|
2865
|
+
apply_tool_name="app_flow_apply",
|
|
2866
|
+
)
|
|
2867
|
+
if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
|
|
2868
|
+
return plan_result
|
|
2869
|
+
plan_args = plan_result.get("normalized_args")
|
|
2870
|
+
if not isinstance(plan_args, dict):
|
|
2871
|
+
plan_args = {}
|
|
2872
|
+
try:
|
|
2873
|
+
request = FlowPlanRequest.model_validate(
|
|
2874
|
+
{
|
|
2875
|
+
"app_key": plan_args.get("app_key") or app_key,
|
|
2876
|
+
"mode": plan_args.get("mode") or mode,
|
|
2877
|
+
"nodes": plan_args.get("nodes") or [],
|
|
2878
|
+
"transitions": plan_args.get("transitions") or [],
|
|
2879
|
+
"preset": None,
|
|
2880
|
+
}
|
|
2313
2881
|
)
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2882
|
+
except ValidationError as exc:
|
|
2883
|
+
return _validation_failure(
|
|
2884
|
+
str(exc),
|
|
2317
2885
|
tool_name="app_flow_apply",
|
|
2318
|
-
|
|
2319
|
-
|
|
2886
|
+
exc=exc,
|
|
2887
|
+
suggested_next_call={
|
|
2888
|
+
"tool_name": "app_flow_apply",
|
|
2889
|
+
"arguments": {
|
|
2890
|
+
"profile": profile,
|
|
2891
|
+
"app_key": str(plan_args.get("app_key") or app_key),
|
|
2892
|
+
"mode": str(plan_args.get("mode") or "replace"),
|
|
2893
|
+
"publish": publish,
|
|
2894
|
+
"nodes": plan_args.get("nodes") or [{"id": "start", "type": "start", "name": "发起"}],
|
|
2895
|
+
"transitions": plan_args.get("transitions") or [],
|
|
2896
|
+
},
|
|
2897
|
+
},
|
|
2320
2898
|
)
|
|
2321
2899
|
normalized_args = {
|
|
2322
|
-
"app_key": app_key,
|
|
2900
|
+
"app_key": request.app_key,
|
|
2901
|
+
"mode": request.mode,
|
|
2323
2902
|
"publish": publish,
|
|
2324
|
-
"
|
|
2325
|
-
"
|
|
2326
|
-
"schema_version": schema_version,
|
|
2903
|
+
"nodes": [node.model_dump(mode="json") for node in request.nodes],
|
|
2904
|
+
"transitions": [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
|
|
2327
2905
|
}
|
|
2328
|
-
|
|
2329
|
-
lambda: self._facade.
|
|
2906
|
+
return _safe_tool_call(
|
|
2907
|
+
lambda: self._facade.app_flow_apply(
|
|
2330
2908
|
profile=profile,
|
|
2331
|
-
app_key=app_key,
|
|
2332
|
-
|
|
2909
|
+
app_key=request.app_key,
|
|
2910
|
+
mode=request.mode,
|
|
2333
2911
|
publish=publish,
|
|
2334
|
-
|
|
2335
|
-
|
|
2912
|
+
nodes=[node.model_dump(mode="json") for node in request.nodes],
|
|
2913
|
+
transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
|
|
2336
2914
|
),
|
|
2337
2915
|
error_code="FLOW_APPLY_FAILED",
|
|
2338
2916
|
normalized_args=normalized_args,
|
|
2339
2917
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2340
2918
|
)
|
|
2341
|
-
result = self._retry_after_self_lock_release(
|
|
2342
|
-
profile=profile,
|
|
2343
|
-
result=result,
|
|
2344
|
-
retry_call=lambda: self._facade.flow_apply(
|
|
2345
|
-
profile=profile,
|
|
2346
|
-
app_key=app_key,
|
|
2347
|
-
spec=spec,
|
|
2348
|
-
publish=publish,
|
|
2349
|
-
idempotency_key=idempotency_key,
|
|
2350
|
-
schema_version=schema_version,
|
|
2351
|
-
),
|
|
2352
|
-
)
|
|
2353
|
-
return _attach_builder_apply_envelope("app_flow_apply", result)
|
|
2354
2919
|
|
|
2355
2920
|
@tool_cn_name("应用视图应用")
|
|
2356
2921
|
def app_views_apply(
|
|
@@ -2365,19 +2930,20 @@ class AiBuilderTools(ToolBase):
|
|
|
2365
2930
|
apps: list[JSONObject] | None = None,
|
|
2366
2931
|
) -> JSONObject:
|
|
2367
2932
|
"""执行应用相关逻辑。"""
|
|
2368
|
-
if apps:
|
|
2369
|
-
return self.
|
|
2933
|
+
if apps is not None:
|
|
2934
|
+
return self._apply_app_batch(
|
|
2935
|
+
tool_name="app_views_apply",
|
|
2370
2936
|
profile=profile,
|
|
2371
2937
|
apps=apps,
|
|
2372
|
-
|
|
2938
|
+
apply_one=lambda _index, item, item_app_key: self.app_views_apply(
|
|
2373
2939
|
profile=profile,
|
|
2374
|
-
app_key=
|
|
2375
|
-
publish=
|
|
2376
|
-
upsert_views=
|
|
2377
|
-
patch_views=
|
|
2378
|
-
remove_views=
|
|
2940
|
+
app_key=item_app_key,
|
|
2941
|
+
publish=_payload_bool(item, "publish", default=publish),
|
|
2942
|
+
upsert_views=_payload_list(item, "upsert_views", "upsertViews", "views", default=upsert_views or []),
|
|
2943
|
+
patch_views=_payload_list(item, "patch_views", "patchViews", default=patch_views or []),
|
|
2944
|
+
remove_views=_payload_list(item, "remove_views", "removeViews", default=remove_views or []),
|
|
2945
|
+
apps=None,
|
|
2379
2946
|
),
|
|
2380
|
-
tool_name="app_views_apply",
|
|
2381
2947
|
)
|
|
2382
2948
|
result = self._app_views_apply_once(
|
|
2383
2949
|
profile=profile,
|
|
@@ -2412,6 +2978,17 @@ class AiBuilderTools(ToolBase):
|
|
|
2412
2978
|
remove_views: list[str],
|
|
2413
2979
|
) -> JSONObject:
|
|
2414
2980
|
"""执行内部辅助逻辑。"""
|
|
2981
|
+
reserved_failure = _reserved_system_view_name_failure(
|
|
2982
|
+
tool_name="app_views_apply",
|
|
2983
|
+
profile=profile,
|
|
2984
|
+
app_key=app_key,
|
|
2985
|
+
publish=publish,
|
|
2986
|
+
upsert_views=upsert_views,
|
|
2987
|
+
patch_views=patch_views,
|
|
2988
|
+
remove_views=remove_views,
|
|
2989
|
+
)
|
|
2990
|
+
if reserved_failure is not None:
|
|
2991
|
+
return reserved_failure
|
|
2415
2992
|
if patch_views:
|
|
2416
2993
|
try:
|
|
2417
2994
|
parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
|
|
@@ -2486,7 +3063,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2486
3063
|
"profile": profile,
|
|
2487
3064
|
"app_key": str(plan_args.get("app_key") or app_key),
|
|
2488
3065
|
"publish": publish,
|
|
2489
|
-
"upsert_views": plan_args.get("upsert_views") or [{"name": "
|
|
3066
|
+
"upsert_views": plan_args.get("upsert_views") or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
|
|
2490
3067
|
"remove_views": plan_args.get("remove_views") or [],
|
|
2491
3068
|
},
|
|
2492
3069
|
},
|
|
@@ -2545,19 +3122,20 @@ class AiBuilderTools(ToolBase):
|
|
|
2545
3122
|
apps: list[JSONObject] | None = None,
|
|
2546
3123
|
) -> JSONObject:
|
|
2547
3124
|
"""执行应用相关逻辑。"""
|
|
2548
|
-
if apps:
|
|
2549
|
-
return self.
|
|
3125
|
+
if apps is not None:
|
|
3126
|
+
return self._apply_app_batch(
|
|
3127
|
+
tool_name="app_charts_apply",
|
|
2550
3128
|
profile=profile,
|
|
2551
3129
|
apps=apps,
|
|
2552
|
-
|
|
3130
|
+
apply_one=lambda _index, item, item_app_key: self.app_charts_apply(
|
|
2553
3131
|
profile=profile,
|
|
2554
|
-
app_key=
|
|
2555
|
-
upsert_charts=
|
|
2556
|
-
patch_charts=
|
|
2557
|
-
remove_chart_ids=
|
|
2558
|
-
reorder_chart_ids=
|
|
3132
|
+
app_key=item_app_key,
|
|
3133
|
+
upsert_charts=_payload_list(item, "upsert_charts", "upsertCharts", "charts", default=upsert_charts or []),
|
|
3134
|
+
patch_charts=_payload_list(item, "patch_charts", "patchCharts", default=patch_charts or []),
|
|
3135
|
+
remove_chart_ids=_payload_list(item, "remove_chart_ids", "removeChartIds", default=remove_chart_ids or []),
|
|
3136
|
+
reorder_chart_ids=_payload_list(item, "reorder_chart_ids", "reorderChartIds", default=reorder_chart_ids or []),
|
|
3137
|
+
apps=None,
|
|
2559
3138
|
),
|
|
2560
|
-
tool_name="app_charts_apply",
|
|
2561
3139
|
)
|
|
2562
3140
|
try:
|
|
2563
3141
|
request = ChartApplyRequest.model_validate(
|
|
@@ -2577,12 +3155,12 @@ class AiBuilderTools(ToolBase):
|
|
|
2577
3155
|
suggested_next_call={
|
|
2578
3156
|
"tool_name": "app_charts_apply",
|
|
2579
3157
|
"arguments": {
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
3158
|
+
"profile": profile,
|
|
3159
|
+
"app_key": app_key,
|
|
3160
|
+
"upsert_charts": [{"name": "销售总量", "chart_type": "target", "metric": "count(*)"}],
|
|
3161
|
+
"remove_chart_ids": [],
|
|
3162
|
+
"reorder_chart_ids": [],
|
|
3163
|
+
},
|
|
2586
3164
|
},
|
|
2587
3165
|
))
|
|
2588
3166
|
normalized_args = request.model_dump(mode="json")
|
|
@@ -2617,25 +3195,23 @@ class AiBuilderTools(ToolBase):
|
|
|
2617
3195
|
patch_sections: list[JSONObject] | None = None,
|
|
2618
3196
|
) -> JSONObject:
|
|
2619
3197
|
"""执行门户相关逻辑。"""
|
|
2620
|
-
if patch_sections:
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
result = _safe_tool_call(
|
|
3198
|
+
if patch_sections is not None:
|
|
3199
|
+
normalized_args = {
|
|
3200
|
+
"dash_key": dash_key,
|
|
3201
|
+
"publish": publish,
|
|
3202
|
+
"patch_sections": patch_sections,
|
|
3203
|
+
}
|
|
3204
|
+
return _attach_builder_apply_envelope("portal_apply", _publicize_package_fields(_safe_tool_call(
|
|
2628
3205
|
lambda: self._facade.portal_patch_sections(
|
|
2629
3206
|
profile=profile,
|
|
2630
3207
|
dash_key=dash_key,
|
|
2631
3208
|
patch_sections=patch_sections,
|
|
2632
3209
|
publish=publish,
|
|
2633
3210
|
),
|
|
2634
|
-
error_code="
|
|
2635
|
-
normalized_args=
|
|
2636
|
-
suggested_next_call={"tool_name": "
|
|
2637
|
-
)
|
|
2638
|
-
return _attach_builder_apply_envelope("portal_apply", result)
|
|
3211
|
+
error_code="PORTAL_PATCH_FAILED",
|
|
3212
|
+
normalized_args=normalized_args,
|
|
3213
|
+
suggested_next_call={"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key}},
|
|
3214
|
+
)))
|
|
2639
3215
|
request_payload: dict[str, Any] = dict(payload) if isinstance(payload, dict) else {}
|
|
2640
3216
|
if dash_key:
|
|
2641
3217
|
request_payload["dash_key"] = dash_key
|
|
@@ -2712,23 +3288,39 @@ class AiBuilderTools(ToolBase):
|
|
|
2712
3288
|
))
|
|
2713
3289
|
return _attach_builder_apply_envelope("portal_apply", result)
|
|
2714
3290
|
|
|
3291
|
+
@tool_cn_name("门户删除")
|
|
3292
|
+
def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
|
|
3293
|
+
"""执行门户删除逻辑。"""
|
|
3294
|
+
normalized_args = {"dash_key": dash_key}
|
|
3295
|
+
result = _publicize_package_fields(_safe_tool_call(
|
|
3296
|
+
lambda: self._facade.portal_delete(profile=profile, dash_key=dash_key),
|
|
3297
|
+
error_code="PORTAL_DELETE_FAILED",
|
|
3298
|
+
normalized_args=normalized_args,
|
|
3299
|
+
suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, **normalized_args}},
|
|
3300
|
+
))
|
|
3301
|
+
return _attach_builder_apply_envelope("portal_delete", result)
|
|
3302
|
+
|
|
2715
3303
|
@tool_cn_name("应用发布校验")
|
|
2716
3304
|
def app_publish_verify(
|
|
2717
3305
|
self,
|
|
2718
3306
|
*,
|
|
2719
3307
|
profile: str,
|
|
2720
|
-
app_key: str
|
|
3308
|
+
app_key: str,
|
|
2721
3309
|
app_keys: list[str] | None = None,
|
|
2722
3310
|
expected_package_id: int | None = None,
|
|
2723
3311
|
) -> JSONObject:
|
|
2724
3312
|
"""执行应用相关逻辑。"""
|
|
2725
|
-
if app_keys:
|
|
2726
|
-
return self.
|
|
2727
|
-
profile=profile,
|
|
2728
|
-
app_keys=app_keys,
|
|
2729
|
-
single_reader=lambda profile, app_key: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_id),
|
|
2730
|
-
data_key="verification",
|
|
3313
|
+
if app_keys is not None:
|
|
3314
|
+
return self._apply_app_batch(
|
|
2731
3315
|
tool_name="app_publish_verify",
|
|
3316
|
+
profile=profile,
|
|
3317
|
+
apps=[{"app_key": item, "expected_package_id": expected_package_id} for item in app_keys],
|
|
3318
|
+
apply_one=lambda _index, item, item_app_key: self.app_publish_verify(
|
|
3319
|
+
profile=profile,
|
|
3320
|
+
app_key=item_app_key,
|
|
3321
|
+
app_keys=None,
|
|
3322
|
+
expected_package_id=item.get("expected_package_id") or item.get("expectedPackageId") or expected_package_id,
|
|
3323
|
+
),
|
|
2732
3324
|
)
|
|
2733
3325
|
normalized_args = {"app_key": app_key, "expected_package_id": expected_package_id}
|
|
2734
3326
|
result = _publicize_package_fields(_safe_tool_call(
|
|
@@ -2862,33 +3454,442 @@ class AiBuilderTools(ToolBase):
|
|
|
2862
3454
|
"tool_name": apply_tool_name,
|
|
2863
3455
|
"arguments": {"profile": profile, **normalized_args, "publish": publish},
|
|
2864
3456
|
}
|
|
2865
|
-
return rewritten
|
|
3457
|
+
return rewritten
|
|
3458
|
+
|
|
3459
|
+
|
|
3460
|
+
def _multi_app_item_failure(index: int, item: object, error_code: str, message: str) -> JSONObject:
|
|
3461
|
+
app_name = None
|
|
3462
|
+
client_key = None
|
|
3463
|
+
app_key = None
|
|
3464
|
+
if isinstance(item, dict):
|
|
3465
|
+
app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip() or None
|
|
3466
|
+
client_key = str(item.get("client_key") or item.get("clientKey") or "").strip() or None
|
|
3467
|
+
app_key = str(item.get("app_key") or item.get("appKey") or "").strip() or None
|
|
3468
|
+
return {
|
|
3469
|
+
"index": index,
|
|
3470
|
+
"row_number": index + 1,
|
|
3471
|
+
"client_key": client_key,
|
|
3472
|
+
"app_name": app_name,
|
|
3473
|
+
"app_key": app_key,
|
|
3474
|
+
"status": "failed",
|
|
3475
|
+
"stage": "validate_item",
|
|
3476
|
+
"error_code": error_code,
|
|
3477
|
+
"message": message,
|
|
3478
|
+
"safe_to_retry": True,
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
|
|
3482
|
+
def _normalize_builder_icon_args(
|
|
3483
|
+
*,
|
|
3484
|
+
icon: str | JSONObject | None,
|
|
3485
|
+
color: str | None,
|
|
3486
|
+
icon_name: str | None = None,
|
|
3487
|
+
icon_color: str | None = None,
|
|
3488
|
+
icon_config: JSONObject | None = None,
|
|
3489
|
+
) -> tuple[str | None, str | None]:
|
|
3490
|
+
payloads: list[JSONObject] = []
|
|
3491
|
+
if isinstance(icon_config, dict):
|
|
3492
|
+
payloads.append(icon_config)
|
|
3493
|
+
if isinstance(icon, dict):
|
|
3494
|
+
payloads.append(icon)
|
|
3495
|
+
|
|
3496
|
+
normalized_icon = None if isinstance(icon, dict) else icon
|
|
3497
|
+
normalized_color = color
|
|
3498
|
+
for payload in payloads:
|
|
3499
|
+
normalized_icon = normalized_icon or payload.get("icon") or payload.get("icon_name") or payload.get("iconName") or payload.get("name")
|
|
3500
|
+
normalized_color = normalized_color or payload.get("color") or payload.get("icon_color") or payload.get("iconColor")
|
|
3501
|
+
normalized_icon = normalized_icon or icon_name
|
|
3502
|
+
normalized_color = normalized_color or icon_color
|
|
3503
|
+
return (
|
|
3504
|
+
str(normalized_icon).strip() if normalized_icon is not None else None,
|
|
3505
|
+
str(normalized_color).strip() if normalized_color is not None else None,
|
|
3506
|
+
)
|
|
3507
|
+
|
|
3508
|
+
|
|
3509
|
+
def _normalize_schema_app_item(item: JSONObject) -> JSONObject:
|
|
3510
|
+
normalized = deepcopy(item)
|
|
3511
|
+
icon, color = _normalize_builder_icon_args(
|
|
3512
|
+
icon=normalized.get("icon"),
|
|
3513
|
+
color=normalized.get("color"),
|
|
3514
|
+
icon_name=normalized.get("icon_name") or normalized.get("iconName"),
|
|
3515
|
+
icon_color=normalized.get("icon_color") or normalized.get("iconColor"),
|
|
3516
|
+
icon_config=normalized.get("icon_config") or normalized.get("iconConfig"),
|
|
3517
|
+
)
|
|
3518
|
+
if icon:
|
|
3519
|
+
normalized["icon"] = icon
|
|
3520
|
+
if color:
|
|
3521
|
+
normalized["color"] = color
|
|
3522
|
+
for key in ("icon_name", "iconName", "icon_color", "iconColor", "icon_config", "iconConfig"):
|
|
3523
|
+
normalized.pop(key, None)
|
|
3524
|
+
return normalized
|
|
3525
|
+
|
|
3526
|
+
|
|
3527
|
+
def _schema_apps_expected_shape() -> JSONObject:
|
|
3528
|
+
return {
|
|
3529
|
+
"package_id": 1001,
|
|
3530
|
+
"apps": [
|
|
3531
|
+
{
|
|
3532
|
+
"client_key": "employee",
|
|
3533
|
+
"app_name": "员工花名册",
|
|
3534
|
+
"icon": "business-personalcard",
|
|
3535
|
+
"color": "emerald",
|
|
3536
|
+
"add_fields": [{"name": "员工名称", "type": "text", "as_data_title": True}],
|
|
3537
|
+
}
|
|
3538
|
+
],
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
|
|
3542
|
+
def _schema_apps_expected_shape_json() -> str:
|
|
3543
|
+
return (
|
|
3544
|
+
'{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
|
|
3545
|
+
'"icon":"business-personalcard","color":"emerald","add_fields":[{"name":"员工名称","type":"text","as_data_title":true}]}]}'
|
|
3546
|
+
)
|
|
3547
|
+
|
|
3548
|
+
|
|
3549
|
+
def _is_schema_apps_wrapper_item(item: object) -> bool:
|
|
3550
|
+
return isinstance(item, dict) and "apps" in item
|
|
3551
|
+
|
|
3552
|
+
|
|
3553
|
+
def _schema_apps_shape_failure(*, tool_name: str, apps: list[JSONObject]) -> JSONObject | None:
|
|
3554
|
+
for index, item in enumerate(apps):
|
|
3555
|
+
if _is_schema_apps_wrapper_item(item):
|
|
3556
|
+
return _config_failure(
|
|
3557
|
+
tool_name=tool_name,
|
|
3558
|
+
error_code="APPS_FILE_SHAPE_INVALID",
|
|
3559
|
+
message="apps[] items must be app schema items, not package wrapper objects.",
|
|
3560
|
+
fix_hint=(
|
|
3561
|
+
"For MCP pass package_id separately and apps=[{app_name, icon, color, add_fields}]. "
|
|
3562
|
+
'For CLI use --apps-file with {"package_id":1001,"apps":[...]}. '
|
|
3563
|
+
"Do not pass multiple {package_id, apps} wrapper objects inside apps[]."
|
|
3564
|
+
),
|
|
3565
|
+
details={
|
|
3566
|
+
"index": index,
|
|
3567
|
+
"row_number": index + 1,
|
|
3568
|
+
"expected_shape": _schema_apps_expected_shape(),
|
|
3569
|
+
"expected_shape_json": _schema_apps_expected_shape_json(),
|
|
3570
|
+
},
|
|
3571
|
+
)
|
|
3572
|
+
return None
|
|
3573
|
+
|
|
3574
|
+
|
|
3575
|
+
def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, apps: list[JSONObject]) -> JSONObject:
|
|
3576
|
+
normalized_package_id = package_id
|
|
3577
|
+
normalized_apps: list[JSONObject] = apps
|
|
3578
|
+
warnings: list[JSONObject] = []
|
|
3579
|
+
|
|
3580
|
+
if len(apps) == 1 and _is_schema_apps_wrapper_item(apps[0]):
|
|
3581
|
+
wrapper = apps[0]
|
|
3582
|
+
wrapper_apps = wrapper.get("apps")
|
|
3583
|
+
if not isinstance(wrapper_apps, list):
|
|
3584
|
+
return {
|
|
3585
|
+
"failure": _config_failure(
|
|
3586
|
+
tool_name=tool_name,
|
|
3587
|
+
error_code="APPS_FILE_SHAPE_INVALID",
|
|
3588
|
+
message="apps singleton wrapper requires an apps array.",
|
|
3589
|
+
fix_hint='Use {"package_id":1001,"apps":[...]} in CLI, or pass MCP package_id=1001 and apps=[...].',
|
|
3590
|
+
details={
|
|
3591
|
+
"expected_shape": _schema_apps_expected_shape(),
|
|
3592
|
+
"expected_shape_json": _schema_apps_expected_shape_json(),
|
|
3593
|
+
},
|
|
3594
|
+
)
|
|
3595
|
+
}
|
|
3596
|
+
if normalized_package_id is None and wrapper.get("package_id") is not None:
|
|
3597
|
+
try:
|
|
3598
|
+
normalized_package_id = int(wrapper.get("package_id"))
|
|
3599
|
+
except (TypeError, ValueError):
|
|
3600
|
+
return {
|
|
3601
|
+
"failure": _config_failure(
|
|
3602
|
+
tool_name=tool_name,
|
|
3603
|
+
error_code="APPS_FILE_SHAPE_INVALID",
|
|
3604
|
+
message="apps singleton wrapper package_id must be an integer.",
|
|
3605
|
+
fix_hint="Pass package_id as a number at the top level.",
|
|
3606
|
+
details={
|
|
3607
|
+
"expected_shape": _schema_apps_expected_shape(),
|
|
3608
|
+
"expected_shape_json": _schema_apps_expected_shape_json(),
|
|
3609
|
+
},
|
|
3610
|
+
)
|
|
3611
|
+
}
|
|
3612
|
+
normalized_apps = wrapper_apps
|
|
3613
|
+
warnings.append(
|
|
3614
|
+
{
|
|
3615
|
+
"code": "APPS_FILE_WRAPPER_ARRAY_UNWRAPPED",
|
|
3616
|
+
"message": "apps was a singleton wrapper array; normalized it to package_id + apps.",
|
|
3617
|
+
}
|
|
3618
|
+
)
|
|
3619
|
+
else:
|
|
3620
|
+
failure = _schema_apps_shape_failure(tool_name=tool_name, apps=apps)
|
|
3621
|
+
if failure is not None:
|
|
3622
|
+
return {"failure": failure}
|
|
3623
|
+
|
|
3624
|
+
normalized_items: list[JSONObject] = []
|
|
3625
|
+
for item in normalized_apps:
|
|
3626
|
+
normalized_items.append(_normalize_schema_app_item(item) if isinstance(item, dict) else item)
|
|
3627
|
+
return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
|
|
3628
|
+
|
|
3629
|
+
|
|
3630
|
+
def _multi_app_item_app_name(item: JSONObject) -> str:
|
|
3631
|
+
return str(item.get("app_name") or item.get("appName") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip()
|
|
3632
|
+
|
|
3633
|
+
|
|
3634
|
+
def _multi_app_item_app_key(item: JSONObject) -> str:
|
|
3635
|
+
return str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
3636
|
+
|
|
3637
|
+
|
|
3638
|
+
def _multi_app_item_client_key(item: JSONObject) -> str:
|
|
3639
|
+
return str(item.get("client_key") or item.get("clientKey") or "").strip()
|
|
3640
|
+
|
|
3641
|
+
|
|
3642
|
+
def _field_name_for_static_validation(field: JSONObject) -> str:
|
|
3643
|
+
return str(field.get("name") or field.get("title") or field.get("label") or "").strip()
|
|
3644
|
+
|
|
3645
|
+
|
|
3646
|
+
def _field_type_for_static_validation(field: JSONObject) -> str:
|
|
3647
|
+
return str(field.get("type") or field.get("type_id") or field.get("typeId") or "").strip()
|
|
3648
|
+
|
|
3649
|
+
|
|
3650
|
+
def _is_data_title_field(field: JSONObject) -> bool:
|
|
3651
|
+
return bool(field.get("as_data_title") or field.get("asDataTitle"))
|
|
3652
|
+
|
|
3653
|
+
|
|
3654
|
+
def _collect_multi_app_target_refs(value: object, *, path: str) -> list[JSONObject]:
|
|
3655
|
+
refs: list[JSONObject] = []
|
|
3656
|
+
if isinstance(value, list):
|
|
3657
|
+
for index, entry in enumerate(value):
|
|
3658
|
+
refs.extend(_collect_multi_app_target_refs(entry, path=f"{path}[{index}]"))
|
|
3659
|
+
return refs
|
|
3660
|
+
if not isinstance(value, dict):
|
|
3661
|
+
return refs
|
|
3662
|
+
for key, entry in value.items():
|
|
3663
|
+
current_path = f"{path}.{key}"
|
|
3664
|
+
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
|
|
3665
|
+
refs.append({"kind": "target_app_ref", "value": str(entry or "").strip(), "path": current_path})
|
|
3666
|
+
continue
|
|
3667
|
+
if key in {"target_app", "targetApp"}:
|
|
3668
|
+
refs.append({"kind": "target_app", "value": str(entry or "").strip(), "path": current_path})
|
|
3669
|
+
continue
|
|
3670
|
+
refs.extend(_collect_multi_app_target_refs(entry, path=current_path))
|
|
3671
|
+
return refs
|
|
3672
|
+
|
|
3673
|
+
|
|
3674
|
+
def _multi_app_static_validation_failure(
|
|
3675
|
+
*,
|
|
3676
|
+
tool_name: str,
|
|
3677
|
+
apps: list[JSONObject],
|
|
3678
|
+
create_if_missing: bool,
|
|
3679
|
+
) -> JSONObject | None:
|
|
3680
|
+
issues: list[JSONObject] = []
|
|
3681
|
+
client_key_indexes: dict[str, int] = {}
|
|
3682
|
+
app_name_indexes: dict[str, list[int]] = {}
|
|
3683
|
+
|
|
3684
|
+
for index, item in enumerate(apps):
|
|
3685
|
+
if not isinstance(item, dict):
|
|
3686
|
+
continue
|
|
3687
|
+
app_name = _multi_app_item_app_name(item)
|
|
3688
|
+
client_key = _multi_app_item_client_key(item)
|
|
3689
|
+
if client_key:
|
|
3690
|
+
first_index = client_key_indexes.get(client_key)
|
|
3691
|
+
if first_index is not None:
|
|
3692
|
+
issues.append(
|
|
3693
|
+
{
|
|
3694
|
+
"index": index,
|
|
3695
|
+
"row_number": index + 1,
|
|
3696
|
+
"path": f"apps[{index}].client_key",
|
|
3697
|
+
"error_code": "DUPLICATE_CLIENT_KEY",
|
|
3698
|
+
"message": f"duplicate client_key '{client_key}' also appears at apps[{first_index}]",
|
|
3699
|
+
"fix_hint": "Use a unique stable client_key for each app item, then reference relations through target_app_ref.",
|
|
3700
|
+
"details": {"client_key": client_key, "first_index": first_index, "duplicate_index": index},
|
|
3701
|
+
}
|
|
3702
|
+
)
|
|
3703
|
+
else:
|
|
3704
|
+
client_key_indexes[client_key] = index
|
|
3705
|
+
if app_name:
|
|
3706
|
+
app_name_indexes.setdefault(app_name, []).append(index)
|
|
3707
|
+
|
|
3708
|
+
for app_name, indexes in sorted(app_name_indexes.items()):
|
|
3709
|
+
if len(indexes) <= 1:
|
|
3710
|
+
continue
|
|
3711
|
+
issues.append(
|
|
3712
|
+
{
|
|
3713
|
+
"index": indexes[1],
|
|
3714
|
+
"row_number": indexes[1] + 1,
|
|
3715
|
+
"path": f"apps[{indexes[1]}].app_name",
|
|
3716
|
+
"error_code": "DUPLICATE_APP_NAME",
|
|
3717
|
+
"message": f"duplicate app_name '{app_name}' appears at apps{indexes}",
|
|
3718
|
+
"fix_hint": "Use unique app_name values in one multi-app payload, or use app_key for existing apps.",
|
|
3719
|
+
"details": {"app_name": app_name, "indexes": indexes},
|
|
3720
|
+
}
|
|
3721
|
+
)
|
|
3722
|
+
|
|
3723
|
+
for index, item in enumerate(apps):
|
|
3724
|
+
if not isinstance(item, dict):
|
|
3725
|
+
continue
|
|
3726
|
+
app_key = _multi_app_item_app_key(item)
|
|
3727
|
+
app_name = _multi_app_item_app_name(item)
|
|
3728
|
+
new_app_item = not bool(app_key)
|
|
3729
|
+
if not app_key and not app_name:
|
|
3730
|
+
issues.append(
|
|
3731
|
+
{
|
|
3732
|
+
"index": index,
|
|
3733
|
+
"row_number": index + 1,
|
|
3734
|
+
"path": f"apps[{index}]",
|
|
3735
|
+
"error_code": "APP_SELECTOR_REQUIRED",
|
|
3736
|
+
"message": "apps[] item requires app_key or app_name",
|
|
3737
|
+
"fix_hint": "For new apps pass app_name; for existing apps pass app_key.",
|
|
3738
|
+
}
|
|
3739
|
+
)
|
|
3740
|
+
continue
|
|
3741
|
+
if new_app_item and not create_if_missing:
|
|
3742
|
+
issues.append(
|
|
3743
|
+
{
|
|
3744
|
+
"index": index,
|
|
3745
|
+
"row_number": index + 1,
|
|
3746
|
+
"path": f"apps[{index}]",
|
|
3747
|
+
"error_code": "CREATE_IF_MISSING_REQUIRED",
|
|
3748
|
+
"message": "new multi-app items require create_if_missing=true",
|
|
3749
|
+
"fix_hint": "Set create_if_missing=true, or pass app_key for an existing app.",
|
|
3750
|
+
}
|
|
3751
|
+
)
|
|
2866
3752
|
|
|
3753
|
+
add_fields = _multi_app_list_value(item, "add_fields", "addFields")
|
|
3754
|
+
if new_app_item:
|
|
3755
|
+
title_fields: list[tuple[int, JSONObject]] = [
|
|
3756
|
+
(field_index, field)
|
|
3757
|
+
for field_index, field in enumerate(add_fields)
|
|
3758
|
+
if isinstance(field, dict) and _is_data_title_field(field)
|
|
3759
|
+
]
|
|
3760
|
+
if not title_fields:
|
|
3761
|
+
issues.append(
|
|
3762
|
+
{
|
|
3763
|
+
"index": index,
|
|
3764
|
+
"row_number": index + 1,
|
|
3765
|
+
"path": f"apps[{index}].add_fields",
|
|
3766
|
+
"error_code": "MISSING_DATA_TITLE_FIELD",
|
|
3767
|
+
"message": "new apps must define exactly one top-level data title field",
|
|
3768
|
+
"fix_hint": "Mark one readable top-level field with as_data_title=true, for example {'name':'客户名称','type':'text','as_data_title':true}.",
|
|
3769
|
+
"details": {"suggested_field_names": [_field_name_for_static_validation(field) for field in add_fields[:8] if isinstance(field, dict)]},
|
|
3770
|
+
}
|
|
3771
|
+
)
|
|
3772
|
+
elif len(title_fields) > 1:
|
|
3773
|
+
issues.append(
|
|
3774
|
+
{
|
|
3775
|
+
"index": index,
|
|
3776
|
+
"row_number": index + 1,
|
|
3777
|
+
"path": f"apps[{index}].add_fields",
|
|
3778
|
+
"error_code": "MULTIPLE_DATA_TITLE_FIELDS",
|
|
3779
|
+
"message": "new apps can mark only one top-level field as data title",
|
|
3780
|
+
"fix_hint": "Keep as_data_title=true on exactly one top-level field.",
|
|
3781
|
+
"details": {
|
|
3782
|
+
"fields": [
|
|
3783
|
+
{
|
|
3784
|
+
"field_index": field_index,
|
|
3785
|
+
"name": _field_name_for_static_validation(field),
|
|
3786
|
+
"type": _field_type_for_static_validation(field),
|
|
3787
|
+
}
|
|
3788
|
+
for field_index, field in title_fields
|
|
3789
|
+
]
|
|
3790
|
+
},
|
|
3791
|
+
}
|
|
3792
|
+
)
|
|
3793
|
+
else:
|
|
3794
|
+
field_index, title_field = title_fields[0]
|
|
3795
|
+
if _field_type_for_static_validation(title_field) in {"subtable", "table"}:
|
|
3796
|
+
issues.append(
|
|
3797
|
+
{
|
|
3798
|
+
"index": index,
|
|
3799
|
+
"row_number": index + 1,
|
|
3800
|
+
"path": f"apps[{index}].add_fields[{field_index}]",
|
|
3801
|
+
"error_code": "INVALID_DATA_TITLE_FIELD",
|
|
3802
|
+
"message": "data title must be a top-level non-subtable field",
|
|
3803
|
+
"fix_hint": "Move as_data_title=true to a normal top-level text/number/date-like field.",
|
|
3804
|
+
"details": {
|
|
3805
|
+
"field_name": _field_name_for_static_validation(title_field),
|
|
3806
|
+
"field_type": _field_type_for_static_validation(title_field),
|
|
3807
|
+
},
|
|
3808
|
+
}
|
|
3809
|
+
)
|
|
3810
|
+
if _contains_multi_app_target_ref(title_field):
|
|
3811
|
+
issues.append(
|
|
3812
|
+
{
|
|
3813
|
+
"index": index,
|
|
3814
|
+
"row_number": index + 1,
|
|
3815
|
+
"path": f"apps[{index}].add_fields[{field_index}]",
|
|
3816
|
+
"error_code": "DATA_TITLE_FIELD_DEFERRED_BY_TARGET_REF",
|
|
3817
|
+
"message": "data title cannot be a relation field that depends on target_app_ref/target_app in the same multi-app call",
|
|
3818
|
+
"fix_hint": "Use a normal title field such as 客户名称/项目名称 as data title; keep relation fields as non-title fields.",
|
|
3819
|
+
"details": {"field_name": _field_name_for_static_validation(title_field)},
|
|
3820
|
+
}
|
|
3821
|
+
)
|
|
2867
3822
|
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
3823
|
+
for ref in _collect_multi_app_target_refs(item, path=f"apps[{index}]"):
|
|
3824
|
+
ref_value = str(ref.get("value") or "").strip()
|
|
3825
|
+
if not ref_value:
|
|
3826
|
+
issues.append(
|
|
3827
|
+
{
|
|
3828
|
+
"index": index,
|
|
3829
|
+
"row_number": index + 1,
|
|
3830
|
+
"path": ref.get("path"),
|
|
3831
|
+
"error_code": "TARGET_APP_REFERENCE_EMPTY",
|
|
3832
|
+
"message": "target app reference cannot be empty",
|
|
3833
|
+
"fix_hint": "Use target_app_ref with another apps[].client_key, or target_app with a unique apps[].app_name.",
|
|
3834
|
+
}
|
|
3835
|
+
)
|
|
3836
|
+
elif ref.get("kind") == "target_app_ref" and ref_value not in client_key_indexes:
|
|
3837
|
+
issues.append(
|
|
3838
|
+
{
|
|
3839
|
+
"index": index,
|
|
3840
|
+
"row_number": index + 1,
|
|
3841
|
+
"path": ref.get("path"),
|
|
3842
|
+
"error_code": "TARGET_APP_REF_UNRESOLVED",
|
|
3843
|
+
"message": f"target_app_ref '{ref_value}' does not match any apps[].client_key in this payload",
|
|
3844
|
+
"fix_hint": "Set target_app_ref to one of apps[].client_key, or use target_app_key for an already-known existing app.",
|
|
3845
|
+
"details": {"target_app_ref": ref_value, "available_client_keys": sorted(client_key_indexes.keys())},
|
|
3846
|
+
}
|
|
3847
|
+
)
|
|
3848
|
+
elif ref.get("kind") == "target_app":
|
|
3849
|
+
matching_indexes = app_name_indexes.get(ref_value, [])
|
|
3850
|
+
if not matching_indexes:
|
|
3851
|
+
issues.append(
|
|
3852
|
+
{
|
|
3853
|
+
"index": index,
|
|
3854
|
+
"row_number": index + 1,
|
|
3855
|
+
"path": ref.get("path"),
|
|
3856
|
+
"error_code": "TARGET_APP_NAME_UNRESOLVED",
|
|
3857
|
+
"message": f"target_app '{ref_value}' does not match any apps[].app_name in this payload",
|
|
3858
|
+
"fix_hint": "Prefer target_app_ref with a stable apps[].client_key, or make target_app match an app_name exactly.",
|
|
3859
|
+
"details": {"target_app": ref_value, "available_app_names": sorted(app_name_indexes.keys())},
|
|
3860
|
+
}
|
|
3861
|
+
)
|
|
3862
|
+
elif len(matching_indexes) > 1:
|
|
3863
|
+
issues.append(
|
|
3864
|
+
{
|
|
3865
|
+
"index": index,
|
|
3866
|
+
"row_number": index + 1,
|
|
3867
|
+
"path": ref.get("path"),
|
|
3868
|
+
"error_code": "TARGET_APP_NAME_AMBIGUOUS",
|
|
3869
|
+
"message": f"target_app '{ref_value}' matches multiple apps[].app_name values",
|
|
3870
|
+
"fix_hint": "Use target_app_ref with a unique apps[].client_key instead of target_app.",
|
|
3871
|
+
"details": {"target_app": ref_value, "matching_indexes": matching_indexes},
|
|
3872
|
+
}
|
|
3873
|
+
)
|
|
3874
|
+
|
|
3875
|
+
if not issues:
|
|
3876
|
+
return None
|
|
3877
|
+
return _config_failure(
|
|
3878
|
+
tool_name=tool_name,
|
|
3879
|
+
error_code="MULTI_APP_STATIC_VALIDATION_FAILED",
|
|
3880
|
+
message="multi-app schema payload has static errors; fix apps[] before writing.",
|
|
3881
|
+
fix_hint="Before creating app shells, ensure each new app has exactly one data title, unique client_key/app_name, and relation refs match apps[].client_key or app_name.",
|
|
3882
|
+
details={"issues": issues, "expected_shape": _schema_apps_expected_shape()},
|
|
3883
|
+
)
|
|
2888
3884
|
|
|
2889
3885
|
|
|
2890
|
-
def _compile_multi_app_schema_item_refs(
|
|
3886
|
+
def _compile_multi_app_schema_item_refs(
|
|
3887
|
+
item: JSONObject,
|
|
3888
|
+
client_key_to_app_key: dict[str, str],
|
|
3889
|
+
app_name_to_app_key: dict[str, str] | None = None,
|
|
3890
|
+
) -> JSONObject:
|
|
2891
3891
|
compiled = deepcopy(item)
|
|
3892
|
+
app_name_to_app_key = app_name_to_app_key or {}
|
|
2892
3893
|
|
|
2893
3894
|
def visit(value):
|
|
2894
3895
|
if isinstance(value, list):
|
|
@@ -2908,6 +3909,13 @@ def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key:
|
|
|
2908
3909
|
if not target_app_key:
|
|
2909
3910
|
raise ValueError(f"target_app_ref '{ref_key}' did not match any apps[].client_key")
|
|
2910
3911
|
payload["target_app_key"] = target_app_key
|
|
3912
|
+
target_app = payload.pop("target_app", None) or payload.pop("targetApp", None)
|
|
3913
|
+
if target_app is not None and not str(payload.get("target_app_key") or "").strip():
|
|
3914
|
+
target_name = str(target_app or "").strip()
|
|
3915
|
+
target_app_key = app_name_to_app_key.get(target_name)
|
|
3916
|
+
if not target_app_key:
|
|
3917
|
+
raise ValueError(f"target_app '{target_name}' did not match any apps[].app_name in the same call")
|
|
3918
|
+
payload["target_app_key"] = target_app_key
|
|
2911
3919
|
return payload
|
|
2912
3920
|
|
|
2913
3921
|
return visit(compiled)
|
|
@@ -2956,7 +3964,7 @@ def _contains_multi_app_target_ref(value: object) -> bool:
|
|
|
2956
3964
|
if not isinstance(value, dict):
|
|
2957
3965
|
return False
|
|
2958
3966
|
for key, entry in value.items():
|
|
2959
|
-
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
|
|
3967
|
+
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey", "target_app", "targetApp"}:
|
|
2960
3968
|
return True
|
|
2961
3969
|
if _contains_multi_app_target_ref(entry):
|
|
2962
3970
|
return True
|
|
@@ -2979,6 +3987,8 @@ def _merge_schema_field_diffs(*diffs: object) -> JSONObject:
|
|
|
2979
3987
|
|
|
2980
3988
|
|
|
2981
3989
|
def _schema_apply_result_has_write(result: JSONObject) -> bool:
|
|
3990
|
+
if "write_executed" in result:
|
|
3991
|
+
return bool(result.get("write_executed"))
|
|
2982
3992
|
if bool(result.get("created")) or bool(result.get("published")) or bool(result.get("app_base_updated")):
|
|
2983
3993
|
return True
|
|
2984
3994
|
field_diff = result.get("field_diff")
|
|
@@ -2987,6 +3997,15 @@ def _schema_apply_result_has_write(result: JSONObject) -> bool:
|
|
|
2987
3997
|
return False
|
|
2988
3998
|
|
|
2989
3999
|
|
|
4000
|
+
def _schema_apply_result_may_have_write(result: JSONObject) -> bool:
|
|
4001
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
4002
|
+
return bool(
|
|
4003
|
+
result.get("write_may_have_succeeded")
|
|
4004
|
+
or str(result.get("next_action") or "") == "readback_before_retry"
|
|
4005
|
+
or verification.get("readback_before_retry")
|
|
4006
|
+
)
|
|
4007
|
+
|
|
4008
|
+
|
|
2990
4009
|
def _validation_failure(
|
|
2991
4010
|
detail: str,
|
|
2992
4011
|
*,
|
|
@@ -3067,6 +4086,197 @@ def _visibility_validation_failure(
|
|
|
3067
4086
|
return result
|
|
3068
4087
|
|
|
3069
4088
|
|
|
4089
|
+
_RESERVED_SYSTEM_FIELD_NAMES = {
|
|
4090
|
+
"数据ID",
|
|
4091
|
+
"编号",
|
|
4092
|
+
"申请人",
|
|
4093
|
+
"申请时间",
|
|
4094
|
+
"创建人",
|
|
4095
|
+
"创建时间",
|
|
4096
|
+
"提交人",
|
|
4097
|
+
"提交时间",
|
|
4098
|
+
"更新时间",
|
|
4099
|
+
"更新人",
|
|
4100
|
+
"当前流程状态",
|
|
4101
|
+
"当前处理人",
|
|
4102
|
+
"当前处理节点",
|
|
4103
|
+
"流程标题",
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
|
|
4107
|
+
def _field_patch_name(item: object) -> str:
|
|
4108
|
+
if not isinstance(item, dict):
|
|
4109
|
+
return ""
|
|
4110
|
+
value = item.get("name")
|
|
4111
|
+
if value is None:
|
|
4112
|
+
value = item.get("title")
|
|
4113
|
+
if value is None:
|
|
4114
|
+
value = item.get("field_name")
|
|
4115
|
+
if value is None:
|
|
4116
|
+
value = item.get("fieldName")
|
|
4117
|
+
return str(value or "").strip()
|
|
4118
|
+
|
|
4119
|
+
|
|
4120
|
+
def _reserved_system_field_name_failure(
|
|
4121
|
+
*,
|
|
4122
|
+
tool_name: str,
|
|
4123
|
+
profile: str,
|
|
4124
|
+
app_key: str = "",
|
|
4125
|
+
package_id: int | None = None,
|
|
4126
|
+
app_name: str = "",
|
|
4127
|
+
icon: str | None = None,
|
|
4128
|
+
color: str | None = None,
|
|
4129
|
+
visibility: JSONObject | None = None,
|
|
4130
|
+
create_if_missing: bool = False,
|
|
4131
|
+
publish: bool | None = None,
|
|
4132
|
+
add_fields: list[JSONObject] | None = None,
|
|
4133
|
+
update_fields: list[JSONObject] | None = None,
|
|
4134
|
+
remove_fields: list[JSONObject] | None = None,
|
|
4135
|
+
app_index: int | None = None,
|
|
4136
|
+
) -> JSONObject | None:
|
|
4137
|
+
for index, item in enumerate(add_fields or []):
|
|
4138
|
+
name = _field_patch_name(item)
|
|
4139
|
+
if name not in _RESERVED_SYSTEM_FIELD_NAMES:
|
|
4140
|
+
continue
|
|
4141
|
+
suggested_add_fields = [field for field in (add_fields or []) if _field_patch_name(field) not in _RESERVED_SYSTEM_FIELD_NAMES]
|
|
4142
|
+
arguments: JSONObject = {
|
|
4143
|
+
"profile": profile,
|
|
4144
|
+
"app_key": app_key,
|
|
4145
|
+
"package_id": package_id,
|
|
4146
|
+
"app_name": app_name,
|
|
4147
|
+
"icon": icon or "",
|
|
4148
|
+
"color": color or "",
|
|
4149
|
+
"visibility": visibility,
|
|
4150
|
+
"create_if_missing": create_if_missing,
|
|
4151
|
+
"add_fields": suggested_add_fields,
|
|
4152
|
+
"update_fields": update_fields or [],
|
|
4153
|
+
"remove_fields": remove_fields or [],
|
|
4154
|
+
}
|
|
4155
|
+
if publish is not None:
|
|
4156
|
+
arguments["publish"] = publish
|
|
4157
|
+
return _config_failure(
|
|
4158
|
+
tool_name=tool_name,
|
|
4159
|
+
error_code="RESERVED_SYSTEM_FIELD_NAME",
|
|
4160
|
+
message=f"add_fields[{index}].name uses built-in system field name '{name}'",
|
|
4161
|
+
fix_hint="Do not create form fields named 数据ID, 编号, 申请人, 申请时间, 创建人, 创建时间, 提交人, 提交时间, 更新时间, 更新人, 当前流程状态, 当前处理人, 当前处理节点, or 流程标题. These fields are provided by Qingflow. Remove them from add_fields; only reference them where the tool explicitly supports system fields, such as button source_field 数据ID.",
|
|
4162
|
+
details={
|
|
4163
|
+
"reserved_field_names": sorted(_RESERVED_SYSTEM_FIELD_NAMES),
|
|
4164
|
+
"blocked_index": index,
|
|
4165
|
+
"blocked_name": name,
|
|
4166
|
+
"app_index": app_index,
|
|
4167
|
+
},
|
|
4168
|
+
suggested_next_call={"tool_name": tool_name, "arguments": arguments},
|
|
4169
|
+
)
|
|
4170
|
+
return None
|
|
4171
|
+
|
|
4172
|
+
|
|
4173
|
+
def _reserved_system_field_name_failure_for_apps(
|
|
4174
|
+
*,
|
|
4175
|
+
tool_name: str,
|
|
4176
|
+
profile: str,
|
|
4177
|
+
package_id: int | None,
|
|
4178
|
+
visibility: JSONObject | None,
|
|
4179
|
+
create_if_missing: bool,
|
|
4180
|
+
publish: bool,
|
|
4181
|
+
apps: list[JSONObject],
|
|
4182
|
+
) -> JSONObject | None:
|
|
4183
|
+
for index, item in enumerate(apps or []):
|
|
4184
|
+
if not isinstance(item, dict):
|
|
4185
|
+
continue
|
|
4186
|
+
failure = _reserved_system_field_name_failure(
|
|
4187
|
+
tool_name=tool_name,
|
|
4188
|
+
profile=profile,
|
|
4189
|
+
app_key=str(item.get("app_key") or item.get("appKey") or ""),
|
|
4190
|
+
package_id=package_id,
|
|
4191
|
+
app_name=str(item.get("app_name") or item.get("appName") or ""),
|
|
4192
|
+
icon=str(item.get("icon") or ""),
|
|
4193
|
+
color=str(item.get("color") or ""),
|
|
4194
|
+
visibility=item.get("visibility") if isinstance(item.get("visibility"), dict) else visibility,
|
|
4195
|
+
create_if_missing=create_if_missing,
|
|
4196
|
+
publish=publish,
|
|
4197
|
+
add_fields=list(item.get("add_fields") or item.get("addFields") or []),
|
|
4198
|
+
update_fields=list(item.get("update_fields") or item.get("updateFields") or []),
|
|
4199
|
+
remove_fields=list(item.get("remove_fields") or item.get("removeFields") or []),
|
|
4200
|
+
app_index=index,
|
|
4201
|
+
)
|
|
4202
|
+
if failure is not None:
|
|
4203
|
+
suggested = deepcopy(failure.get("suggested_next_call") or {})
|
|
4204
|
+
arguments = suggested.get("arguments")
|
|
4205
|
+
if isinstance(arguments, dict):
|
|
4206
|
+
fixed_apps = deepcopy(apps)
|
|
4207
|
+
if isinstance(fixed_apps[index], dict):
|
|
4208
|
+
fixed_apps[index]["add_fields"] = arguments.get("add_fields") or []
|
|
4209
|
+
arguments = {
|
|
4210
|
+
"profile": profile,
|
|
4211
|
+
"package_id": package_id,
|
|
4212
|
+
"visibility": visibility,
|
|
4213
|
+
"create_if_missing": create_if_missing,
|
|
4214
|
+
"publish": publish,
|
|
4215
|
+
"apps": fixed_apps,
|
|
4216
|
+
}
|
|
4217
|
+
failure["suggested_next_call"] = {"tool_name": tool_name, "arguments": arguments}
|
|
4218
|
+
return failure
|
|
4219
|
+
return None
|
|
4220
|
+
|
|
4221
|
+
|
|
4222
|
+
_RESERVED_SYSTEM_VIEW_NAMES = {
|
|
4223
|
+
"全部数据",
|
|
4224
|
+
"我的数据",
|
|
4225
|
+
"我发起的",
|
|
4226
|
+
"待办",
|
|
4227
|
+
"已办",
|
|
4228
|
+
"抄送",
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4231
|
+
|
|
4232
|
+
def _reserved_system_view_name_failure(
|
|
4233
|
+
*,
|
|
4234
|
+
tool_name: str,
|
|
4235
|
+
profile: str,
|
|
4236
|
+
app_key: str,
|
|
4237
|
+
publish: bool | None = None,
|
|
4238
|
+
upsert_views: list[JSONObject] | None = None,
|
|
4239
|
+
patch_views: list[JSONObject] | None = None,
|
|
4240
|
+
remove_views: list[str] | None = None,
|
|
4241
|
+
) -> JSONObject | None:
|
|
4242
|
+
for index, item in enumerate(upsert_views or []):
|
|
4243
|
+
if not isinstance(item, dict):
|
|
4244
|
+
continue
|
|
4245
|
+
name = str(item.get("name") or "").strip()
|
|
4246
|
+
view_key = str(item.get("view_key") or item.get("viewKey") or "").strip()
|
|
4247
|
+
if name in _RESERVED_SYSTEM_VIEW_NAMES and not view_key:
|
|
4248
|
+
suggested: JSONObject = {
|
|
4249
|
+
"tool_name": tool_name,
|
|
4250
|
+
"arguments": {
|
|
4251
|
+
"profile": profile,
|
|
4252
|
+
"app_key": app_key,
|
|
4253
|
+
"upsert_views": [
|
|
4254
|
+
{
|
|
4255
|
+
**item,
|
|
4256
|
+
"name": "业务台账视图",
|
|
4257
|
+
}
|
|
4258
|
+
],
|
|
4259
|
+
"patch_views": patch_views or [],
|
|
4260
|
+
"remove_views": remove_views or [],
|
|
4261
|
+
},
|
|
4262
|
+
}
|
|
4263
|
+
if publish is not None:
|
|
4264
|
+
suggested["arguments"]["publish"] = publish
|
|
4265
|
+
return _config_failure(
|
|
4266
|
+
tool_name=tool_name,
|
|
4267
|
+
error_code="RESERVED_SYSTEM_VIEW_NAME",
|
|
4268
|
+
message=f"upsert_views[{index}].name uses built-in system view name '{name}' without view_key",
|
|
4269
|
+
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.",
|
|
4270
|
+
details={
|
|
4271
|
+
"reserved_view_names": sorted(_RESERVED_SYSTEM_VIEW_NAMES),
|
|
4272
|
+
"blocked_index": index,
|
|
4273
|
+
"blocked_name": name,
|
|
4274
|
+
},
|
|
4275
|
+
suggested_next_call=suggested,
|
|
4276
|
+
)
|
|
4277
|
+
return None
|
|
4278
|
+
|
|
4279
|
+
|
|
3070
4280
|
def _config_failure(
|
|
3071
4281
|
*,
|
|
3072
4282
|
tool_name: str,
|
|
@@ -3075,6 +4285,7 @@ def _config_failure(
|
|
|
3075
4285
|
error_code: str = "CONFIG_ERROR",
|
|
3076
4286
|
details: JSONObject | None = None,
|
|
3077
4287
|
allowed_values: JSONObject | None = None,
|
|
4288
|
+
suggested_next_call: JSONObject | None = None,
|
|
3078
4289
|
) -> JSONObject:
|
|
3079
4290
|
contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
|
|
3080
4291
|
public_allowed_values = deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {}
|
|
@@ -3095,7 +4306,7 @@ def _config_failure(
|
|
|
3095
4306
|
"missing_fields": [],
|
|
3096
4307
|
"allowed_values": public_allowed_values,
|
|
3097
4308
|
"details": public_details,
|
|
3098
|
-
"suggested_next_call":
|
|
4309
|
+
"suggested_next_call": suggested_next_call,
|
|
3099
4310
|
"request_id": None,
|
|
3100
4311
|
"backend_code": None,
|
|
3101
4312
|
"http_status": None,
|
|
@@ -3223,11 +4434,58 @@ def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) ->
|
|
|
3223
4434
|
"schema_version": BUILDER_APPLY_SCHEMA_VERSION,
|
|
3224
4435
|
"preferred_ui_fields": ["operation", "summary", "resources"],
|
|
3225
4436
|
"resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "icon_config", "error_code", "message"],
|
|
4437
|
+
"json_paths": _builder_apply_json_paths(),
|
|
3226
4438
|
"legacy_fields_preserved": True,
|
|
3227
4439
|
}
|
|
3228
4440
|
return public
|
|
3229
4441
|
|
|
3230
4442
|
|
|
4443
|
+
def _builder_tool_contract_summary(tool_name: str, contract: JSONObject) -> JSONObject:
|
|
4444
|
+
allowed_values = contract.get("allowed_values") if isinstance(contract, dict) else {}
|
|
4445
|
+
allowed_values_keys = sorted(str(key) for key in allowed_values) if isinstance(allowed_values, dict) else []
|
|
4446
|
+
return {
|
|
4447
|
+
"tool_name": tool_name,
|
|
4448
|
+
"contract_path": "$.contract",
|
|
4449
|
+
"allowed_keys_path": "$.contract.allowed_keys",
|
|
4450
|
+
"allowed_values_path": "$.contract.allowed_values",
|
|
4451
|
+
"allowed_values_key_style": "flat_dotted_keys",
|
|
4452
|
+
"minimal_example_path": "$.contract.minimal_example",
|
|
4453
|
+
"execution_notes_path": "$.contract.execution_notes",
|
|
4454
|
+
"top_level_allowed_values_usage": "empty on successful contract lookup; use $.contract.allowed_values instead",
|
|
4455
|
+
"allowed_values_keys_sample": allowed_values_keys[:12],
|
|
4456
|
+
"json_paths": {
|
|
4457
|
+
"contract": "$.contract",
|
|
4458
|
+
"allowed_keys": "$.contract.allowed_keys",
|
|
4459
|
+
"allowed_values": "$.contract.allowed_values",
|
|
4460
|
+
"minimal_example": "$.contract.minimal_example",
|
|
4461
|
+
"execution_notes": "$.contract.execution_notes",
|
|
4462
|
+
"aliases": "$.contract.aliases",
|
|
4463
|
+
},
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
|
|
4467
|
+
def _builder_apply_json_paths() -> JSONObject:
|
|
4468
|
+
return {
|
|
4469
|
+
"status": "$.status",
|
|
4470
|
+
"schema_version": "$.schema_version",
|
|
4471
|
+
"operation": "$.operation",
|
|
4472
|
+
"summary": "$.summary",
|
|
4473
|
+
"resources": "$.resources",
|
|
4474
|
+
"warnings": "$.warnings",
|
|
4475
|
+
"verification": "$.verification",
|
|
4476
|
+
"details": "$.details",
|
|
4477
|
+
"write_executed": "$.write_executed",
|
|
4478
|
+
"write_may_have_succeeded": "$.write_may_have_succeeded",
|
|
4479
|
+
"safe_to_retry": "$.safe_to_retry",
|
|
4480
|
+
"next_action": "$.next_action",
|
|
4481
|
+
"summary_write_executed": "$.summary.write_executed",
|
|
4482
|
+
"summary_safe_to_retry": "$.summary.safe_to_retry",
|
|
4483
|
+
"resource_keys": "$.resources[].key",
|
|
4484
|
+
"resource_ids": "$.resources[].id",
|
|
4485
|
+
"resource_statuses": "$.resources[].status",
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
|
|
3231
4489
|
def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONObject:
|
|
3232
4490
|
if not isinstance(payload, dict):
|
|
3233
4491
|
return payload
|
|
@@ -3236,6 +4494,7 @@ def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONO
|
|
|
3236
4494
|
payload["operation"] = tool_name
|
|
3237
4495
|
payload["resources"] = resources
|
|
3238
4496
|
payload["summary"] = _builder_apply_summary(payload, resources)
|
|
4497
|
+
payload["json_paths"] = _builder_apply_json_paths()
|
|
3239
4498
|
return payload
|
|
3240
4499
|
|
|
3241
4500
|
|
|
@@ -3269,8 +4528,14 @@ def _builder_apply_summary(payload: JSONObject, resources: list[JSONObject]) ->
|
|
|
3269
4528
|
}
|
|
3270
4529
|
if "write_executed" in payload:
|
|
3271
4530
|
summary["write_executed"] = bool(payload.get("write_executed"))
|
|
4531
|
+
if "write_may_have_succeeded" in payload:
|
|
4532
|
+
summary["write_may_have_succeeded"] = bool(payload.get("write_may_have_succeeded"))
|
|
3272
4533
|
if "safe_to_retry" in payload:
|
|
3273
4534
|
summary["safe_to_retry"] = bool(payload.get("safe_to_retry"))
|
|
4535
|
+
if payload.get("next_action"):
|
|
4536
|
+
summary["next_action"] = payload.get("next_action")
|
|
4537
|
+
if payload.get("pending_readback") is not None:
|
|
4538
|
+
summary["pending_readback"] = payload.get("pending_readback")
|
|
3274
4539
|
return summary
|
|
3275
4540
|
|
|
3276
4541
|
|
|
@@ -3291,6 +4556,8 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
|
|
|
3291
4556
|
resources = _builder_package_resources(payload)
|
|
3292
4557
|
elif tool_name == "app_schema_apply":
|
|
3293
4558
|
resources = _builder_schema_resources(payload)
|
|
4559
|
+
elif isinstance(payload.get("apps"), list) and _builder_apply_tool_is_app_scoped(tool_name):
|
|
4560
|
+
resources = _builder_batch_app_resources(tool_name, payload)
|
|
3294
4561
|
elif tool_name == "app_layout_apply":
|
|
3295
4562
|
resources = [_builder_app_resource(payload, operation="layout_updated")]
|
|
3296
4563
|
elif tool_name == "app_flow_apply":
|
|
@@ -3301,6 +4568,8 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
|
|
|
3301
4568
|
resources = _builder_chart_resources(payload)
|
|
3302
4569
|
elif tool_name == "portal_apply":
|
|
3303
4570
|
resources = _builder_portal_resources(payload)
|
|
4571
|
+
elif tool_name == "portal_delete":
|
|
4572
|
+
resources = _builder_portal_resources(payload, operation_override="removed")
|
|
3304
4573
|
elif tool_name == "app_custom_buttons_apply":
|
|
3305
4574
|
resources = _builder_button_resources(payload)
|
|
3306
4575
|
elif tool_name == "app_associated_resources_apply":
|
|
@@ -3316,6 +4585,38 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
|
|
|
3316
4585
|
return resources
|
|
3317
4586
|
|
|
3318
4587
|
|
|
4588
|
+
def _builder_batch_app_resources(tool_name: str, payload: JSONObject) -> list[JSONObject]:
|
|
4589
|
+
resources: list[JSONObject] = []
|
|
4590
|
+
operation_by_tool = {
|
|
4591
|
+
"app_schema_apply": "updated",
|
|
4592
|
+
"app_layout_apply": "layout_updated",
|
|
4593
|
+
"app_flow_apply": "workflow_updated",
|
|
4594
|
+
"app_views_apply": "updated",
|
|
4595
|
+
"app_custom_buttons_apply": "updated",
|
|
4596
|
+
"app_associated_resources_apply": "updated",
|
|
4597
|
+
"app_charts_apply": "updated",
|
|
4598
|
+
"app_publish_verify": "verified",
|
|
4599
|
+
}
|
|
4600
|
+
operation = operation_by_tool.get(tool_name, "updated")
|
|
4601
|
+
for item in payload.get("apps") or []:
|
|
4602
|
+
if not isinstance(item, dict):
|
|
4603
|
+
continue
|
|
4604
|
+
result = item.get("result") if isinstance(item.get("result"), dict) else {}
|
|
4605
|
+
nested_resources = result.get("resources") if isinstance(result, dict) else None
|
|
4606
|
+
if isinstance(nested_resources, list) and nested_resources:
|
|
4607
|
+
resources.extend(resource for resource in nested_resources if isinstance(resource, dict))
|
|
4608
|
+
continue
|
|
4609
|
+
app_payload: JSONObject = {
|
|
4610
|
+
**(result if isinstance(result, dict) else {}),
|
|
4611
|
+
"app_key": item.get("app_key") or (result.get("app_key") if isinstance(result, dict) else None),
|
|
4612
|
+
"status": item.get("status") or (result.get("status") if isinstance(result, dict) else None),
|
|
4613
|
+
"error_code": item.get("error_code") or (result.get("error_code") if isinstance(result, dict) else None),
|
|
4614
|
+
"message": item.get("message") or (result.get("message") if isinstance(result, dict) else None),
|
|
4615
|
+
}
|
|
4616
|
+
resources.append(_builder_app_resource(app_payload, operation=operation))
|
|
4617
|
+
return resources
|
|
4618
|
+
|
|
4619
|
+
|
|
3319
4620
|
def _builder_apply_tool_is_app_scoped(tool_name: str) -> bool:
|
|
3320
4621
|
return tool_name in {
|
|
3321
4622
|
"app_schema_apply",
|
|
@@ -3496,7 +4797,15 @@ def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3496
4797
|
package_id = payload.get("package_id") or payload.get("id")
|
|
3497
4798
|
package_name = payload.get("package_name") or payload.get("name")
|
|
3498
4799
|
status = _builder_status(payload, "success")
|
|
3499
|
-
operation =
|
|
4800
|
+
operation = (
|
|
4801
|
+
"failed"
|
|
4802
|
+
if status == "failed"
|
|
4803
|
+
else "removed"
|
|
4804
|
+
if bool(payload.get("deleted")) or bool(payload.get("delete_executed"))
|
|
4805
|
+
else "created"
|
|
4806
|
+
if bool(payload.get("created"))
|
|
4807
|
+
else "updated"
|
|
4808
|
+
)
|
|
3500
4809
|
normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
|
|
3501
4810
|
icon_config = (
|
|
3502
4811
|
_builder_container_icon_config(payload, raw_keys=("icon", "tagIcon", "tag_icon"))
|
|
@@ -3547,7 +4856,7 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3547
4856
|
parent=package_parent,
|
|
3548
4857
|
icon_config=icon_config,
|
|
3549
4858
|
error_code=item.get("error_code"),
|
|
3550
|
-
message=item.get("message") if status
|
|
4859
|
+
message=item.get("message") if status in {"failed", "pending_readback"} else None,
|
|
3551
4860
|
)
|
|
3552
4861
|
)
|
|
3553
4862
|
resources.extend(_builder_field_resources(item.get("field_diff_details") or item.get("field_diff"), parent=parent))
|
|
@@ -3748,7 +5057,7 @@ def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3748
5057
|
return resources
|
|
3749
5058
|
|
|
3750
5059
|
|
|
3751
|
-
def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
|
|
5060
|
+
def _builder_portal_resources(payload: JSONObject, *, operation_override: str | None = None) -> list[JSONObject]:
|
|
3752
5061
|
status = _builder_status(payload, "success")
|
|
3753
5062
|
draft_result = payload.get("draft_result") if isinstance(payload.get("draft_result"), dict) else {}
|
|
3754
5063
|
live_result = payload.get("live_result") if isinstance(payload.get("live_result"), dict) else {}
|
|
@@ -3779,7 +5088,7 @@ def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3779
5088
|
first_tag = draft_result.get("tags")[0]
|
|
3780
5089
|
if isinstance(first_tag, dict):
|
|
3781
5090
|
package_id = first_tag.get("tagId") or first_tag.get("tag_id") or first_tag.get("id")
|
|
3782
|
-
operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
|
|
5091
|
+
operation = operation_override or ("failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated"))
|
|
3783
5092
|
parent = None
|
|
3784
5093
|
if package_id:
|
|
3785
5094
|
parent = _builder_parent("package", id_value=package_id, key=package_id)
|
|
@@ -3968,7 +5277,7 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
|
3968
5277
|
|
|
3969
5278
|
|
|
3970
5279
|
def _public_error_message(error_code: str, error: QingflowApiError) -> str:
|
|
3971
|
-
if error
|
|
5280
|
+
if backend_code_int(error) == 40074 or error_code == "APP_EDIT_LOCKED":
|
|
3972
5281
|
return "app is currently locked by another active editor session"
|
|
3973
5282
|
if error.http_status != 404:
|
|
3974
5283
|
return error.message
|
|
@@ -4143,13 +5452,29 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4143
5452
|
},
|
|
4144
5453
|
},
|
|
4145
5454
|
"package_apply": {
|
|
4146
|
-
"allowed_keys": [
|
|
5455
|
+
"allowed_keys": [
|
|
5456
|
+
"package_id",
|
|
5457
|
+
"package_name",
|
|
5458
|
+
"create_if_missing",
|
|
5459
|
+
"icon",
|
|
5460
|
+
"color",
|
|
5461
|
+
"icon_name",
|
|
5462
|
+
"icon_color",
|
|
5463
|
+
"icon_config",
|
|
5464
|
+
"visibility",
|
|
5465
|
+
"items",
|
|
5466
|
+
"allow_detach",
|
|
5467
|
+
],
|
|
4147
5468
|
"aliases": {
|
|
4148
5469
|
"packageId": "package_id",
|
|
4149
5470
|
"packageName": "package_name",
|
|
4150
5471
|
"createIfMissing": "create_if_missing",
|
|
4151
5472
|
"iconName": "icon",
|
|
4152
5473
|
"iconColor": "color",
|
|
5474
|
+
"icon_config.name": "icon",
|
|
5475
|
+
"icon_config.icon_name": "icon",
|
|
5476
|
+
"icon_config.color": "color",
|
|
5477
|
+
"icon_config.icon_color": "color",
|
|
4153
5478
|
"allowDetach": "allow_detach",
|
|
4154
5479
|
},
|
|
4155
5480
|
"allowed_values": {
|
|
@@ -4161,12 +5486,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4161
5486
|
"execution_notes": [
|
|
4162
5487
|
"create or update package metadata, visibility, grouping, and ordering in one call",
|
|
4163
5488
|
"creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5489
|
+
"agent-facing primary icon shape is icon + color; icon_name/icon_color, icon_config, and icon={name,color} are compatibility aliases that normalize to icon/color",
|
|
4164
5490
|
"updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4165
5491
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
4166
5492
|
"metadata keys omitted on update are preserved",
|
|
4167
5493
|
"package_id maps internally to backend tagId; do not use tag_id in public calls",
|
|
4168
5494
|
"items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
|
|
4169
5495
|
"item shapes: {type:'app', app_key}, {type:'portal', dash_key}, or {type:'group', group_id?, name, items:[...]}",
|
|
5496
|
+
"layout apply calls backend package ordering (MoveGroupAuth), so it requires package edit_app permission even when items=[] only clears/deletes existing groups",
|
|
4170
5497
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4171
5498
|
],
|
|
4172
5499
|
"minimal_example": {
|
|
@@ -4309,7 +5636,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4309
5636
|
"app_custom_buttons_apply": {
|
|
4310
5637
|
"allowed_keys": [
|
|
4311
5638
|
"app_key",
|
|
4312
|
-
"apps",
|
|
4313
5639
|
"upsert_buttons",
|
|
4314
5640
|
"patch_buttons",
|
|
4315
5641
|
"remove_buttons",
|
|
@@ -4394,6 +5720,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4394
5720
|
"field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id maps to current record id (-17), 编号/record_number maps to visible record number (0)",
|
|
4395
5721
|
"to fill a target relation field with the current source record, map source_field='数据ID' to the target relation field; default_values is for static constants, not dynamic current-record values",
|
|
4396
5722
|
"do not write raw que_relation unless maintaining a legacy config; field_mappings/default_values and que_relation are mutually exclusive",
|
|
5723
|
+
"permission split follows backend routes: upsert_buttons/patch_buttons/remove_buttons require EditAppAuth; view_configs also requires ViewManagementAuth (beingViewManageStatus), which falls back to DataManageAuth when advanced app permissions are not enabled",
|
|
4397
5724
|
"view_configs binds custom buttons into views in the same apply call; button_ref may be a same-call client_key, a button_id, or an exact unique existing button_text",
|
|
4398
5725
|
"view_configs[].view_key is the raw builder view key from app_get.views[].view_key; do not pass record-data view_id values like custom:VIEW_KEY",
|
|
4399
5726
|
"view_configs[].buttons is required in merge mode; omitting buttons is blocked to avoid no-op writes and accidental publish",
|
|
@@ -4406,7 +5733,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4406
5733
|
"if a removed button returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
|
|
4407
5734
|
"all operations share one edit context and publish after at least one write succeeds; there is no draft-only mode for this tool",
|
|
4408
5735
|
"background_color and text_color cannot both be white",
|
|
4409
|
-
"accepts apps[] for multi-app batch; each item is {app_key, upsert_buttons?, patch_buttons?, remove_buttons?, view_configs?}",
|
|
4410
5736
|
],
|
|
4411
5737
|
"minimal_example": {
|
|
4412
5738
|
"profile": "default",
|
|
@@ -4482,7 +5808,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4482
5808
|
"app_associated_resources_apply": {
|
|
4483
5809
|
"allowed_keys": [
|
|
4484
5810
|
"app_key",
|
|
4485
|
-
"apps",
|
|
4486
5811
|
"upsert_resources",
|
|
4487
5812
|
"patch_resources",
|
|
4488
5813
|
"remove_associated_item_ids",
|
|
@@ -4539,6 +5864,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4539
5864
|
"this tool manages Qingflow in-app associated report/view display; it does not create or edit QingBI report bodies/configs",
|
|
4540
5865
|
"create or edit app-source BI report bodies first with app_charts_apply, then attach the resulting chart_id here with graph_type=chart; dataset BI reports can only be attached when they already exist",
|
|
4541
5866
|
"this is the default associated report/view path; it manages both the app-level associated resource pool and per-view display config",
|
|
5867
|
+
"permission split follows backend routes: upsert_resources/patch_resources/remove/reorder require EditAppAuth; view_configs require ViewManagementAuth (beingViewManageStatus), which falls back to DataManageAuth when advanced app permissions are not enabled",
|
|
4542
5868
|
"use patch_resources for partial parameter replacement on existing associated resources; the tool reads the current resource including backend-required raw fields, merges patch_resources[].set/unset, then submits the backend full-save payload internally",
|
|
4543
5869
|
"associated_item_id is form_asos_chart.id from app_get.associated_resources[].associated_item_id; view_configs/remove/reorder may also pass an existing associated resource's chart_id/chart_key/view_key and the tool resolves it to the internal id",
|
|
4544
5870
|
"before creating an associated resource, read app_get.associated_resources and reuse an existing item with patch_resources when target_app_key + view_key/chart_key already matches; repeated upsert_resources without associated_item_id can create duplicate associated items",
|
|
@@ -4552,7 +5878,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4552
5878
|
"if an associated resource delete returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
|
|
4553
5879
|
"this tool publishes after at least one write succeeds; there is no draft-only mode",
|
|
4554
5880
|
"visible=false hides the associated-resource area without clearing previous selected ids; visible=true with limit_type=all shows the whole app-level pool",
|
|
4555
|
-
"accepts apps[] for multi-app batch; each item is {app_key, upsert_resources?, patch_resources?, remove_associated_item_ids?, reorder_associated_item_ids?, view_configs?}",
|
|
4556
5881
|
],
|
|
4557
5882
|
"minimal_example": {
|
|
4558
5883
|
"profile": "default",
|
|
@@ -4583,22 +5908,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4583
5908
|
}
|
|
4584
5909
|
],
|
|
4585
5910
|
},
|
|
4586
|
-
"batch_example": {
|
|
4587
|
-
"profile": "default",
|
|
4588
|
-
"apps": [
|
|
4589
|
-
{
|
|
4590
|
-
"app_key": "APP_1",
|
|
4591
|
-
"upsert_resources": [
|
|
4592
|
-
{"client_key": "orders_view", "graph_type": "view", "target_app_key": "ORDER_APP", "view_key": "ORDER_VIEW"}
|
|
4593
|
-
],
|
|
4594
|
-
"view_configs": [{"view_key": "MAIN_VIEW", "limit_type": "select", "associated_item_refs": ["orders_view"]}],
|
|
4595
|
-
},
|
|
4596
|
-
{
|
|
4597
|
-
"app_key": "APP_2",
|
|
4598
|
-
"view_configs": [{"view_key": "MAIN_VIEW", "visible": True, "limit_type": "all"}],
|
|
4599
|
-
},
|
|
4600
|
-
],
|
|
4601
|
-
},
|
|
4602
5911
|
},
|
|
4603
5912
|
"app_schema_plan": {
|
|
4604
5913
|
"allowed_keys": ["app_key", "package_id", "app_name", "icon", "color", "visibility", "create_if_missing", "add_fields", "update_fields", "remove_fields"],
|
|
@@ -4642,6 +5951,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4642
5951
|
"create mode may set visibility for the new app; edit mode may update visibility on an existing app",
|
|
4643
5952
|
"create mode should include explicit non-template icon + color; apply mode enforces this before writing",
|
|
4644
5953
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
5954
|
+
"do not add form fields named 数据ID, 编号, 申请人, 申请时间, 创建人, 创建时间, 提交人, 提交时间, 更新时间, 更新人, 当前流程状态, 当前处理人, 当前处理节点, or 流程标题; these are Qingflow built-in system fields, not fields to create",
|
|
4645
5955
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4646
5956
|
],
|
|
4647
5957
|
"minimal_example": {
|
|
@@ -4678,8 +5988,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4678
5988
|
"app_key",
|
|
4679
5989
|
"package_id",
|
|
4680
5990
|
"app_name",
|
|
5991
|
+
"app_title",
|
|
4681
5992
|
"icon",
|
|
4682
5993
|
"color",
|
|
5994
|
+
"icon_name",
|
|
5995
|
+
"icon_color",
|
|
5996
|
+
"icon_config",
|
|
4683
5997
|
"visibility",
|
|
4684
5998
|
"create_if_missing",
|
|
4685
5999
|
"publish",
|
|
@@ -4692,11 +6006,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4692
6006
|
"apps[].app_name",
|
|
4693
6007
|
"apps[].icon",
|
|
4694
6008
|
"apps[].color",
|
|
6009
|
+
"apps[].icon_name",
|
|
6010
|
+
"apps[].icon_color",
|
|
6011
|
+
"apps[].icon_config",
|
|
4695
6012
|
"apps[].visibility",
|
|
4696
6013
|
"apps[].add_fields",
|
|
4697
6014
|
"apps[].update_fields",
|
|
4698
6015
|
"apps[].remove_fields",
|
|
4699
6016
|
"apps[].add_fields[].target_app_ref",
|
|
6017
|
+
"apps[].add_fields[].target_app",
|
|
4700
6018
|
],
|
|
4701
6019
|
"aliases": {
|
|
4702
6020
|
"app_title": "app_name",
|
|
@@ -4706,8 +6024,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4706
6024
|
"apps[].appKey": "apps[].app_key",
|
|
4707
6025
|
"apps[].appName": "apps[].app_name",
|
|
4708
6026
|
"apps[].appTitle": "apps[].app_name",
|
|
6027
|
+
"apps[].iconName": "apps[].icon",
|
|
6028
|
+
"apps[].iconColor": "apps[].color",
|
|
6029
|
+
"apps[].icon_config.name": "apps[].icon",
|
|
6030
|
+
"apps[].icon_config.color": "apps[].color",
|
|
4709
6031
|
"field.targetAppRef": "field.target_app_ref",
|
|
4710
6032
|
"field.targetAppClientKey": "field.target_app_ref",
|
|
6033
|
+
"field.targetApp": "field.target_app",
|
|
6034
|
+
"field.options[].label": "field.options[]",
|
|
6035
|
+
"field.options[].value": "field.options[]",
|
|
6036
|
+
"field.options[].optValue": "field.options[]",
|
|
4711
6037
|
"field.title": "field.name",
|
|
4712
6038
|
"field.label": "field.name",
|
|
4713
6039
|
"field.fields": "field.subfields",
|
|
@@ -4733,6 +6059,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4733
6059
|
},
|
|
4734
6060
|
"allowed_values": {
|
|
4735
6061
|
"field.type": [member.value for member in PublicFieldType],
|
|
6062
|
+
"field.type_aliases": {alias: field_type.value for alias, field_type in sorted(FIELD_TYPE_ALIASES.items())},
|
|
4736
6063
|
"field.relation_mode": [member.value for member in PublicRelationMode],
|
|
4737
6064
|
"field.department_scope.mode": ["all", "custom"],
|
|
4738
6065
|
"field.code_block_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
|
|
@@ -4744,14 +6071,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4744
6071
|
"use exactly one resource mode",
|
|
4745
6072
|
"edit mode: app_key, optional app_name to rename the existing app",
|
|
4746
6073
|
"create mode: package_id + app_name + create_if_missing=true",
|
|
6074
|
+
"create mode follows backend CreateAppBean: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
|
|
4747
6075
|
"multi-app mode: pass package_id + create_if_missing + apps[]; do not mix apps with top-level app_key/app_name/add_fields/update_fields/remove_fields",
|
|
6076
|
+
"CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
|
|
6077
|
+
"multi-app mode preflights static errors before writing: duplicate client_key/app_name, missing data title on new apps, create_if_missing omissions, and unresolved target_app_ref/target_app",
|
|
4748
6078
|
"multi-app relation fields may use target_app_ref to point at another apps[].client_key; the tool creates/resolves app shells first and compiles it to target_app_key",
|
|
6079
|
+
"multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
|
|
4749
6080
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
4750
6081
|
"create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
|
|
4751
6082
|
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
6083
|
+
"agent-facing primary icon shape is icon + color; icon_name/icon_color, icon_config, and icon={name,color} are compatibility aliases that normalize to icon/color",
|
|
4752
6084
|
"multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
|
|
6085
|
+
"agent-friendly field type aliases are normalized before writing: multiline/multiline_text/textarea -> long_text; select/single_choice/dropdown -> single_select; multi_choice/multiple_choice/checkbox -> multi_select",
|
|
6086
|
+
"single_select and multi_select options accept strings or objects such as {label,value}; builder normalizes them to option labels before writing",
|
|
4753
6087
|
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4754
6088
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
6089
|
+
"do not add form fields named 数据ID, 编号, 申请人, 申请时间, 创建人, 创建时间, 提交人, 提交时间, 更新时间, 更新人, 当前流程状态, 当前处理人, 当前处理节点, or 流程标题; these are Qingflow built-in system fields, not fields to create",
|
|
4755
6090
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4756
6091
|
"update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
|
|
4757
6092
|
"multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
|
|
@@ -4759,6 +6094,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4759
6094
|
"relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
|
|
4760
6095
|
"relation fields now require both display_field and visible_fields in MCP/CLI payloads",
|
|
4761
6096
|
"if relation target metadata lookup is blocked by 40161/40002/40027, explicit display_field.name and visible_fields[].name let builder degrade verification and still continue schema write",
|
|
6097
|
+
"relation write readback returns details.relation_readback_matrix; mismatches include details.relation_repair_plan with a minimal update_fields relation patch and data_impact",
|
|
4762
6098
|
"update_fields[].set.subfield_updates is the safe patch path for editing existing subtable child fields without rebuilding the entire subtable",
|
|
4763
6099
|
"subfield_updates only supports safe child overlays: name, required, description, and nested subfield_updates",
|
|
4764
6100
|
"set.subfields remains the full replace/rebuild path for a subtable and is higher risk when hidden relation/reference children exist",
|
|
@@ -4974,7 +6310,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4974
6310
|
"preset_example": {"profile": "default", "app_key": "APP_KEY", "mode": "merge", "preset": "balanced", "sections": []},
|
|
4975
6311
|
},
|
|
4976
6312
|
"app_layout_apply": {
|
|
4977
|
-
"allowed_keys": ["app_key", "mode", "publish", "sections"
|
|
6313
|
+
"allowed_keys": ["app_key", "mode", "publish", "sections"],
|
|
4978
6314
|
"aliases": {"overwrite": "replace", "sectionId": "section_id"},
|
|
4979
6315
|
"section_allowed_keys": ["type", "paragraph_id", "section_id", "title", "rows"],
|
|
4980
6316
|
"section_aliases": {
|
|
@@ -4991,7 +6327,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4991
6327
|
"mode=replace is full layout replacement and should be used only when intentionally rewriting all sections",
|
|
4992
6328
|
"layout verification is split into layout_verified and layout_summary_verified",
|
|
4993
6329
|
"LAYOUT_SUMMARY_UNVERIFIED means raw form readback is stronger than the compact summary",
|
|
4994
|
-
"accepts apps[] for multi-app batch; each item is {app_key, mode?, sections, publish?}; top-level publish is used as default for items that omit publish",
|
|
4995
6330
|
],
|
|
4996
6331
|
"minimal_section_example": {"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]},
|
|
4997
6332
|
"minimal_example": {
|
|
@@ -5001,73 +6336,95 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5001
6336
|
"publish": True,
|
|
5002
6337
|
"sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["项目名称", "项目负责人", "项目阶段", "优先级"]]}],
|
|
5003
6338
|
},
|
|
5004
|
-
"batch_example": {
|
|
5005
|
-
"profile": "default",
|
|
5006
|
-
"publish": True,
|
|
5007
|
-
"apps": [
|
|
5008
|
-
{"app_key": "APP_KEY_1", "mode": "merge", "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B"]]}]},
|
|
5009
|
-
{"app_key": "APP_KEY_2", "mode": "merge", "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段C", "字段D"]]}]},
|
|
5010
|
-
],
|
|
5011
|
-
},
|
|
5012
6339
|
},
|
|
5013
|
-
"
|
|
5014
|
-
"allowed_keys": ["
|
|
5015
|
-
"aliases": {
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
"
|
|
5019
|
-
"
|
|
5020
|
-
"
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
"
|
|
5024
|
-
"
|
|
6340
|
+
"app_flow_plan": {
|
|
6341
|
+
"allowed_keys": ["app_key", "mode", "nodes", "transitions", "preset"],
|
|
6342
|
+
"aliases": {
|
|
6343
|
+
"overwrite": "replace",
|
|
6344
|
+
"base_preset": "preset",
|
|
6345
|
+
"default_approval": "basic_approval",
|
|
6346
|
+
"node.role_names": "node.assignees.role_names",
|
|
6347
|
+
"node.role_ids": "node.assignees.role_ids",
|
|
6348
|
+
"node.member_names": "node.assignees.member_names",
|
|
6349
|
+
"node.member_emails": "node.assignees.member_emails",
|
|
6350
|
+
"node.member_uids": "node.assignees.member_uids",
|
|
6351
|
+
"node.editable_fields": "node.permissions.editable_fields",
|
|
6352
|
+
"default_approval": "basic_approval",
|
|
5025
6353
|
},
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
6354
|
+
"allowed_values": {
|
|
6355
|
+
"mode": ["replace"],
|
|
6356
|
+
"preset": [member.value for member in FlowPreset],
|
|
6357
|
+
"node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
|
|
6358
|
+
},
|
|
6359
|
+
"dependency_hints": [
|
|
6360
|
+
"approval-style workflows require an explicit business status select field before app_flow_apply, such as 状态 / 处理状态 / 审批状态 / 工单状态",
|
|
6361
|
+
"approve/fill/copy nodes require at least one assignee",
|
|
6362
|
+
],
|
|
5031
6363
|
"execution_notes": [
|
|
5032
|
-
"
|
|
5033
|
-
"
|
|
6364
|
+
"public flow building is intentionally limited to linear workflows",
|
|
6365
|
+
"branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
|
|
6366
|
+
"do not create platform system workflow fields such as 当前流程状态 / 当前处理人 / 当前处理节点 / 流程标题; use an explicit business status field instead",
|
|
5034
6367
|
],
|
|
5035
6368
|
"minimal_example": {
|
|
5036
6369
|
"profile": "default",
|
|
5037
6370
|
"app_key": "APP_KEY",
|
|
6371
|
+
"mode": "replace",
|
|
6372
|
+
"preset": "basic_approval",
|
|
6373
|
+
"nodes": [
|
|
6374
|
+
{
|
|
6375
|
+
"id": "approve_1",
|
|
6376
|
+
"type": "approve",
|
|
6377
|
+
"name": "部门审批",
|
|
6378
|
+
"assignees": {"role_names": ["项目经理"]},
|
|
6379
|
+
"permissions": {"editable_fields": ["状态", "审批意见"]},
|
|
6380
|
+
}
|
|
6381
|
+
],
|
|
6382
|
+
"transitions": [],
|
|
5038
6383
|
},
|
|
5039
6384
|
},
|
|
5040
6385
|
"app_flow_apply": {
|
|
5041
|
-
"allowed_keys": ["app_key", "
|
|
6386
|
+
"allowed_keys": ["app_key", "mode", "publish", "nodes", "transitions"],
|
|
5042
6387
|
"aliases": {
|
|
5043
|
-
"
|
|
5044
|
-
"
|
|
6388
|
+
"overwrite": "replace",
|
|
6389
|
+
"node.role_names": "node.assignees.role_names",
|
|
6390
|
+
"node.role_ids": "node.assignees.role_ids",
|
|
6391
|
+
"node.member_names": "node.assignees.member_names",
|
|
6392
|
+
"node.member_emails": "node.assignees.member_emails",
|
|
6393
|
+
"node.member_uids": "node.assignees.member_uids",
|
|
6394
|
+
"node.editable_fields": "node.permissions.editable_fields",
|
|
6395
|
+
},
|
|
6396
|
+
"allowed_values": {
|
|
6397
|
+
"mode": ["replace"],
|
|
6398
|
+
"node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
|
|
5045
6399
|
},
|
|
5046
|
-
"allowed_values": {},
|
|
5047
6400
|
"dependency_hints": [
|
|
5048
|
-
"
|
|
5049
|
-
"
|
|
6401
|
+
"approval-style workflows require an explicit business status select field before app_flow_apply, such as 状态 / 处理状态 / 审批状态 / 工单状态",
|
|
6402
|
+
"approve/fill/copy nodes require at least one assignee",
|
|
5050
6403
|
],
|
|
5051
6404
|
"execution_notes": [
|
|
5052
|
-
"
|
|
5053
|
-
"
|
|
5054
|
-
"
|
|
5055
|
-
"
|
|
6405
|
+
"public flow building is intentionally limited to linear workflows",
|
|
6406
|
+
"app_flow_apply is replace-only; do not treat node snippets as partial patches",
|
|
6407
|
+
"branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
|
|
6408
|
+
"do not create platform system workflow fields such as 当前流程状态 / 当前处理人 / 当前处理节点 / 流程标题; use an explicit business status field instead",
|
|
6409
|
+
"workflow verification only covers linear node structure in the public tool surface",
|
|
5056
6410
|
],
|
|
5057
6411
|
"minimal_example": {
|
|
5058
6412
|
"profile": "default",
|
|
5059
6413
|
"app_key": "APP_KEY",
|
|
6414
|
+
"mode": "replace",
|
|
5060
6415
|
"publish": True,
|
|
5061
|
-
"
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
6416
|
+
"nodes": [
|
|
6417
|
+
{"id": "start", "type": "start", "name": "发起"},
|
|
6418
|
+
{
|
|
6419
|
+
"id": "approve_1",
|
|
6420
|
+
"type": "approve",
|
|
6421
|
+
"name": "部门审批",
|
|
6422
|
+
"assignees": {"role_names": ["项目经理"]},
|
|
6423
|
+
"permissions": {"editable_fields": ["状态", "审批意见"]},
|
|
6424
|
+
},
|
|
6425
|
+
{"id": "end", "type": "end", "name": "结束"},
|
|
6426
|
+
],
|
|
6427
|
+
"transitions": [{"from": "start", "to": "approve_1"}, {"from": "approve_1", "to": "end"}],
|
|
5071
6428
|
},
|
|
5072
6429
|
},
|
|
5073
6430
|
"app_views_plan": {
|
|
@@ -5125,12 +6482,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5125
6482
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
5126
6483
|
},
|
|
5127
6484
|
"execution_notes": [
|
|
6485
|
+
"creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
|
|
5128
6486
|
"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",
|
|
5129
6487
|
"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",
|
|
5130
|
-
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
6488
|
+
"upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
|
|
6489
|
+
"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",
|
|
5131
6490
|
"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",
|
|
5132
6491
|
"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",
|
|
5133
6492
|
"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",
|
|
6493
|
+
"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",
|
|
5134
6494
|
"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",
|
|
5135
6495
|
"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",
|
|
5136
6496
|
"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",
|
|
@@ -5139,7 +6499,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5139
6499
|
"minimal_example": {
|
|
5140
6500
|
"profile": "default",
|
|
5141
6501
|
"app_key": "APP_KEY",
|
|
5142
|
-
"upsert_views": [{"name": "
|
|
6502
|
+
"upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
|
|
5143
6503
|
"remove_views": [],
|
|
5144
6504
|
},
|
|
5145
6505
|
"query_conditions_example": {
|
|
@@ -5177,6 +6537,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5177
6537
|
],
|
|
5178
6538
|
"remove_views": [],
|
|
5179
6539
|
},
|
|
6540
|
+
"query_conditions_field_rules": {
|
|
6541
|
+
"supported_field_types": ["text", "long_text", "number", "amount", "date", "datetime", "single_select", "multi_select", "phone", "email", "boolean", "member", "department"],
|
|
6542
|
+
"unsupported_field_types": ["relation", "attachment", "subtable", "address", "code_block", "q_linker"],
|
|
6543
|
+
"use_instead": {
|
|
6544
|
+
"fixed_filter": "filters",
|
|
6545
|
+
"current_record_related_report_or_view": "app_associated_resources_apply.match_mappings",
|
|
6546
|
+
},
|
|
6547
|
+
},
|
|
5180
6548
|
"gantt_example": {
|
|
5181
6549
|
"profile": "default",
|
|
5182
6550
|
"app_key": "APP_KEY",
|
|
@@ -5197,7 +6565,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5197
6565
|
"app_views_apply": {
|
|
5198
6566
|
"allowed_keys": [
|
|
5199
6567
|
"app_key",
|
|
5200
|
-
"apps",
|
|
5201
6568
|
"publish",
|
|
5202
6569
|
"upsert_views",
|
|
5203
6570
|
"patch_views",
|
|
@@ -5250,27 +6617,29 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5250
6617
|
},
|
|
5251
6618
|
"execution_notes": [
|
|
5252
6619
|
"apply may return partial_success when some views land and others fail",
|
|
6620
|
+
"creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
|
|
5253
6621
|
"when duplicate view names exist, supply view_key to target the exact view",
|
|
5254
6622
|
"read back app_get after any failed or partial view apply",
|
|
5255
6623
|
"view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
|
|
5256
6624
|
"buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
|
|
5257
6625
|
"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",
|
|
5258
6626
|
"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",
|
|
5259
|
-
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
6627
|
+
"upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
|
|
6628
|
+
"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",
|
|
5260
6629
|
"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",
|
|
5261
6630
|
"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",
|
|
5262
6631
|
"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",
|
|
6632
|
+
"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",
|
|
5263
6633
|
"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",
|
|
5264
6634
|
"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",
|
|
5265
6635
|
"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",
|
|
5266
|
-
"accepts apps[] for multi-app batch; each item is {app_key, upsert_views?, patch_views?, remove_views?, publish?}; top-level publish is used as default for items that omit publish",
|
|
5267
6636
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
5268
6637
|
],
|
|
5269
6638
|
"minimal_example": {
|
|
5270
6639
|
"profile": "default",
|
|
5271
6640
|
"app_key": "APP_KEY",
|
|
5272
6641
|
"publish": True,
|
|
5273
|
-
"upsert_views": [{"name": "
|
|
6642
|
+
"upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
|
|
5274
6643
|
"remove_views": [],
|
|
5275
6644
|
},
|
|
5276
6645
|
"query_conditions_example": {
|
|
@@ -5310,6 +6679,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5310
6679
|
],
|
|
5311
6680
|
"remove_views": [],
|
|
5312
6681
|
},
|
|
6682
|
+
"query_conditions_field_rules": {
|
|
6683
|
+
"supported_field_types": ["text", "long_text", "number", "amount", "date", "datetime", "single_select", "multi_select", "phone", "email", "boolean", "member", "department"],
|
|
6684
|
+
"unsupported_field_types": ["relation", "attachment", "subtable", "address", "code_block", "q_linker"],
|
|
6685
|
+
"use_instead": {
|
|
6686
|
+
"fixed_filter": "filters",
|
|
6687
|
+
"current_record_related_report_or_view": "app_associated_resources_apply.match_mappings",
|
|
6688
|
+
},
|
|
6689
|
+
},
|
|
5313
6690
|
"gantt_example": {
|
|
5314
6691
|
"profile": "default",
|
|
5315
6692
|
"app_key": "APP_KEY",
|
|
@@ -5329,19 +6706,18 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5329
6706
|
},
|
|
5330
6707
|
},
|
|
5331
6708
|
"app_get": {
|
|
5332
|
-
"allowed_keys": ["app_key"
|
|
6709
|
+
"allowed_keys": ["app_key"],
|
|
5333
6710
|
"aliases": {},
|
|
5334
6711
|
"allowed_values": {},
|
|
5335
6712
|
"execution_notes": [
|
|
5336
6713
|
"returns builder-side app map: base summary, editability, field/view/chart/button counts, compact views, compact charts, custom_buttons, and app-level associated_resources",
|
|
5337
6714
|
"use this as the default builder discovery read before view_get/chart_get/apply detail work",
|
|
5338
6715
|
"editability is route-aware builder capability summary, not end-user data visibility",
|
|
5339
|
-
"can_edit_app_base covers app base-info writes such as app_name, icon, and visibility",
|
|
5340
|
-
"can_edit_form covers form/schema routes
|
|
6716
|
+
"can_edit_app_base covers app base-info writes such as app_name, icon, and visibility; it follows backend EditAppAuth and does not require package edit_tag",
|
|
6717
|
+
"can_edit_form covers form/schema routes and also follows backend EditAppAuth",
|
|
5341
6718
|
"returns normalized app visibility when backend auth is readable",
|
|
5342
6719
|
"custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
|
|
5343
6720
|
"associated_resources[].associated_item_id is the internal id; app_associated_resources_apply.view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key",
|
|
5344
|
-
"accepts app_keys[] for batch summary read; batch returns {status, apps[], errors[]} where each apps[] item preserves the single app_get summary fields",
|
|
5345
6721
|
],
|
|
5346
6722
|
"minimal_example": {
|
|
5347
6723
|
"profile": "default",
|
|
@@ -5349,7 +6725,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5349
6725
|
},
|
|
5350
6726
|
},
|
|
5351
6727
|
"app_get_fields": {
|
|
5352
|
-
"allowed_keys": ["app_key"
|
|
6728
|
+
"allowed_keys": ["app_key"],
|
|
5353
6729
|
"aliases": {},
|
|
5354
6730
|
"allowed_values": {},
|
|
5355
6731
|
"execution_notes": [
|
|
@@ -5357,8 +6733,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5357
6733
|
"use this before app_schema_apply when you need exact field definitions",
|
|
5358
6734
|
"also returns chart_fields from QingBI datasource fields; app_charts_apply field selectors should use chart_fields because record/schema-visible fields and QingBI fields are not the same schema",
|
|
5359
6735
|
"chart_fields[].field_id supports field_<queId> selectors, while chart_fields[].bi_field_id is the raw QingBI fieldId accepted by report configs",
|
|
6736
|
+
"chart_fields[].chart_apply_examples contains copyable semantic app_charts_apply snippets such as count_by_field, filtered_count, and numeric sum_metric",
|
|
5360
6737
|
"subtable fields include nested subfields using the same compact field shape",
|
|
5361
|
-
"accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, fields}",
|
|
5362
6738
|
],
|
|
5363
6739
|
"minimal_example": {
|
|
5364
6740
|
"profile": "default",
|
|
@@ -5366,13 +6742,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5366
6742
|
},
|
|
5367
6743
|
},
|
|
5368
6744
|
"app_get_layout": {
|
|
5369
|
-
"allowed_keys": ["app_key"
|
|
6745
|
+
"allowed_keys": ["app_key"],
|
|
5370
6746
|
"aliases": {},
|
|
5371
6747
|
"allowed_values": {},
|
|
5372
6748
|
"execution_notes": [
|
|
5373
6749
|
"returns compact current layout configuration for one app",
|
|
5374
6750
|
"use this before app_layout_apply when you need paragraph and row structure",
|
|
5375
|
-
"accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, sections}",
|
|
5376
6751
|
],
|
|
5377
6752
|
"minimal_example": {
|
|
5378
6753
|
"profile": "default",
|
|
@@ -5380,7 +6755,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5380
6755
|
},
|
|
5381
6756
|
},
|
|
5382
6757
|
"app_get_views": {
|
|
5383
|
-
"allowed_keys": ["app_key"
|
|
6758
|
+
"allowed_keys": ["app_key"],
|
|
5384
6759
|
"aliases": {},
|
|
5385
6760
|
"allowed_values": {},
|
|
5386
6761
|
"execution_notes": [
|
|
@@ -5388,7 +6763,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5388
6763
|
"compatibility/specialized inventory tool; default builder discovery should start with app_get",
|
|
5389
6764
|
"use this before app_views_apply only when you need an exact current view inventory beyond app_get",
|
|
5390
6765
|
"view items include visibility_summary when backend view auth is readable",
|
|
5391
|
-
"accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, views}",
|
|
5392
6766
|
],
|
|
5393
6767
|
"minimal_example": {
|
|
5394
6768
|
"profile": "default",
|
|
@@ -5396,13 +6770,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5396
6770
|
},
|
|
5397
6771
|
},
|
|
5398
6772
|
"app_get_flow": {
|
|
5399
|
-
"allowed_keys": ["app_key"
|
|
6773
|
+
"allowed_keys": ["app_key"],
|
|
5400
6774
|
"aliases": {},
|
|
5401
6775
|
"allowed_values": {},
|
|
5402
6776
|
"execution_notes": [
|
|
5403
|
-
"returns
|
|
5404
|
-
"use
|
|
5405
|
-
"accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, spec}",
|
|
6777
|
+
"returns workflow configuration summary for one app",
|
|
6778
|
+
"use this before app_flow_apply when you need the current node structure",
|
|
5406
6779
|
],
|
|
5407
6780
|
"minimal_example": {
|
|
5408
6781
|
"profile": "default",
|
|
@@ -5410,7 +6783,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5410
6783
|
},
|
|
5411
6784
|
},
|
|
5412
6785
|
"app_get_charts": {
|
|
5413
|
-
"allowed_keys": ["app_key"
|
|
6786
|
+
"allowed_keys": ["app_key"],
|
|
5414
6787
|
"aliases": {},
|
|
5415
6788
|
"allowed_values": {},
|
|
5416
6789
|
"execution_notes": [
|
|
@@ -5419,37 +6792,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5419
6792
|
"use this before app_charts_apply when you need exact current chart_id values beyond the app_get summary",
|
|
5420
6793
|
"chart summaries do not include full qingbi config payloads",
|
|
5421
6794
|
"chart items include visibility_summary when QingBI base info is readable",
|
|
5422
|
-
"accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, charts}",
|
|
5423
|
-
],
|
|
5424
|
-
"minimal_example": {
|
|
5425
|
-
"profile": "default",
|
|
5426
|
-
"app_key": "APP_KEY",
|
|
5427
|
-
},
|
|
5428
|
-
},
|
|
5429
|
-
"app_get_buttons": {
|
|
5430
|
-
"allowed_keys": ["app_key", "app_keys"],
|
|
5431
|
-
"aliases": {},
|
|
5432
|
-
"allowed_values": {},
|
|
5433
|
-
"execution_notes": [
|
|
5434
|
-
"returns custom button list (draft state) for one app",
|
|
5435
|
-
"also returns view_configs read from view bindings, so app_custom_buttons_apply view_configs can be patched from the same read result",
|
|
5436
|
-
"accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item preserves the single-read fields such as {app_key, buttons, view_configs, warnings, verification}",
|
|
5437
|
-
"use before app_custom_buttons_apply when you need current button_id values",
|
|
5438
|
-
],
|
|
5439
|
-
"minimal_example": {
|
|
5440
|
-
"profile": "default",
|
|
5441
|
-
"app_key": "APP_KEY",
|
|
5442
|
-
},
|
|
5443
|
-
},
|
|
5444
|
-
"app_get_associated_resources": {
|
|
5445
|
-
"allowed_keys": ["app_key", "app_keys"],
|
|
5446
|
-
"aliases": {},
|
|
5447
|
-
"allowed_values": {},
|
|
5448
|
-
"execution_notes": [
|
|
5449
|
-
"returns associated resource pool (draft state) for one app",
|
|
5450
|
-
"also returns view_configs read from view bindings, so app_associated_resources_apply view_configs can be patched from the same read result",
|
|
5451
|
-
"accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item preserves the single-read fields such as {app_key, associated_resources, view_configs, warnings, verification}",
|
|
5452
|
-
"use before app_associated_resources_apply when you need current associated_item_id values",
|
|
5453
6795
|
],
|
|
5454
6796
|
"minimal_example": {
|
|
5455
6797
|
"profile": "default",
|
|
@@ -5464,6 +6806,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5464
6806
|
"returns builder-configurable portal list items only",
|
|
5465
6807
|
"use this as the builder portal discovery path before portal_get",
|
|
5466
6808
|
"results are compact list items, not raw dash payloads",
|
|
6809
|
+
"large workspaces may return an unfiltered discovery list with portal_permissions_verified=false instead of probing every portal detail",
|
|
5467
6810
|
],
|
|
5468
6811
|
"minimal_example": {
|
|
5469
6812
|
"profile": "default",
|
|
@@ -5486,7 +6829,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5486
6829
|
},
|
|
5487
6830
|
},
|
|
5488
6831
|
"app_charts_apply": {
|
|
5489
|
-
"allowed_keys": [
|
|
6832
|
+
"allowed_keys": [
|
|
6833
|
+
"app_key",
|
|
6834
|
+
"upsert_charts",
|
|
6835
|
+
"patch_charts",
|
|
6836
|
+
"remove_chart_ids",
|
|
6837
|
+
"reorder_chart_ids",
|
|
6838
|
+
"upsert_charts[].metric",
|
|
6839
|
+
"upsert_charts[].metrics",
|
|
6840
|
+
"upsert_charts[].group_by",
|
|
6841
|
+
"upsert_charts[].where",
|
|
6842
|
+
"upsert_charts[].visibility",
|
|
6843
|
+
"patch_charts[].chart_id",
|
|
6844
|
+
"patch_charts[].name",
|
|
6845
|
+
"patch_charts[].set",
|
|
6846
|
+
"patch_charts[].unset",
|
|
6847
|
+
],
|
|
5490
6848
|
"aliases": {
|
|
5491
6849
|
"patchCharts": "patch_charts",
|
|
5492
6850
|
"chart.id": "chart.chart_id",
|
|
@@ -5494,6 +6852,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5494
6852
|
"chart.dimension_fields": "chart.dimension_field_ids",
|
|
5495
6853
|
"chart.indicator_fields": "chart.indicator_field_ids",
|
|
5496
6854
|
"chart.metric_field_ids": "chart.indicator_field_ids",
|
|
6855
|
+
"chart.dimensions": "chart.group_by",
|
|
6856
|
+
"chart.groupBy": "chart.group_by",
|
|
6857
|
+
"chart.where": "chart.filters",
|
|
6858
|
+
"chart.metric.operation": "chart.metric.op",
|
|
6859
|
+
"chart.metric.aggregation": "chart.metric.op",
|
|
5497
6860
|
"chart.filter.op": "chart.filter.operator",
|
|
5498
6861
|
},
|
|
5499
6862
|
"allowed_values": {
|
|
@@ -5514,18 +6877,20 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5514
6877
|
"upsert_charts[].visibility compiles to QingBI base visibleAuth only",
|
|
5515
6878
|
"visibility-only updates keep the existing chart config and do not rewrite rawDataConfigDTO.authInfo",
|
|
5516
6879
|
"chart dimension/metric/filter/query fields are resolved from app_get_fields.chart_fields (QingBI datasource fields), not record schema or form-only fields",
|
|
6880
|
+
"preferred chart metric DSL is SQL-like: metric='count(*)', metric='sum(金额)', metrics=['sum(金额)'], group_by=['状态'], where=[{field, op, value}]",
|
|
6881
|
+
"legacy dimension_field_ids/indicator_field_ids/config.aggregate remain supported as advanced compatibility input, but should not be the first choice for agents",
|
|
6882
|
+
"chart_get returns semantic group_by and metrics; raw QingBI config is diagnostic detail",
|
|
5517
6883
|
"system fields such as 申请人/申请时间/编号 are usable only when they appear in chart_fields; otherwise app_charts_apply returns CHART_FIELD_NOT_IN_QINGBI_SCHEMA",
|
|
5518
6884
|
"low-frequency chart types have local prevalidation: gauge requires 0 dimensions and 2 non-duplicated metrics; histogram requires at most 1 dimension and exactly 1 plain numeric metric",
|
|
5519
6885
|
"chart rule failures return chart_results[].diagnostics with rule_code, expected, actual, offending_fields, and next_action; backend 81002/81005 are translated when possible",
|
|
5520
6886
|
"remove_chart_ids deletes by chart_id and verifies each deleted chart with single chart_id readback; pure delete does not read the full chart list",
|
|
5521
6887
|
"if delete readback is unavailable or still finds the chart, chart_results[] returns delete_executed=true, readback_status, and safe_to_retry_delete=false; do not blindly repeat delete",
|
|
5522
|
-
"accepts apps[] for multi-app batch; each item is {app_key, upsert_charts?, patch_charts?, remove_chart_ids?, reorder_chart_ids?}",
|
|
5523
6888
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
5524
6889
|
],
|
|
5525
6890
|
"minimal_example": {
|
|
5526
6891
|
"profile": "default",
|
|
5527
6892
|
"app_key": "APP_KEY",
|
|
5528
|
-
"upsert_charts": [{"name": "数据总量", "chart_type": "target", "
|
|
6893
|
+
"upsert_charts": [{"name": "数据总量", "chart_type": "target", "metric": "count(*)", "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
|
|
5529
6894
|
"patch_charts": [{"chart_id": "CHART_ID", "set": {"name": "数据总量-新版"}}],
|
|
5530
6895
|
"remove_chart_ids": [],
|
|
5531
6896
|
"reorder_chart_ids": [],
|
|
@@ -5563,7 +6928,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5563
6928
|
},
|
|
5564
6929
|
},
|
|
5565
6930
|
"portal_apply": {
|
|
5566
|
-
"allowed_keys": ["dash_key", "dash_name", "name", "package_id", "publish", "sections", "
|
|
6931
|
+
"allowed_keys": ["dash_key", "dash_name", "name", "package_id", "publish", "sections", "pages", "payload", "layout_preset", "visibility", "auth", "icon", "color", "hide_copyright", "dash_global_config", "config"],
|
|
5567
6932
|
"aliases": {
|
|
5568
6933
|
"packageId": "package_id",
|
|
5569
6934
|
"name": "dash_name",
|
|
@@ -5575,13 +6940,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5575
6940
|
"section_allowed_keys": ["title", "source_type", "role", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
|
|
5576
6941
|
"section_aliases": {
|
|
5577
6942
|
"sourceType": "source_type",
|
|
6943
|
+
"zone": "role",
|
|
6944
|
+
"sectionRole": "role",
|
|
5578
6945
|
"chartRef": "chart_ref",
|
|
5579
6946
|
"viewRef": "view_ref",
|
|
5580
6947
|
"dashStyleConfigBO": "dash_style_config",
|
|
5581
6948
|
},
|
|
5582
6949
|
"allowed_values": {
|
|
5583
6950
|
"section.source_type": ["chart", "view", "grid", "filter", "text", "link"],
|
|
5584
|
-
"section.role": ["metric"],
|
|
5585
6951
|
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
5586
6952
|
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
5587
6953
|
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
@@ -5591,23 +6957,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5591
6957
|
"use exactly one resource mode",
|
|
5592
6958
|
"update mode: dash_key",
|
|
5593
6959
|
"create mode: package_id + dash_name",
|
|
6960
|
+
"create mode follows backend DashCtrl.createDash: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
|
|
5594
6961
|
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5595
6962
|
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
5596
6963
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
5597
6964
|
"portal_apply uses replace semantics for sections",
|
|
5598
6965
|
"when editing an existing portal, sections may be omitted to update only base info such as visibility, icon, or package",
|
|
5599
|
-
"
|
|
5600
|
-
"
|
|
5601
|
-
"remove a section by omitting it from the sections list (replace mode) or by unset in patch_sections (patch mode)",
|
|
6966
|
+
"portal section-level patch is not exposed; supplying sections means full sections replacement",
|
|
6967
|
+
"remove a section by omitting it from the new sections list",
|
|
5602
6968
|
"package_id is required when creating a new portal",
|
|
5603
6969
|
"publish=false only guarantees draft and base-info updates; it does not claim live has changed",
|
|
5604
|
-
"chart_ref resolves by chart_id
|
|
6970
|
+
"chart_ref resolves by chart_id first, then exact unique chart_name",
|
|
5605
6971
|
"view_ref resolves by view_key first, then exact unique view_name",
|
|
5606
6972
|
"pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
|
|
5607
|
-
"set section.role=metric only for target/indicator chart cards; metric cards use pc.rows >= 5 and pc.cols >= 6, while ordinary BI charts use pc.rows >= 7 and pc.cols >= 8",
|
|
5608
6973
|
"if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
|
|
5609
6974
|
"two-column pc layout should use x=0/12 with cols=12; three-column pc layout should use x=0/8/16 with cols=8",
|
|
5610
6975
|
"x=0/6 with cols=6 only occupies the left half of the pc portal and triggers PORTAL_LAYOUT_HALF_WIDTH",
|
|
6976
|
+
"grid sections must include config.items; an empty grid triggers PORTAL_GRID_ITEMS_EMPTY because the frontend only shows an empty entry container",
|
|
6977
|
+
"metric portal sections may set role=metric; role=metric requires a target/indicator chart and otherwise triggers PORTAL_METRIC_SECTION_CHART_TYPE_MISMATCH",
|
|
6978
|
+
"standard workbench count diagnostics warn when metric cards are not 4-6, BI charts are not 2-3, or business views are not 1-2",
|
|
5611
6979
|
"position.pc/mobile is the canonical portal layout shape",
|
|
5612
6980
|
"compat payload accepts name -> dash_name and single pages[0].components -> sections",
|
|
5613
6981
|
"visibility is the canonical public auth shape; auth is kept only as a deprecated compatibility alias",
|
|
@@ -5661,14 +7029,19 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5661
7029
|
"mobile": {"x": 0, "y": 0, "cols": 6, "rows": 8},
|
|
5662
7030
|
},
|
|
5663
7031
|
},
|
|
5664
|
-
|
|
7032
|
+
},
|
|
7033
|
+
"portal_delete": {
|
|
7034
|
+
"allowed_keys": ["dash_key"],
|
|
7035
|
+
"aliases": {"dashKey": "dash_key"},
|
|
7036
|
+
"allowed_values": {},
|
|
7037
|
+
"execution_notes": [
|
|
7038
|
+
"deletes one portal by dash_key",
|
|
7039
|
+
"delete results separate DELETE execution from readback verification",
|
|
7040
|
+
"if delete_executed=true and readback_status=unavailable or still_exists, do not blindly repeat delete; confirm later with portal_get or portal_list",
|
|
7041
|
+
],
|
|
7042
|
+
"minimal_example": {
|
|
5665
7043
|
"profile": "default",
|
|
5666
7044
|
"dash_key": "DASH_KEY",
|
|
5667
|
-
"publish": True,
|
|
5668
|
-
"patch_sections": [
|
|
5669
|
-
{"chart_ref": {"chart_id": "CHART_ID"}, "set": {"title": "销售总览-新版"}},
|
|
5670
|
-
{"order": 2, "set": {"title": "任务列表-新版"}},
|
|
5671
|
-
],
|
|
5672
7045
|
},
|
|
5673
7046
|
},
|
|
5674
7047
|
"app_publish_verify": {
|
|
@@ -5708,6 +7081,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5708
7081
|
_PRIVATE_BUILDER_TOOL_CONTRACTS = {
|
|
5709
7082
|
"app_schema_plan",
|
|
5710
7083
|
"app_layout_plan",
|
|
7084
|
+
"app_flow_plan",
|
|
5711
7085
|
"app_views_plan",
|
|
5712
7086
|
}
|
|
5713
7087
|
|