@qingflow-tech/qingflow-app-user-mcp 1.0.43 → 1.1.0
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 +2 -2
- package/npm/bin/qingflow-app-user-mcp.mjs +31 -2
- package/npm/lib/runtime.mjs +43 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +1 -1
- package/skills/qingflow-record-analysis/SKILL.md +1 -1
- package/skills/qingflow-record-delete/SKILL.md +1 -1
- package/skills/qingflow-record-import/SKILL.md +1 -1
- package/skills/qingflow-record-insert/SKILL.md +5 -2
- package/skills/qingflow-record-update/SKILL.md +1 -1
- package/skills/qingflow-task-ops/SKILL.md +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +0 -39
- package/src/qingflow_mcp/builder_facade/service.py +532 -859
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
- package/src/qingflow_mcp/cli/commands/builder.py +44 -12
- package/src/qingflow_mcp/cli/main.py +4 -6
- package/src/qingflow_mcp/public_surface.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +16 -8
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/executor.py +3 -133
- package/src/qingflow_mcp/tools/ai_builder_tools.py +177 -241
- package/src/qingflow_mcp/tools/solution_tools.py +30 -2
- package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
- package/src/qingflow_mcp/version.py +71 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
|
@@ -33,12 +33,11 @@ from ..tools.qingbi_report_tools import QingbiReportTools
|
|
|
33
33
|
from ..tools.role_tools import RoleTools
|
|
34
34
|
from ..tools.solution_tools import SolutionTools
|
|
35
35
|
from ..tools.view_tools import ViewTools
|
|
36
|
-
from
|
|
36
|
+
from . import workflow_spec as workflow_spec_api
|
|
37
37
|
from .button_style_catalog import button_style_catalog_payload
|
|
38
38
|
from .models import (
|
|
39
39
|
AppChartsReadResponse,
|
|
40
40
|
AppFieldsReadResponse,
|
|
41
|
-
AppFlowReadResponse,
|
|
42
41
|
ChartGetResponse,
|
|
43
42
|
AppLayoutReadResponse,
|
|
44
43
|
AppReadSummaryResponse,
|
|
@@ -64,8 +63,6 @@ from .models import (
|
|
|
64
63
|
FieldRemovePatch,
|
|
65
64
|
FieldSelector,
|
|
66
65
|
FieldUpdatePatch,
|
|
67
|
-
FlowPlanRequest,
|
|
68
|
-
FlowAssigneePatch,
|
|
69
66
|
LayoutApplyMode,
|
|
70
67
|
LayoutPlanRequest,
|
|
71
68
|
LayoutSectionPatch,
|
|
@@ -96,11 +93,10 @@ from .models import (
|
|
|
96
93
|
ViewGetResponse,
|
|
97
94
|
ViewsPlanRequest,
|
|
98
95
|
ViewsPreset,
|
|
99
|
-
FlowPreset,
|
|
100
|
-
FlowNodePermissionsPatch,
|
|
101
96
|
)
|
|
102
97
|
|
|
103
98
|
BUILDER_PORTAL_LIST_DETAIL_VERIFY_LIMIT = 50
|
|
99
|
+
CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE = 8
|
|
104
100
|
|
|
105
101
|
|
|
106
102
|
QUESTION_TYPE_TO_FIELD_TYPE: dict[int, str] = {
|
|
@@ -186,6 +182,37 @@ QUERY_CONDITION_UNSUPPORTED_FIELD_TYPES = {
|
|
|
186
182
|
FieldType.relation.value,
|
|
187
183
|
FieldType.subtable.value,
|
|
188
184
|
}
|
|
185
|
+
QUERY_CONDITION_UNSUPPORTED_FIELD_TYPES_PUBLIC = sorted(
|
|
186
|
+
{
|
|
187
|
+
"address",
|
|
188
|
+
"attachment",
|
|
189
|
+
"code_block",
|
|
190
|
+
"q_linker",
|
|
191
|
+
"relation",
|
|
192
|
+
"subtable",
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
QUERY_CONDITION_SUPPORTED_FIELD_TYPES_PUBLIC = sorted(
|
|
196
|
+
{
|
|
197
|
+
"text",
|
|
198
|
+
"long_text",
|
|
199
|
+
"number",
|
|
200
|
+
"amount",
|
|
201
|
+
"date",
|
|
202
|
+
"datetime",
|
|
203
|
+
"single_select",
|
|
204
|
+
"multi_select",
|
|
205
|
+
"phone",
|
|
206
|
+
"email",
|
|
207
|
+
"boolean",
|
|
208
|
+
"member",
|
|
209
|
+
"department",
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
QUERY_CONDITION_FIX_HINT = (
|
|
213
|
+
"Remove this field from query_conditions. query_conditions only configures the frontend query panel for supported query fields. "
|
|
214
|
+
"Use filters for fixed saved filters, and use associated-resource match_mappings for current-record relation/report matching."
|
|
215
|
+
)
|
|
189
216
|
|
|
190
217
|
ASSOCIATED_RESOURCE_LIMIT_TYPE_ALL = 1
|
|
191
218
|
ASSOCIATED_RESOURCE_LIMIT_TYPE_SELECT = 2
|
|
@@ -234,7 +261,6 @@ class AiBuilderFacade:
|
|
|
234
261
|
buttons: CustomButtonTools,
|
|
235
262
|
packages: PackageTools,
|
|
236
263
|
views: ViewTools,
|
|
237
|
-
workflows: WorkflowTools,
|
|
238
264
|
portals: PortalTools,
|
|
239
265
|
charts: QingbiReportTools,
|
|
240
266
|
roles: RoleTools,
|
|
@@ -245,13 +271,216 @@ class AiBuilderFacade:
|
|
|
245
271
|
self.buttons = buttons
|
|
246
272
|
self.packages = packages
|
|
247
273
|
self.views = views
|
|
248
|
-
self.workflows = workflows
|
|
249
274
|
self.portals = portals
|
|
250
275
|
self.charts = charts
|
|
251
276
|
self.roles = roles
|
|
252
277
|
self.directory = directory
|
|
253
278
|
self.solutions = solutions
|
|
254
279
|
|
|
280
|
+
def _with_backend_context(self, profile: str, handler):
|
|
281
|
+
return self.apps._run(profile, lambda _session_profile, context: handler(context))
|
|
282
|
+
|
|
283
|
+
def flow_get_schema(self, *, profile: str, schema_version: str | None = None) -> JSONObject:
|
|
284
|
+
normalized_args = {"schema_version": schema_version or workflow_spec_api.DEFAULT_SCHEMA_VERSION}
|
|
285
|
+
try:
|
|
286
|
+
payload = self._with_backend_context(
|
|
287
|
+
profile,
|
|
288
|
+
lambda context: workflow_spec_api.fetch_schema(
|
|
289
|
+
self.apps.backend,
|
|
290
|
+
context,
|
|
291
|
+
schema_version=schema_version,
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
295
|
+
api_error = _coerce_api_error(error)
|
|
296
|
+
return _failed_from_api_error(
|
|
297
|
+
"FLOW_SPEC_SCHEMA_FAILED",
|
|
298
|
+
api_error,
|
|
299
|
+
normalized_args=normalized_args,
|
|
300
|
+
details={},
|
|
301
|
+
suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile, **normalized_args}},
|
|
302
|
+
)
|
|
303
|
+
return {
|
|
304
|
+
"status": "success",
|
|
305
|
+
"error_code": None,
|
|
306
|
+
"recoverable": False,
|
|
307
|
+
"message": "read workflow spec schema",
|
|
308
|
+
"normalized_args": normalized_args,
|
|
309
|
+
"missing_fields": [],
|
|
310
|
+
"allowed_values": {},
|
|
311
|
+
"details": {"schema": payload},
|
|
312
|
+
"request_id": None,
|
|
313
|
+
"suggested_next_call": None,
|
|
314
|
+
"noop": False,
|
|
315
|
+
"warnings": [],
|
|
316
|
+
"verification": {"schema_loaded": True},
|
|
317
|
+
"verified": True,
|
|
318
|
+
"schema": payload,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
def flow_get(
|
|
322
|
+
self,
|
|
323
|
+
*,
|
|
324
|
+
profile: str,
|
|
325
|
+
app_key: str,
|
|
326
|
+
version_id: str | None = None,
|
|
327
|
+
) -> JSONObject:
|
|
328
|
+
normalized_args = {"app_key": app_key, "version_id": version_id}
|
|
329
|
+
permission_outcome = self._guard_app_permission(
|
|
330
|
+
profile=profile,
|
|
331
|
+
app_key=app_key,
|
|
332
|
+
required_permission="data_manage",
|
|
333
|
+
normalized_args=normalized_args,
|
|
334
|
+
)
|
|
335
|
+
if permission_outcome.block is not None:
|
|
336
|
+
return permission_outcome.block
|
|
337
|
+
try:
|
|
338
|
+
payload = self._with_backend_context(
|
|
339
|
+
profile,
|
|
340
|
+
lambda context: workflow_spec_api.fetch_spec(
|
|
341
|
+
self.apps.backend,
|
|
342
|
+
context,
|
|
343
|
+
app_key=app_key,
|
|
344
|
+
version_id=version_id,
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
348
|
+
api_error = _coerce_api_error(error)
|
|
349
|
+
return _failed_from_api_error(
|
|
350
|
+
"FLOW_SPEC_GET_FAILED",
|
|
351
|
+
api_error,
|
|
352
|
+
normalized_args=normalized_args,
|
|
353
|
+
details={"app_key": app_key},
|
|
354
|
+
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
355
|
+
)
|
|
356
|
+
response = {
|
|
357
|
+
"status": "success",
|
|
358
|
+
"error_code": None,
|
|
359
|
+
"recoverable": False,
|
|
360
|
+
"message": "read workflow spec",
|
|
361
|
+
"normalized_args": normalized_args,
|
|
362
|
+
"missing_fields": [],
|
|
363
|
+
"allowed_values": {},
|
|
364
|
+
"details": {},
|
|
365
|
+
"request_id": None,
|
|
366
|
+
"suggested_next_call": None,
|
|
367
|
+
"noop": False,
|
|
368
|
+
"warnings": list(permission_outcome.warnings),
|
|
369
|
+
"verification": {"spec_loaded": True},
|
|
370
|
+
"verified": True,
|
|
371
|
+
"app_key": app_key,
|
|
372
|
+
}
|
|
373
|
+
if isinstance(payload, dict):
|
|
374
|
+
response.update(payload)
|
|
375
|
+
else:
|
|
376
|
+
response["result"] = payload
|
|
377
|
+
return _apply_permission_outcomes(response, permission_outcome)
|
|
378
|
+
|
|
379
|
+
def flow_apply(
|
|
380
|
+
self,
|
|
381
|
+
*,
|
|
382
|
+
profile: str,
|
|
383
|
+
app_key: str,
|
|
384
|
+
spec: dict[str, Any],
|
|
385
|
+
publish: bool = True,
|
|
386
|
+
idempotency_key: str | None = None,
|
|
387
|
+
schema_version: str | None = None,
|
|
388
|
+
) -> JSONObject:
|
|
389
|
+
normalized_args = {
|
|
390
|
+
"app_key": app_key,
|
|
391
|
+
"spec": spec,
|
|
392
|
+
"publish": publish,
|
|
393
|
+
"idempotency_key": idempotency_key,
|
|
394
|
+
"schema_version": schema_version or workflow_spec_api.DEFAULT_SCHEMA_VERSION,
|
|
395
|
+
}
|
|
396
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
397
|
+
permission_outcome = self._guard_app_permission(
|
|
398
|
+
profile=profile,
|
|
399
|
+
app_key=app_key,
|
|
400
|
+
required_permission="data_manage",
|
|
401
|
+
normalized_args=normalized_args,
|
|
402
|
+
)
|
|
403
|
+
if permission_outcome.block is not None:
|
|
404
|
+
return permission_outcome.block
|
|
405
|
+
permission_outcomes.append(permission_outcome)
|
|
406
|
+
|
|
407
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
408
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
409
|
+
|
|
410
|
+
if not isinstance(spec, dict) or not spec:
|
|
411
|
+
return finalize(
|
|
412
|
+
_failed(
|
|
413
|
+
"FLOW_SPEC_REQUIRED",
|
|
414
|
+
"spec must be a non-empty WorkflowSpecDTO object",
|
|
415
|
+
normalized_args=normalized_args,
|
|
416
|
+
suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile}},
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
apply_body = {
|
|
420
|
+
"appKey": app_key,
|
|
421
|
+
"idempotencyKey": idempotency_key or str(uuid4()),
|
|
422
|
+
"schemaVersion": schema_version or workflow_spec_api.DEFAULT_SCHEMA_VERSION,
|
|
423
|
+
"spec": spec,
|
|
424
|
+
}
|
|
425
|
+
try:
|
|
426
|
+
apply_result = self._with_backend_context(
|
|
427
|
+
profile,
|
|
428
|
+
lambda context: workflow_spec_api.post_apply(self.apps.backend, context, apply_body=apply_body),
|
|
429
|
+
)
|
|
430
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
431
|
+
api_error = _coerce_api_error(error)
|
|
432
|
+
return finalize(
|
|
433
|
+
_failed_from_api_error(
|
|
434
|
+
"FLOW_SPEC_APPLY_FAILED",
|
|
435
|
+
api_error,
|
|
436
|
+
normalized_args=normalized_args,
|
|
437
|
+
details={"app_key": app_key},
|
|
438
|
+
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
if not isinstance(apply_result, dict):
|
|
442
|
+
apply_result = {"result": apply_result}
|
|
443
|
+
verification, verify_warnings, verified = workflow_spec_api.verify_apply_response(
|
|
444
|
+
input_spec=spec,
|
|
445
|
+
apply_result=apply_result,
|
|
446
|
+
)
|
|
447
|
+
backend_status = str(apply_result.get("status") or "SUCCESS").upper()
|
|
448
|
+
if backend_status not in {"SUCCESS", "OK"}:
|
|
449
|
+
return finalize(
|
|
450
|
+
_failed(
|
|
451
|
+
"FLOW_SPEC_APPLY_FAILED",
|
|
452
|
+
str(apply_result.get("message") or "workflow spec apply failed"),
|
|
453
|
+
normalized_args=normalized_args,
|
|
454
|
+
details={"apply_result": apply_result},
|
|
455
|
+
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
status = "success" if verified else "partial_success"
|
|
459
|
+
response = {
|
|
460
|
+
"status": status,
|
|
461
|
+
"error_code": None if verified else "FLOW_SPEC_VERIFY_FAILED",
|
|
462
|
+
"recoverable": not verified,
|
|
463
|
+
"message": "applied workflow spec" if verified else "applied workflow spec; verification reported issues",
|
|
464
|
+
"normalized_args": normalized_args,
|
|
465
|
+
"missing_fields": [],
|
|
466
|
+
"allowed_values": {},
|
|
467
|
+
"details": {
|
|
468
|
+
"apply_result": apply_result,
|
|
469
|
+
"appliedSpec": apply_result.get("appliedSpec"),
|
|
470
|
+
"diffSummary": apply_result.get("diffSummary"),
|
|
471
|
+
"semanticLint": apply_result.get("semanticLint"),
|
|
472
|
+
},
|
|
473
|
+
"request_id": None,
|
|
474
|
+
"suggested_next_call": None if verified else {"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
475
|
+
"noop": False,
|
|
476
|
+
"warnings": verify_warnings,
|
|
477
|
+
"verification": verification,
|
|
478
|
+
"verified": verified,
|
|
479
|
+
"app_key": app_key,
|
|
480
|
+
"current_version_id": apply_result.get("currentVersionId"),
|
|
481
|
+
}
|
|
482
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
483
|
+
|
|
255
484
|
def package_resolve(self, *, profile: str, package_name: str) -> JSONObject:
|
|
256
485
|
requested = str(package_name or "").strip()
|
|
257
486
|
if not requested:
|
|
@@ -2058,83 +2287,6 @@ class AiBuilderFacade:
|
|
|
2058
2287
|
},
|
|
2059
2288
|
}
|
|
2060
2289
|
|
|
2061
|
-
def _normalize_flow_nodes(
|
|
2062
|
-
self,
|
|
2063
|
-
*,
|
|
2064
|
-
profile: str,
|
|
2065
|
-
current_fields: list[dict[str, Any]],
|
|
2066
|
-
nodes: list[dict[str, Any]],
|
|
2067
|
-
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
2068
|
-
field_name_to_field = {
|
|
2069
|
-
str(field.get("name") or ""): field
|
|
2070
|
-
for field in current_fields
|
|
2071
|
-
if str(field.get("name") or "")
|
|
2072
|
-
}
|
|
2073
|
-
field_name_to_que_id = {
|
|
2074
|
-
str(field.get("name") or ""): int(field.get("que_id"))
|
|
2075
|
-
for field in current_fields
|
|
2076
|
-
if str(field.get("name") or "") and isinstance(field.get("que_id"), int)
|
|
2077
|
-
}
|
|
2078
|
-
normalized_nodes: list[dict[str, Any]] = []
|
|
2079
|
-
issues: list[dict[str, Any]] = []
|
|
2080
|
-
for node in nodes:
|
|
2081
|
-
if not isinstance(node, dict):
|
|
2082
|
-
continue
|
|
2083
|
-
normalized_node = deepcopy(node)
|
|
2084
|
-
assignees = FlowAssigneePatch.model_validate(node.get("assignees") or {})
|
|
2085
|
-
permissions = FlowNodePermissionsPatch.model_validate(node.get("permissions") or {})
|
|
2086
|
-
role_resolution = self._resolve_role_references(
|
|
2087
|
-
profile=profile,
|
|
2088
|
-
role_ids=assignees.role_ids,
|
|
2089
|
-
role_names=assignees.role_names,
|
|
2090
|
-
)
|
|
2091
|
-
member_resolution = self._resolve_member_references(
|
|
2092
|
-
profile=profile,
|
|
2093
|
-
member_uids=assignees.member_uids,
|
|
2094
|
-
member_emails=assignees.member_emails,
|
|
2095
|
-
member_names=assignees.member_names,
|
|
2096
|
-
)
|
|
2097
|
-
issues.extend({**issue, "node_id": node.get("id")} for issue in [*role_resolution["issues"], *member_resolution["issues"]])
|
|
2098
|
-
editable_que_ids: list[int] = []
|
|
2099
|
-
missing_editable_fields: list[str] = []
|
|
2100
|
-
for field_name in permissions.editable_fields:
|
|
2101
|
-
if field_name not in field_name_to_que_id:
|
|
2102
|
-
missing_editable_fields.append(field_name)
|
|
2103
|
-
else:
|
|
2104
|
-
editable_que_ids.append(field_name_to_que_id[field_name])
|
|
2105
|
-
if missing_editable_fields:
|
|
2106
|
-
issues.append(
|
|
2107
|
-
{
|
|
2108
|
-
"node_id": node.get("id"),
|
|
2109
|
-
"kind": "editable_fields",
|
|
2110
|
-
"error_code": "UNKNOWN_FLOW_FIELD",
|
|
2111
|
-
"missing_fields": missing_editable_fields,
|
|
2112
|
-
}
|
|
2113
|
-
)
|
|
2114
|
-
condition_matrix, condition_issues = _build_flow_condition_matrix(
|
|
2115
|
-
current_fields_by_name=field_name_to_field,
|
|
2116
|
-
node=normalized_node,
|
|
2117
|
-
)
|
|
2118
|
-
issues.extend({**issue, "node_id": node.get("id")} for issue in condition_issues)
|
|
2119
|
-
config_payload = deepcopy(normalized_node.get("config") or {}) if isinstance(normalized_node.get("config"), dict) else {}
|
|
2120
|
-
if condition_matrix:
|
|
2121
|
-
# Backend judge nodes persist branch conditions from `autoJudges`.
|
|
2122
|
-
# Keep the legacy mirror key for internal verification logic.
|
|
2123
|
-
config_payload["autoJudges"] = condition_matrix
|
|
2124
|
-
config_payload["conditionFormatMatrix"] = condition_matrix
|
|
2125
|
-
normalized_node["assignees"] = {
|
|
2126
|
-
"member_uids": member_resolution["member_uids"],
|
|
2127
|
-
"role_entries": role_resolution["role_entries"],
|
|
2128
|
-
"include_sub_departs": assignees.include_sub_departs,
|
|
2129
|
-
}
|
|
2130
|
-
normalized_node["permissions"] = {
|
|
2131
|
-
"editable_fields": permissions.editable_fields,
|
|
2132
|
-
"editable_que_ids": editable_que_ids,
|
|
2133
|
-
}
|
|
2134
|
-
if config_payload:
|
|
2135
|
-
normalized_node["config"] = config_payload
|
|
2136
|
-
normalized_nodes.append(normalized_node)
|
|
2137
|
-
return normalized_nodes, issues
|
|
2138
2290
|
|
|
2139
2291
|
def _unsupported_public_flow_nodes(self, *, nodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
2140
2292
|
unsupported: list[dict[str, Any]] = []
|
|
@@ -4378,7 +4530,7 @@ class AiBuilderFacade:
|
|
|
4378
4530
|
"details": {"edit_version_no": None, "associated_item_ids_by_client_key": client_key_to_id, "readback_failed": False},
|
|
4379
4531
|
"request_id": None,
|
|
4380
4532
|
"suggested_next_call": None,
|
|
4381
|
-
"noop":
|
|
4533
|
+
"noop": False,
|
|
4382
4534
|
"warnings": [],
|
|
4383
4535
|
"verification": {"associated_resources_verified": True, "associated_resource_view_configs_verified": True, "readback_loaded": True, "published": False},
|
|
4384
4536
|
"verified": True,
|
|
@@ -5236,36 +5388,13 @@ class AiBuilderFacade:
|
|
|
5236
5388
|
except (QingflowApiError, RuntimeError) as error:
|
|
5237
5389
|
custom_buttons_unavailable = True
|
|
5238
5390
|
custom_buttons = []
|
|
5239
|
-
|
|
5240
|
-
{
|
|
5241
|
-
"resource": "custom_buttons",
|
|
5242
|
-
"phase": "summary",
|
|
5243
|
-
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
5244
|
-
}
|
|
5245
|
-
)
|
|
5246
|
-
workflow, workflow_unavailable, workflow_read_error = self._load_workflow_result(
|
|
5391
|
+
workflow_spec, workflow_unavailable = self._load_workflow_spec_snapshot(
|
|
5247
5392
|
profile=profile,
|
|
5248
5393
|
app_key=app_key,
|
|
5249
5394
|
tolerate_404=True,
|
|
5250
5395
|
tolerate_permission_restricted=True,
|
|
5251
|
-
include_error=True,
|
|
5252
5396
|
)
|
|
5253
|
-
|
|
5254
|
-
readback_errors.append(
|
|
5255
|
-
{
|
|
5256
|
-
"resource": "views",
|
|
5257
|
-
"phase": "summary",
|
|
5258
|
-
"transport_error": _transport_error_payload(views_read_error),
|
|
5259
|
-
}
|
|
5260
|
-
)
|
|
5261
|
-
if workflow_read_error is not None:
|
|
5262
|
-
readback_errors.append(
|
|
5263
|
-
{
|
|
5264
|
-
"resource": "workflow",
|
|
5265
|
-
"phase": "summary",
|
|
5266
|
-
"transport_error": _transport_error_payload(workflow_read_error),
|
|
5267
|
-
}
|
|
5268
|
-
)
|
|
5397
|
+
workflow_nodes = _extract_workflow_spec_nodes(workflow_spec)
|
|
5269
5398
|
verification_hints = _build_verification_hints(
|
|
5270
5399
|
tag_ids=_coerce_int_list(base_result.get("tagIds")),
|
|
5271
5400
|
fields=parsed["fields"],
|
|
@@ -5321,7 +5450,7 @@ class AiBuilderFacade:
|
|
|
5321
5450
|
charts=chart_summaries,
|
|
5322
5451
|
associated_resources=associated_resources,
|
|
5323
5452
|
custom_buttons=custom_buttons,
|
|
5324
|
-
workflow_enabled=bool(
|
|
5453
|
+
workflow_enabled=bool(workflow_nodes),
|
|
5325
5454
|
verification_hints=verification_hints,
|
|
5326
5455
|
form_settings={} if schema_unavailable else _form_settings_from_schema(schema_result, parsed["fields"]),
|
|
5327
5456
|
)
|
|
@@ -5731,7 +5860,7 @@ class AiBuilderFacade:
|
|
|
5731
5860
|
return result
|
|
5732
5861
|
|
|
5733
5862
|
def app_get_flow(self, *, profile: str, app_key: str) -> JSONObject:
|
|
5734
|
-
result = self.
|
|
5863
|
+
result = self.flow_get(profile=profile, app_key=app_key)
|
|
5735
5864
|
if result.get("status") == "success":
|
|
5736
5865
|
result["message"] = "read app flow config"
|
|
5737
5866
|
return result
|
|
@@ -5912,41 +6041,6 @@ class AiBuilderFacade:
|
|
|
5912
6041
|
**response.model_dump(mode="json"),
|
|
5913
6042
|
}
|
|
5914
6043
|
|
|
5915
|
-
def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
5916
|
-
try:
|
|
5917
|
-
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
5918
|
-
except (QingflowApiError, RuntimeError) as error:
|
|
5919
|
-
api_error = _coerce_api_error(error)
|
|
5920
|
-
return _failed_from_api_error(
|
|
5921
|
-
"FLOW_READ_FAILED",
|
|
5922
|
-
api_error,
|
|
5923
|
-
normalized_args={"app_key": app_key},
|
|
5924
|
-
details={"app_key": app_key},
|
|
5925
|
-
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5926
|
-
)
|
|
5927
|
-
response = AppFlowReadResponse(
|
|
5928
|
-
app_key=app_key,
|
|
5929
|
-
enabled=bool(workflow),
|
|
5930
|
-
nodes=_summarize_workflow_nodes(workflow),
|
|
5931
|
-
transitions=[],
|
|
5932
|
-
)
|
|
5933
|
-
return {
|
|
5934
|
-
"status": "success",
|
|
5935
|
-
"error_code": None,
|
|
5936
|
-
"recoverable": False,
|
|
5937
|
-
"message": "read app flow summary",
|
|
5938
|
-
"normalized_args": {"app_key": app_key},
|
|
5939
|
-
"missing_fields": [],
|
|
5940
|
-
"allowed_values": {},
|
|
5941
|
-
"details": {},
|
|
5942
|
-
"request_id": None,
|
|
5943
|
-
"suggested_next_call": None,
|
|
5944
|
-
"noop": False,
|
|
5945
|
-
"warnings": [_warning("WORKFLOW_READ_UNAVAILABLE", "workflow summary readback is unavailable")] if workflow_unavailable else [],
|
|
5946
|
-
"verification": {"app_exists": True, "workflow_read_unavailable": workflow_unavailable},
|
|
5947
|
-
"verified": not workflow_unavailable,
|
|
5948
|
-
**response.model_dump(mode="json"),
|
|
5949
|
-
}
|
|
5950
6044
|
|
|
5951
6045
|
def app_read_charts_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
5952
6046
|
try:
|
|
@@ -7399,154 +7493,6 @@ class AiBuilderFacade:
|
|
|
7399
7493
|
"verification": {"field_count": len(current_names)},
|
|
7400
7494
|
}
|
|
7401
7495
|
|
|
7402
|
-
def app_flow_plan(self, *, profile: str, request: FlowPlanRequest) -> JSONObject:
|
|
7403
|
-
nodes = [node.model_dump(mode="json") for node in request.nodes]
|
|
7404
|
-
transitions = [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions]
|
|
7405
|
-
if request.preset is not None:
|
|
7406
|
-
preset_nodes, preset_transitions = _build_flow_preset(request.preset)
|
|
7407
|
-
nodes, transitions = _merge_flow_graph(
|
|
7408
|
-
base_nodes=preset_nodes,
|
|
7409
|
-
base_transitions=preset_transitions,
|
|
7410
|
-
override_nodes=nodes,
|
|
7411
|
-
override_transitions=transitions,
|
|
7412
|
-
)
|
|
7413
|
-
fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
7414
|
-
if fields_result.get("status") == "failed":
|
|
7415
|
-
return fields_result
|
|
7416
|
-
current_fields = fields_result.get("fields", [])
|
|
7417
|
-
normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
|
|
7418
|
-
public_nodes = self._canonicalize_flow_nodes_for_public_output(normalized_nodes)
|
|
7419
|
-
unsupported_nodes = self._unsupported_public_flow_nodes(nodes=public_nodes)
|
|
7420
|
-
if unsupported_nodes:
|
|
7421
|
-
return _failed(
|
|
7422
|
-
"FLOW_NODE_TYPE_UNSUPPORTED",
|
|
7423
|
-
"app_flow_apply currently supports only linear workflows; branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types.",
|
|
7424
|
-
normalized_args={
|
|
7425
|
-
"app_key": request.app_key,
|
|
7426
|
-
"mode": str(request.mode or "replace"),
|
|
7427
|
-
"preset": request.preset.value if request.preset else None,
|
|
7428
|
-
"nodes": public_nodes,
|
|
7429
|
-
"transitions": transitions,
|
|
7430
|
-
},
|
|
7431
|
-
details={
|
|
7432
|
-
"unsupported_nodes": unsupported_nodes,
|
|
7433
|
-
"supported_node_types": sorted(STABLE_PUBLIC_FLOW_NODE_TYPES),
|
|
7434
|
-
"disabled_node_types": sorted(DISABLED_PUBLIC_FLOW_NODE_TYPES),
|
|
7435
|
-
},
|
|
7436
|
-
suggested_next_call={"tool_name": "builder_tool_contract", "arguments": {"tool_name": "app_flow_apply"}},
|
|
7437
|
-
)
|
|
7438
|
-
if resolution_issues:
|
|
7439
|
-
first_issue = resolution_issues[0]
|
|
7440
|
-
suggested_call = None
|
|
7441
|
-
if first_issue.get("kind", "").startswith("role"):
|
|
7442
|
-
suggested_call = {"tool_name": "role_search", "arguments": {"profile": profile, "keyword": first_issue.get("value") or ""}}
|
|
7443
|
-
elif first_issue.get("kind", "").startswith("member"):
|
|
7444
|
-
suggested_call = {"tool_name": "member_search", "arguments": {"profile": profile, "query": first_issue.get("value") or ""}}
|
|
7445
|
-
elif first_issue.get("kind") in {"editable_fields", "condition_fields"}:
|
|
7446
|
-
suggested_call = {"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": request.app_key}}
|
|
7447
|
-
return _failed(
|
|
7448
|
-
first_issue.get("error_code") or "FLOW_ASSIGNEE_UNRESOLVED",
|
|
7449
|
-
"workflow contains unresolved assignees or field permissions",
|
|
7450
|
-
normalized_args={
|
|
7451
|
-
"app_key": request.app_key,
|
|
7452
|
-
"mode": str(request.mode or "replace"),
|
|
7453
|
-
"preset": request.preset.value if request.preset else None,
|
|
7454
|
-
"nodes": public_nodes,
|
|
7455
|
-
"transitions": transitions,
|
|
7456
|
-
},
|
|
7457
|
-
details={"issues": resolution_issues},
|
|
7458
|
-
suggested_next_call=suggested_call,
|
|
7459
|
-
)
|
|
7460
|
-
status_field_present = _infer_status_field_id(current_fields) is not None
|
|
7461
|
-
node_types = {str(node.get("type") or "") for node in normalized_nodes}
|
|
7462
|
-
assignee_required_nodes = [
|
|
7463
|
-
node.get("id")
|
|
7464
|
-
for node in normalized_nodes
|
|
7465
|
-
if str(node.get("type") or "") in {"approve", "fill", "copy"}
|
|
7466
|
-
and not (
|
|
7467
|
-
(node.get("assignees") or {}).get("role_entries")
|
|
7468
|
-
or (node.get("assignees") or {}).get("member_uids")
|
|
7469
|
-
)
|
|
7470
|
-
]
|
|
7471
|
-
if assignee_required_nodes:
|
|
7472
|
-
return _failed(
|
|
7473
|
-
"FLOW_ASSIGNEE_REQUIRED",
|
|
7474
|
-
"workflow approval/fill/copy nodes must declare at least one role or member assignee",
|
|
7475
|
-
normalized_args={
|
|
7476
|
-
"app_key": request.app_key,
|
|
7477
|
-
"mode": str(request.mode or "replace"),
|
|
7478
|
-
"preset": request.preset.value if request.preset else None,
|
|
7479
|
-
"nodes": public_nodes,
|
|
7480
|
-
"transitions": transitions,
|
|
7481
|
-
},
|
|
7482
|
-
details={"node_ids": assignee_required_nodes, "policy": "prefer role assignees; explicit members are also supported"},
|
|
7483
|
-
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, "keyword": ""}},
|
|
7484
|
-
)
|
|
7485
|
-
workflow = _build_public_workflow_spec(nodes=normalized_nodes, transitions=transitions)
|
|
7486
|
-
if workflow.get("status") == "failed":
|
|
7487
|
-
workflow["normalized_args"] = {
|
|
7488
|
-
"app_key": request.app_key,
|
|
7489
|
-
"mode": str(request.mode or "replace"),
|
|
7490
|
-
"nodes": public_nodes,
|
|
7491
|
-
"transitions": transitions,
|
|
7492
|
-
}
|
|
7493
|
-
workflow["suggested_next_call"] = {
|
|
7494
|
-
"tool_name": "app_flow_plan",
|
|
7495
|
-
"arguments": {
|
|
7496
|
-
"profile": profile,
|
|
7497
|
-
"app_key": request.app_key,
|
|
7498
|
-
"mode": "replace",
|
|
7499
|
-
"nodes": public_nodes,
|
|
7500
|
-
"transitions": transitions,
|
|
7501
|
-
},
|
|
7502
|
-
}
|
|
7503
|
-
return workflow
|
|
7504
|
-
if ("approve" in node_types or request.preset in {FlowPreset.basic_approval, FlowPreset.basic_fill_then_approve}) and not status_field_present:
|
|
7505
|
-
return _failed(
|
|
7506
|
-
"FLOW_DEPENDENCY_MISSING",
|
|
7507
|
-
"workflow requires an explicit status field",
|
|
7508
|
-
normalized_args={
|
|
7509
|
-
"app_key": request.app_key,
|
|
7510
|
-
"mode": str(request.mode or "replace"),
|
|
7511
|
-
"nodes": public_nodes,
|
|
7512
|
-
"transitions": transitions,
|
|
7513
|
-
},
|
|
7514
|
-
details={"missing_dependencies": ["status field"]},
|
|
7515
|
-
missing_fields=["status"],
|
|
7516
|
-
suggested_next_call={
|
|
7517
|
-
"tool_name": "app_schema_apply",
|
|
7518
|
-
"arguments": {
|
|
7519
|
-
"profile": profile,
|
|
7520
|
-
"app_key": request.app_key,
|
|
7521
|
-
"create_if_missing": False,
|
|
7522
|
-
"add_fields": [{"name": "状态", "type": "single_select", "options": ["草稿", "进行中", "已完成"], "required": True}],
|
|
7523
|
-
"update_fields": [],
|
|
7524
|
-
"remove_fields": [],
|
|
7525
|
-
},
|
|
7526
|
-
},
|
|
7527
|
-
)
|
|
7528
|
-
normalized_args = {
|
|
7529
|
-
"app_key": request.app_key,
|
|
7530
|
-
"mode": str(request.mode or "replace"),
|
|
7531
|
-
"nodes": public_nodes,
|
|
7532
|
-
"transitions": transitions,
|
|
7533
|
-
}
|
|
7534
|
-
return {
|
|
7535
|
-
"status": "success",
|
|
7536
|
-
"error_code": None,
|
|
7537
|
-
"recoverable": False,
|
|
7538
|
-
"message": "planned workflow patch",
|
|
7539
|
-
"normalized_args": normalized_args,
|
|
7540
|
-
"missing_fields": [],
|
|
7541
|
-
"allowed_values": {"presets": [preset.value for preset in FlowPreset]},
|
|
7542
|
-
"details": {},
|
|
7543
|
-
"request_id": None,
|
|
7544
|
-
"flow_diff_preview": {"mode": "replace", "node_count": len([node for node in nodes if node.get("type") != "end"])},
|
|
7545
|
-
"dependency_issues": [],
|
|
7546
|
-
"suggested_next_call": {"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7547
|
-
"noop": False,
|
|
7548
|
-
"verification": {"status_field_present": status_field_present},
|
|
7549
|
-
}
|
|
7550
7496
|
|
|
7551
7497
|
def app_views_plan(self, *, profile: str, request: ViewsPlanRequest) -> JSONObject:
|
|
7552
7498
|
fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
@@ -7839,8 +7785,9 @@ class AiBuilderFacade:
|
|
|
7839
7785
|
"form_settings": _form_settings_from_schema(schema_result, parsed["fields"]),
|
|
7840
7786
|
"layout": parsed["layout"],
|
|
7841
7787
|
"flow_summary": {
|
|
7842
|
-
"enabled": bool(state["
|
|
7843
|
-
"nodes":
|
|
7788
|
+
"enabled": bool(_extract_workflow_spec_nodes(state["workflow_spec"])),
|
|
7789
|
+
"nodes": _summarize_workflow_spec_nodes(_extract_workflow_spec_nodes(state["workflow_spec"])),
|
|
7790
|
+
"source": "workflow_spec",
|
|
7844
7791
|
},
|
|
7845
7792
|
"views_summary": {
|
|
7846
7793
|
"views": _summarize_views(state["views"]),
|
|
@@ -7851,7 +7798,7 @@ class AiBuilderFacade:
|
|
|
7851
7798
|
"base": base_result,
|
|
7852
7799
|
"schema": schema_result,
|
|
7853
7800
|
"views": state["views"],
|
|
7854
|
-
"
|
|
7801
|
+
"workflow_spec": state["workflow_spec"],
|
|
7855
7802
|
}
|
|
7856
7803
|
return response
|
|
7857
7804
|
|
|
@@ -8266,7 +8213,16 @@ class AiBuilderFacade:
|
|
|
8266
8213
|
relation_field_count = _count_relation_fields(current_fields)
|
|
8267
8214
|
relation_limit_verified = relation_field_count <= 1
|
|
8268
8215
|
relation_warnings = (
|
|
8269
|
-
[
|
|
8216
|
+
[
|
|
8217
|
+
_warning(
|
|
8218
|
+
"RELATION_FIELD_LIMIT_RISK",
|
|
8219
|
+
"multiple relation fields were written and verified, but this backend capability can be unstable; do not downgrade to text unless the write fails or relation readback mismatches",
|
|
8220
|
+
severity="warning",
|
|
8221
|
+
relation_field_count=relation_field_count,
|
|
8222
|
+
relation_fields_created=True,
|
|
8223
|
+
do_not_downgrade_to_text_unless_apply_failed=True,
|
|
8224
|
+
)
|
|
8225
|
+
]
|
|
8270
8226
|
if not relation_limit_verified
|
|
8271
8227
|
else []
|
|
8272
8228
|
)
|
|
@@ -8397,6 +8353,8 @@ class AiBuilderFacade:
|
|
|
8397
8353
|
"app_key": target.app_key,
|
|
8398
8354
|
"field_diff": {"added": added, "updated": updated, "removed": removed},
|
|
8399
8355
|
"relation_field_count": relation_field_count,
|
|
8356
|
+
"relation_fields_created": False,
|
|
8357
|
+
"recommended_modeling_fallback": "Keep the primary relation field as relation; use text/reference summary fields only for secondary cross-object links, and report the downgrade as partial.",
|
|
8400
8358
|
"transport_error": {
|
|
8401
8359
|
"http_status": api_error.http_status,
|
|
8402
8360
|
"backend_code": api_error.backend_code,
|
|
@@ -9041,228 +8999,19 @@ class AiBuilderFacade:
|
|
|
9041
8999
|
*,
|
|
9042
9000
|
profile: str,
|
|
9043
9001
|
app_key: str,
|
|
9044
|
-
|
|
9045
|
-
transitions: list[dict[str, Any]],
|
|
9046
|
-
mode: str = "replace",
|
|
9002
|
+
spec: dict[str, Any],
|
|
9047
9003
|
publish: bool = True,
|
|
9004
|
+
idempotency_key: str | None = None,
|
|
9005
|
+
schema_version: str | None = None,
|
|
9048
9006
|
) -> JSONObject:
|
|
9049
|
-
|
|
9050
|
-
"app_key": app_key,
|
|
9051
|
-
"mode": mode,
|
|
9052
|
-
"nodes": nodes,
|
|
9053
|
-
"transitions": transitions,
|
|
9054
|
-
"publish": publish,
|
|
9055
|
-
}
|
|
9056
|
-
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
9057
|
-
permission_outcome = self._guard_app_permission(
|
|
9007
|
+
return self.flow_apply(
|
|
9058
9008
|
profile=profile,
|
|
9059
9009
|
app_key=app_key,
|
|
9060
|
-
|
|
9061
|
-
|
|
9010
|
+
spec=spec,
|
|
9011
|
+
publish=publish,
|
|
9012
|
+
idempotency_key=idempotency_key,
|
|
9013
|
+
schema_version=schema_version,
|
|
9062
9014
|
)
|
|
9063
|
-
if permission_outcome.block is not None:
|
|
9064
|
-
return permission_outcome.block
|
|
9065
|
-
permission_outcomes.append(permission_outcome)
|
|
9066
|
-
|
|
9067
|
-
def finalize(response: JSONObject) -> JSONObject:
|
|
9068
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
9069
|
-
|
|
9070
|
-
if mode != "replace":
|
|
9071
|
-
return finalize(_failed(
|
|
9072
|
-
"UNSUPPORTED_FLOW_MODE",
|
|
9073
|
-
"only mode='replace' is supported",
|
|
9074
|
-
normalized_args=normalized_args,
|
|
9075
|
-
allowed_values={"modes": ["replace"]},
|
|
9076
|
-
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9077
|
-
))
|
|
9078
|
-
try:
|
|
9079
|
-
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
9080
|
-
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
9081
|
-
except (QingflowApiError, RuntimeError) as error:
|
|
9082
|
-
api_error = _coerce_api_error(error)
|
|
9083
|
-
return finalize(_failed_from_api_error(
|
|
9084
|
-
"FLOW_READ_FAILED",
|
|
9085
|
-
api_error,
|
|
9086
|
-
normalized_args=normalized_args,
|
|
9087
|
-
details=_with_state_read_blocked_details({"app_key": app_key}, resource="workflow", error=api_error),
|
|
9088
|
-
suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9089
|
-
))
|
|
9090
|
-
app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_key).strip() or app_key
|
|
9091
|
-
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
9092
|
-
current_fields = _parse_schema(schema)["fields"]
|
|
9093
|
-
normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
|
|
9094
|
-
public_nodes = self._canonicalize_flow_nodes_for_public_output(normalized_nodes)
|
|
9095
|
-
unsupported_nodes = self._unsupported_public_flow_nodes(nodes=public_nodes)
|
|
9096
|
-
normalized_args["nodes"] = public_nodes
|
|
9097
|
-
if unsupported_nodes:
|
|
9098
|
-
return _failed(
|
|
9099
|
-
"FLOW_NODE_TYPE_UNSUPPORTED",
|
|
9100
|
-
"app_flow_apply currently supports only linear workflows; branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types.",
|
|
9101
|
-
normalized_args=normalized_args,
|
|
9102
|
-
details={
|
|
9103
|
-
"unsupported_nodes": unsupported_nodes,
|
|
9104
|
-
"supported_node_types": sorted(STABLE_PUBLIC_FLOW_NODE_TYPES),
|
|
9105
|
-
"disabled_node_types": sorted(DISABLED_PUBLIC_FLOW_NODE_TYPES),
|
|
9106
|
-
},
|
|
9107
|
-
suggested_next_call={"tool_name": "builder_tool_contract", "arguments": {"tool_name": "app_flow_apply"}},
|
|
9108
|
-
)
|
|
9109
|
-
if resolution_issues:
|
|
9110
|
-
first_issue = resolution_issues[0]
|
|
9111
|
-
suggested_call = None
|
|
9112
|
-
if first_issue.get("kind", "").startswith("role"):
|
|
9113
|
-
suggested_call = {"tool_name": "role_search", "arguments": {"profile": profile, "keyword": first_issue.get("value") or ""}}
|
|
9114
|
-
elif first_issue.get("kind", "").startswith("member"):
|
|
9115
|
-
suggested_call = {"tool_name": "member_search", "arguments": {"profile": profile, "query": first_issue.get("value") or ""}}
|
|
9116
|
-
elif first_issue.get("kind") in {"editable_fields", "condition_fields"}:
|
|
9117
|
-
suggested_call = {"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}}
|
|
9118
|
-
return _failed(
|
|
9119
|
-
first_issue.get("error_code") or "FLOW_ASSIGNEE_UNRESOLVED",
|
|
9120
|
-
"workflow contains unresolved assignees or field permissions",
|
|
9121
|
-
normalized_args=normalized_args,
|
|
9122
|
-
details={"issues": resolution_issues},
|
|
9123
|
-
suggested_next_call=suggested_call,
|
|
9124
|
-
)
|
|
9125
|
-
assignee_required_nodes = [
|
|
9126
|
-
node.get("id")
|
|
9127
|
-
for node in normalized_nodes
|
|
9128
|
-
if str(node.get("type") or "") in {"approve", "fill", "copy"}
|
|
9129
|
-
and not (
|
|
9130
|
-
(node.get("assignees") or {}).get("role_entries")
|
|
9131
|
-
or (node.get("assignees") or {}).get("member_uids")
|
|
9132
|
-
)
|
|
9133
|
-
]
|
|
9134
|
-
if assignee_required_nodes:
|
|
9135
|
-
return _failed(
|
|
9136
|
-
"FLOW_ASSIGNEE_REQUIRED",
|
|
9137
|
-
"workflow approval/fill/copy nodes must declare at least one role or member assignee",
|
|
9138
|
-
normalized_args=normalized_args,
|
|
9139
|
-
details={"node_ids": assignee_required_nodes, "policy": "prefer role assignees; explicit members are also supported"},
|
|
9140
|
-
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, "keyword": ""}},
|
|
9141
|
-
)
|
|
9142
|
-
workflow_spec = _build_public_workflow_spec(nodes=normalized_nodes, transitions=transitions)
|
|
9143
|
-
if workflow_spec.get("status") == "failed":
|
|
9144
|
-
workflow_spec["normalized_args"] = normalized_args
|
|
9145
|
-
workflow_spec.setdefault("request_id", None)
|
|
9146
|
-
workflow_spec["suggested_next_call"] = {"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}}
|
|
9147
|
-
return workflow_spec
|
|
9148
|
-
desired_node_count = len([node for node in normalized_nodes if node.get("type") != "end"])
|
|
9149
|
-
build_id = f"facade-flow-{uuid4().hex[:12]}"
|
|
9150
|
-
previous_build_home = os.getenv("QINGFLOW_MCP_BUILD_HOME")
|
|
9151
|
-
temporary_build_home: str | None = None
|
|
9152
|
-
if previous_build_home is None:
|
|
9153
|
-
temporary_build_home = tempfile.mkdtemp(prefix="qingflow-mcp-build-", dir="/tmp")
|
|
9154
|
-
os.environ["QINGFLOW_MCP_BUILD_HOME"] = temporary_build_home
|
|
9155
|
-
try:
|
|
9156
|
-
assembly = BuildAssemblyStore.open(build_id=build_id, create=True)
|
|
9157
|
-
manifest = default_manifest()
|
|
9158
|
-
manifest["solution_name"] = base.get("formTitle") or app_key
|
|
9159
|
-
manifest["preferences"]["create_package"] = False
|
|
9160
|
-
manifest["preferences"]["create_portal"] = False
|
|
9161
|
-
manifest["preferences"]["create_navigation"] = False
|
|
9162
|
-
manifest["entities"] = [entity]
|
|
9163
|
-
assembly.set_manifest(manifest)
|
|
9164
|
-
artifacts = default_artifacts()
|
|
9165
|
-
artifacts["apps"][entity["entity_id"]] = {"app_key": app_key}
|
|
9166
|
-
assembly.set_artifacts(artifacts)
|
|
9167
|
-
flow_stage_spec = {
|
|
9168
|
-
"solution_name": manifest["solution_name"],
|
|
9169
|
-
"entities": [{"entity_id": entity["entity_id"], "workflow": workflow_spec["workflow"]}],
|
|
9170
|
-
}
|
|
9171
|
-
assembly.set_stage_spec("app_flow", flow_stage_spec)
|
|
9172
|
-
stage = self.solutions.solution_build_flow(
|
|
9173
|
-
profile=profile,
|
|
9174
|
-
mode="apply",
|
|
9175
|
-
build_id=build_id,
|
|
9176
|
-
flow_spec=flow_stage_spec,
|
|
9177
|
-
publish=False,
|
|
9178
|
-
run_label=None,
|
|
9179
|
-
repair_patch={},
|
|
9180
|
-
)
|
|
9181
|
-
finally:
|
|
9182
|
-
if previous_build_home is None:
|
|
9183
|
-
os.environ.pop("QINGFLOW_MCP_BUILD_HOME", None)
|
|
9184
|
-
if stage.get("status") != "success":
|
|
9185
|
-
failed = _normalize_flow_stage_failure(stage, profile=profile, app_key=app_key, entity=entity)
|
|
9186
|
-
failed["normalized_args"] = normalized_args
|
|
9187
|
-
suggested_next_call = failed.get("suggested_next_call")
|
|
9188
|
-
if not isinstance(suggested_next_call, dict):
|
|
9189
|
-
suggested_next_call = {"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}}
|
|
9190
|
-
elif suggested_next_call.get("tool_name") == "app_flow_plan":
|
|
9191
|
-
arguments = suggested_next_call.get("arguments")
|
|
9192
|
-
if not isinstance(arguments, dict):
|
|
9193
|
-
arguments = {}
|
|
9194
|
-
arguments.setdefault("profile", profile)
|
|
9195
|
-
arguments.setdefault("app_key", app_key)
|
|
9196
|
-
arguments.setdefault("mode", mode)
|
|
9197
|
-
arguments.setdefault("nodes", public_nodes)
|
|
9198
|
-
arguments.setdefault("transitions", transitions)
|
|
9199
|
-
suggested_next_call["tool_name"] = "app_flow_apply"
|
|
9200
|
-
suggested_next_call["arguments"] = arguments
|
|
9201
|
-
failed["suggested_next_call"] = suggested_next_call
|
|
9202
|
-
return finalize(failed)
|
|
9203
|
-
verified_nodes, verified_nodes_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
9204
|
-
workflow_structure_verified = bool(verified_nodes) and _workflow_nodes_semantically_equal(
|
|
9205
|
-
current_workflow=verified_nodes,
|
|
9206
|
-
requested_nodes=normalized_nodes,
|
|
9207
|
-
)
|
|
9208
|
-
branch_structure_verified = bool(verified_nodes) and _workflow_branch_structure_verified(
|
|
9209
|
-
current_workflow=verified_nodes,
|
|
9210
|
-
requested_nodes=normalized_nodes,
|
|
9211
|
-
)
|
|
9212
|
-
workflow_verified = workflow_structure_verified and branch_structure_verified
|
|
9213
|
-
warnings: list[dict[str, Any]] = []
|
|
9214
|
-
if verified_nodes_unavailable:
|
|
9215
|
-
status = "partial_success"
|
|
9216
|
-
error_code = "FLOW_READBACK_PENDING"
|
|
9217
|
-
recoverable = True
|
|
9218
|
-
message = "applied workflow patch; flow readback pending"
|
|
9219
|
-
suggested_next_call = {"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}}
|
|
9220
|
-
elif workflow_verified:
|
|
9221
|
-
status = "success"
|
|
9222
|
-
error_code = None
|
|
9223
|
-
recoverable = False
|
|
9224
|
-
message = "applied workflow patch"
|
|
9225
|
-
suggested_next_call = None
|
|
9226
|
-
else:
|
|
9227
|
-
status = "partial_success"
|
|
9228
|
-
error_code = "FLOW_BRANCH_VERIFICATION_FAILED" if workflow_structure_verified and not branch_structure_verified else "FLOW_VERIFICATION_FAILED"
|
|
9229
|
-
recoverable = True
|
|
9230
|
-
message = (
|
|
9231
|
-
"applied workflow patch; branch or condition structure did not fully verify"
|
|
9232
|
-
if workflow_structure_verified and not branch_structure_verified
|
|
9233
|
-
else "applied workflow patch; flow readback did not confirm the requested workflow"
|
|
9234
|
-
)
|
|
9235
|
-
suggested_next_call = {"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}}
|
|
9236
|
-
if not branch_structure_verified:
|
|
9237
|
-
warnings.append(_warning("WORKFLOW_BRANCH_STRUCTURE_UNVERIFIED", "branch or condition structure was written, but MCP could not fully verify downstream lane structure"))
|
|
9238
|
-
response = {
|
|
9239
|
-
"status": status,
|
|
9240
|
-
"error_code": error_code,
|
|
9241
|
-
"recoverable": recoverable,
|
|
9242
|
-
"message": message,
|
|
9243
|
-
"normalized_args": normalized_args,
|
|
9244
|
-
"missing_fields": [],
|
|
9245
|
-
"allowed_values": {"modes": ["replace"]},
|
|
9246
|
-
"details": {},
|
|
9247
|
-
"request_id": None,
|
|
9248
|
-
"suggested_next_call": suggested_next_call,
|
|
9249
|
-
"noop": False,
|
|
9250
|
-
"warnings": warnings,
|
|
9251
|
-
"verification": {
|
|
9252
|
-
"workflow_verified": workflow_verified,
|
|
9253
|
-
"workflow_structure_verified": workflow_structure_verified,
|
|
9254
|
-
"branch_structure_verified": branch_structure_verified,
|
|
9255
|
-
"workflow_read_unavailable": verified_nodes_unavailable,
|
|
9256
|
-
},
|
|
9257
|
-
"app_key": app_key,
|
|
9258
|
-
"app_name": app_name,
|
|
9259
|
-
"flow_diff": {"mode": "replace", "node_count": desired_node_count},
|
|
9260
|
-
"verified": workflow_verified,
|
|
9261
|
-
"write_executed": True,
|
|
9262
|
-
"write_succeeded": True,
|
|
9263
|
-
"safe_to_retry": False,
|
|
9264
|
-
}
|
|
9265
|
-
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
9266
9015
|
|
|
9267
9016
|
def app_views_apply(
|
|
9268
9017
|
self,
|
|
@@ -10154,6 +9903,12 @@ class AiBuilderFacade:
|
|
|
10154
9903
|
"backend_code": api_error.backend_code,
|
|
10155
9904
|
"category": api_error.category,
|
|
10156
9905
|
},
|
|
9906
|
+
**_view_apply_failure_diagnostics(
|
|
9907
|
+
patch=patch,
|
|
9908
|
+
current_fields_by_name=current_fields_by_name,
|
|
9909
|
+
backend_code=api_error.backend_code,
|
|
9910
|
+
operation_phase=operation_phase,
|
|
9911
|
+
),
|
|
10157
9912
|
},
|
|
10158
9913
|
}
|
|
10159
9914
|
failed_views.append(failure_entry)
|
|
@@ -11224,6 +10979,56 @@ class AiBuilderFacade:
|
|
|
11224
10979
|
normalized_args["upsert_charts"] = [patch.model_dump(mode="json") for patch in upsert_charts]
|
|
11225
10980
|
normalized_args["patch_results"] = patch_results
|
|
11226
10981
|
|
|
10982
|
+
if len(upsert_charts) > CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE:
|
|
10983
|
+
batching_context = _chart_apply_batching_context(
|
|
10984
|
+
request=request,
|
|
10985
|
+
upsert_charts=upsert_charts,
|
|
10986
|
+
batch_size=CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE,
|
|
10987
|
+
)
|
|
10988
|
+
first_batch = batching_context["suggested_batch_payloads"][0]
|
|
10989
|
+
return finalize({
|
|
10990
|
+
"status": "failed",
|
|
10991
|
+
"error_code": "CHART_UPSERT_BATCH_TOO_LARGE",
|
|
10992
|
+
"recoverable": True,
|
|
10993
|
+
"message": (
|
|
10994
|
+
f"upsert_charts contains {len(upsert_charts)} items; split into batches of "
|
|
10995
|
+
f"{CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE} or fewer before writing"
|
|
10996
|
+
),
|
|
10997
|
+
"normalized_args": normalized_args,
|
|
10998
|
+
"missing_fields": [],
|
|
10999
|
+
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
11000
|
+
"details": batching_context,
|
|
11001
|
+
"request_id": None,
|
|
11002
|
+
"suggested_next_call": {"tool_name": "app_charts_apply", "arguments": {"profile": profile, **first_batch}},
|
|
11003
|
+
"backend_code": None,
|
|
11004
|
+
"http_status": None,
|
|
11005
|
+
"noop": False,
|
|
11006
|
+
"warnings": [
|
|
11007
|
+
_warning(
|
|
11008
|
+
"CHART_UPSERT_BATCH_SIZE_BLOCKED",
|
|
11009
|
+
"large chart upsert batches are blocked before write to avoid timeout and unknown write state",
|
|
11010
|
+
upsert_count=len(upsert_charts),
|
|
11011
|
+
max_upsert_count=CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE,
|
|
11012
|
+
)
|
|
11013
|
+
],
|
|
11014
|
+
"verification": {
|
|
11015
|
+
"charts_verified": False,
|
|
11016
|
+
"readback_unavailable": False,
|
|
11017
|
+
"readback_before_retry": False,
|
|
11018
|
+
"chart_delete_readback_results": [],
|
|
11019
|
+
"chart_order_verified": True,
|
|
11020
|
+
"chart_list_source": existing_chart_list_source,
|
|
11021
|
+
},
|
|
11022
|
+
"app_key": app_key,
|
|
11023
|
+
"app_name": app_name,
|
|
11024
|
+
"chart_results": [],
|
|
11025
|
+
"verified": False,
|
|
11026
|
+
"write_executed": False,
|
|
11027
|
+
"write_succeeded": False,
|
|
11028
|
+
"safe_to_retry": True,
|
|
11029
|
+
"next_action": "split_upsert_charts_and_retry",
|
|
11030
|
+
})
|
|
11031
|
+
|
|
11227
11032
|
chart_results: list[dict[str, Any]] = []
|
|
11228
11033
|
created_ids: list[str] = []
|
|
11229
11034
|
updated_ids: list[str] = []
|
|
@@ -11510,6 +11315,15 @@ class AiBuilderFacade:
|
|
|
11510
11315
|
primary_failure = failed_items[0]
|
|
11511
11316
|
primary_error_code = str(primary_failure.get("error_code") or "").strip()
|
|
11512
11317
|
primary_message = str(primary_failure.get("message") or "").strip()
|
|
11318
|
+
retry_context = _chart_apply_retry_context(
|
|
11319
|
+
request=request,
|
|
11320
|
+
failed_items=failed_items,
|
|
11321
|
+
created_ids=created_ids,
|
|
11322
|
+
updated_ids=updated_ids,
|
|
11323
|
+
removed_ids=removed_ids,
|
|
11324
|
+
reordered=reordered,
|
|
11325
|
+
write_executed=write_executed,
|
|
11326
|
+
)
|
|
11513
11327
|
return finalize({
|
|
11514
11328
|
"status": "partial_success" if successful_changes else "failed",
|
|
11515
11329
|
"error_code": "CHART_APPLY_PARTIAL" if successful_changes else primary_error_code or "CHART_APPLY_FAILED",
|
|
@@ -11520,6 +11334,7 @@ class AiBuilderFacade:
|
|
|
11520
11334
|
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
11521
11335
|
"details": {
|
|
11522
11336
|
"per_chart_results": chart_results,
|
|
11337
|
+
**retry_context,
|
|
11523
11338
|
**({"readback_error": _transport_error_payload(readback_error)} if readback_error is not None else {}),
|
|
11524
11339
|
},
|
|
11525
11340
|
"request_id": failed_items[0].get("request_id") or (readback_error.request_id if readback_error is not None else None),
|
|
@@ -11536,6 +11351,7 @@ class AiBuilderFacade:
|
|
|
11536
11351
|
"verification": {
|
|
11537
11352
|
"charts_verified": False if failed_items else verified,
|
|
11538
11353
|
"readback_unavailable": any_readback_unavailable,
|
|
11354
|
+
"readback_before_retry": bool(write_executed),
|
|
11539
11355
|
"chart_delete_readback_results": [deepcopy(item) for item in chart_results if item.get("operation") == "delete"],
|
|
11540
11356
|
"chart_order_verified": False if request.reorder_chart_ids else True,
|
|
11541
11357
|
"chart_list_source": readback_list_source or existing_chart_list_source,
|
|
@@ -11546,6 +11362,7 @@ class AiBuilderFacade:
|
|
|
11546
11362
|
"verified": False if failed_items else verified,
|
|
11547
11363
|
"write_executed": write_executed,
|
|
11548
11364
|
"write_succeeded": write_succeeded,
|
|
11365
|
+
**({"write_may_have_succeeded": True, "next_action": "readback_before_retry"} if write_executed else {}),
|
|
11549
11366
|
"safe_to_retry": not write_executed,
|
|
11550
11367
|
})
|
|
11551
11368
|
result_verified = verified or noop
|
|
@@ -12376,7 +12193,7 @@ class AiBuilderFacade:
|
|
|
12376
12193
|
result = legacy_normalized or normalized_views
|
|
12377
12194
|
return (result, False, None) if include_error else (result, False)
|
|
12378
12195
|
|
|
12379
|
-
def
|
|
12196
|
+
def _load_workflow_spec_snapshot(
|
|
12380
12197
|
self,
|
|
12381
12198
|
*,
|
|
12382
12199
|
profile: str,
|
|
@@ -12386,24 +12203,34 @@ class AiBuilderFacade:
|
|
|
12386
12203
|
include_error: bool = False,
|
|
12387
12204
|
) -> tuple[Any, bool] | tuple[Any, bool, QingflowApiError | None]:
|
|
12388
12205
|
try:
|
|
12389
|
-
|
|
12206
|
+
payload = self._with_backend_context(
|
|
12207
|
+
profile,
|
|
12208
|
+
lambda context: workflow_spec_api.fetch_spec(
|
|
12209
|
+
self.apps.backend,
|
|
12210
|
+
context,
|
|
12211
|
+
app_key=app_key,
|
|
12212
|
+
),
|
|
12213
|
+
)
|
|
12390
12214
|
except (QingflowApiError, RuntimeError) as error:
|
|
12391
12215
|
api_error = _coerce_api_error(error)
|
|
12392
12216
|
if tolerate_404 and (
|
|
12393
12217
|
api_error.http_status == 404
|
|
12394
12218
|
or (tolerate_permission_restricted and _is_permission_restricted_api_error(api_error))
|
|
12395
12219
|
):
|
|
12396
|
-
return
|
|
12220
|
+
return {}, True
|
|
12397
12221
|
raise
|
|
12398
|
-
|
|
12399
|
-
return (result, False, None) if include_error else (result, False)
|
|
12222
|
+
return payload if isinstance(payload, dict) else {}, False
|
|
12400
12223
|
|
|
12401
12224
|
def _load_app_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
|
|
12402
12225
|
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
12403
12226
|
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
12404
|
-
|
|
12227
|
+
workflow_spec, workflow_unavailable = self._load_workflow_spec_snapshot(
|
|
12228
|
+
profile=profile,
|
|
12229
|
+
app_key=app_key,
|
|
12230
|
+
tolerate_404=True,
|
|
12231
|
+
)
|
|
12405
12232
|
state["views"] = views
|
|
12406
|
-
state["
|
|
12233
|
+
state["workflow_spec"] = workflow_spec
|
|
12407
12234
|
state["views_unavailable"] = views_unavailable
|
|
12408
12235
|
state["workflow_unavailable"] = workflow_unavailable
|
|
12409
12236
|
return state
|
|
@@ -15682,6 +15509,155 @@ def _public_chart_filter_groups_from_qingbi_config(config: dict[str, Any]) -> li
|
|
|
15682
15509
|
return groups
|
|
15683
15510
|
|
|
15684
15511
|
|
|
15512
|
+
def _chart_apply_retry_context(
|
|
15513
|
+
*,
|
|
15514
|
+
request: ChartApplyRequest,
|
|
15515
|
+
failed_items: list[dict[str, Any]],
|
|
15516
|
+
created_ids: list[str],
|
|
15517
|
+
updated_ids: list[str],
|
|
15518
|
+
removed_ids: list[str],
|
|
15519
|
+
reordered: bool,
|
|
15520
|
+
write_executed: bool,
|
|
15521
|
+
) -> dict[str, Any]:
|
|
15522
|
+
failed_chart_names = [
|
|
15523
|
+
str(item.get("name") or "").strip()
|
|
15524
|
+
for item in failed_items
|
|
15525
|
+
if str(item.get("name") or "").strip()
|
|
15526
|
+
]
|
|
15527
|
+
failed_delete_ids = [
|
|
15528
|
+
str(item.get("chart_id") or "").strip()
|
|
15529
|
+
for item in failed_items
|
|
15530
|
+
if str(item.get("operation") or "") == "delete" and str(item.get("chart_id") or "").strip()
|
|
15531
|
+
]
|
|
15532
|
+
reorder_failed = any(str(item.get("operation") or "") == "reorder" for item in failed_items)
|
|
15533
|
+
failed_name_set = set(failed_chart_names)
|
|
15534
|
+
retry_payload: dict[str, Any] = {"app_key": request.app_key}
|
|
15535
|
+
retry_upserts = [
|
|
15536
|
+
patch.model_dump(mode="json", exclude_none=True)
|
|
15537
|
+
for patch in request.upsert_charts
|
|
15538
|
+
if patch.name in failed_name_set
|
|
15539
|
+
]
|
|
15540
|
+
if retry_upserts:
|
|
15541
|
+
retry_payload["upsert_charts"] = retry_upserts
|
|
15542
|
+
if failed_delete_ids:
|
|
15543
|
+
retry_payload["remove_chart_ids"] = failed_delete_ids
|
|
15544
|
+
if reorder_failed and request.reorder_chart_ids:
|
|
15545
|
+
retry_payload["reorder_chart_ids"] = list(request.reorder_chart_ids)
|
|
15546
|
+
has_retry_payload = any(key in retry_payload for key in ("upsert_charts", "remove_chart_ids", "reorder_chart_ids"))
|
|
15547
|
+
context: dict[str, Any] = {
|
|
15548
|
+
"created_chart_ids": list(created_ids),
|
|
15549
|
+
"updated_chart_ids": list(updated_ids),
|
|
15550
|
+
"removed_chart_ids": list(removed_ids),
|
|
15551
|
+
"failed_chart_names": failed_chart_names,
|
|
15552
|
+
"failed_delete_chart_ids": failed_delete_ids,
|
|
15553
|
+
"readback_first": bool(write_executed),
|
|
15554
|
+
"retry_rule": (
|
|
15555
|
+
"read app charts before retrying; retry only the failed chart payloads if they are still missing or unverified"
|
|
15556
|
+
if write_executed
|
|
15557
|
+
else "fix the failed chart payloads and retry only those items"
|
|
15558
|
+
),
|
|
15559
|
+
}
|
|
15560
|
+
if has_retry_payload:
|
|
15561
|
+
context["suggested_retry_payload"] = retry_payload
|
|
15562
|
+
return context
|
|
15563
|
+
|
|
15564
|
+
|
|
15565
|
+
def _chart_apply_batching_context(
|
|
15566
|
+
*,
|
|
15567
|
+
request: ChartApplyRequest,
|
|
15568
|
+
upsert_charts: list[ChartUpsertPatch],
|
|
15569
|
+
batch_size: int,
|
|
15570
|
+
) -> dict[str, Any]:
|
|
15571
|
+
suggested_batch_payloads: list[dict[str, Any]] = []
|
|
15572
|
+
for start in range(0, len(upsert_charts), batch_size):
|
|
15573
|
+
batch = upsert_charts[start : start + batch_size]
|
|
15574
|
+
suggested_batch_payloads.append(
|
|
15575
|
+
{
|
|
15576
|
+
"app_key": request.app_key,
|
|
15577
|
+
"upsert_charts": [patch.model_dump(mode="json", exclude_none=True) for patch in batch],
|
|
15578
|
+
}
|
|
15579
|
+
)
|
|
15580
|
+
|
|
15581
|
+
followup_payload: dict[str, Any] = {"app_key": request.app_key}
|
|
15582
|
+
if request.remove_chart_ids:
|
|
15583
|
+
followup_payload["remove_chart_ids"] = list(request.remove_chart_ids)
|
|
15584
|
+
if request.reorder_chart_ids:
|
|
15585
|
+
followup_payload["reorder_chart_ids"] = list(request.reorder_chart_ids)
|
|
15586
|
+
if len(followup_payload) > 1:
|
|
15587
|
+
suggested_batch_payloads.append(followup_payload)
|
|
15588
|
+
|
|
15589
|
+
return {
|
|
15590
|
+
"upsert_count": len(upsert_charts),
|
|
15591
|
+
"max_upsert_count": batch_size,
|
|
15592
|
+
"write_executed": False,
|
|
15593
|
+
"readback_first": False,
|
|
15594
|
+
"retry_rule": "run suggested_batch_payloads one at a time; do not submit the original large upsert_charts array",
|
|
15595
|
+
"suggested_batch_payloads": suggested_batch_payloads,
|
|
15596
|
+
}
|
|
15597
|
+
|
|
15598
|
+
|
|
15599
|
+
def _view_apply_failure_diagnostics(
|
|
15600
|
+
*,
|
|
15601
|
+
patch: ViewUpsertPatch,
|
|
15602
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
15603
|
+
backend_code: Any,
|
|
15604
|
+
operation_phase: str,
|
|
15605
|
+
) -> dict[str, Any]:
|
|
15606
|
+
references: list[tuple[str, str]] = []
|
|
15607
|
+
|
|
15608
|
+
def add_reference(name: Any, source: str) -> None:
|
|
15609
|
+
text = str(name or "").strip()
|
|
15610
|
+
if text:
|
|
15611
|
+
references.append((text, source))
|
|
15612
|
+
|
|
15613
|
+
for column in patch.columns:
|
|
15614
|
+
add_reference(column, "columns")
|
|
15615
|
+
add_reference(patch.group_by, "group_by")
|
|
15616
|
+
add_reference(patch.start_field, "start_field")
|
|
15617
|
+
add_reference(patch.end_field, "end_field")
|
|
15618
|
+
add_reference(patch.title_field, "title_field")
|
|
15619
|
+
for rule in patch.filters:
|
|
15620
|
+
add_reference(rule.field_name, "filters")
|
|
15621
|
+
|
|
15622
|
+
seen: set[str] = set()
|
|
15623
|
+
field_entries: list[dict[str, Any]] = []
|
|
15624
|
+
for field_name, source in references:
|
|
15625
|
+
key = f"{field_name}\0{source}"
|
|
15626
|
+
if key in seen:
|
|
15627
|
+
continue
|
|
15628
|
+
seen.add(key)
|
|
15629
|
+
field = current_fields_by_name.get(field_name) or {}
|
|
15630
|
+
field_entries.append(
|
|
15631
|
+
{
|
|
15632
|
+
"field_name": field_name,
|
|
15633
|
+
"source": source,
|
|
15634
|
+
"field_id": field.get("field_id"),
|
|
15635
|
+
"que_id": field.get("que_id"),
|
|
15636
|
+
"field_type": field.get("type"),
|
|
15637
|
+
"que_type": field.get("que_type"),
|
|
15638
|
+
"exists_in_schema": bool(field),
|
|
15639
|
+
}
|
|
15640
|
+
)
|
|
15641
|
+
diagnostics: dict[str, Any] = {
|
|
15642
|
+
"operation_phase": operation_phase,
|
|
15643
|
+
"field_level_diagnostics": field_entries,
|
|
15644
|
+
}
|
|
15645
|
+
if backend_code_value_int(backend_code) == 40038:
|
|
15646
|
+
diagnostics.update(
|
|
15647
|
+
{
|
|
15648
|
+
"suspected_fields": field_entries,
|
|
15649
|
+
"recovery_hint": "Do not delete app fields or recreate the app. First retry the view with the smallest required columns; then add non-critical columns back one by one.",
|
|
15650
|
+
"recommended_minimal_retry": {
|
|
15651
|
+
"name": patch.name,
|
|
15652
|
+
"type": patch.type.value,
|
|
15653
|
+
"columns": [field_entries[0]["field_name"]] if field_entries else [],
|
|
15654
|
+
**({"filters": [rule.model_dump(mode="json", exclude_none=True) for rule in patch.filters]} if patch.filters else {}),
|
|
15655
|
+
},
|
|
15656
|
+
}
|
|
15657
|
+
)
|
|
15658
|
+
return diagnostics
|
|
15659
|
+
|
|
15660
|
+
|
|
15685
15661
|
def _public_chart_group_by_from_qingbi_config(config: dict[str, Any]) -> list[str]:
|
|
15686
15662
|
fields: list[dict[str, Any]] = []
|
|
15687
15663
|
for key in ("selectedDimensions", "xDimensions", "yDimensions", "selectedTime"):
|
|
@@ -20236,30 +20212,6 @@ def _build_layout_preset_sections(*, preset: LayoutPreset, field_names: list[str
|
|
|
20236
20212
|
return sections
|
|
20237
20213
|
|
|
20238
20214
|
|
|
20239
|
-
def _build_flow_preset(preset: FlowPreset) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
20240
|
-
if preset == FlowPreset.basic_fill_then_approve:
|
|
20241
|
-
nodes = [
|
|
20242
|
-
{"id": "start", "type": "start", "name": "发起"},
|
|
20243
|
-
{"id": "fill_1", "type": "fill", "name": "补充信息"},
|
|
20244
|
-
{"id": "approve_1", "type": "approve", "name": "审批"},
|
|
20245
|
-
{"id": "end", "type": "end", "name": "结束"},
|
|
20246
|
-
]
|
|
20247
|
-
transitions = [
|
|
20248
|
-
{"from": "start", "to": "fill_1"},
|
|
20249
|
-
{"from": "fill_1", "to": "approve_1"},
|
|
20250
|
-
{"from": "approve_1", "to": "end"},
|
|
20251
|
-
]
|
|
20252
|
-
return nodes, transitions
|
|
20253
|
-
nodes = [
|
|
20254
|
-
{"id": "start", "type": "start", "name": "发起"},
|
|
20255
|
-
{"id": "approve_1", "type": "approve", "name": "审批"},
|
|
20256
|
-
{"id": "end", "type": "end", "name": "结束"},
|
|
20257
|
-
]
|
|
20258
|
-
transitions = [
|
|
20259
|
-
{"from": "start", "to": "approve_1"},
|
|
20260
|
-
{"from": "approve_1", "to": "end"},
|
|
20261
|
-
]
|
|
20262
|
-
return nodes, transitions
|
|
20263
20215
|
|
|
20264
20216
|
|
|
20265
20217
|
def _merge_flow_graph(
|
|
@@ -22114,144 +22066,35 @@ def _apply_row_widths(row: list[dict[str, Any]]) -> None:
|
|
|
22114
22066
|
question["queWidth"] = width + (1 if index < remainder else 0)
|
|
22115
22067
|
|
|
22116
22068
|
|
|
22117
|
-
def
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
|
|
22123
|
-
|
|
22124
|
-
|
|
22125
|
-
|
|
22126
|
-
|
|
22127
|
-
|
|
22128
|
-
node_id = _coerce_positive_int(item.get("auditNodeId"))
|
|
22129
|
-
name = item.get("auditNodeName")
|
|
22130
|
-
if node_id is None or not name:
|
|
22131
|
-
continue
|
|
22132
|
-
nodes.append({"id": node_id, "name": name, "type": item.get("type"), "deal_type": item.get("dealType")})
|
|
22133
|
-
return nodes
|
|
22134
|
-
|
|
22135
|
-
|
|
22136
|
-
def _workflow_node_iterable(result: Any) -> list[dict[str, Any]]:
|
|
22137
|
-
if isinstance(result, dict):
|
|
22138
|
-
iterable = result.values()
|
|
22139
|
-
elif isinstance(result, list):
|
|
22140
|
-
iterable = result
|
|
22141
|
-
else:
|
|
22142
|
-
iterable = []
|
|
22143
|
-
return [item for item in iterable if isinstance(item, dict)]
|
|
22144
|
-
|
|
22145
|
-
|
|
22146
|
-
def _workflow_condition_matrix_value(node: dict[str, Any]) -> list[Any]:
|
|
22147
|
-
config = node.get("config")
|
|
22148
|
-
if isinstance(config, dict):
|
|
22149
|
-
matrix = config.get("autoJudges")
|
|
22150
|
-
if isinstance(matrix, list):
|
|
22151
|
-
return deepcopy(matrix)
|
|
22152
|
-
matrix = config.get("conditionFormatMatrix")
|
|
22153
|
-
if isinstance(matrix, list):
|
|
22154
|
-
return deepcopy(matrix)
|
|
22155
|
-
matrix = node.get("autoJudges")
|
|
22156
|
-
if isinstance(matrix, list):
|
|
22157
|
-
return deepcopy(matrix)
|
|
22158
|
-
matrix = node.get("conditionFormatMatrix")
|
|
22159
|
-
if isinstance(matrix, list):
|
|
22160
|
-
return deepcopy(matrix)
|
|
22069
|
+
def _extract_workflow_spec_nodes(payload: Any) -> list[dict[str, Any]]:
|
|
22070
|
+
if not isinstance(payload, dict):
|
|
22071
|
+
return []
|
|
22072
|
+
spec = payload.get("spec")
|
|
22073
|
+
if isinstance(spec, dict):
|
|
22074
|
+
nodes = spec.get("nodes")
|
|
22075
|
+
if isinstance(nodes, list):
|
|
22076
|
+
return [item for item in nodes if isinstance(item, dict)]
|
|
22077
|
+
nodes = payload.get("nodes")
|
|
22078
|
+
if isinstance(nodes, list):
|
|
22079
|
+
return [item for item in nodes if isinstance(item, dict)]
|
|
22161
22080
|
return []
|
|
22162
22081
|
|
|
22163
22082
|
|
|
22164
|
-
def
|
|
22165
|
-
|
|
22166
|
-
|
|
22167
|
-
|
|
22168
|
-
|
|
22169
|
-
|
|
22170
|
-
if not requested_branch_like:
|
|
22171
|
-
return True
|
|
22172
|
-
actual_nodes = _workflow_node_iterable(current_workflow)
|
|
22173
|
-
actual_branch_like = [
|
|
22174
|
-
node
|
|
22175
|
-
for node in actual_nodes
|
|
22176
|
-
if _normalize_existing_workflow_node_type(node.get("type"), node.get("dealType")) in {"branch", "condition"}
|
|
22177
|
-
]
|
|
22178
|
-
if len(actual_branch_like) < len(requested_branch_like):
|
|
22179
|
-
return False
|
|
22180
|
-
actual_condition_nodes = [
|
|
22181
|
-
node
|
|
22182
|
-
for node in actual_nodes
|
|
22183
|
-
if _normalize_existing_workflow_node_type(node.get("type"), node.get("dealType")) == "condition"
|
|
22184
|
-
]
|
|
22185
|
-
for requested in requested_branch_like:
|
|
22186
|
-
if str(requested.get("type") or "").strip().lower() != "condition":
|
|
22187
|
-
continue
|
|
22188
|
-
expected_matrix = _workflow_condition_matrix_value(requested)
|
|
22189
|
-
if not expected_matrix:
|
|
22083
|
+
def _summarize_workflow_spec_nodes(spec_nodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
22084
|
+
summaries: list[dict[str, Any]] = []
|
|
22085
|
+
for item in spec_nodes:
|
|
22086
|
+
node_id = str(item.get("id") or "").strip()
|
|
22087
|
+
name = str(item.get("name") or "").strip()
|
|
22088
|
+
if not node_id and not name:
|
|
22190
22089
|
continue
|
|
22191
|
-
|
|
22192
|
-
|
|
22193
|
-
|
|
22194
|
-
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
),
|
|
22198
|
-
None,
|
|
22199
|
-
)
|
|
22200
|
-
if match is None or not _workflow_condition_matrix_value(match):
|
|
22201
|
-
return False
|
|
22202
|
-
return True
|
|
22203
|
-
|
|
22204
|
-
|
|
22205
|
-
def _workflow_nodes_semantically_equal(*, current_workflow: Any, requested_nodes: list[dict[str, Any]]) -> bool:
|
|
22206
|
-
current_nodes = _summarize_workflow_nodes(current_workflow)
|
|
22207
|
-
current_effective = [
|
|
22208
|
-
node
|
|
22209
|
-
for node in current_nodes
|
|
22210
|
-
if _normalize_existing_workflow_node_type(node.get("type"), node.get("deal_type")) not in {"start", "end"}
|
|
22211
|
-
]
|
|
22212
|
-
requested_effective = [
|
|
22213
|
-
node for node in requested_nodes if str(node.get("type") or "") not in {"start", "end"}
|
|
22214
|
-
]
|
|
22215
|
-
if not current_effective or not requested_effective or len(current_effective) != len(requested_effective):
|
|
22216
|
-
return False
|
|
22217
|
-
current_signatures = sorted(
|
|
22218
|
-
(
|
|
22219
|
-
_normalize_existing_workflow_node_type(node.get("type"), node.get("deal_type")),
|
|
22220
|
-
str(node.get("name") or "").strip(),
|
|
22221
|
-
)
|
|
22222
|
-
for node in current_effective
|
|
22223
|
-
)
|
|
22224
|
-
requested_signatures = sorted(
|
|
22225
|
-
(
|
|
22226
|
-
str(node.get("type") or "").strip().lower(),
|
|
22227
|
-
str(node.get("name") or "").strip(),
|
|
22090
|
+
summaries.append(
|
|
22091
|
+
{
|
|
22092
|
+
"id": node_id or None,
|
|
22093
|
+
"name": name or None,
|
|
22094
|
+
"type": item.get("type"),
|
|
22095
|
+
}
|
|
22228
22096
|
)
|
|
22229
|
-
|
|
22230
|
-
)
|
|
22231
|
-
return current_signatures == requested_signatures
|
|
22232
|
-
|
|
22233
|
-
|
|
22234
|
-
def _normalize_existing_workflow_node_type(raw_type: Any, deal_type: Any) -> str:
|
|
22235
|
-
normalized = "" if raw_type is None else str(raw_type).strip().lower()
|
|
22236
|
-
if normalized in {"audit", "approve"}:
|
|
22237
|
-
return "approve"
|
|
22238
|
-
if normalized in {"fill", "copy", "branch", "condition", "webhook", "start", "end"}:
|
|
22239
|
-
return normalized
|
|
22240
|
-
if normalized in {"1"}:
|
|
22241
|
-
return "branch"
|
|
22242
|
-
if normalized in {"2"}:
|
|
22243
|
-
return "condition"
|
|
22244
|
-
if normalized in {"3"} or deal_type == 10:
|
|
22245
|
-
return "webhook"
|
|
22246
|
-
if normalized in {"0"} and deal_type == 0:
|
|
22247
|
-
return "approve"
|
|
22248
|
-
if normalized in {"0"} and deal_type == 1:
|
|
22249
|
-
return "fill"
|
|
22250
|
-
if normalized in {"0"} and deal_type == 2:
|
|
22251
|
-
return "copy"
|
|
22252
|
-
if normalized in {"0"} or deal_type == 3:
|
|
22253
|
-
return "start"
|
|
22254
|
-
return normalized or "unknown"
|
|
22097
|
+
return summaries
|
|
22255
22098
|
|
|
22256
22099
|
|
|
22257
22100
|
def _summarize_views(result: Any) -> list[dict[str, Any]]:
|
|
@@ -23449,98 +23292,6 @@ def _field_id_for_name(fields: list[dict[str, Any]], name: str) -> str:
|
|
|
23449
23292
|
raise KeyError(name)
|
|
23450
23293
|
|
|
23451
23294
|
|
|
23452
|
-
def _build_public_workflow_spec(*, nodes: list[dict[str, Any]], transitions: list[dict[str, Any]]) -> JSONObject:
|
|
23453
|
-
node_map = {str(node.get("id") or ""): node for node in nodes if isinstance(node, dict) and node.get("id")}
|
|
23454
|
-
if not node_map:
|
|
23455
|
-
return _failed("INVALID_FLOW", "nodes must be a non-empty list", suggested_next_call=None)
|
|
23456
|
-
start_nodes = [node_id for node_id, node in node_map.items() if node.get("type") == "start"]
|
|
23457
|
-
if len(start_nodes) != 1:
|
|
23458
|
-
return _failed("INVALID_FLOW", "flow must contain exactly one start node", suggested_next_call=None)
|
|
23459
|
-
inbound: dict[str, list[str]] = {node_id: [] for node_id in node_map}
|
|
23460
|
-
outbound: dict[str, list[str]] = {node_id: [] for node_id in node_map}
|
|
23461
|
-
for transition in transitions:
|
|
23462
|
-
if not isinstance(transition, dict):
|
|
23463
|
-
continue
|
|
23464
|
-
source = str(transition.get("from") or "")
|
|
23465
|
-
target = str(transition.get("to") or "")
|
|
23466
|
-
if source not in node_map or target not in node_map:
|
|
23467
|
-
return _failed("INVALID_FLOW_EDGE", "transition references unknown node", details={"transition": transition}, suggested_next_call=None)
|
|
23468
|
-
outbound[source].append(target)
|
|
23469
|
-
inbound[target].append(source)
|
|
23470
|
-
branch_lane_nodes: dict[str, dict[str, Any]] = {}
|
|
23471
|
-
for node_id, node in node_map.items():
|
|
23472
|
-
if str(node.get("type") or "") != "condition":
|
|
23473
|
-
continue
|
|
23474
|
-
if len(inbound[node_id]) != 1:
|
|
23475
|
-
return _failed(
|
|
23476
|
-
"INVALID_FLOW_EDGE",
|
|
23477
|
-
f"condition lane '{node_id}' must have exactly one inbound transition",
|
|
23478
|
-
details={"node_id": node_id, "inbound": inbound[node_id]},
|
|
23479
|
-
suggested_next_call=None,
|
|
23480
|
-
)
|
|
23481
|
-
parent_id = inbound[node_id][0]
|
|
23482
|
-
if node_map[parent_id].get("type") != "branch":
|
|
23483
|
-
continue
|
|
23484
|
-
branch_lane_nodes[node_id] = {
|
|
23485
|
-
"branch_parent_id": parent_id,
|
|
23486
|
-
"branch_index": outbound[parent_id].index(node_id) + 1,
|
|
23487
|
-
}
|
|
23488
|
-
internal_nodes = []
|
|
23489
|
-
for node_id, node in node_map.items():
|
|
23490
|
-
node_type = str(node.get("type") or "")
|
|
23491
|
-
if node_type == "end":
|
|
23492
|
-
continue
|
|
23493
|
-
if node_type != "start" and len(inbound[node_id]) != 1:
|
|
23494
|
-
return _failed(
|
|
23495
|
-
"INVALID_FLOW_EDGE",
|
|
23496
|
-
f"node '{node_id}' must have exactly one inbound transition",
|
|
23497
|
-
details={"node_id": node_id, "inbound": inbound[node_id]},
|
|
23498
|
-
suggested_next_call=None,
|
|
23499
|
-
)
|
|
23500
|
-
config_payload = deepcopy(node.get("config") or {}) if isinstance(node.get("config"), dict) else {}
|
|
23501
|
-
assignees = node.get("assignees") or {}
|
|
23502
|
-
if node_id in branch_lane_nodes:
|
|
23503
|
-
lane_meta = branch_lane_nodes[node_id]
|
|
23504
|
-
config_payload["__lane_only__"] = True
|
|
23505
|
-
payload = {
|
|
23506
|
-
"node_id": f"__branch_lane__{lane_meta['branch_parent_id']}__{lane_meta['branch_index']}",
|
|
23507
|
-
"name": node.get("name") or node_id,
|
|
23508
|
-
"node_type": "condition",
|
|
23509
|
-
"parent_node_id": lane_meta["branch_parent_id"],
|
|
23510
|
-
"branch_parent_id": lane_meta["branch_parent_id"],
|
|
23511
|
-
"branch_index": lane_meta["branch_index"],
|
|
23512
|
-
}
|
|
23513
|
-
else:
|
|
23514
|
-
payload = {
|
|
23515
|
-
"node_id": node_id,
|
|
23516
|
-
"name": node.get("name") or node_id,
|
|
23517
|
-
"node_type": "audit" if node_type == "approve" else node_type,
|
|
23518
|
-
}
|
|
23519
|
-
if node_type != "start":
|
|
23520
|
-
parent_id = inbound[node_id][0]
|
|
23521
|
-
lane_parent = branch_lane_nodes.get(parent_id)
|
|
23522
|
-
if lane_parent is not None:
|
|
23523
|
-
payload["parent_node_id"] = lane_parent["branch_parent_id"]
|
|
23524
|
-
payload["branch_parent_id"] = lane_parent["branch_parent_id"]
|
|
23525
|
-
payload["branch_index"] = lane_parent["branch_index"]
|
|
23526
|
-
else:
|
|
23527
|
-
payload["parent_node_id"] = parent_id
|
|
23528
|
-
if node_map[parent_id].get("type") == "branch":
|
|
23529
|
-
payload["branch_parent_id"] = parent_id
|
|
23530
|
-
payload["branch_index"] = outbound[parent_id].index(node_id) + 1
|
|
23531
|
-
permissions = node.get("permissions") or {}
|
|
23532
|
-
editable_que_ids = permissions.get("editable_que_ids") or []
|
|
23533
|
-
if editable_que_ids:
|
|
23534
|
-
config_payload["editableQueIds"] = editable_que_ids
|
|
23535
|
-
if config_payload:
|
|
23536
|
-
payload["config"] = config_payload
|
|
23537
|
-
if assignees:
|
|
23538
|
-
payload["assignees"] = deepcopy(assignees)
|
|
23539
|
-
internal_nodes.append(payload)
|
|
23540
|
-
return {
|
|
23541
|
-
"status": "success",
|
|
23542
|
-
"workflow": {"enabled": True, "nodes": internal_nodes},
|
|
23543
|
-
}
|
|
23544
23295
|
|
|
23545
23296
|
|
|
23546
23297
|
def _build_flow_condition_matrix(
|
|
@@ -23737,6 +23488,9 @@ def _resolve_query_condition_field(
|
|
|
23737
23488
|
"field": raw,
|
|
23738
23489
|
"parent_field": subfield_parent.get("name"),
|
|
23739
23490
|
"message": "subtable subfields cannot be used as view query conditions",
|
|
23491
|
+
"fix_hint": QUERY_CONDITION_FIX_HINT,
|
|
23492
|
+
"supported_field_types": QUERY_CONDITION_SUPPORTED_FIELD_TYPES_PUBLIC,
|
|
23493
|
+
"unsupported_field_types": QUERY_CONDITION_UNSUPPORTED_FIELD_TYPES_PUBLIC,
|
|
23740
23494
|
}
|
|
23741
23495
|
return {}, {
|
|
23742
23496
|
"error_code": "QUERY_CONDITION_FIELD_NOT_FOUND",
|
|
@@ -23753,6 +23507,9 @@ def _resolve_query_condition_field(
|
|
|
23753
23507
|
"field": field.get("name"),
|
|
23754
23508
|
"field_type": field_type,
|
|
23755
23509
|
"message": "this field type is not supported by the frontend query condition panel",
|
|
23510
|
+
"fix_hint": QUERY_CONDITION_FIX_HINT,
|
|
23511
|
+
"supported_field_types": QUERY_CONDITION_SUPPORTED_FIELD_TYPES_PUBLIC,
|
|
23512
|
+
"unsupported_field_types": QUERY_CONDITION_UNSUPPORTED_FIELD_TYPES_PUBLIC,
|
|
23756
23513
|
}
|
|
23757
23514
|
que_id = _coerce_positive_int(field.get("que_id"))
|
|
23758
23515
|
if que_id is None:
|
|
@@ -23762,6 +23519,8 @@ def _resolve_query_condition_field(
|
|
|
23762
23519
|
"missing_fields": [],
|
|
23763
23520
|
"field": field.get("name"),
|
|
23764
23521
|
"message": "query condition field has no backend queId",
|
|
23522
|
+
"fix_hint": QUERY_CONDITION_FIX_HINT,
|
|
23523
|
+
"supported_field_types": QUERY_CONDITION_SUPPORTED_FIELD_TYPES_PUBLIC,
|
|
23765
23524
|
}
|
|
23766
23525
|
return field, None
|
|
23767
23526
|
|
|
@@ -25638,92 +25397,6 @@ def _infer_status_field_id(fields: list[dict[str, Any]]) -> str | None:
|
|
|
25638
25397
|
return None
|
|
25639
25398
|
|
|
25640
25399
|
|
|
25641
|
-
def _normalize_flow_stage_failure(stage: JSONObject, *, profile: str, app_key: str, entity: dict[str, Any]) -> JSONObject:
|
|
25642
|
-
stage_error_code = str(stage.get("error_code") or "FLOW_APPLY_FAILED")
|
|
25643
|
-
detail_text = _extract_stage_failure_text(stage)
|
|
25644
|
-
request_id = stage.get("request_id")
|
|
25645
|
-
backend_code = stage.get("backend_code")
|
|
25646
|
-
http_status = stage.get("http_status")
|
|
25647
|
-
public_stage_result = _public_stage_result(stage)
|
|
25648
|
-
if request_id is None and isinstance(public_stage_result.get("errors"), list) and public_stage_result["errors"]:
|
|
25649
|
-
first_error = public_stage_result["errors"][0]
|
|
25650
|
-
if isinstance(first_error, dict):
|
|
25651
|
-
request_id = first_error.get("request_id")
|
|
25652
|
-
backend_code = first_error.get("backend_code")
|
|
25653
|
-
http_status = first_error.get("http_status")
|
|
25654
|
-
lowered_detail = detail_text.lower()
|
|
25655
|
-
if "must declare status field" in detail_text:
|
|
25656
|
-
return _failed(
|
|
25657
|
-
"STATUS_FIELD_REQUIRED",
|
|
25658
|
-
detail_text,
|
|
25659
|
-
details={
|
|
25660
|
-
"app_key": app_key,
|
|
25661
|
-
"entity_id": entity.get("entity_id"),
|
|
25662
|
-
"existing_fields": entity.get("fields") or [],
|
|
25663
|
-
"stage_result": public_stage_result,
|
|
25664
|
-
},
|
|
25665
|
-
suggested_next_call={
|
|
25666
|
-
"tool_name": "app_schema_apply",
|
|
25667
|
-
"arguments": {
|
|
25668
|
-
"profile": profile,
|
|
25669
|
-
"app_key": app_key,
|
|
25670
|
-
"create_if_missing": False,
|
|
25671
|
-
"add_fields": [
|
|
25672
|
-
{
|
|
25673
|
-
"name": "状态",
|
|
25674
|
-
"type": "single_select",
|
|
25675
|
-
"options": ["草稿", "进行中", "已完成"],
|
|
25676
|
-
"required": True,
|
|
25677
|
-
}
|
|
25678
|
-
],
|
|
25679
|
-
"update_fields": [],
|
|
25680
|
-
"remove_fields": [],
|
|
25681
|
-
},
|
|
25682
|
-
},
|
|
25683
|
-
request_id=request_id,
|
|
25684
|
-
backend_code=backend_code,
|
|
25685
|
-
http_status=http_status,
|
|
25686
|
-
)
|
|
25687
|
-
if (
|
|
25688
|
-
"run solution_build_app first" in lowered_detail
|
|
25689
|
-
or "run solution_build_app_flow first" in lowered_detail
|
|
25690
|
-
or ("is not defined yet" in lowered_detail and "solution_build_" in lowered_detail)
|
|
25691
|
-
):
|
|
25692
|
-
return _failed(
|
|
25693
|
-
"FLOW_STAGE_CONTEXT_MISSING",
|
|
25694
|
-
"workflow apply lost the app context required by the internal flow builder",
|
|
25695
|
-
details={
|
|
25696
|
-
"app_key": app_key,
|
|
25697
|
-
"entity_id": entity.get("entity_id"),
|
|
25698
|
-
"existing_fields": entity.get("fields") or [],
|
|
25699
|
-
"internal_detail": detail_text,
|
|
25700
|
-
"stage_result": public_stage_result,
|
|
25701
|
-
},
|
|
25702
|
-
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, "app_key": app_key}},
|
|
25703
|
-
request_id=request_id,
|
|
25704
|
-
backend_code=backend_code,
|
|
25705
|
-
http_status=http_status,
|
|
25706
|
-
)
|
|
25707
|
-
message = detail_text or "failed to apply workflow patch"
|
|
25708
|
-
details = {"app_key": app_key, "entity_id": entity.get("entity_id"), "stage_result": public_stage_result}
|
|
25709
|
-
public_http_status = http_status
|
|
25710
|
-
if http_status == 404:
|
|
25711
|
-
message = "workflow write route is unavailable for this app in the current route"
|
|
25712
|
-
details["transport_error"] = {
|
|
25713
|
-
"http_status": http_status,
|
|
25714
|
-
"backend_code": backend_code,
|
|
25715
|
-
"category": "http",
|
|
25716
|
-
}
|
|
25717
|
-
public_http_status = None
|
|
25718
|
-
return _failed(
|
|
25719
|
-
stage_error_code,
|
|
25720
|
-
message,
|
|
25721
|
-
details=details,
|
|
25722
|
-
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, "app_key": app_key}},
|
|
25723
|
-
request_id=request_id,
|
|
25724
|
-
backend_code=backend_code,
|
|
25725
|
-
http_status=public_http_status,
|
|
25726
|
-
)
|
|
25727
25400
|
|
|
25728
25401
|
|
|
25729
25402
|
def _extract_stage_failure_text(stage: JSONObject) -> str:
|