@qingflow-tech/qingflow-app-builder-mcp 1.0.44 → 1.1.1
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 +33 -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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..backend_client import BackendClient, BackendRequestContext
|
|
6
|
+
from ..json_types import JSONObject, JSONValue
|
|
7
|
+
|
|
8
|
+
WORKFLOW_SPEC_SCHEMA_PATH = "/api/workflow/spec/schema"
|
|
9
|
+
WORKFLOW_SPEC_GET_PATH = "/api/workflow/spec"
|
|
10
|
+
WORKFLOW_SPEC_APPLY_PATH = "/api/workflow/spec:apply"
|
|
11
|
+
DEFAULT_SCHEMA_VERSION = "vnext-2026-06"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def fetch_schema(
|
|
15
|
+
backend: BackendClient,
|
|
16
|
+
context: BackendRequestContext,
|
|
17
|
+
*,
|
|
18
|
+
schema_version: str | None = None,
|
|
19
|
+
) -> JSONValue:
|
|
20
|
+
params: JSONObject = {}
|
|
21
|
+
if schema_version:
|
|
22
|
+
params["schemaVersion"] = schema_version
|
|
23
|
+
return backend.request(
|
|
24
|
+
"GET",
|
|
25
|
+
context,
|
|
26
|
+
WORKFLOW_SPEC_SCHEMA_PATH,
|
|
27
|
+
params=params or None,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def fetch_spec(
|
|
32
|
+
backend: BackendClient,
|
|
33
|
+
context: BackendRequestContext,
|
|
34
|
+
*,
|
|
35
|
+
app_key: str,
|
|
36
|
+
version_id: str | None = None,
|
|
37
|
+
) -> JSONValue:
|
|
38
|
+
params: JSONObject = {"appKey": app_key}
|
|
39
|
+
if version_id:
|
|
40
|
+
params["versionId"] = version_id
|
|
41
|
+
return backend.request("GET", context, WORKFLOW_SPEC_GET_PATH, params=params)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def post_apply(
|
|
45
|
+
backend: BackendClient,
|
|
46
|
+
context: BackendRequestContext,
|
|
47
|
+
*,
|
|
48
|
+
apply_body: JSONObject,
|
|
49
|
+
) -> JSONValue:
|
|
50
|
+
return backend.request("POST", context, WORKFLOW_SPEC_APPLY_PATH, json_body=apply_body)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def verify_apply_response(
|
|
54
|
+
*,
|
|
55
|
+
input_spec: JSONObject,
|
|
56
|
+
apply_result: JSONObject,
|
|
57
|
+
) -> tuple[JSONObject, list[dict[str, Any]], bool]:
|
|
58
|
+
warnings: list[dict[str, Any]] = []
|
|
59
|
+
applied_spec = apply_result.get("appliedSpec")
|
|
60
|
+
diff_summary = apply_result.get("diffSummary")
|
|
61
|
+
semantic_lint = apply_result.get("semanticLint") if isinstance(apply_result.get("semanticLint"), list) else []
|
|
62
|
+
|
|
63
|
+
verification: JSONObject = {
|
|
64
|
+
"applied_spec_available": isinstance(applied_spec, dict),
|
|
65
|
+
"version_snapshot_confirmed": False,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
lint_errors = [
|
|
69
|
+
item
|
|
70
|
+
for item in semantic_lint
|
|
71
|
+
if isinstance(item, dict) and str(item.get("severity") or item.get("level") or "").upper() == "ERROR"
|
|
72
|
+
]
|
|
73
|
+
if lint_errors:
|
|
74
|
+
verification["semantic_lint_error_count"] = len(lint_errors)
|
|
75
|
+
for item in lint_errors:
|
|
76
|
+
warnings.append(
|
|
77
|
+
{
|
|
78
|
+
"code": item.get("code") or "SEMANTIC_LINT_ERROR",
|
|
79
|
+
"message": item.get("message") or "workflow spec semantic lint error",
|
|
80
|
+
"path": item.get("path"),
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
diff_failed = False
|
|
85
|
+
input_ids: set[str] = set()
|
|
86
|
+
if isinstance(input_spec, dict):
|
|
87
|
+
input_ids = {
|
|
88
|
+
str(node.get("id"))
|
|
89
|
+
for node in (input_spec.get("nodes") or [])
|
|
90
|
+
if isinstance(node, dict) and node.get("id") is not None
|
|
91
|
+
}
|
|
92
|
+
if isinstance(diff_summary, dict) and input_ids:
|
|
93
|
+
removed = diff_summary.get("nodes", {}).get("removed") if isinstance(diff_summary.get("nodes"), dict) else None
|
|
94
|
+
if isinstance(removed, list):
|
|
95
|
+
unexpected = [node_id for node_id in removed if str(node_id) in input_ids]
|
|
96
|
+
if unexpected:
|
|
97
|
+
diff_failed = True
|
|
98
|
+
verification["unexpected_removed_node_ids"] = unexpected
|
|
99
|
+
|
|
100
|
+
if isinstance(applied_spec, dict) and isinstance(input_spec, dict):
|
|
101
|
+
applied_ids = {
|
|
102
|
+
str(node.get("id"))
|
|
103
|
+
for node in (applied_spec.get("nodes") or [])
|
|
104
|
+
if isinstance(node, dict) and node.get("id") is not None
|
|
105
|
+
}
|
|
106
|
+
missing_input_ids = sorted(node_id for node_id in input_ids if node_id not in applied_ids)
|
|
107
|
+
if missing_input_ids and not diff_failed:
|
|
108
|
+
verification["missing_input_node_ids"] = missing_input_ids
|
|
109
|
+
|
|
110
|
+
verified = not lint_errors and not diff_failed and verification.get("applied_spec_available") is True
|
|
111
|
+
return verification, warnings, verified
|
|
@@ -222,15 +222,25 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
222
222
|
views_apply_apply.add_argument("--remove-views-file")
|
|
223
223
|
views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary", force_json_output=True)
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
225
|
+
flow_parser = builder_subparsers.add_parser("flow", help="流程(WorkflowSpec)")
|
|
226
|
+
flow_subparsers = flow_parser.add_subparsers(dest="builder_flow_command", required=True)
|
|
227
|
+
|
|
228
|
+
flow_schema = flow_subparsers.add_parser("schema", help="读取 WorkflowSpec JSON Schema")
|
|
229
|
+
flow_schema.add_argument("--schema-version", default="")
|
|
230
|
+
flow_schema.set_defaults(handler=_handle_flow_schema, format_hint="builder_summary")
|
|
231
|
+
|
|
232
|
+
flow_get = flow_subparsers.add_parser("get", help="读取 WorkflowSpec")
|
|
233
|
+
flow_get.add_argument("--app-key", required=True)
|
|
234
|
+
flow_get.add_argument("--version-id", default="")
|
|
235
|
+
flow_get.set_defaults(handler=_handle_flow_get, format_hint="builder_summary")
|
|
236
|
+
|
|
237
|
+
flow_apply = flow_subparsers.add_parser("apply", help="应用 WorkflowSpec")
|
|
238
|
+
flow_apply.add_argument("--app-key", required=True)
|
|
239
|
+
flow_apply.add_argument("--spec-file", required=True)
|
|
240
|
+
flow_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
|
|
241
|
+
flow_apply.add_argument("--idempotency-key", default="")
|
|
242
|
+
flow_apply.add_argument("--schema-version", default="")
|
|
243
|
+
flow_apply.set_defaults(handler=_handle_flow_apply, format_hint="builder_summary", force_json_output=True)
|
|
234
244
|
|
|
235
245
|
charts_apply = builder_subparsers.add_parser("charts", help="报表")
|
|
236
246
|
charts_apply_subparsers = charts_apply.add_subparsers(dest="builder_charts_command", required=True)
|
|
@@ -720,14 +730,36 @@ def _handle_views_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
720
730
|
)
|
|
721
731
|
|
|
722
732
|
|
|
733
|
+
def _handle_flow_schema(args: argparse.Namespace, context: CliContext) -> dict:
|
|
734
|
+
return context.builder.app_flow_get_schema(
|
|
735
|
+
profile=args.profile,
|
|
736
|
+
schema_version=args.schema_version or None,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _handle_flow_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
741
|
+
return context.builder.app_get_flow(
|
|
742
|
+
profile=args.profile,
|
|
743
|
+
app_key=args.app_key,
|
|
744
|
+
version_id=args.version_id or None,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
723
748
|
def _handle_flow_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
749
|
+
spec_payload = load_object_arg(args.spec_file, option_name="--spec-file")
|
|
750
|
+
if not isinstance(spec_payload, dict):
|
|
751
|
+
raise_config_error("flow apply --spec-file must contain a JSON object.")
|
|
752
|
+
if "spec" in spec_payload and isinstance(spec_payload.get("spec"), dict):
|
|
753
|
+
spec = spec_payload["spec"]
|
|
754
|
+
else:
|
|
755
|
+
spec = spec_payload
|
|
724
756
|
return context.builder.app_flow_apply(
|
|
725
757
|
profile=args.profile,
|
|
726
758
|
app_key=args.app_key,
|
|
727
|
-
mode=args.mode,
|
|
728
759
|
publish=bool(args.publish),
|
|
729
|
-
|
|
730
|
-
|
|
760
|
+
spec=spec,
|
|
761
|
+
idempotency_key=args.idempotency_key or None,
|
|
762
|
+
schema_version=args.schema_version or None,
|
|
731
763
|
)
|
|
732
764
|
|
|
733
765
|
|
|
@@ -158,6 +158,8 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
158
158
|
PublicToolSpec(BUILDER_DOMAIN, "chart_get", ("chart_get",), ("builder", "chart", "get"), has_contract=True, cli_show_effective_context=True),
|
|
159
159
|
PublicToolSpec(BUILDER_DOMAIN, "app_schema_apply", ("app_schema_apply",), ("builder", "schema", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
160
160
|
PublicToolSpec(BUILDER_DOMAIN, "app_layout_apply", ("app_layout_apply",), ("builder", "layout", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
161
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_flow_get_schema", ("app_flow_get_schema",), ("builder", "flow", "schema"), has_contract=True, cli_show_effective_context=True),
|
|
162
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_flow_get", ("app_flow_get",), ("builder", "flow", "get"), has_contract=True, cli_show_effective_context=True),
|
|
161
163
|
PublicToolSpec(BUILDER_DOMAIN, "app_flow_apply", ("app_flow_apply",), ("builder", "flow", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
162
164
|
PublicToolSpec(BUILDER_DOMAIN, "app_views_apply", ("app_views_apply",), ("builder", "views", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
163
165
|
PublicToolSpec(BUILDER_DOMAIN, "app_charts_apply", ("app_charts_apply",), ("builder", "charts", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
@@ -37,7 +37,7 @@ def build_builder_server() -> FastMCP:
|
|
|
37
37
|
"Use solution_install when the user explicitly wants to install a packaged solution/template by solution_key, optionally copying bundled demo data. "
|
|
38
38
|
"Use package_list to find visible app packages by keyword and package_get to read package detail before editing; if creating or updating an app package may be appropriate, use package_apply with explicit user intent; otherwise use app_resolve to locate app resources, "
|
|
39
39
|
"Use workspace_icon_catalog_get before creating app packages, apps, or portals when supported icon/color candidates are needed; new workspace resources require explicit non-template icon + color, and the CLI validates choices without inferring business defaults. "
|
|
40
|
-
"app_get as the default app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for focused configuration reads, "
|
|
40
|
+
"app_get as the default app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_flow_get/app_flow_get_schema/app_get_charts/portal_list/portal_get/view_get/chart_get for focused configuration reads, "
|
|
41
41
|
"member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
|
|
42
42
|
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_custom_buttons_apply/app_associated_resources_apply/app_charts_apply/portal_apply/portal_delete to execute normalized patches; these apply/delete tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal delete separates DELETE execution from readback verification, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
|
|
43
43
|
"Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
|
|
@@ -52,7 +52,7 @@ def build_builder_server() -> FastMCP:
|
|
|
52
52
|
"For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
|
|
53
53
|
"For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
|
|
54
54
|
"Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
|
|
55
|
-
"For workflow edits,
|
|
55
|
+
"For workflow edits, use app_flow_get_schema and app_flow_get (GET-first), author a complete WorkflowSpecDTO in spec, then app_flow_apply; CLI equivalent is qingflow builder flow schema|get|apply --spec-file. Do not use general-server legacy workflow config reads (auditNodes list/detail, global settings, qsource config reads); they are removed. "
|
|
56
56
|
"If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
|
|
57
57
|
"Do not handcraft internal solution payloads or rely on build_id/stage/repair. "
|
|
58
58
|
"If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, first summarize the gap, ask whether to submit feedback, and call feedback_submit only after explicit user confirmation."
|
|
@@ -485,22 +485,30 @@ def build_builder_server() -> FastMCP:
|
|
|
485
485
|
) -> dict:
|
|
486
486
|
return ai_builder.app_layout_apply(profile=profile, app_key=app_key, mode=mode, publish=publish, sections=sections or [])
|
|
487
487
|
|
|
488
|
+
@server.tool()
|
|
489
|
+
def app_flow_get(profile: str = DEFAULT_PROFILE, app_key: str = "", version_id: str = "") -> dict:
|
|
490
|
+
return ai_builder.app_get_flow(profile=profile, app_key=app_key, version_id=version_id or None)
|
|
491
|
+
|
|
492
|
+
@server.tool()
|
|
493
|
+
def app_flow_get_schema(profile: str = DEFAULT_PROFILE, schema_version: str = "") -> dict:
|
|
494
|
+
return ai_builder.app_flow_get_schema(profile=profile, schema_version=schema_version or None)
|
|
495
|
+
|
|
488
496
|
@server.tool()
|
|
489
497
|
def app_flow_apply(
|
|
490
498
|
profile: str = DEFAULT_PROFILE,
|
|
491
499
|
app_key: str = "",
|
|
492
|
-
mode: str = "replace",
|
|
493
500
|
publish: bool = True,
|
|
494
|
-
|
|
495
|
-
|
|
501
|
+
spec: dict | None = None,
|
|
502
|
+
idempotency_key: str = "",
|
|
503
|
+
schema_version: str = "",
|
|
496
504
|
) -> dict:
|
|
497
505
|
return ai_builder.app_flow_apply(
|
|
498
506
|
profile=profile,
|
|
499
507
|
app_key=app_key,
|
|
500
|
-
mode=mode,
|
|
501
508
|
publish=publish,
|
|
502
|
-
|
|
503
|
-
|
|
509
|
+
spec=spec or {},
|
|
510
|
+
idempotency_key=idempotency_key or None,
|
|
511
|
+
schema_version=schema_version or None,
|
|
504
512
|
)
|
|
505
513
|
|
|
506
514
|
@server.tool()
|
|
@@ -10,8 +10,6 @@ from .navigation_compiler import compile_navigation
|
|
|
10
10
|
from .package_compiler import compile_package
|
|
11
11
|
from .portal_compiler import compile_portal
|
|
12
12
|
from .view_compiler import compile_views
|
|
13
|
-
from .workflow_compiler import compile_workflow
|
|
14
|
-
|
|
15
13
|
|
|
16
14
|
@dataclass(slots=True)
|
|
17
15
|
class ExecutionStep:
|
|
@@ -101,7 +99,7 @@ def compile_solution(spec: SolutionSpec) -> CompiledSolution:
|
|
|
101
99
|
|
|
102
100
|
def compile_entity(entity: EntitySpec, *, include_package: bool) -> CompiledEntity:
|
|
103
101
|
app_create_payload, form_base_payload, form_relation_payload, field_specs, field_labels = compile_entity_form(entity, include_package=include_package)
|
|
104
|
-
workflow_plan =
|
|
102
|
+
workflow_plan = None
|
|
105
103
|
view_plans = compile_views(entity)
|
|
106
104
|
chart_plans = compile_charts(entity)
|
|
107
105
|
return CompiledEntity(
|
|
@@ -14,7 +14,6 @@ from ..tools.qingbi_report_tools import QingbiReportTools
|
|
|
14
14
|
from ..tools.record_tools import RecordTools
|
|
15
15
|
from ..tools.role_tools import RoleTools
|
|
16
16
|
from ..tools.view_tools import ViewTools
|
|
17
|
-
from ..tools.workflow_tools import WorkflowTools
|
|
18
17
|
from ..tools.workspace_tools import WorkspaceTools
|
|
19
18
|
from .compiler import CompiledEntity, CompiledRole, CompiledSolution
|
|
20
19
|
from .compiler.form_compiler import QUESTION_TYPE_MAP
|
|
@@ -36,7 +35,6 @@ class SolutionExecutor:
|
|
|
36
35
|
role_tools: RoleTools,
|
|
37
36
|
app_tools: AppTools,
|
|
38
37
|
record_tools: RecordTools,
|
|
39
|
-
workflow_tools: WorkflowTools,
|
|
40
38
|
view_tools: ViewTools,
|
|
41
39
|
chart_tools: QingbiReportTools,
|
|
42
40
|
portal_tools: PortalTools,
|
|
@@ -47,7 +45,6 @@ class SolutionExecutor:
|
|
|
47
45
|
self.role_tools = role_tools
|
|
48
46
|
self.app_tools = app_tools
|
|
49
47
|
self.record_tools = record_tools
|
|
50
|
-
self.workflow_tools = workflow_tools
|
|
51
48
|
self.view_tools = view_tools
|
|
52
49
|
self.chart_tools = chart_tools
|
|
53
50
|
self.portal_tools = portal_tools
|
|
@@ -456,123 +453,10 @@ class SolutionExecutor:
|
|
|
456
453
|
def _build_workflow(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
457
454
|
if entity.workflow_plan is None:
|
|
458
455
|
return
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
entity.entity_id,
|
|
463
|
-
store,
|
|
464
|
-
app_key=app_key,
|
|
465
|
-
force_new=True,
|
|
466
|
-
)
|
|
467
|
-
node_artifacts = store.get_artifact("apps", entity.entity_id, {}).get("workflow_nodes", {})
|
|
468
|
-
existing_nodes = self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
|
|
469
|
-
current_nodes = _coerce_workflow_nodes(existing_nodes)
|
|
470
|
-
existing_nodes_by_name = {
|
|
471
|
-
node.get("auditNodeName"): int(node_id)
|
|
472
|
-
for node_id, node in current_nodes.items()
|
|
473
|
-
if isinstance(node, dict) and node.get("auditNodeName")
|
|
474
|
-
}
|
|
475
|
-
applicant_node_id = next(
|
|
476
|
-
(
|
|
477
|
-
int(node_id)
|
|
478
|
-
for node_id, node in current_nodes.items()
|
|
479
|
-
if isinstance(node, dict) and node.get("type") == 0 and node.get("dealType") == 3
|
|
480
|
-
),
|
|
481
|
-
None,
|
|
456
|
+
raise RuntimeError(
|
|
457
|
+
"Legacy auditNode workflow execution was removed. "
|
|
458
|
+
"Pass {app_key, spec} to solution_build_flow or use qingflow builder flow apply."
|
|
482
459
|
)
|
|
483
|
-
if applicant_node_id is not None:
|
|
484
|
-
node_artifacts.setdefault("__applicant__", applicant_node_id)
|
|
485
|
-
|
|
486
|
-
desired_global_settings = deepcopy(entity.workflow_plan["global_settings"])
|
|
487
|
-
explicit_global_settings = _has_explicit_workflow_global_settings(desired_global_settings)
|
|
488
|
-
current_global_settings: dict[str, Any] = {}
|
|
489
|
-
if explicit_global_settings:
|
|
490
|
-
current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
|
|
491
|
-
else:
|
|
492
|
-
try:
|
|
493
|
-
current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
|
|
494
|
-
except (QingflowApiError, RuntimeError) as error:
|
|
495
|
-
api_error = QingflowApiError(**_coerce_nested_error_payload(error))
|
|
496
|
-
if api_error.http_status != 404:
|
|
497
|
-
raise
|
|
498
|
-
current_global_settings = {}
|
|
499
|
-
if explicit_global_settings:
|
|
500
|
-
global_settings = deepcopy(current_global_settings if isinstance(current_global_settings, dict) else {})
|
|
501
|
-
global_settings.update(desired_global_settings)
|
|
502
|
-
global_settings["editVersionNo"] = workflow_edit_version_no or global_settings.get("editVersionNo") or 1
|
|
503
|
-
self.workflow_tools.workflow_update_global_settings(profile=profile, app_key=app_key, payload=global_settings)
|
|
504
|
-
for action in entity.workflow_plan["actions"]:
|
|
505
|
-
if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is not None:
|
|
506
|
-
continue
|
|
507
|
-
if action["action"] == "add_node":
|
|
508
|
-
if action.get("node_type") == "branch":
|
|
509
|
-
existing_branch_id = node_artifacts.get(action["node_id"])
|
|
510
|
-
if existing_branch_id is not None and not _workflow_node_is_branch(current_nodes, existing_branch_id):
|
|
511
|
-
existing_branch_id = None
|
|
512
|
-
if existing_branch_id is not None:
|
|
513
|
-
for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, existing_branch_id), start=1):
|
|
514
|
-
node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
|
|
515
|
-
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
516
|
-
apps_artifact["workflow_nodes"] = node_artifacts
|
|
517
|
-
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
518
|
-
continue
|
|
519
|
-
existing_node_id = node_artifacts.get(action["node_id"]) or existing_nodes_by_name.get(action.get("node_name"))
|
|
520
|
-
if existing_node_id is not None:
|
|
521
|
-
node_artifacts[action["node_id"]] = existing_node_id
|
|
522
|
-
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
523
|
-
apps_artifact["workflow_nodes"] = node_artifacts
|
|
524
|
-
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
525
|
-
continue
|
|
526
|
-
before_node_ids = set(current_nodes)
|
|
527
|
-
payload = self._resolve_workflow_payload(action["payload"], node_artifacts)
|
|
528
|
-
if workflow_edit_version_no is not None:
|
|
529
|
-
payload["editVersionNo"] = int(workflow_edit_version_no)
|
|
530
|
-
if action["action"] == "create_sub_branch":
|
|
531
|
-
result = self.workflow_tools.workflow_create_sub_branch(profile=profile, app_key=app_key, payload=payload)
|
|
532
|
-
elif action["action"] == "update_node":
|
|
533
|
-
target_node_id = node_artifacts.get(action["node_id"])
|
|
534
|
-
if target_node_id is None:
|
|
535
|
-
raise RuntimeError(f"workflow lane '{action['node_id']}' could not be resolved before update")
|
|
536
|
-
result = self.workflow_tools.workflow_update_node(
|
|
537
|
-
profile=profile,
|
|
538
|
-
app_key=app_key,
|
|
539
|
-
audit_node_id=target_node_id,
|
|
540
|
-
payload=payload,
|
|
541
|
-
)
|
|
542
|
-
else:
|
|
543
|
-
result = self.workflow_tools.workflow_add_node(profile=profile, app_key=app_key, payload=payload)
|
|
544
|
-
expected_type = 1 if action.get("node_type") == "branch" else None
|
|
545
|
-
audit_node_id = _extract_workflow_node_id(result.get("result"), expected_type=expected_type)
|
|
546
|
-
if action.get("node_type") == "branch" or action["action"] == "create_sub_branch":
|
|
547
|
-
current_nodes = _coerce_workflow_nodes(
|
|
548
|
-
self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
|
|
549
|
-
)
|
|
550
|
-
if audit_node_id is not None:
|
|
551
|
-
node_artifacts[action["node_id"]] = audit_node_id
|
|
552
|
-
if action.get("node_type") == "branch":
|
|
553
|
-
branch_node_id = node_artifacts.get(action["node_id"]) or _find_created_branch_node_id(
|
|
554
|
-
current_nodes,
|
|
555
|
-
before_node_ids=before_node_ids,
|
|
556
|
-
prev_id=payload.get("prevId"),
|
|
557
|
-
)
|
|
558
|
-
if branch_node_id is not None:
|
|
559
|
-
node_artifacts[action["node_id"]] = branch_node_id
|
|
560
|
-
for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, branch_node_id), start=1):
|
|
561
|
-
node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
|
|
562
|
-
if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is None:
|
|
563
|
-
created_lane_id = audit_node_id or _find_created_sub_branch_lane_id(
|
|
564
|
-
current_nodes,
|
|
565
|
-
before_node_ids=before_node_ids,
|
|
566
|
-
branch_node_id=payload.get("auditNodeId"),
|
|
567
|
-
)
|
|
568
|
-
if created_lane_id is not None:
|
|
569
|
-
node_artifacts[action["node_id"]] = created_lane_id
|
|
570
|
-
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
571
|
-
apps_artifact["workflow_nodes"] = node_artifacts
|
|
572
|
-
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
573
|
-
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
574
|
-
apps_artifact["workflow_nodes"] = node_artifacts
|
|
575
|
-
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
576
460
|
|
|
577
461
|
def _build_views(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
578
462
|
app_key = self._get_app_key(store, entity.entity_id)
|
|
@@ -2157,20 +2041,6 @@ def _find_created_sub_branch_lane_id(
|
|
|
2157
2041
|
return candidates[0] if candidates else None
|
|
2158
2042
|
|
|
2159
2043
|
|
|
2160
|
-
def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | None) -> bool:
|
|
2161
|
-
if not isinstance(global_settings, dict):
|
|
2162
|
-
return False
|
|
2163
|
-
for key, value in global_settings.items():
|
|
2164
|
-
if key == "editVersionNo":
|
|
2165
|
-
continue
|
|
2166
|
-
if value is None:
|
|
2167
|
-
continue
|
|
2168
|
-
if isinstance(value, (list, dict)) and not value:
|
|
2169
|
-
continue
|
|
2170
|
-
return True
|
|
2171
|
-
return False
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
2044
|
def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
|
|
2175
2045
|
if is_auth_like_error(error):
|
|
2176
2046
|
return False
|