@qingflow-tech/qingflow-app-builder-mcp 1.0.44 → 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 +4 -2
- package/npm/bin/qingflow-app-builder-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-builder-code-integrations/SKILL.md +1 -1
- 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/skills/qingflow-workflow-builder/SKILL.md +98 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- 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 +262 -862
- 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/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 +92 -233
- package/src/qingflow_mcp/tools/solution_tools.py +30 -2
- package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
- 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,8 +93,6 @@ 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
|
|
@@ -266,7 +261,6 @@ class AiBuilderFacade:
|
|
|
266
261
|
buttons: CustomButtonTools,
|
|
267
262
|
packages: PackageTools,
|
|
268
263
|
views: ViewTools,
|
|
269
|
-
workflows: WorkflowTools,
|
|
270
264
|
portals: PortalTools,
|
|
271
265
|
charts: QingbiReportTools,
|
|
272
266
|
roles: RoleTools,
|
|
@@ -277,13 +271,216 @@ class AiBuilderFacade:
|
|
|
277
271
|
self.buttons = buttons
|
|
278
272
|
self.packages = packages
|
|
279
273
|
self.views = views
|
|
280
|
-
self.workflows = workflows
|
|
281
274
|
self.portals = portals
|
|
282
275
|
self.charts = charts
|
|
283
276
|
self.roles = roles
|
|
284
277
|
self.directory = directory
|
|
285
278
|
self.solutions = solutions
|
|
286
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
|
+
|
|
287
484
|
def package_resolve(self, *, profile: str, package_name: str) -> JSONObject:
|
|
288
485
|
requested = str(package_name or "").strip()
|
|
289
486
|
if not requested:
|
|
@@ -2090,83 +2287,6 @@ class AiBuilderFacade:
|
|
|
2090
2287
|
},
|
|
2091
2288
|
}
|
|
2092
2289
|
|
|
2093
|
-
def _normalize_flow_nodes(
|
|
2094
|
-
self,
|
|
2095
|
-
*,
|
|
2096
|
-
profile: str,
|
|
2097
|
-
current_fields: list[dict[str, Any]],
|
|
2098
|
-
nodes: list[dict[str, Any]],
|
|
2099
|
-
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
2100
|
-
field_name_to_field = {
|
|
2101
|
-
str(field.get("name") or ""): field
|
|
2102
|
-
for field in current_fields
|
|
2103
|
-
if str(field.get("name") or "")
|
|
2104
|
-
}
|
|
2105
|
-
field_name_to_que_id = {
|
|
2106
|
-
str(field.get("name") or ""): int(field.get("que_id"))
|
|
2107
|
-
for field in current_fields
|
|
2108
|
-
if str(field.get("name") or "") and isinstance(field.get("que_id"), int)
|
|
2109
|
-
}
|
|
2110
|
-
normalized_nodes: list[dict[str, Any]] = []
|
|
2111
|
-
issues: list[dict[str, Any]] = []
|
|
2112
|
-
for node in nodes:
|
|
2113
|
-
if not isinstance(node, dict):
|
|
2114
|
-
continue
|
|
2115
|
-
normalized_node = deepcopy(node)
|
|
2116
|
-
assignees = FlowAssigneePatch.model_validate(node.get("assignees") or {})
|
|
2117
|
-
permissions = FlowNodePermissionsPatch.model_validate(node.get("permissions") or {})
|
|
2118
|
-
role_resolution = self._resolve_role_references(
|
|
2119
|
-
profile=profile,
|
|
2120
|
-
role_ids=assignees.role_ids,
|
|
2121
|
-
role_names=assignees.role_names,
|
|
2122
|
-
)
|
|
2123
|
-
member_resolution = self._resolve_member_references(
|
|
2124
|
-
profile=profile,
|
|
2125
|
-
member_uids=assignees.member_uids,
|
|
2126
|
-
member_emails=assignees.member_emails,
|
|
2127
|
-
member_names=assignees.member_names,
|
|
2128
|
-
)
|
|
2129
|
-
issues.extend({**issue, "node_id": node.get("id")} for issue in [*role_resolution["issues"], *member_resolution["issues"]])
|
|
2130
|
-
editable_que_ids: list[int] = []
|
|
2131
|
-
missing_editable_fields: list[str] = []
|
|
2132
|
-
for field_name in permissions.editable_fields:
|
|
2133
|
-
if field_name not in field_name_to_que_id:
|
|
2134
|
-
missing_editable_fields.append(field_name)
|
|
2135
|
-
else:
|
|
2136
|
-
editable_que_ids.append(field_name_to_que_id[field_name])
|
|
2137
|
-
if missing_editable_fields:
|
|
2138
|
-
issues.append(
|
|
2139
|
-
{
|
|
2140
|
-
"node_id": node.get("id"),
|
|
2141
|
-
"kind": "editable_fields",
|
|
2142
|
-
"error_code": "UNKNOWN_FLOW_FIELD",
|
|
2143
|
-
"missing_fields": missing_editable_fields,
|
|
2144
|
-
}
|
|
2145
|
-
)
|
|
2146
|
-
condition_matrix, condition_issues = _build_flow_condition_matrix(
|
|
2147
|
-
current_fields_by_name=field_name_to_field,
|
|
2148
|
-
node=normalized_node,
|
|
2149
|
-
)
|
|
2150
|
-
issues.extend({**issue, "node_id": node.get("id")} for issue in condition_issues)
|
|
2151
|
-
config_payload = deepcopy(normalized_node.get("config") or {}) if isinstance(normalized_node.get("config"), dict) else {}
|
|
2152
|
-
if condition_matrix:
|
|
2153
|
-
# Backend judge nodes persist branch conditions from `autoJudges`.
|
|
2154
|
-
# Keep the legacy mirror key for internal verification logic.
|
|
2155
|
-
config_payload["autoJudges"] = condition_matrix
|
|
2156
|
-
config_payload["conditionFormatMatrix"] = condition_matrix
|
|
2157
|
-
normalized_node["assignees"] = {
|
|
2158
|
-
"member_uids": member_resolution["member_uids"],
|
|
2159
|
-
"role_entries": role_resolution["role_entries"],
|
|
2160
|
-
"include_sub_departs": assignees.include_sub_departs,
|
|
2161
|
-
}
|
|
2162
|
-
normalized_node["permissions"] = {
|
|
2163
|
-
"editable_fields": permissions.editable_fields,
|
|
2164
|
-
"editable_que_ids": editable_que_ids,
|
|
2165
|
-
}
|
|
2166
|
-
if config_payload:
|
|
2167
|
-
normalized_node["config"] = config_payload
|
|
2168
|
-
normalized_nodes.append(normalized_node)
|
|
2169
|
-
return normalized_nodes, issues
|
|
2170
2290
|
|
|
2171
2291
|
def _unsupported_public_flow_nodes(self, *, nodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
2172
2292
|
unsupported: list[dict[str, Any]] = []
|
|
@@ -5268,36 +5388,13 @@ class AiBuilderFacade:
|
|
|
5268
5388
|
except (QingflowApiError, RuntimeError) as error:
|
|
5269
5389
|
custom_buttons_unavailable = True
|
|
5270
5390
|
custom_buttons = []
|
|
5271
|
-
|
|
5272
|
-
{
|
|
5273
|
-
"resource": "custom_buttons",
|
|
5274
|
-
"phase": "summary",
|
|
5275
|
-
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
5276
|
-
}
|
|
5277
|
-
)
|
|
5278
|
-
workflow, workflow_unavailable, workflow_read_error = self._load_workflow_result(
|
|
5391
|
+
workflow_spec, workflow_unavailable = self._load_workflow_spec_snapshot(
|
|
5279
5392
|
profile=profile,
|
|
5280
5393
|
app_key=app_key,
|
|
5281
5394
|
tolerate_404=True,
|
|
5282
5395
|
tolerate_permission_restricted=True,
|
|
5283
|
-
include_error=True,
|
|
5284
5396
|
)
|
|
5285
|
-
|
|
5286
|
-
readback_errors.append(
|
|
5287
|
-
{
|
|
5288
|
-
"resource": "views",
|
|
5289
|
-
"phase": "summary",
|
|
5290
|
-
"transport_error": _transport_error_payload(views_read_error),
|
|
5291
|
-
}
|
|
5292
|
-
)
|
|
5293
|
-
if workflow_read_error is not None:
|
|
5294
|
-
readback_errors.append(
|
|
5295
|
-
{
|
|
5296
|
-
"resource": "workflow",
|
|
5297
|
-
"phase": "summary",
|
|
5298
|
-
"transport_error": _transport_error_payload(workflow_read_error),
|
|
5299
|
-
}
|
|
5300
|
-
)
|
|
5397
|
+
workflow_nodes = _extract_workflow_spec_nodes(workflow_spec)
|
|
5301
5398
|
verification_hints = _build_verification_hints(
|
|
5302
5399
|
tag_ids=_coerce_int_list(base_result.get("tagIds")),
|
|
5303
5400
|
fields=parsed["fields"],
|
|
@@ -5353,7 +5450,7 @@ class AiBuilderFacade:
|
|
|
5353
5450
|
charts=chart_summaries,
|
|
5354
5451
|
associated_resources=associated_resources,
|
|
5355
5452
|
custom_buttons=custom_buttons,
|
|
5356
|
-
workflow_enabled=bool(
|
|
5453
|
+
workflow_enabled=bool(workflow_nodes),
|
|
5357
5454
|
verification_hints=verification_hints,
|
|
5358
5455
|
form_settings={} if schema_unavailable else _form_settings_from_schema(schema_result, parsed["fields"]),
|
|
5359
5456
|
)
|
|
@@ -5763,7 +5860,7 @@ class AiBuilderFacade:
|
|
|
5763
5860
|
return result
|
|
5764
5861
|
|
|
5765
5862
|
def app_get_flow(self, *, profile: str, app_key: str) -> JSONObject:
|
|
5766
|
-
result = self.
|
|
5863
|
+
result = self.flow_get(profile=profile, app_key=app_key)
|
|
5767
5864
|
if result.get("status") == "success":
|
|
5768
5865
|
result["message"] = "read app flow config"
|
|
5769
5866
|
return result
|
|
@@ -5944,41 +6041,6 @@ class AiBuilderFacade:
|
|
|
5944
6041
|
**response.model_dump(mode="json"),
|
|
5945
6042
|
}
|
|
5946
6043
|
|
|
5947
|
-
def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
5948
|
-
try:
|
|
5949
|
-
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
5950
|
-
except (QingflowApiError, RuntimeError) as error:
|
|
5951
|
-
api_error = _coerce_api_error(error)
|
|
5952
|
-
return _failed_from_api_error(
|
|
5953
|
-
"FLOW_READ_FAILED",
|
|
5954
|
-
api_error,
|
|
5955
|
-
normalized_args={"app_key": app_key},
|
|
5956
|
-
details={"app_key": app_key},
|
|
5957
|
-
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5958
|
-
)
|
|
5959
|
-
response = AppFlowReadResponse(
|
|
5960
|
-
app_key=app_key,
|
|
5961
|
-
enabled=bool(workflow),
|
|
5962
|
-
nodes=_summarize_workflow_nodes(workflow),
|
|
5963
|
-
transitions=[],
|
|
5964
|
-
)
|
|
5965
|
-
return {
|
|
5966
|
-
"status": "success",
|
|
5967
|
-
"error_code": None,
|
|
5968
|
-
"recoverable": False,
|
|
5969
|
-
"message": "read app flow summary",
|
|
5970
|
-
"normalized_args": {"app_key": app_key},
|
|
5971
|
-
"missing_fields": [],
|
|
5972
|
-
"allowed_values": {},
|
|
5973
|
-
"details": {},
|
|
5974
|
-
"request_id": None,
|
|
5975
|
-
"suggested_next_call": None,
|
|
5976
|
-
"noop": False,
|
|
5977
|
-
"warnings": [_warning("WORKFLOW_READ_UNAVAILABLE", "workflow summary readback is unavailable")] if workflow_unavailable else [],
|
|
5978
|
-
"verification": {"app_exists": True, "workflow_read_unavailable": workflow_unavailable},
|
|
5979
|
-
"verified": not workflow_unavailable,
|
|
5980
|
-
**response.model_dump(mode="json"),
|
|
5981
|
-
}
|
|
5982
6044
|
|
|
5983
6045
|
def app_read_charts_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
5984
6046
|
try:
|
|
@@ -7431,159 +7493,6 @@ class AiBuilderFacade:
|
|
|
7431
7493
|
"verification": {"field_count": len(current_names)},
|
|
7432
7494
|
}
|
|
7433
7495
|
|
|
7434
|
-
def app_flow_plan(self, *, profile: str, request: FlowPlanRequest) -> JSONObject:
|
|
7435
|
-
nodes = [node.model_dump(mode="json") for node in request.nodes]
|
|
7436
|
-
transitions = [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions]
|
|
7437
|
-
if request.preset is not None:
|
|
7438
|
-
preset_nodes, preset_transitions = _build_flow_preset(request.preset)
|
|
7439
|
-
nodes, transitions = _merge_flow_graph(
|
|
7440
|
-
base_nodes=preset_nodes,
|
|
7441
|
-
base_transitions=preset_transitions,
|
|
7442
|
-
override_nodes=nodes,
|
|
7443
|
-
override_transitions=transitions,
|
|
7444
|
-
)
|
|
7445
|
-
fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
7446
|
-
if fields_result.get("status") == "failed":
|
|
7447
|
-
return fields_result
|
|
7448
|
-
current_fields = fields_result.get("fields", [])
|
|
7449
|
-
normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
|
|
7450
|
-
public_nodes = self._canonicalize_flow_nodes_for_public_output(normalized_nodes)
|
|
7451
|
-
unsupported_nodes = self._unsupported_public_flow_nodes(nodes=public_nodes)
|
|
7452
|
-
if unsupported_nodes:
|
|
7453
|
-
return _failed(
|
|
7454
|
-
"FLOW_NODE_TYPE_UNSUPPORTED",
|
|
7455
|
-
"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.",
|
|
7456
|
-
normalized_args={
|
|
7457
|
-
"app_key": request.app_key,
|
|
7458
|
-
"mode": str(request.mode or "replace"),
|
|
7459
|
-
"preset": request.preset.value if request.preset else None,
|
|
7460
|
-
"nodes": public_nodes,
|
|
7461
|
-
"transitions": transitions,
|
|
7462
|
-
},
|
|
7463
|
-
details={
|
|
7464
|
-
"unsupported_nodes": unsupported_nodes,
|
|
7465
|
-
"supported_node_types": sorted(STABLE_PUBLIC_FLOW_NODE_TYPES),
|
|
7466
|
-
"disabled_node_types": sorted(DISABLED_PUBLIC_FLOW_NODE_TYPES),
|
|
7467
|
-
},
|
|
7468
|
-
suggested_next_call={"tool_name": "builder_tool_contract", "arguments": {"tool_name": "app_flow_apply"}},
|
|
7469
|
-
)
|
|
7470
|
-
if resolution_issues:
|
|
7471
|
-
first_issue = resolution_issues[0]
|
|
7472
|
-
suggested_call = None
|
|
7473
|
-
if first_issue.get("kind", "").startswith("role"):
|
|
7474
|
-
suggested_call = {"tool_name": "role_search", "arguments": {"profile": profile, "keyword": first_issue.get("value") or ""}}
|
|
7475
|
-
elif first_issue.get("kind", "").startswith("member"):
|
|
7476
|
-
suggested_call = {"tool_name": "member_search", "arguments": {"profile": profile, "query": first_issue.get("value") or ""}}
|
|
7477
|
-
elif first_issue.get("kind") in {"editable_fields", "condition_fields"}:
|
|
7478
|
-
suggested_call = {"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": request.app_key}}
|
|
7479
|
-
return _failed(
|
|
7480
|
-
first_issue.get("error_code") or "FLOW_ASSIGNEE_UNRESOLVED",
|
|
7481
|
-
"workflow contains unresolved assignees or field permissions",
|
|
7482
|
-
normalized_args={
|
|
7483
|
-
"app_key": request.app_key,
|
|
7484
|
-
"mode": str(request.mode or "replace"),
|
|
7485
|
-
"preset": request.preset.value if request.preset else None,
|
|
7486
|
-
"nodes": public_nodes,
|
|
7487
|
-
"transitions": transitions,
|
|
7488
|
-
},
|
|
7489
|
-
details={"issues": resolution_issues},
|
|
7490
|
-
suggested_next_call=suggested_call,
|
|
7491
|
-
)
|
|
7492
|
-
status_field_present = _infer_status_field_id(current_fields) is not None
|
|
7493
|
-
node_types = {str(node.get("type") or "") for node in normalized_nodes}
|
|
7494
|
-
assignee_required_nodes = [
|
|
7495
|
-
node.get("id")
|
|
7496
|
-
for node in normalized_nodes
|
|
7497
|
-
if str(node.get("type") or "") in {"approve", "fill", "copy"}
|
|
7498
|
-
and not (
|
|
7499
|
-
(node.get("assignees") or {}).get("role_entries")
|
|
7500
|
-
or (node.get("assignees") or {}).get("member_uids")
|
|
7501
|
-
)
|
|
7502
|
-
]
|
|
7503
|
-
if assignee_required_nodes:
|
|
7504
|
-
return _failed(
|
|
7505
|
-
"FLOW_ASSIGNEE_REQUIRED",
|
|
7506
|
-
"workflow approval/fill/copy nodes must declare at least one role or member assignee",
|
|
7507
|
-
normalized_args={
|
|
7508
|
-
"app_key": request.app_key,
|
|
7509
|
-
"mode": str(request.mode or "replace"),
|
|
7510
|
-
"preset": request.preset.value if request.preset else None,
|
|
7511
|
-
"nodes": public_nodes,
|
|
7512
|
-
"transitions": transitions,
|
|
7513
|
-
},
|
|
7514
|
-
details={"node_ids": assignee_required_nodes, "policy": "prefer role assignees; explicit members are also supported"},
|
|
7515
|
-
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, "keyword": ""}},
|
|
7516
|
-
)
|
|
7517
|
-
workflow = _build_public_workflow_spec(nodes=normalized_nodes, transitions=transitions)
|
|
7518
|
-
if workflow.get("status") == "failed":
|
|
7519
|
-
workflow["normalized_args"] = {
|
|
7520
|
-
"app_key": request.app_key,
|
|
7521
|
-
"mode": str(request.mode or "replace"),
|
|
7522
|
-
"nodes": public_nodes,
|
|
7523
|
-
"transitions": transitions,
|
|
7524
|
-
}
|
|
7525
|
-
workflow["suggested_next_call"] = {
|
|
7526
|
-
"tool_name": "app_flow_plan",
|
|
7527
|
-
"arguments": {
|
|
7528
|
-
"profile": profile,
|
|
7529
|
-
"app_key": request.app_key,
|
|
7530
|
-
"mode": "replace",
|
|
7531
|
-
"nodes": public_nodes,
|
|
7532
|
-
"transitions": transitions,
|
|
7533
|
-
},
|
|
7534
|
-
}
|
|
7535
|
-
return workflow
|
|
7536
|
-
if ("approve" in node_types or request.preset in {FlowPreset.basic_approval, FlowPreset.basic_fill_then_approve}) and not status_field_present:
|
|
7537
|
-
return _failed(
|
|
7538
|
-
"FLOW_DEPENDENCY_MISSING",
|
|
7539
|
-
"workflow requires an explicit status field",
|
|
7540
|
-
normalized_args={
|
|
7541
|
-
"app_key": request.app_key,
|
|
7542
|
-
"mode": str(request.mode or "replace"),
|
|
7543
|
-
"nodes": public_nodes,
|
|
7544
|
-
"transitions": transitions,
|
|
7545
|
-
},
|
|
7546
|
-
details={
|
|
7547
|
-
"missing_dependencies": ["status field"],
|
|
7548
|
-
"fix_hint": "Add an explicit business status select field before applying approval workflows. Do not create platform system fields such as 当前流程状态.",
|
|
7549
|
-
"recommended_field_names": ["状态", "处理状态", "审批状态", "工单状态", "流程阶段"],
|
|
7550
|
-
"forbidden_system_field_names": ["当前流程状态", "当前处理人", "当前处理节点", "流程标题"],
|
|
7551
|
-
},
|
|
7552
|
-
missing_fields=["status"],
|
|
7553
|
-
suggested_next_call={
|
|
7554
|
-
"tool_name": "app_schema_apply",
|
|
7555
|
-
"arguments": {
|
|
7556
|
-
"profile": profile,
|
|
7557
|
-
"app_key": request.app_key,
|
|
7558
|
-
"create_if_missing": False,
|
|
7559
|
-
"add_fields": [{"name": "状态", "type": "select", "options": ["草稿", "进行中", "已完成"], "required": True}],
|
|
7560
|
-
"update_fields": [],
|
|
7561
|
-
"remove_fields": [],
|
|
7562
|
-
},
|
|
7563
|
-
},
|
|
7564
|
-
)
|
|
7565
|
-
normalized_args = {
|
|
7566
|
-
"app_key": request.app_key,
|
|
7567
|
-
"mode": str(request.mode or "replace"),
|
|
7568
|
-
"nodes": public_nodes,
|
|
7569
|
-
"transitions": transitions,
|
|
7570
|
-
}
|
|
7571
|
-
return {
|
|
7572
|
-
"status": "success",
|
|
7573
|
-
"error_code": None,
|
|
7574
|
-
"recoverable": False,
|
|
7575
|
-
"message": "planned workflow patch",
|
|
7576
|
-
"normalized_args": normalized_args,
|
|
7577
|
-
"missing_fields": [],
|
|
7578
|
-
"allowed_values": {"presets": [preset.value for preset in FlowPreset]},
|
|
7579
|
-
"details": {},
|
|
7580
|
-
"request_id": None,
|
|
7581
|
-
"flow_diff_preview": {"mode": "replace", "node_count": len([node for node in nodes if node.get("type") != "end"])},
|
|
7582
|
-
"dependency_issues": [],
|
|
7583
|
-
"suggested_next_call": {"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7584
|
-
"noop": False,
|
|
7585
|
-
"verification": {"status_field_present": status_field_present},
|
|
7586
|
-
}
|
|
7587
7496
|
|
|
7588
7497
|
def app_views_plan(self, *, profile: str, request: ViewsPlanRequest) -> JSONObject:
|
|
7589
7498
|
fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
@@ -7876,8 +7785,9 @@ class AiBuilderFacade:
|
|
|
7876
7785
|
"form_settings": _form_settings_from_schema(schema_result, parsed["fields"]),
|
|
7877
7786
|
"layout": parsed["layout"],
|
|
7878
7787
|
"flow_summary": {
|
|
7879
|
-
"enabled": bool(state["
|
|
7880
|
-
"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",
|
|
7881
7791
|
},
|
|
7882
7792
|
"views_summary": {
|
|
7883
7793
|
"views": _summarize_views(state["views"]),
|
|
@@ -7888,7 +7798,7 @@ class AiBuilderFacade:
|
|
|
7888
7798
|
"base": base_result,
|
|
7889
7799
|
"schema": schema_result,
|
|
7890
7800
|
"views": state["views"],
|
|
7891
|
-
"
|
|
7801
|
+
"workflow_spec": state["workflow_spec"],
|
|
7892
7802
|
}
|
|
7893
7803
|
return response
|
|
7894
7804
|
|
|
@@ -9089,228 +8999,19 @@ class AiBuilderFacade:
|
|
|
9089
8999
|
*,
|
|
9090
9000
|
profile: str,
|
|
9091
9001
|
app_key: str,
|
|
9092
|
-
|
|
9093
|
-
transitions: list[dict[str, Any]],
|
|
9094
|
-
mode: str = "replace",
|
|
9002
|
+
spec: dict[str, Any],
|
|
9095
9003
|
publish: bool = True,
|
|
9004
|
+
idempotency_key: str | None = None,
|
|
9005
|
+
schema_version: str | None = None,
|
|
9096
9006
|
) -> JSONObject:
|
|
9097
|
-
|
|
9098
|
-
"app_key": app_key,
|
|
9099
|
-
"mode": mode,
|
|
9100
|
-
"nodes": nodes,
|
|
9101
|
-
"transitions": transitions,
|
|
9102
|
-
"publish": publish,
|
|
9103
|
-
}
|
|
9104
|
-
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
9105
|
-
permission_outcome = self._guard_app_permission(
|
|
9007
|
+
return self.flow_apply(
|
|
9106
9008
|
profile=profile,
|
|
9107
9009
|
app_key=app_key,
|
|
9108
|
-
|
|
9109
|
-
|
|
9010
|
+
spec=spec,
|
|
9011
|
+
publish=publish,
|
|
9012
|
+
idempotency_key=idempotency_key,
|
|
9013
|
+
schema_version=schema_version,
|
|
9110
9014
|
)
|
|
9111
|
-
if permission_outcome.block is not None:
|
|
9112
|
-
return permission_outcome.block
|
|
9113
|
-
permission_outcomes.append(permission_outcome)
|
|
9114
|
-
|
|
9115
|
-
def finalize(response: JSONObject) -> JSONObject:
|
|
9116
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
9117
|
-
|
|
9118
|
-
if mode != "replace":
|
|
9119
|
-
return finalize(_failed(
|
|
9120
|
-
"UNSUPPORTED_FLOW_MODE",
|
|
9121
|
-
"only mode='replace' is supported",
|
|
9122
|
-
normalized_args=normalized_args,
|
|
9123
|
-
allowed_values={"modes": ["replace"]},
|
|
9124
|
-
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9125
|
-
))
|
|
9126
|
-
try:
|
|
9127
|
-
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
9128
|
-
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
9129
|
-
except (QingflowApiError, RuntimeError) as error:
|
|
9130
|
-
api_error = _coerce_api_error(error)
|
|
9131
|
-
return finalize(_failed_from_api_error(
|
|
9132
|
-
"FLOW_READ_FAILED",
|
|
9133
|
-
api_error,
|
|
9134
|
-
normalized_args=normalized_args,
|
|
9135
|
-
details=_with_state_read_blocked_details({"app_key": app_key}, resource="workflow", error=api_error),
|
|
9136
|
-
suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9137
|
-
))
|
|
9138
|
-
app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_key).strip() or app_key
|
|
9139
|
-
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
9140
|
-
current_fields = _parse_schema(schema)["fields"]
|
|
9141
|
-
normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
|
|
9142
|
-
public_nodes = self._canonicalize_flow_nodes_for_public_output(normalized_nodes)
|
|
9143
|
-
unsupported_nodes = self._unsupported_public_flow_nodes(nodes=public_nodes)
|
|
9144
|
-
normalized_args["nodes"] = public_nodes
|
|
9145
|
-
if unsupported_nodes:
|
|
9146
|
-
return _failed(
|
|
9147
|
-
"FLOW_NODE_TYPE_UNSUPPORTED",
|
|
9148
|
-
"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.",
|
|
9149
|
-
normalized_args=normalized_args,
|
|
9150
|
-
details={
|
|
9151
|
-
"unsupported_nodes": unsupported_nodes,
|
|
9152
|
-
"supported_node_types": sorted(STABLE_PUBLIC_FLOW_NODE_TYPES),
|
|
9153
|
-
"disabled_node_types": sorted(DISABLED_PUBLIC_FLOW_NODE_TYPES),
|
|
9154
|
-
},
|
|
9155
|
-
suggested_next_call={"tool_name": "builder_tool_contract", "arguments": {"tool_name": "app_flow_apply"}},
|
|
9156
|
-
)
|
|
9157
|
-
if resolution_issues:
|
|
9158
|
-
first_issue = resolution_issues[0]
|
|
9159
|
-
suggested_call = None
|
|
9160
|
-
if first_issue.get("kind", "").startswith("role"):
|
|
9161
|
-
suggested_call = {"tool_name": "role_search", "arguments": {"profile": profile, "keyword": first_issue.get("value") or ""}}
|
|
9162
|
-
elif first_issue.get("kind", "").startswith("member"):
|
|
9163
|
-
suggested_call = {"tool_name": "member_search", "arguments": {"profile": profile, "query": first_issue.get("value") or ""}}
|
|
9164
|
-
elif first_issue.get("kind") in {"editable_fields", "condition_fields"}:
|
|
9165
|
-
suggested_call = {"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}}
|
|
9166
|
-
return _failed(
|
|
9167
|
-
first_issue.get("error_code") or "FLOW_ASSIGNEE_UNRESOLVED",
|
|
9168
|
-
"workflow contains unresolved assignees or field permissions",
|
|
9169
|
-
normalized_args=normalized_args,
|
|
9170
|
-
details={"issues": resolution_issues},
|
|
9171
|
-
suggested_next_call=suggested_call,
|
|
9172
|
-
)
|
|
9173
|
-
assignee_required_nodes = [
|
|
9174
|
-
node.get("id")
|
|
9175
|
-
for node in normalized_nodes
|
|
9176
|
-
if str(node.get("type") or "") in {"approve", "fill", "copy"}
|
|
9177
|
-
and not (
|
|
9178
|
-
(node.get("assignees") or {}).get("role_entries")
|
|
9179
|
-
or (node.get("assignees") or {}).get("member_uids")
|
|
9180
|
-
)
|
|
9181
|
-
]
|
|
9182
|
-
if assignee_required_nodes:
|
|
9183
|
-
return _failed(
|
|
9184
|
-
"FLOW_ASSIGNEE_REQUIRED",
|
|
9185
|
-
"workflow approval/fill/copy nodes must declare at least one role or member assignee",
|
|
9186
|
-
normalized_args=normalized_args,
|
|
9187
|
-
details={"node_ids": assignee_required_nodes, "policy": "prefer role assignees; explicit members are also supported"},
|
|
9188
|
-
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, "keyword": ""}},
|
|
9189
|
-
)
|
|
9190
|
-
workflow_spec = _build_public_workflow_spec(nodes=normalized_nodes, transitions=transitions)
|
|
9191
|
-
if workflow_spec.get("status") == "failed":
|
|
9192
|
-
workflow_spec["normalized_args"] = normalized_args
|
|
9193
|
-
workflow_spec.setdefault("request_id", None)
|
|
9194
|
-
workflow_spec["suggested_next_call"] = {"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}}
|
|
9195
|
-
return workflow_spec
|
|
9196
|
-
desired_node_count = len([node for node in normalized_nodes if node.get("type") != "end"])
|
|
9197
|
-
build_id = f"facade-flow-{uuid4().hex[:12]}"
|
|
9198
|
-
previous_build_home = os.getenv("QINGFLOW_MCP_BUILD_HOME")
|
|
9199
|
-
temporary_build_home: str | None = None
|
|
9200
|
-
if previous_build_home is None:
|
|
9201
|
-
temporary_build_home = tempfile.mkdtemp(prefix="qingflow-mcp-build-", dir="/tmp")
|
|
9202
|
-
os.environ["QINGFLOW_MCP_BUILD_HOME"] = temporary_build_home
|
|
9203
|
-
try:
|
|
9204
|
-
assembly = BuildAssemblyStore.open(build_id=build_id, create=True)
|
|
9205
|
-
manifest = default_manifest()
|
|
9206
|
-
manifest["solution_name"] = base.get("formTitle") or app_key
|
|
9207
|
-
manifest["preferences"]["create_package"] = False
|
|
9208
|
-
manifest["preferences"]["create_portal"] = False
|
|
9209
|
-
manifest["preferences"]["create_navigation"] = False
|
|
9210
|
-
manifest["entities"] = [entity]
|
|
9211
|
-
assembly.set_manifest(manifest)
|
|
9212
|
-
artifacts = default_artifacts()
|
|
9213
|
-
artifacts["apps"][entity["entity_id"]] = {"app_key": app_key}
|
|
9214
|
-
assembly.set_artifacts(artifacts)
|
|
9215
|
-
flow_stage_spec = {
|
|
9216
|
-
"solution_name": manifest["solution_name"],
|
|
9217
|
-
"entities": [{"entity_id": entity["entity_id"], "workflow": workflow_spec["workflow"]}],
|
|
9218
|
-
}
|
|
9219
|
-
assembly.set_stage_spec("app_flow", flow_stage_spec)
|
|
9220
|
-
stage = self.solutions.solution_build_flow(
|
|
9221
|
-
profile=profile,
|
|
9222
|
-
mode="apply",
|
|
9223
|
-
build_id=build_id,
|
|
9224
|
-
flow_spec=flow_stage_spec,
|
|
9225
|
-
publish=False,
|
|
9226
|
-
run_label=None,
|
|
9227
|
-
repair_patch={},
|
|
9228
|
-
)
|
|
9229
|
-
finally:
|
|
9230
|
-
if previous_build_home is None:
|
|
9231
|
-
os.environ.pop("QINGFLOW_MCP_BUILD_HOME", None)
|
|
9232
|
-
if stage.get("status") != "success":
|
|
9233
|
-
failed = _normalize_flow_stage_failure(stage, profile=profile, app_key=app_key, entity=entity)
|
|
9234
|
-
failed["normalized_args"] = normalized_args
|
|
9235
|
-
suggested_next_call = failed.get("suggested_next_call")
|
|
9236
|
-
if not isinstance(suggested_next_call, dict):
|
|
9237
|
-
suggested_next_call = {"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}}
|
|
9238
|
-
elif suggested_next_call.get("tool_name") == "app_flow_plan":
|
|
9239
|
-
arguments = suggested_next_call.get("arguments")
|
|
9240
|
-
if not isinstance(arguments, dict):
|
|
9241
|
-
arguments = {}
|
|
9242
|
-
arguments.setdefault("profile", profile)
|
|
9243
|
-
arguments.setdefault("app_key", app_key)
|
|
9244
|
-
arguments.setdefault("mode", mode)
|
|
9245
|
-
arguments.setdefault("nodes", public_nodes)
|
|
9246
|
-
arguments.setdefault("transitions", transitions)
|
|
9247
|
-
suggested_next_call["tool_name"] = "app_flow_apply"
|
|
9248
|
-
suggested_next_call["arguments"] = arguments
|
|
9249
|
-
failed["suggested_next_call"] = suggested_next_call
|
|
9250
|
-
return finalize(failed)
|
|
9251
|
-
verified_nodes, verified_nodes_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
9252
|
-
workflow_structure_verified = bool(verified_nodes) and _workflow_nodes_semantically_equal(
|
|
9253
|
-
current_workflow=verified_nodes,
|
|
9254
|
-
requested_nodes=normalized_nodes,
|
|
9255
|
-
)
|
|
9256
|
-
branch_structure_verified = bool(verified_nodes) and _workflow_branch_structure_verified(
|
|
9257
|
-
current_workflow=verified_nodes,
|
|
9258
|
-
requested_nodes=normalized_nodes,
|
|
9259
|
-
)
|
|
9260
|
-
workflow_verified = workflow_structure_verified and branch_structure_verified
|
|
9261
|
-
warnings: list[dict[str, Any]] = []
|
|
9262
|
-
if verified_nodes_unavailable:
|
|
9263
|
-
status = "partial_success"
|
|
9264
|
-
error_code = "FLOW_READBACK_PENDING"
|
|
9265
|
-
recoverable = True
|
|
9266
|
-
message = "applied workflow patch; flow readback pending"
|
|
9267
|
-
suggested_next_call = {"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}}
|
|
9268
|
-
elif workflow_verified:
|
|
9269
|
-
status = "success"
|
|
9270
|
-
error_code = None
|
|
9271
|
-
recoverable = False
|
|
9272
|
-
message = "applied workflow patch"
|
|
9273
|
-
suggested_next_call = None
|
|
9274
|
-
else:
|
|
9275
|
-
status = "partial_success"
|
|
9276
|
-
error_code = "FLOW_BRANCH_VERIFICATION_FAILED" if workflow_structure_verified and not branch_structure_verified else "FLOW_VERIFICATION_FAILED"
|
|
9277
|
-
recoverable = True
|
|
9278
|
-
message = (
|
|
9279
|
-
"applied workflow patch; branch or condition structure did not fully verify"
|
|
9280
|
-
if workflow_structure_verified and not branch_structure_verified
|
|
9281
|
-
else "applied workflow patch; flow readback did not confirm the requested workflow"
|
|
9282
|
-
)
|
|
9283
|
-
suggested_next_call = {"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}}
|
|
9284
|
-
if not branch_structure_verified:
|
|
9285
|
-
warnings.append(_warning("WORKFLOW_BRANCH_STRUCTURE_UNVERIFIED", "branch or condition structure was written, but MCP could not fully verify downstream lane structure"))
|
|
9286
|
-
response = {
|
|
9287
|
-
"status": status,
|
|
9288
|
-
"error_code": error_code,
|
|
9289
|
-
"recoverable": recoverable,
|
|
9290
|
-
"message": message,
|
|
9291
|
-
"normalized_args": normalized_args,
|
|
9292
|
-
"missing_fields": [],
|
|
9293
|
-
"allowed_values": {"modes": ["replace"]},
|
|
9294
|
-
"details": {},
|
|
9295
|
-
"request_id": None,
|
|
9296
|
-
"suggested_next_call": suggested_next_call,
|
|
9297
|
-
"noop": False,
|
|
9298
|
-
"warnings": warnings,
|
|
9299
|
-
"verification": {
|
|
9300
|
-
"workflow_verified": workflow_verified,
|
|
9301
|
-
"workflow_structure_verified": workflow_structure_verified,
|
|
9302
|
-
"branch_structure_verified": branch_structure_verified,
|
|
9303
|
-
"workflow_read_unavailable": verified_nodes_unavailable,
|
|
9304
|
-
},
|
|
9305
|
-
"app_key": app_key,
|
|
9306
|
-
"app_name": app_name,
|
|
9307
|
-
"flow_diff": {"mode": "replace", "node_count": desired_node_count},
|
|
9308
|
-
"verified": workflow_verified,
|
|
9309
|
-
"write_executed": True,
|
|
9310
|
-
"write_succeeded": True,
|
|
9311
|
-
"safe_to_retry": False,
|
|
9312
|
-
}
|
|
9313
|
-
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
9314
9015
|
|
|
9315
9016
|
def app_views_apply(
|
|
9316
9017
|
self,
|
|
@@ -12492,7 +12193,7 @@ class AiBuilderFacade:
|
|
|
12492
12193
|
result = legacy_normalized or normalized_views
|
|
12493
12194
|
return (result, False, None) if include_error else (result, False)
|
|
12494
12195
|
|
|
12495
|
-
def
|
|
12196
|
+
def _load_workflow_spec_snapshot(
|
|
12496
12197
|
self,
|
|
12497
12198
|
*,
|
|
12498
12199
|
profile: str,
|
|
@@ -12502,24 +12203,34 @@ class AiBuilderFacade:
|
|
|
12502
12203
|
include_error: bool = False,
|
|
12503
12204
|
) -> tuple[Any, bool] | tuple[Any, bool, QingflowApiError | None]:
|
|
12504
12205
|
try:
|
|
12505
|
-
|
|
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
|
+
)
|
|
12506
12214
|
except (QingflowApiError, RuntimeError) as error:
|
|
12507
12215
|
api_error = _coerce_api_error(error)
|
|
12508
12216
|
if tolerate_404 and (
|
|
12509
12217
|
api_error.http_status == 404
|
|
12510
12218
|
or (tolerate_permission_restricted and _is_permission_restricted_api_error(api_error))
|
|
12511
12219
|
):
|
|
12512
|
-
return
|
|
12220
|
+
return {}, True
|
|
12513
12221
|
raise
|
|
12514
|
-
|
|
12515
|
-
return (result, False, None) if include_error else (result, False)
|
|
12222
|
+
return payload if isinstance(payload, dict) else {}, False
|
|
12516
12223
|
|
|
12517
12224
|
def _load_app_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
|
|
12518
12225
|
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
12519
12226
|
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
12520
|
-
|
|
12227
|
+
workflow_spec, workflow_unavailable = self._load_workflow_spec_snapshot(
|
|
12228
|
+
profile=profile,
|
|
12229
|
+
app_key=app_key,
|
|
12230
|
+
tolerate_404=True,
|
|
12231
|
+
)
|
|
12521
12232
|
state["views"] = views
|
|
12522
|
-
state["
|
|
12233
|
+
state["workflow_spec"] = workflow_spec
|
|
12523
12234
|
state["views_unavailable"] = views_unavailable
|
|
12524
12235
|
state["workflow_unavailable"] = workflow_unavailable
|
|
12525
12236
|
return state
|
|
@@ -20501,30 +20212,6 @@ def _build_layout_preset_sections(*, preset: LayoutPreset, field_names: list[str
|
|
|
20501
20212
|
return sections
|
|
20502
20213
|
|
|
20503
20214
|
|
|
20504
|
-
def _build_flow_preset(preset: FlowPreset) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
20505
|
-
if preset == FlowPreset.basic_fill_then_approve:
|
|
20506
|
-
nodes = [
|
|
20507
|
-
{"id": "start", "type": "start", "name": "发起"},
|
|
20508
|
-
{"id": "fill_1", "type": "fill", "name": "补充信息"},
|
|
20509
|
-
{"id": "approve_1", "type": "approve", "name": "审批"},
|
|
20510
|
-
{"id": "end", "type": "end", "name": "结束"},
|
|
20511
|
-
]
|
|
20512
|
-
transitions = [
|
|
20513
|
-
{"from": "start", "to": "fill_1"},
|
|
20514
|
-
{"from": "fill_1", "to": "approve_1"},
|
|
20515
|
-
{"from": "approve_1", "to": "end"},
|
|
20516
|
-
]
|
|
20517
|
-
return nodes, transitions
|
|
20518
|
-
nodes = [
|
|
20519
|
-
{"id": "start", "type": "start", "name": "发起"},
|
|
20520
|
-
{"id": "approve_1", "type": "approve", "name": "审批"},
|
|
20521
|
-
{"id": "end", "type": "end", "name": "结束"},
|
|
20522
|
-
]
|
|
20523
|
-
transitions = [
|
|
20524
|
-
{"from": "start", "to": "approve_1"},
|
|
20525
|
-
{"from": "approve_1", "to": "end"},
|
|
20526
|
-
]
|
|
20527
|
-
return nodes, transitions
|
|
20528
20215
|
|
|
20529
20216
|
|
|
20530
20217
|
def _merge_flow_graph(
|
|
@@ -22379,144 +22066,35 @@ def _apply_row_widths(row: list[dict[str, Any]]) -> None:
|
|
|
22379
22066
|
question["queWidth"] = width + (1 if index < remainder else 0)
|
|
22380
22067
|
|
|
22381
22068
|
|
|
22382
|
-
def
|
|
22383
|
-
|
|
22384
|
-
|
|
22385
|
-
|
|
22386
|
-
|
|
22387
|
-
|
|
22388
|
-
|
|
22389
|
-
|
|
22390
|
-
|
|
22391
|
-
|
|
22392
|
-
|
|
22393
|
-
node_id = _coerce_positive_int(item.get("auditNodeId"))
|
|
22394
|
-
name = item.get("auditNodeName")
|
|
22395
|
-
if node_id is None or not name:
|
|
22396
|
-
continue
|
|
22397
|
-
nodes.append({"id": node_id, "name": name, "type": item.get("type"), "deal_type": item.get("dealType")})
|
|
22398
|
-
return nodes
|
|
22399
|
-
|
|
22400
|
-
|
|
22401
|
-
def _workflow_node_iterable(result: Any) -> list[dict[str, Any]]:
|
|
22402
|
-
if isinstance(result, dict):
|
|
22403
|
-
iterable = result.values()
|
|
22404
|
-
elif isinstance(result, list):
|
|
22405
|
-
iterable = result
|
|
22406
|
-
else:
|
|
22407
|
-
iterable = []
|
|
22408
|
-
return [item for item in iterable if isinstance(item, dict)]
|
|
22409
|
-
|
|
22410
|
-
|
|
22411
|
-
def _workflow_condition_matrix_value(node: dict[str, Any]) -> list[Any]:
|
|
22412
|
-
config = node.get("config")
|
|
22413
|
-
if isinstance(config, dict):
|
|
22414
|
-
matrix = config.get("autoJudges")
|
|
22415
|
-
if isinstance(matrix, list):
|
|
22416
|
-
return deepcopy(matrix)
|
|
22417
|
-
matrix = config.get("conditionFormatMatrix")
|
|
22418
|
-
if isinstance(matrix, list):
|
|
22419
|
-
return deepcopy(matrix)
|
|
22420
|
-
matrix = node.get("autoJudges")
|
|
22421
|
-
if isinstance(matrix, list):
|
|
22422
|
-
return deepcopy(matrix)
|
|
22423
|
-
matrix = node.get("conditionFormatMatrix")
|
|
22424
|
-
if isinstance(matrix, list):
|
|
22425
|
-
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)]
|
|
22426
22080
|
return []
|
|
22427
22081
|
|
|
22428
22082
|
|
|
22429
|
-
def
|
|
22430
|
-
|
|
22431
|
-
|
|
22432
|
-
|
|
22433
|
-
|
|
22434
|
-
|
|
22435
|
-
if not requested_branch_like:
|
|
22436
|
-
return True
|
|
22437
|
-
actual_nodes = _workflow_node_iterable(current_workflow)
|
|
22438
|
-
actual_branch_like = [
|
|
22439
|
-
node
|
|
22440
|
-
for node in actual_nodes
|
|
22441
|
-
if _normalize_existing_workflow_node_type(node.get("type"), node.get("dealType")) in {"branch", "condition"}
|
|
22442
|
-
]
|
|
22443
|
-
if len(actual_branch_like) < len(requested_branch_like):
|
|
22444
|
-
return False
|
|
22445
|
-
actual_condition_nodes = [
|
|
22446
|
-
node
|
|
22447
|
-
for node in actual_nodes
|
|
22448
|
-
if _normalize_existing_workflow_node_type(node.get("type"), node.get("dealType")) == "condition"
|
|
22449
|
-
]
|
|
22450
|
-
for requested in requested_branch_like:
|
|
22451
|
-
if str(requested.get("type") or "").strip().lower() != "condition":
|
|
22452
|
-
continue
|
|
22453
|
-
expected_matrix = _workflow_condition_matrix_value(requested)
|
|
22454
|
-
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:
|
|
22455
22089
|
continue
|
|
22456
|
-
|
|
22457
|
-
|
|
22458
|
-
|
|
22459
|
-
|
|
22460
|
-
|
|
22461
|
-
|
|
22462
|
-
),
|
|
22463
|
-
None,
|
|
22464
|
-
)
|
|
22465
|
-
if match is None or not _workflow_condition_matrix_value(match):
|
|
22466
|
-
return False
|
|
22467
|
-
return True
|
|
22468
|
-
|
|
22469
|
-
|
|
22470
|
-
def _workflow_nodes_semantically_equal(*, current_workflow: Any, requested_nodes: list[dict[str, Any]]) -> bool:
|
|
22471
|
-
current_nodes = _summarize_workflow_nodes(current_workflow)
|
|
22472
|
-
current_effective = [
|
|
22473
|
-
node
|
|
22474
|
-
for node in current_nodes
|
|
22475
|
-
if _normalize_existing_workflow_node_type(node.get("type"), node.get("deal_type")) not in {"start", "end"}
|
|
22476
|
-
]
|
|
22477
|
-
requested_effective = [
|
|
22478
|
-
node for node in requested_nodes if str(node.get("type") or "") not in {"start", "end"}
|
|
22479
|
-
]
|
|
22480
|
-
if not current_effective or not requested_effective or len(current_effective) != len(requested_effective):
|
|
22481
|
-
return False
|
|
22482
|
-
current_signatures = sorted(
|
|
22483
|
-
(
|
|
22484
|
-
_normalize_existing_workflow_node_type(node.get("type"), node.get("deal_type")),
|
|
22485
|
-
str(node.get("name") or "").strip(),
|
|
22486
|
-
)
|
|
22487
|
-
for node in current_effective
|
|
22488
|
-
)
|
|
22489
|
-
requested_signatures = sorted(
|
|
22490
|
-
(
|
|
22491
|
-
str(node.get("type") or "").strip().lower(),
|
|
22492
|
-
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
|
+
}
|
|
22493
22096
|
)
|
|
22494
|
-
|
|
22495
|
-
)
|
|
22496
|
-
return current_signatures == requested_signatures
|
|
22497
|
-
|
|
22498
|
-
|
|
22499
|
-
def _normalize_existing_workflow_node_type(raw_type: Any, deal_type: Any) -> str:
|
|
22500
|
-
normalized = "" if raw_type is None else str(raw_type).strip().lower()
|
|
22501
|
-
if normalized in {"audit", "approve"}:
|
|
22502
|
-
return "approve"
|
|
22503
|
-
if normalized in {"fill", "copy", "branch", "condition", "webhook", "start", "end"}:
|
|
22504
|
-
return normalized
|
|
22505
|
-
if normalized in {"1"}:
|
|
22506
|
-
return "branch"
|
|
22507
|
-
if normalized in {"2"}:
|
|
22508
|
-
return "condition"
|
|
22509
|
-
if normalized in {"3"} or deal_type == 10:
|
|
22510
|
-
return "webhook"
|
|
22511
|
-
if normalized in {"0"} and deal_type == 0:
|
|
22512
|
-
return "approve"
|
|
22513
|
-
if normalized in {"0"} and deal_type == 1:
|
|
22514
|
-
return "fill"
|
|
22515
|
-
if normalized in {"0"} and deal_type == 2:
|
|
22516
|
-
return "copy"
|
|
22517
|
-
if normalized in {"0"} or deal_type == 3:
|
|
22518
|
-
return "start"
|
|
22519
|
-
return normalized or "unknown"
|
|
22097
|
+
return summaries
|
|
22520
22098
|
|
|
22521
22099
|
|
|
22522
22100
|
def _summarize_views(result: Any) -> list[dict[str, Any]]:
|
|
@@ -23714,98 +23292,6 @@ def _field_id_for_name(fields: list[dict[str, Any]], name: str) -> str:
|
|
|
23714
23292
|
raise KeyError(name)
|
|
23715
23293
|
|
|
23716
23294
|
|
|
23717
|
-
def _build_public_workflow_spec(*, nodes: list[dict[str, Any]], transitions: list[dict[str, Any]]) -> JSONObject:
|
|
23718
|
-
node_map = {str(node.get("id") or ""): node for node in nodes if isinstance(node, dict) and node.get("id")}
|
|
23719
|
-
if not node_map:
|
|
23720
|
-
return _failed("INVALID_FLOW", "nodes must be a non-empty list", suggested_next_call=None)
|
|
23721
|
-
start_nodes = [node_id for node_id, node in node_map.items() if node.get("type") == "start"]
|
|
23722
|
-
if len(start_nodes) != 1:
|
|
23723
|
-
return _failed("INVALID_FLOW", "flow must contain exactly one start node", suggested_next_call=None)
|
|
23724
|
-
inbound: dict[str, list[str]] = {node_id: [] for node_id in node_map}
|
|
23725
|
-
outbound: dict[str, list[str]] = {node_id: [] for node_id in node_map}
|
|
23726
|
-
for transition in transitions:
|
|
23727
|
-
if not isinstance(transition, dict):
|
|
23728
|
-
continue
|
|
23729
|
-
source = str(transition.get("from") or "")
|
|
23730
|
-
target = str(transition.get("to") or "")
|
|
23731
|
-
if source not in node_map or target not in node_map:
|
|
23732
|
-
return _failed("INVALID_FLOW_EDGE", "transition references unknown node", details={"transition": transition}, suggested_next_call=None)
|
|
23733
|
-
outbound[source].append(target)
|
|
23734
|
-
inbound[target].append(source)
|
|
23735
|
-
branch_lane_nodes: dict[str, dict[str, Any]] = {}
|
|
23736
|
-
for node_id, node in node_map.items():
|
|
23737
|
-
if str(node.get("type") or "") != "condition":
|
|
23738
|
-
continue
|
|
23739
|
-
if len(inbound[node_id]) != 1:
|
|
23740
|
-
return _failed(
|
|
23741
|
-
"INVALID_FLOW_EDGE",
|
|
23742
|
-
f"condition lane '{node_id}' must have exactly one inbound transition",
|
|
23743
|
-
details={"node_id": node_id, "inbound": inbound[node_id]},
|
|
23744
|
-
suggested_next_call=None,
|
|
23745
|
-
)
|
|
23746
|
-
parent_id = inbound[node_id][0]
|
|
23747
|
-
if node_map[parent_id].get("type") != "branch":
|
|
23748
|
-
continue
|
|
23749
|
-
branch_lane_nodes[node_id] = {
|
|
23750
|
-
"branch_parent_id": parent_id,
|
|
23751
|
-
"branch_index": outbound[parent_id].index(node_id) + 1,
|
|
23752
|
-
}
|
|
23753
|
-
internal_nodes = []
|
|
23754
|
-
for node_id, node in node_map.items():
|
|
23755
|
-
node_type = str(node.get("type") or "")
|
|
23756
|
-
if node_type == "end":
|
|
23757
|
-
continue
|
|
23758
|
-
if node_type != "start" and len(inbound[node_id]) != 1:
|
|
23759
|
-
return _failed(
|
|
23760
|
-
"INVALID_FLOW_EDGE",
|
|
23761
|
-
f"node '{node_id}' must have exactly one inbound transition",
|
|
23762
|
-
details={"node_id": node_id, "inbound": inbound[node_id]},
|
|
23763
|
-
suggested_next_call=None,
|
|
23764
|
-
)
|
|
23765
|
-
config_payload = deepcopy(node.get("config") or {}) if isinstance(node.get("config"), dict) else {}
|
|
23766
|
-
assignees = node.get("assignees") or {}
|
|
23767
|
-
if node_id in branch_lane_nodes:
|
|
23768
|
-
lane_meta = branch_lane_nodes[node_id]
|
|
23769
|
-
config_payload["__lane_only__"] = True
|
|
23770
|
-
payload = {
|
|
23771
|
-
"node_id": f"__branch_lane__{lane_meta['branch_parent_id']}__{lane_meta['branch_index']}",
|
|
23772
|
-
"name": node.get("name") or node_id,
|
|
23773
|
-
"node_type": "condition",
|
|
23774
|
-
"parent_node_id": lane_meta["branch_parent_id"],
|
|
23775
|
-
"branch_parent_id": lane_meta["branch_parent_id"],
|
|
23776
|
-
"branch_index": lane_meta["branch_index"],
|
|
23777
|
-
}
|
|
23778
|
-
else:
|
|
23779
|
-
payload = {
|
|
23780
|
-
"node_id": node_id,
|
|
23781
|
-
"name": node.get("name") or node_id,
|
|
23782
|
-
"node_type": "audit" if node_type == "approve" else node_type,
|
|
23783
|
-
}
|
|
23784
|
-
if node_type != "start":
|
|
23785
|
-
parent_id = inbound[node_id][0]
|
|
23786
|
-
lane_parent = branch_lane_nodes.get(parent_id)
|
|
23787
|
-
if lane_parent is not None:
|
|
23788
|
-
payload["parent_node_id"] = lane_parent["branch_parent_id"]
|
|
23789
|
-
payload["branch_parent_id"] = lane_parent["branch_parent_id"]
|
|
23790
|
-
payload["branch_index"] = lane_parent["branch_index"]
|
|
23791
|
-
else:
|
|
23792
|
-
payload["parent_node_id"] = parent_id
|
|
23793
|
-
if node_map[parent_id].get("type") == "branch":
|
|
23794
|
-
payload["branch_parent_id"] = parent_id
|
|
23795
|
-
payload["branch_index"] = outbound[parent_id].index(node_id) + 1
|
|
23796
|
-
permissions = node.get("permissions") or {}
|
|
23797
|
-
editable_que_ids = permissions.get("editable_que_ids") or []
|
|
23798
|
-
if editable_que_ids:
|
|
23799
|
-
config_payload["editableQueIds"] = editable_que_ids
|
|
23800
|
-
if config_payload:
|
|
23801
|
-
payload["config"] = config_payload
|
|
23802
|
-
if assignees:
|
|
23803
|
-
payload["assignees"] = deepcopy(assignees)
|
|
23804
|
-
internal_nodes.append(payload)
|
|
23805
|
-
return {
|
|
23806
|
-
"status": "success",
|
|
23807
|
-
"workflow": {"enabled": True, "nodes": internal_nodes},
|
|
23808
|
-
}
|
|
23809
23295
|
|
|
23810
23296
|
|
|
23811
23297
|
def _build_flow_condition_matrix(
|
|
@@ -25911,92 +25397,6 @@ def _infer_status_field_id(fields: list[dict[str, Any]]) -> str | None:
|
|
|
25911
25397
|
return None
|
|
25912
25398
|
|
|
25913
25399
|
|
|
25914
|
-
def _normalize_flow_stage_failure(stage: JSONObject, *, profile: str, app_key: str, entity: dict[str, Any]) -> JSONObject:
|
|
25915
|
-
stage_error_code = str(stage.get("error_code") or "FLOW_APPLY_FAILED")
|
|
25916
|
-
detail_text = _extract_stage_failure_text(stage)
|
|
25917
|
-
request_id = stage.get("request_id")
|
|
25918
|
-
backend_code = stage.get("backend_code")
|
|
25919
|
-
http_status = stage.get("http_status")
|
|
25920
|
-
public_stage_result = _public_stage_result(stage)
|
|
25921
|
-
if request_id is None and isinstance(public_stage_result.get("errors"), list) and public_stage_result["errors"]:
|
|
25922
|
-
first_error = public_stage_result["errors"][0]
|
|
25923
|
-
if isinstance(first_error, dict):
|
|
25924
|
-
request_id = first_error.get("request_id")
|
|
25925
|
-
backend_code = first_error.get("backend_code")
|
|
25926
|
-
http_status = first_error.get("http_status")
|
|
25927
|
-
lowered_detail = detail_text.lower()
|
|
25928
|
-
if "must declare status field" in detail_text:
|
|
25929
|
-
return _failed(
|
|
25930
|
-
"STATUS_FIELD_REQUIRED",
|
|
25931
|
-
detail_text,
|
|
25932
|
-
details={
|
|
25933
|
-
"app_key": app_key,
|
|
25934
|
-
"entity_id": entity.get("entity_id"),
|
|
25935
|
-
"existing_fields": entity.get("fields") or [],
|
|
25936
|
-
"stage_result": public_stage_result,
|
|
25937
|
-
},
|
|
25938
|
-
suggested_next_call={
|
|
25939
|
-
"tool_name": "app_schema_apply",
|
|
25940
|
-
"arguments": {
|
|
25941
|
-
"profile": profile,
|
|
25942
|
-
"app_key": app_key,
|
|
25943
|
-
"create_if_missing": False,
|
|
25944
|
-
"add_fields": [
|
|
25945
|
-
{
|
|
25946
|
-
"name": "状态",
|
|
25947
|
-
"type": "single_select",
|
|
25948
|
-
"options": ["草稿", "进行中", "已完成"],
|
|
25949
|
-
"required": True,
|
|
25950
|
-
}
|
|
25951
|
-
],
|
|
25952
|
-
"update_fields": [],
|
|
25953
|
-
"remove_fields": [],
|
|
25954
|
-
},
|
|
25955
|
-
},
|
|
25956
|
-
request_id=request_id,
|
|
25957
|
-
backend_code=backend_code,
|
|
25958
|
-
http_status=http_status,
|
|
25959
|
-
)
|
|
25960
|
-
if (
|
|
25961
|
-
"run solution_build_app first" in lowered_detail
|
|
25962
|
-
or "run solution_build_app_flow first" in lowered_detail
|
|
25963
|
-
or ("is not defined yet" in lowered_detail and "solution_build_" in lowered_detail)
|
|
25964
|
-
):
|
|
25965
|
-
return _failed(
|
|
25966
|
-
"FLOW_STAGE_CONTEXT_MISSING",
|
|
25967
|
-
"workflow apply lost the app context required by the internal flow builder",
|
|
25968
|
-
details={
|
|
25969
|
-
"app_key": app_key,
|
|
25970
|
-
"entity_id": entity.get("entity_id"),
|
|
25971
|
-
"existing_fields": entity.get("fields") or [],
|
|
25972
|
-
"internal_detail": detail_text,
|
|
25973
|
-
"stage_result": public_stage_result,
|
|
25974
|
-
},
|
|
25975
|
-
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, "app_key": app_key}},
|
|
25976
|
-
request_id=request_id,
|
|
25977
|
-
backend_code=backend_code,
|
|
25978
|
-
http_status=http_status,
|
|
25979
|
-
)
|
|
25980
|
-
message = detail_text or "failed to apply workflow patch"
|
|
25981
|
-
details = {"app_key": app_key, "entity_id": entity.get("entity_id"), "stage_result": public_stage_result}
|
|
25982
|
-
public_http_status = http_status
|
|
25983
|
-
if http_status == 404:
|
|
25984
|
-
message = "workflow write route is unavailable for this app in the current route"
|
|
25985
|
-
details["transport_error"] = {
|
|
25986
|
-
"http_status": http_status,
|
|
25987
|
-
"backend_code": backend_code,
|
|
25988
|
-
"category": "http",
|
|
25989
|
-
}
|
|
25990
|
-
public_http_status = None
|
|
25991
|
-
return _failed(
|
|
25992
|
-
stage_error_code,
|
|
25993
|
-
message,
|
|
25994
|
-
details=details,
|
|
25995
|
-
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, "app_key": app_key}},
|
|
25996
|
-
request_id=request_id,
|
|
25997
|
-
backend_code=backend_code,
|
|
25998
|
-
http_status=public_http_status,
|
|
25999
|
-
)
|
|
26000
25400
|
|
|
26001
25401
|
|
|
26002
25402
|
def _extract_stage_failure_text(stage: JSONObject) -> str:
|