cancan-microstack 0.0.1__py3-none-any.whl
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.
- cancan_microstack/__init__.py +14 -0
- cancan_microstack/__version__.py +10 -0
- cancan_microstack/assets/__init__.py +6 -0
- cancan_microstack/assets/builds/caddy/Caddyfile +187 -0
- cancan_microstack/assets/builds/caddy/DEPLOYMENT.md +303 -0
- cancan_microstack/assets/builds/caddy/Dockerfile +46 -0
- cancan_microstack/assets/builds/caddy/README.md +343 -0
- cancan_microstack/assets/builds/caddy/geoip/README.md +5 -0
- cancan_microstack/assets/builds/caddy/start.sh +78 -0
- cancan_microstack/assets/builds/caddy/waf/coraza.conf +179 -0
- cancan_microstack/assets/builds/service/Dockerfile +59 -0
- cancan_microstack/assets/builds/service/README.md +13 -0
- cancan_microstack/assets/ddl/create_db.sql +22 -0
- cancan_microstack/assets/ddl/infra/execution_log_tbl.sql +46 -0
- cancan_microstack/assets/ddl/infra/node_instance_tbl.sql +56 -0
- cancan_microstack/assets/ddl/infra/service_action_log_tbl.sql +36 -0
- cancan_microstack/assets/ddl/infra/service_config_tbl.sql +26 -0
- cancan_microstack/assets/ddl/infra/service_info_tbl.sql +45 -0
- cancan_microstack/assets/ddl/infra/service_instance_tbl.sql +54 -0
- cancan_microstack/assets/ddl/infra/service_operation_tbl.sql +47 -0
- cancan_microstack/assets/ddl/infra/workflow_definition_tbl.sql +60 -0
- cancan_microstack/assets/ddl/infra/workflow_definition_version_tbl.sql +35 -0
- cancan_microstack/assets/ddl/infra/workflow_engine_alert_tbl.sql +34 -0
- cancan_microstack/assets/ddl/infra/workflow_run_tbl.sql +52 -0
- cancan_microstack/assets/ddl/ops/admin_user_tbl.sql +34 -0
- cancan_microstack/assets/ddl/ops/caddy_access_log_tbl.sql +91 -0
- cancan_microstack/assets/ddl/ops/caddy_certificate_tbl.sql +59 -0
- cancan_microstack/assets/ddl/ops/caddy_rate_limit_tbl.sql +64 -0
- cancan_microstack/assets/ddl/ops/caddy_route_tbl.sql +63 -0
- cancan_microstack/assets/ddl/ops/caddy_stats_tbl.sql +77 -0
- cancan_microstack/assets/ddl/trigger.sql +21 -0
- cancan_microstack/assets/docker/docker-compose.infra.yml +401 -0
- cancan_microstack/assets/scripts/README.md +195 -0
- cancan_microstack/assets/scripts/docker/build_images.sh +44 -0
- cancan_microstack/assets/scripts/docker/force_rebuild_images.sh +38 -0
- cancan_microstack/assets/scripts/docker/rebuild_all.sh +34 -0
- cancan_microstack/assets/scripts/docker/rebuild_compose.sh +61 -0
- cancan_microstack/assets/scripts/docker/restart.sh +35 -0
- cancan_microstack/assets/scripts/docker/restart_compose.sh +35 -0
- cancan_microstack/assets/scripts/docker/start.sh +78 -0
- cancan_microstack/assets/scripts/docker/start_all.sh +46 -0
- cancan_microstack/assets/scripts/docker/start_compose.sh +66 -0
- cancan_microstack/assets/scripts/docker/stop.sh +67 -0
- cancan_microstack/assets/scripts/docker/stop_all.sh +38 -0
- cancan_microstack/assets/scripts/docker/stop_compose.sh +38 -0
- cancan_microstack/assets/scripts/podman/build_images_podman.sh +59 -0
- cancan_microstack/assets/scripts/podman/cleanup_podman.sh +25 -0
- cancan_microstack/assets/scripts/podman/force_rebuild_images_podman.sh +56 -0
- cancan_microstack/assets/scripts/podman/rebuild_all_podman.sh +37 -0
- cancan_microstack/assets/scripts/podman/rebuild_compose_podman.sh +60 -0
- cancan_microstack/assets/scripts/podman/restart_compose_podman.sh +73 -0
- cancan_microstack/assets/scripts/podman/start_all_podman.sh +66 -0
- cancan_microstack/assets/scripts/podman/start_compose_podman.sh +80 -0
- cancan_microstack/assets/scripts/podman/start_podman.sh +91 -0
- cancan_microstack/assets/scripts/podman/stop.sh +73 -0
- cancan_microstack/assets/scripts/podman/stop_all_podman.sh +34 -0
- cancan_microstack/assets/scripts/podman/stop_compose_podman.sh +58 -0
- cancan_microstack/assets/scripts/start_controllersrv.sh +9 -0
- cancan_microstack/assets/scripts/utils/check_all_db_tables.sh +104 -0
- cancan_microstack/assets/scripts/utils/check_env.sh +177 -0
- cancan_microstack/assets/scripts/utils/check_service_management_deployment.sh +225 -0
- cancan_microstack/assets/scripts/utils/deploy_service_management.sh +176 -0
- cancan_microstack/assets/scripts/utils/force_reload_infrasrv.sh +52 -0
- cancan_microstack/assets/scripts/utils/monitor_service_management.sh +187 -0
- cancan_microstack/assets/scripts/utils/reset_postgres_volume.sh +68 -0
- cancan_microstack/assets/scripts/utils/test_async_operations.sh +141 -0
- cancan_microstack/assets/scripts/utils/verify_real_operations.sh +76 -0
- cancan_microstack/assets/service/Dockerfile +65 -0
- cancan_microstack/assets/www/adminops/assets/AppEmpty.vue_vue_type_script_setup_true_lang-BOKUurnM.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ConfigManage-DKV5YOUz.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ConfigManage-Y5bhy7wG.css +1 -0
- cancan_microstack/assets/www/adminops/assets/ConsoleManage-8ljYvCW2.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ConsoleManage-BWpyqbuQ.css +1 -0
- cancan_microstack/assets/www/adminops/assets/DashboardNew-B9Nf1OPl.js +1 -0
- cancan_microstack/assets/www/adminops/assets/DashboardNew-DYWZKQ1V.css +1 -0
- cancan_microstack/assets/www/adminops/assets/LogSearch-CA0Jhe78.js +1 -0
- cancan_microstack/assets/www/adminops/assets/LogSearch-CCZfTNPF.css +1 -0
- cancan_microstack/assets/www/adminops/assets/LoginView-BId3kP3M.css +1 -0
- cancan_microstack/assets/www/adminops/assets/LoginView-BQZTV_Qy.js +1 -0
- cancan_microstack/assets/www/adminops/assets/OperationProgressDialog-BdEYwqFq.js +1 -0
- cancan_microstack/assets/www/adminops/assets/OperationProgressDialog-D-pASR8G.css +1 -0
- cancan_microstack/assets/www/adminops/assets/PageContainer-Byss-yUC.js +1 -0
- cancan_microstack/assets/www/adminops/assets/PageContainer-C3nSZwM7.css +1 -0
- cancan_microstack/assets/www/adminops/assets/RateLimitManage-BDI8jLpC.css +1 -0
- cancan_microstack/assets/www/adminops/assets/RateLimitManage-DJY4NiF-.js +1 -0
- cancan_microstack/assets/www/adminops/assets/RouteManage-DaUQ4QLw.css +1 -0
- cancan_microstack/assets/www/adminops/assets/RouteManage-w9XCU0UA.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceCard-BFzHe6Tw.css +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceCard-BJUhWnA-.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceDetail-Cw24WuKp.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceDetail-Yum47zdB.css +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceList-C7ryvbhE.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceList-Cgd01fUx.css +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceLogs-COpG9H0h.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceLogs-H_Alq0cf.css +1 -0
- cancan_microstack/assets/www/adminops/assets/StatsOverview-D0TwMQkA.js +39 -0
- cancan_microstack/assets/www/adminops/assets/StatsOverview-lqAN6pqM.css +1 -0
- cancan_microstack/assets/www/adminops/assets/TotpBindView-CWlAmzFt.js +1 -0
- cancan_microstack/assets/www/adminops/assets/TotpBindView-HoQC1lhx.css +1 -0
- cancan_microstack/assets/www/adminops/assets/TotpVerifyView-BHN1VtX1.css +1 -0
- cancan_microstack/assets/www/adminops/assets/TotpVerifyView-D3w_lZk8.js +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowCenter-DU_mpIA0.css +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowCenter-i50rZyxN.js +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowDesigner-CnHokPL9.js +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowDesigner-DaZaZpLd.css +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowRuns-B09hK48c.js +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowRuns-wGutKIIU.css +1 -0
- cancan_microstack/assets/www/adminops/assets/caddy-nnCKf8fG.js +1 -0
- cancan_microstack/assets/www/adminops/assets/format-Cuzxgna9.js +1 -0
- cancan_microstack/assets/www/adminops/assets/index-CiFlm8oc.js +64 -0
- cancan_microstack/assets/www/adminops/assets/index-UW0T1Dkc.css +1 -0
- cancan_microstack/assets/www/adminops/assets/service-BYlgGPs_.js +1 -0
- cancan_microstack/assets/www/adminops/assets/service-operation-6GzLw2Z1.js +1 -0
- cancan_microstack/assets/www/adminops/assets/style-CcIXnQ5y.css +1 -0
- cancan_microstack/assets/www/adminops/assets/style-lRnStdGu.js +39 -0
- cancan_microstack/assets/www/adminops/assets/useDebounce-BRlqfXqf.js +1 -0
- cancan_microstack/assets/www/adminops/assets/workflow-CUXs39Ac.js +1 -0
- cancan_microstack/assets/www/adminops/index.html +16 -0
- cancan_microstack/assets/www/adminops/vite.svg +1 -0
- cancan_microstack/cli/__init__.py +14 -0
- cancan_microstack/cli/__main__.py +9 -0
- cancan_microstack/cli/main.py +552 -0
- cancan_microstack/cmd/__init__.py +54 -0
- cancan_microstack/cmd/cancan/__init__.py +12 -0
- cancan_microstack/cmd/cancan/run.py +395 -0
- cancan_microstack/cmd/controllersrv/__init__.py +0 -0
- cancan_microstack/cmd/controllersrv/run.py +131 -0
- cancan_microstack/cmd/infrasrv/__init__.py +5 -0
- cancan_microstack/cmd/infrasrv/run.py +100 -0
- cancan_microstack/cmd/opsbffsrv/__init__.py +5 -0
- cancan_microstack/cmd/opsbffsrv/run.py +96 -0
- cancan_microstack/core/__init__.py +5 -0
- cancan_microstack/core/assets.py +123 -0
- cancan_microstack/core/compose_builder.py +102 -0
- cancan_microstack/core/doctor.py +152 -0
- cancan_microstack/core/microstack.py +71 -0
- cancan_microstack/core/runner.py +56 -0
- cancan_microstack/core/stack_manager.py +186 -0
- cancan_microstack/public/__init__.py +7 -0
- cancan_microstack/public/api/__init__.py +1 -0
- cancan_microstack/public/api/controllersrv_client.py +277 -0
- cancan_microstack/public/api/infrasrv_client.py +404 -0
- cancan_microstack/public/const/__init__.py +1 -0
- cancan_microstack/public/const/action_consts.py +18 -0
- cancan_microstack/public/const/app_consts.py +42 -0
- cancan_microstack/public/const/caddy_consts.py +22 -0
- cancan_microstack/public/const/controllersrv_consts.py +163 -0
- cancan_microstack/public/const/docker_consts.py +15 -0
- cancan_microstack/public/const/error.py +56 -0
- cancan_microstack/public/const/health_consts.py +52 -0
- cancan_microstack/public/const/hook_enums.py +56 -0
- cancan_microstack/public/const/logging_enums.py +13 -0
- cancan_microstack/public/const/metrics_enums.py +36 -0
- cancan_microstack/public/const/monitor_enums.py +26 -0
- cancan_microstack/public/const/operation_consts.py +53 -0
- cancan_microstack/public/const/opsbffsrv_error.py +92 -0
- cancan_microstack/public/const/overrides_consts.py +13 -0
- cancan_microstack/public/const/redis.py +17 -0
- cancan_microstack/public/const/service_consts.py +15 -0
- cancan_microstack/public/const/workflow_consts.py +65 -0
- cancan_microstack/public/error.py +41 -0
- cancan_microstack/public/logging/__init__.py +0 -0
- cancan_microstack/public/logging/initializer.py +109 -0
- cancan_microstack/public/logging/mq_handler.py +279 -0
- cancan_microstack/public/schemas/__init__.py +1 -0
- cancan_microstack/public/schemas/caddy/__init__.py +381 -0
- cancan_microstack/public/schemas/caddy/analysis.py +90 -0
- cancan_microstack/public/schemas/caddy/route.py +18 -0
- cancan_microstack/public/schemas/common.py +79 -0
- cancan_microstack/public/schemas/controllersrv/__init__.py +3 -0
- cancan_microstack/public/schemas/controllersrv/async_requests.py +30 -0
- cancan_microstack/public/schemas/controllersrv/compose_models.py +47 -0
- cancan_microstack/public/schemas/controllersrv/const.py +24 -0
- cancan_microstack/public/schemas/controllersrv/docker_models.py +45 -0
- cancan_microstack/public/schemas/controllersrv/docker_responses.py +104 -0
- cancan_microstack/public/schemas/controllersrv/requests.py +54 -0
- cancan_microstack/public/schemas/controllersrv/responses.py +124 -0
- cancan_microstack/public/schemas/controllersrv/task_models.py +102 -0
- cancan_microstack/public/schemas/controllersrv/validation.py +23 -0
- cancan_microstack/public/schemas/hook_metrics.py +124 -0
- cancan_microstack/public/schemas/hooks.py +39 -0
- cancan_microstack/public/schemas/infra/__init__.py +0 -0
- cancan_microstack/public/schemas/infra/cleanup.py +25 -0
- cancan_microstack/public/schemas/infra/container.py +74 -0
- cancan_microstack/public/schemas/infra/enums.py +135 -0
- cancan_microstack/public/schemas/infra/health_check.py +42 -0
- cancan_microstack/public/schemas/infra/hook_log.py +42 -0
- cancan_microstack/public/schemas/infra/operation.py +90 -0
- cancan_microstack/public/schemas/infra/overview.py +25 -0
- cancan_microstack/public/schemas/infra/push.py +33 -0
- cancan_microstack/public/schemas/infra/service_action_log.py +47 -0
- cancan_microstack/public/schemas/infra/service_config.py +10 -0
- cancan_microstack/public/schemas/infra/service_info.py +69 -0
- cancan_microstack/public/schemas/infra/service_instance.py +93 -0
- cancan_microstack/public/schemas/infra/service_management.py +152 -0
- cancan_microstack/public/schemas/infra/service_operation.py +79 -0
- cancan_microstack/public/schemas/infra/service_registry.py +158 -0
- cancan_microstack/public/schemas/infra/status_types.py +19 -0
- cancan_microstack/public/schemas/infra/workflow.py +566 -0
- cancan_microstack/public/schemas/logging/__init__.py +1 -0
- cancan_microstack/public/schemas/logging/log_event.py +121 -0
- cancan_microstack/public/schemas/opsbffsrv/__init__.py +1 -0
- cancan_microstack/public/schemas/opsbffsrv/async_ops.py +17 -0
- cancan_microstack/public/schemas/opsbffsrv/db_admin.py +147 -0
- cancan_microstack/public/schemas/opsbffsrv/db_init.py +48 -0
- cancan_microstack/public/schemas/opsbffsrv/service_config.py +89 -0
- cancan_microstack/public/schemas/opsbffsrv/service_logs.py +54 -0
- cancan_microstack/public/schemas/service_operation.py +24 -0
- cancan_microstack/public/schemas/service_registry.py +40 -0
- cancan_microstack/public/types/__init__.py +7 -0
- cancan_microstack/public/web/__init__.py +0 -0
- cancan_microstack/public/web/config_value.py +105 -0
- cancan_microstack/public/web/server.py +385 -0
- cancan_microstack/py.typed +0 -0
- cancan_microstack/runtime/__init__.py +0 -0
- cancan_microstack/runtime/compose_cmd.py +228 -0
- cancan_microstack/runtime/host_daemon.py +318 -0
- cancan_microstack/runtime/overrides.py +103 -0
- cancan_microstack/runtime/resources.py +25 -0
- cancan_microstack/runtime/workspace.py +94 -0
- cancan_microstack/services/__init__.py +0 -0
- cancan_microstack/services/controllersrv/__init__.py +8 -0
- cancan_microstack/services/controllersrv/application/__init__.py +0 -0
- cancan_microstack/services/controllersrv/application/docker_compose_app.py +427 -0
- cancan_microstack/services/controllersrv/conf/__init__.py +0 -0
- cancan_microstack/services/controllersrv/conf/config.py +76 -0
- cancan_microstack/services/controllersrv/conf/settings.py +54 -0
- cancan_microstack/services/controllersrv/domain/__init__.py +0 -0
- cancan_microstack/services/controllersrv/domain/docker_compose/__init__.py +0 -0
- cancan_microstack/services/controllersrv/domain/docker_compose/docker_compose_domain.py +278 -0
- cancan_microstack/services/controllersrv/domain/service_validator.py +327 -0
- cancan_microstack/services/controllersrv/domain/task/__init__.py +17 -0
- cancan_microstack/services/controllersrv/domain/task/task_queue.py +286 -0
- cancan_microstack/services/controllersrv/domain/task/task_worker.py +495 -0
- cancan_microstack/services/controllersrv/infrastructure/__init__.py +0 -0
- cancan_microstack/services/controllersrv/interface/__init__.py +0 -0
- cancan_microstack/services/controllersrv/interface/api/__init__.py +0 -0
- cancan_microstack/services/controllersrv/interface/api/docker_control_api.py +470 -0
- cancan_microstack/services/controllersrv/router.py +132 -0
- cancan_microstack/services/infrasrv/__init__.py +4 -0
- cancan_microstack/services/infrasrv/application/__init__.py +0 -0
- cancan_microstack/services/infrasrv/application/health_check_app.py +24 -0
- cancan_microstack/services/infrasrv/application/logging/__init__.py +1 -0
- cancan_microstack/services/infrasrv/application/logging/log_ingestion_service.py +183 -0
- cancan_microstack/services/infrasrv/application/service_config.py +22 -0
- cancan_microstack/services/infrasrv/application/service_logs_app.py +53 -0
- cancan_microstack/services/infrasrv/application/service_management_app.py +689 -0
- cancan_microstack/services/infrasrv/application/service_operation_tracker.py +251 -0
- cancan_microstack/services/infrasrv/application/service_registry.py +53 -0
- cancan_microstack/services/infrasrv/application/workflow/__init__.py +0 -0
- cancan_microstack/services/infrasrv/application/workflow/workflow_app.py +991 -0
- cancan_microstack/services/infrasrv/application/workflow/workflow_queue.py +302 -0
- cancan_microstack/services/infrasrv/application/workflow/workflow_tasks.py +46 -0
- cancan_microstack/services/infrasrv/application/workflow/workflow_worker_runtime.py +122 -0
- cancan_microstack/services/infrasrv/conf/__init__.py +0 -0
- cancan_microstack/services/infrasrv/conf/config.py +98 -0
- cancan_microstack/services/infrasrv/domain/__init__.py +0 -0
- cancan_microstack/services/infrasrv/domain/health_check/__init__.py +3 -0
- cancan_microstack/services/infrasrv/domain/health_check/health_check_domain.py +576 -0
- cancan_microstack/services/infrasrv/domain/hooks/__init__.py +19 -0
- cancan_microstack/services/infrasrv/domain/hooks/builtin_hooks.py +308 -0
- cancan_microstack/services/infrasrv/domain/hooks/hook_registry.py +43 -0
- cancan_microstack/services/infrasrv/domain/hooks/hooks_log_utils.py +275 -0
- cancan_microstack/services/infrasrv/domain/hooks/init.py +17 -0
- cancan_microstack/services/infrasrv/domain/hooks/metrics.py +205 -0
- cancan_microstack/services/infrasrv/domain/hooks/pre_registration_hooks.py +490 -0
- cancan_microstack/services/infrasrv/domain/registry/__init__.py +0 -0
- cancan_microstack/services/infrasrv/domain/registry/service_registry.py +509 -0
- cancan_microstack/services/infrasrv/domain/service_config/__init__.py +0 -0
- cancan_microstack/services/infrasrv/domain/service_config/service_config.py +50 -0
- cancan_microstack/services/infrasrv/domain/service_logs/__init__.py +0 -0
- cancan_microstack/services/infrasrv/domain/service_logs/service_logs_domain.py +51 -0
- cancan_microstack/services/infrasrv/domain/workflow/__init__.py +4 -0
- cancan_microstack/services/infrasrv/domain/workflow/engine.py +159 -0
- cancan_microstack/services/infrasrv/domain/workflow/node_handlers.py +509 -0
- cancan_microstack/services/infrasrv/domain/workflow/workflow_domain.py +164 -0
- cancan_microstack/services/infrasrv/infrastructure/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/api/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/api/controllersrv_api.py +165 -0
- cancan_microstack/services/infrasrv/infrastructure/cache/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/cache/service_registry_cache.py +174 -0
- cancan_microstack/services/infrasrv/infrastructure/db/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/execution_log_tbl.py +53 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/node_instance_tbl.py +55 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/service_action_log_tbl.py +44 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/service_config_tbl.py +30 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/service_info_tbl.py +59 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/service_instance_tbl.py +88 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/service_operation_tbl.py +73 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_definition_tbl.py +55 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_definition_version_tbl.py +43 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_engine_alert_tbl.py +57 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_run_tbl.py +56 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_action_log_op.py +239 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_config.py +80 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_config_manager.py +198 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_info_op.py +297 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_instance_op.py +688 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_operation_op.py +387 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_registry.py +124 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/workflow_op.py +804 -0
- cancan_microstack/services/infrasrv/infrastructure/ddl_manager.py +31 -0
- cancan_microstack/services/infrasrv/infrastructure/mongo/__init__.py +1 -0
- cancan_microstack/services/infrasrv/infrastructure/mongo/log_repository.py +129 -0
- cancan_microstack/services/infrasrv/interface/__init__.py +0 -0
- cancan_microstack/services/infrasrv/interface/api/__init__.py +0 -0
- cancan_microstack/services/infrasrv/interface/api/health_check_api.py +29 -0
- cancan_microstack/services/infrasrv/interface/api/hooks.py +284 -0
- cancan_microstack/services/infrasrv/interface/api/internal.py +49 -0
- cancan_microstack/services/infrasrv/interface/api/internal_instance_api.py +265 -0
- cancan_microstack/services/infrasrv/interface/api/internal_operation_api.py +206 -0
- cancan_microstack/services/infrasrv/interface/api/service_config.py +50 -0
- cancan_microstack/services/infrasrv/interface/api/service_logs_api.py +49 -0
- cancan_microstack/services/infrasrv/interface/api/service_management_api.py +113 -0
- cancan_microstack/services/infrasrv/interface/api/service_registry.py +117 -0
- cancan_microstack/services/infrasrv/interface/api/workflow_api.py +303 -0
- cancan_microstack/services/infrasrv/interface/schedule/__init__.py +0 -0
- cancan_microstack/services/infrasrv/interface/schedule/cleanup.py +13 -0
- cancan_microstack/services/infrasrv/interface/schedule/health_check.py +27 -0
- cancan_microstack/services/infrasrv/interface/schedule/log_cleanup.py +26 -0
- cancan_microstack/services/infrasrv/interface/schedule/operation_tracker.py +25 -0
- cancan_microstack/services/infrasrv/interface/schedule/scheduler.py +39 -0
- cancan_microstack/services/infrasrv/interface/schedule/workflow_scheduler.py +115 -0
- cancan_microstack/services/infrasrv/router.py +341 -0
- cancan_microstack/services/opsbffsrv/__init__.py +4 -0
- cancan_microstack/services/opsbffsrv/application/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/application/async_operation_app.py +150 -0
- cancan_microstack/services/opsbffsrv/application/auth_app.py +285 -0
- cancan_microstack/services/opsbffsrv/application/caddy/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/application/caddy/access_log_analysis_app.py +344 -0
- cancan_microstack/services/opsbffsrv/application/caddy/access_log_ingestion_service.py +169 -0
- cancan_microstack/services/opsbffsrv/application/caddy/certificate_management_app.py +355 -0
- cancan_microstack/services/opsbffsrv/application/caddy/rate_limit_management_app.py +496 -0
- cancan_microstack/services/opsbffsrv/application/caddy/route_management_app.py +401 -0
- cancan_microstack/services/opsbffsrv/application/caddy/stats_aggregation_app.py +364 -0
- cancan_microstack/services/opsbffsrv/application/db_admin_app.py +103 -0
- cancan_microstack/services/opsbffsrv/application/db_init_app.py +283 -0
- cancan_microstack/services/opsbffsrv/application/logging/__init__.py +1 -0
- cancan_microstack/services/opsbffsrv/application/logging/log_query_app.py +28 -0
- cancan_microstack/services/opsbffsrv/application/service_config.py +158 -0
- cancan_microstack/services/opsbffsrv/application/service_logs_app.py +74 -0
- cancan_microstack/services/opsbffsrv/application/service_registry.py +36 -0
- cancan_microstack/services/opsbffsrv/application/workflow_ops_app.py +730 -0
- cancan_microstack/services/opsbffsrv/conf/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/conf/config.py +224 -0
- cancan_microstack/services/opsbffsrv/domain/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/auth/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/auth/admin_init.py +38 -0
- cancan_microstack/services/opsbffsrv/domain/auth/auth_domain.py +108 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/access_log_analysis.py +358 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/certificate_management.py +325 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/default_routes.py +53 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/rate_limit_management.py +308 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/route_management.py +279 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/stats_aggregation.py +654 -0
- cancan_microstack/services/opsbffsrv/domain/db_admin/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/db_admin/db_admin_domain.py +118 -0
- cancan_microstack/services/opsbffsrv/domain/db_init/__init__.py +3 -0
- cancan_microstack/services/opsbffsrv/domain/db_init/db_init_domain.py +358 -0
- cancan_microstack/services/opsbffsrv/domain/logging/__init__.py +1 -0
- cancan_microstack/services/opsbffsrv/domain/logging/log_query_domain.py +99 -0
- cancan_microstack/services/opsbffsrv/domain/service_config/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/service_config/service_config.py +81 -0
- cancan_microstack/services/opsbffsrv/domain/service_registry/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/service_registry/service_registry.py +292 -0
- cancan_microstack/services/opsbffsrv/infrastructure/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/api/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/api/infrasrv_api.py +242 -0
- cancan_microstack/services/opsbffsrv/infrastructure/auth/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/auth/captcha_service.py +67 -0
- cancan_microstack/services/opsbffsrv/infrastructure/auth/password_service.py +12 -0
- cancan_microstack/services/opsbffsrv/infrastructure/auth/redis_store.py +131 -0
- cancan_microstack/services/opsbffsrv/infrastructure/auth/totp_service.py +59 -0
- cancan_microstack/services/opsbffsrv/infrastructure/caddy/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/caddy/access_log_parser.py +307 -0
- cancan_microstack/services/opsbffsrv/infrastructure/caddy/admin_api_client.py +678 -0
- cancan_microstack/services/opsbffsrv/infrastructure/caddy/ip_geo_locator.py +176 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/admin_user_tbl.py +33 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_access_log_tbl.py +90 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_certificate_tbl.py +65 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_rate_limit_tbl.py +69 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_route_tbl.py +66 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_stats_tbl.py +78 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_action_log_tbl.py +44 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_config_tbl.py +30 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_info_tbl.py +51 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_instance_tbl.py +68 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/admin_user_operate.py +59 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_access_log.py +531 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_certificate.py +451 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_rate_limit.py +360 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_route.py +271 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_stats.py +343 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_action_log_op.py +57 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_config.py +86 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_info_op.py +79 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_instance.py +58 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_registry.py +138 -0
- cancan_microstack/services/opsbffsrv/infrastructure/ddl_manager.py +31 -0
- cancan_microstack/services/opsbffsrv/infrastructure/mongo/__init__.py +1 -0
- cancan_microstack/services/opsbffsrv/infrastructure/mongo/log_query_repository.py +87 -0
- cancan_microstack/services/opsbffsrv/interface/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/interface/api/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/interface/api/async_operation_api.py +137 -0
- cancan_microstack/services/opsbffsrv/interface/api/auth_api.py +113 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/__init__.py +3 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/access_log_api.py +174 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/certificate_api.py +235 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/rate_limit_api.py +302 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/route_api.py +250 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/stats_api.py +243 -0
- cancan_microstack/services/opsbffsrv/interface/api/db_admin_api.py +62 -0
- cancan_microstack/services/opsbffsrv/interface/api/db_init_api.py +109 -0
- cancan_microstack/services/opsbffsrv/interface/api/instance_management_api.py +165 -0
- cancan_microstack/services/opsbffsrv/interface/api/log_query_api.py +41 -0
- cancan_microstack/services/opsbffsrv/interface/api/mongo_express_proxy_api.py +181 -0
- cancan_microstack/services/opsbffsrv/interface/api/pgweb_proxy_api.py +154 -0
- cancan_microstack/services/opsbffsrv/interface/api/rabbitmq_mgmt_proxy_api.py +518 -0
- cancan_microstack/services/opsbffsrv/interface/api/redis_commander_proxy_api.py +133 -0
- cancan_microstack/services/opsbffsrv/interface/api/service_config.py +146 -0
- cancan_microstack/services/opsbffsrv/interface/api/service_logs_api.py +81 -0
- cancan_microstack/services/opsbffsrv/interface/api/service_registry.py +66 -0
- cancan_microstack/services/opsbffsrv/interface/api/workflow_ops_api.py +413 -0
- cancan_microstack/services/opsbffsrv/interface/middleware/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/interface/middleware/auth_middleware.py +52 -0
- cancan_microstack/services/opsbffsrv/router.py +901 -0
- cancan_microstack/utils/__init__.py +1 -0
- cancan_microstack/utils/container_env.py +218 -0
- cancan_microstack-0.0.1.dist-info/METADATA +155 -0
- cancan_microstack-0.0.1.dist-info/RECORD +440 -0
- cancan_microstack-0.0.1.dist-info/WHEEL +5 -0
- cancan_microstack-0.0.1.dist-info/entry_points.txt +2 -0
- cancan_microstack-0.0.1.dist-info/licenses/LICENSE +21 -0
- cancan_microstack-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
"""
|
|
2
|
+
工作流应用服务
|
|
3
|
+
Workflow Application Service
|
|
4
|
+
|
|
5
|
+
作为应用层,它负责编排领域服务和基础设施服务,以完成完整的工作流功能。
|
|
6
|
+
As the application layer, it orchestrates domain and infrastructure services to deliver complete workflow functionality.
|
|
7
|
+
"""
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
Dict,
|
|
12
|
+
List,
|
|
13
|
+
Optional,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from cancan_microstack.public.error import (
|
|
17
|
+
HTTPException,
|
|
18
|
+
ParamError,
|
|
19
|
+
)
|
|
20
|
+
from cancan_microstack.public.const.workflow_consts import WorkflowEngineAlertReason
|
|
21
|
+
from cancan_microstack.public.schemas.infra import workflow as wt
|
|
22
|
+
from cancan_microstack.public.schemas.infra.enums import (
|
|
23
|
+
CallbackAckStatus,
|
|
24
|
+
ExecutionLogStatus,
|
|
25
|
+
NodeType,
|
|
26
|
+
WorkflowEngineAlertSeverity,
|
|
27
|
+
WorkflowEngineAlertCategory,
|
|
28
|
+
)
|
|
29
|
+
from linglong_web import LinglongConfig
|
|
30
|
+
from linglong_web.utils import (
|
|
31
|
+
get_request_id,
|
|
32
|
+
set_request_id,
|
|
33
|
+
)
|
|
34
|
+
from cancan_microstack.services.infrasrv.application.workflow.workflow_queue import (
|
|
35
|
+
enqueue_node_execution,
|
|
36
|
+
register_inline_orchestrator,
|
|
37
|
+
)
|
|
38
|
+
from cancan_microstack.services.infrasrv.infrastructure.db.operate import workflow_op
|
|
39
|
+
from cancan_microstack.services.infrasrv.domain.workflow.engine import workflow_engine
|
|
40
|
+
from cancan_microstack.services.infrasrv.domain.workflow.workflow_domain import workflow_domain
|
|
41
|
+
from linglong_web.utils import logger
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class WorkflowApp:
|
|
45
|
+
"""编排工作流相关用例 / Orchestrates workflow-related use cases."""
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _serialize_output_payload(output: Any) -> Optional[Dict[str, Any]]:
|
|
49
|
+
"""将领域输出转换为可存储的 JSON 字典 / Convert node output into a JSON-serializable dict."""
|
|
50
|
+
|
|
51
|
+
if output is None:
|
|
52
|
+
return None
|
|
53
|
+
if hasattr(output, "model_dump"):
|
|
54
|
+
dumped = output.model_dump()
|
|
55
|
+
return dumped if isinstance(dumped, dict) else {"data": dumped}
|
|
56
|
+
if isinstance(output, dict):
|
|
57
|
+
return output
|
|
58
|
+
if isinstance(output, list):
|
|
59
|
+
return {"items": output}
|
|
60
|
+
return {"value": output}
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _merge_runtime_context(
|
|
64
|
+
current_context: Optional[Dict[str, Any]],
|
|
65
|
+
node_id: str,
|
|
66
|
+
node_status: wt.NodeStatus,
|
|
67
|
+
output: Optional[Dict[str, Any]],
|
|
68
|
+
) -> Dict[str, Any]:
|
|
69
|
+
"""将节点执行结果写入全局上下文运行时命名空间。
|
|
70
|
+
Merge node execution result into global context runtime namespace.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
merged_context = dict(current_context or {})
|
|
74
|
+
runtime_payload = dict(merged_context.get("__runtime__") or {})
|
|
75
|
+
node_outputs = dict(runtime_payload.get("node_outputs") or {})
|
|
76
|
+
|
|
77
|
+
node_outputs[node_id] = {
|
|
78
|
+
"status": node_status.value,
|
|
79
|
+
"output": output,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
runtime_payload["node_outputs"] = node_outputs
|
|
83
|
+
runtime_payload["last_node_id"] = node_id
|
|
84
|
+
runtime_payload["last_output"] = output
|
|
85
|
+
runtime_payload["last_status"] = node_status.value
|
|
86
|
+
merged_context["__runtime__"] = runtime_payload
|
|
87
|
+
|
|
88
|
+
return merged_context
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _extract_value_by_path(payload: Optional[Dict[str, Any]], path: str) -> tuple[Any, bool]:
|
|
92
|
+
"""按点路径提取值 / Extract value by dot path."""
|
|
93
|
+
|
|
94
|
+
if not isinstance(payload, dict) or not path:
|
|
95
|
+
return None, False
|
|
96
|
+
|
|
97
|
+
current: Any = payload
|
|
98
|
+
for segment in path.split("."):
|
|
99
|
+
seg = segment.strip()
|
|
100
|
+
if not seg:
|
|
101
|
+
return None, False
|
|
102
|
+
|
|
103
|
+
if isinstance(current, dict):
|
|
104
|
+
if seg not in current:
|
|
105
|
+
return None, False
|
|
106
|
+
current = current.get(seg)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
if isinstance(current, list):
|
|
110
|
+
if not seg.isdigit():
|
|
111
|
+
return None, False
|
|
112
|
+
index = int(seg)
|
|
113
|
+
if index < 0 or index >= len(current):
|
|
114
|
+
return None, False
|
|
115
|
+
current = current[index]
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
return None, False
|
|
119
|
+
|
|
120
|
+
return current, True
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _set_nested_value(target: Dict[str, Any], path: str, value: Any) -> None:
|
|
124
|
+
"""按点路径写入值 / Set value by dot path."""
|
|
125
|
+
|
|
126
|
+
segments = [seg.strip() for seg in path.split(".") if seg.strip()]
|
|
127
|
+
if not segments:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
cursor = target
|
|
131
|
+
for segment in segments[:-1]:
|
|
132
|
+
next_value = cursor.get(segment)
|
|
133
|
+
if not isinstance(next_value, dict):
|
|
134
|
+
next_value = {}
|
|
135
|
+
cursor[segment] = next_value
|
|
136
|
+
cursor = next_value
|
|
137
|
+
|
|
138
|
+
cursor[segments[-1]] = value
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def _apply_action_context_mappings(
|
|
142
|
+
cls,
|
|
143
|
+
current_context: Optional[Dict[str, Any]],
|
|
144
|
+
node_config: wt.NodeConfig,
|
|
145
|
+
output: Optional[Dict[str, Any]],
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
"""将 Action 节点输出映射到 global_context 业务键。"""
|
|
148
|
+
|
|
149
|
+
merged_context = dict(current_context or {})
|
|
150
|
+
if node_config.type != NodeType.ACTION:
|
|
151
|
+
return merged_context
|
|
152
|
+
|
|
153
|
+
config = node_config.config
|
|
154
|
+
mappings = getattr(config, "context_mappings", None) if config else None
|
|
155
|
+
if not isinstance(mappings, dict) or not mappings:
|
|
156
|
+
return merged_context
|
|
157
|
+
|
|
158
|
+
for target_key, source_path in mappings.items():
|
|
159
|
+
if not isinstance(target_key, str) or not target_key.strip():
|
|
160
|
+
continue
|
|
161
|
+
if target_key.startswith("__runtime__"):
|
|
162
|
+
logger.warning("Skip mapping to reserved key '__runtime__': %s", target_key)
|
|
163
|
+
continue
|
|
164
|
+
if not isinstance(source_path, str) or not source_path.strip():
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
value, found = cls._extract_value_by_path(output, source_path.strip())
|
|
168
|
+
if not found:
|
|
169
|
+
logger.warning(
|
|
170
|
+
"Action context mapping source path not found: node=%s target=%s source=%s",
|
|
171
|
+
node_config.id,
|
|
172
|
+
target_key,
|
|
173
|
+
source_path,
|
|
174
|
+
)
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
cls._set_nested_value(merged_context, target_key.strip(), value)
|
|
178
|
+
|
|
179
|
+
return merged_context
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _requires_async_callback(node_config: wt.NodeConfig) -> bool:
|
|
183
|
+
"""判断节点是否需要异步回调 / Return True when the node expects async callbacks."""
|
|
184
|
+
|
|
185
|
+
if node_config.type != NodeType.ACTION:
|
|
186
|
+
return False
|
|
187
|
+
config = node_config.config
|
|
188
|
+
return bool(config and getattr(config, "async_mode", False))
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _build_callback_url(node_instance_id: uuid.UUID) -> str:
|
|
192
|
+
"""根据节点实例构造回调地址 / Build callback URL for async workers."""
|
|
193
|
+
|
|
194
|
+
base = LinglongConfig.INFRASRV_HOST.rstrip("/") if LinglongConfig.INFRASRV_HOST else ""
|
|
195
|
+
return f"{base}/v1/infrasrv/callbacks/{node_instance_id}" if base else f"/v1/infrasrv/callbacks/{node_instance_id}"
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _build_definition_snapshot(definition: wt.WorkflowDefinition) -> Dict[str, Any]:
|
|
199
|
+
"""生成运行使用的工作流定义快照 / Build immutable workflow definition snapshot for runs."""
|
|
200
|
+
|
|
201
|
+
return definition.model_dump(mode="json")
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _normalize_trigger_context(trigger_context: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
205
|
+
"""确保触发上下文带有 reqid,并复用 Linglong 协程上下文能力。
|
|
206
|
+
Ensure trigger context always includes reqid and reuses Linglong coroutine context.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
normalized = dict(trigger_context or {})
|
|
210
|
+
provided_reqid = str(normalized.get("reqid") or "").strip()
|
|
211
|
+
|
|
212
|
+
if provided_reqid:
|
|
213
|
+
set_request_id(provided_reqid)
|
|
214
|
+
reqid = provided_reqid
|
|
215
|
+
else:
|
|
216
|
+
reqid = str(get_request_id() or "").strip()
|
|
217
|
+
if not reqid:
|
|
218
|
+
set_request_id(None)
|
|
219
|
+
reqid = str(get_request_id() or "").strip()
|
|
220
|
+
|
|
221
|
+
normalized["reqid"] = reqid
|
|
222
|
+
return normalized
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def _extract_reqid_from_run(run: wt.WorkflowRun) -> str:
|
|
226
|
+
"""从运行上下文提取 reqid。
|
|
227
|
+
Extract reqid from run contexts.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
for context_data in (run.trigger_context, run.global_context):
|
|
231
|
+
if not isinstance(context_data, dict):
|
|
232
|
+
continue
|
|
233
|
+
candidate = str(context_data.get("reqid") or "").strip()
|
|
234
|
+
if candidate:
|
|
235
|
+
return candidate
|
|
236
|
+
return ""
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _build_graph_adjacency(workflow_def: wt.WorkflowDefinition) -> Dict[str, List[str]]:
|
|
240
|
+
"""构建工作流邻接表,优先使用 graph_data.edges。
|
|
241
|
+
Build workflow adjacency map, preferring graph_data.edges.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
adjacency: Dict[str, List[str]] = {}
|
|
245
|
+
graph_data = workflow_def.graph_data
|
|
246
|
+
|
|
247
|
+
if graph_data and graph_data.edges:
|
|
248
|
+
for edge in graph_data.edges:
|
|
249
|
+
source = edge.source
|
|
250
|
+
target = edge.target
|
|
251
|
+
if not source or not target:
|
|
252
|
+
continue
|
|
253
|
+
adjacency.setdefault(source, [])
|
|
254
|
+
if target not in adjacency[source]:
|
|
255
|
+
adjacency[source].append(target)
|
|
256
|
+
return adjacency
|
|
257
|
+
|
|
258
|
+
for node_id, raw_node in workflow_def.nodes_config.items():
|
|
259
|
+
node = wt.NodeConfig.model_validate(raw_node)
|
|
260
|
+
adjacency[node_id] = list(node.next_node_ids or [])
|
|
261
|
+
return adjacency
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def _is_node_in_loop_body_subgraph(
|
|
265
|
+
cls,
|
|
266
|
+
workflow_def: wt.WorkflowDefinition,
|
|
267
|
+
loop_node_id: str,
|
|
268
|
+
loop_node: wt.NodeConfig,
|
|
269
|
+
current_node_id: str,
|
|
270
|
+
) -> bool:
|
|
271
|
+
"""判断节点是否位于 LOOP 循环体子图。
|
|
272
|
+
Determine whether a node belongs to LOOP body subgraph.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
loop_config = loop_node.config
|
|
276
|
+
if not isinstance(loop_config, wt.LoopNodeConfig):
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
body_entry_id = loop_config.body_entry_id or loop_config.jump_target_id
|
|
280
|
+
if not body_entry_id:
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
if current_node_id == body_entry_id:
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
boundary_nodes = set(loop_node.next_node_ids or [])
|
|
287
|
+
if loop_config.exit_node_id:
|
|
288
|
+
boundary_nodes.add(loop_config.exit_node_id)
|
|
289
|
+
boundary_nodes.add(loop_node_id)
|
|
290
|
+
|
|
291
|
+
adjacency = cls._build_graph_adjacency(workflow_def)
|
|
292
|
+
visited = set()
|
|
293
|
+
stack = [body_entry_id]
|
|
294
|
+
|
|
295
|
+
while stack:
|
|
296
|
+
node_id = stack.pop()
|
|
297
|
+
if node_id in visited:
|
|
298
|
+
continue
|
|
299
|
+
visited.add(node_id)
|
|
300
|
+
|
|
301
|
+
if node_id == current_node_id:
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
for next_id in adjacency.get(node_id, []):
|
|
305
|
+
if next_id in boundary_nodes:
|
|
306
|
+
continue
|
|
307
|
+
if next_id not in visited:
|
|
308
|
+
stack.append(next_id)
|
|
309
|
+
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
@classmethod
|
|
313
|
+
def _resolve_loop_reentry_controller(
|
|
314
|
+
cls,
|
|
315
|
+
workflow_def: wt.WorkflowDefinition,
|
|
316
|
+
current_node_id: str,
|
|
317
|
+
) -> Optional[str]:
|
|
318
|
+
"""为循环体内无下游节点查找应回派的 LOOP 控制节点。
|
|
319
|
+
Resolve LOOP controller for body-node dead-end re-dispatch.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
for candidate_node_id, candidate_raw_config in workflow_def.nodes_config.items():
|
|
323
|
+
candidate_node = wt.NodeConfig.model_validate(candidate_raw_config)
|
|
324
|
+
if candidate_node.type != wt.NodeType.LOOP:
|
|
325
|
+
continue
|
|
326
|
+
if cls._is_node_in_loop_body_subgraph(
|
|
327
|
+
workflow_def=workflow_def,
|
|
328
|
+
loop_node_id=candidate_node_id,
|
|
329
|
+
loop_node=candidate_node,
|
|
330
|
+
current_node_id=current_node_id,
|
|
331
|
+
):
|
|
332
|
+
return candidate_node_id
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
async def _resolve_run_definition(self, run: wt.WorkflowRun) -> Optional[wt.WorkflowDefinition]:
|
|
336
|
+
"""优先使用运行绑定的快照,还原对应的工作流定义
|
|
337
|
+
Prefer the definition snapshot bound to the run before hitting live storage."""
|
|
338
|
+
|
|
339
|
+
if run.definition_snapshot:
|
|
340
|
+
try:
|
|
341
|
+
return wt.WorkflowDefinition.model_validate(run.definition_snapshot)
|
|
342
|
+
except Exception as exc: # noqa: BLE001 - fallback to DB fetch when snapshot corrupt
|
|
343
|
+
logger.error(
|
|
344
|
+
"Failed to hydrate workflow definition snapshot for run %s: %s",
|
|
345
|
+
run.id,
|
|
346
|
+
exc,
|
|
347
|
+
exc_info=True,
|
|
348
|
+
)
|
|
349
|
+
return await workflow_op.get_workflow_definition_by_id(run.workflow_id)
|
|
350
|
+
|
|
351
|
+
async def _emit_engine_alert(
|
|
352
|
+
self,
|
|
353
|
+
*,
|
|
354
|
+
run_id: Optional[uuid.UUID],
|
|
355
|
+
node_id: str,
|
|
356
|
+
loop_index: int,
|
|
357
|
+
reason: WorkflowEngineAlertReason,
|
|
358
|
+
detail: wt.WorkflowEngineAlertDetail,
|
|
359
|
+
severity: WorkflowEngineAlertSeverity = WorkflowEngineAlertSeverity.CRITICAL,
|
|
360
|
+
category: WorkflowEngineAlertCategory = WorkflowEngineAlertCategory.ORCHESTRATOR_GUARD,
|
|
361
|
+
) -> None:
|
|
362
|
+
"""持久化工作流引擎告警,确保运维可见 / Persist workflow engine alert for operator visibility."""
|
|
363
|
+
|
|
364
|
+
payload = wt.WorkflowEngineAlertCreate(
|
|
365
|
+
run_id=run_id,
|
|
366
|
+
node_id=node_id,
|
|
367
|
+
loop_index=max(loop_index, 1),
|
|
368
|
+
reason=reason,
|
|
369
|
+
detail=detail,
|
|
370
|
+
severity=severity,
|
|
371
|
+
category=category,
|
|
372
|
+
)
|
|
373
|
+
try:
|
|
374
|
+
await workflow_op.create_engine_alert(payload)
|
|
375
|
+
except Exception as exc: # noqa: BLE001 - best effort alert persistence
|
|
376
|
+
logger.error("Failed to persist workflow engine alert: %s", exc, exc_info=True)
|
|
377
|
+
|
|
378
|
+
async def _execute_node_orchestrator(self, run_id_str: str, node_id: str, loop_index: int):
|
|
379
|
+
"""协调单个节点执行(加载上下文 -> 执行 -> 落库 -> 派发下一节点)。"""
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
run_id = uuid.UUID(run_id_str)
|
|
383
|
+
except ValueError:
|
|
384
|
+
logger.error(f"Invalid run ID format: {run_id_str}")
|
|
385
|
+
await self._emit_engine_alert(
|
|
386
|
+
run_id=None,
|
|
387
|
+
node_id=node_id,
|
|
388
|
+
loop_index=loop_index,
|
|
389
|
+
reason=WorkflowEngineAlertReason.INVALID_RUN_ID,
|
|
390
|
+
detail=wt.WorkflowEngineAlertDetail(run_id=run_id_str),
|
|
391
|
+
)
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
# 1. 基础数据加载 / Load run + definition metadata
|
|
395
|
+
run = await workflow_op.get_workflow_run_by_id(run_id)
|
|
396
|
+
if not run:
|
|
397
|
+
await self._emit_engine_alert(
|
|
398
|
+
run_id=run_id,
|
|
399
|
+
node_id=node_id,
|
|
400
|
+
loop_index=loop_index,
|
|
401
|
+
reason=WorkflowEngineAlertReason.RUN_NOT_FOUND,
|
|
402
|
+
detail=wt.WorkflowEngineAlertDetail(run_id=run_id_str),
|
|
403
|
+
)
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
run_reqid = self._extract_reqid_from_run(run)
|
|
407
|
+
set_request_id(run_reqid or None)
|
|
408
|
+
|
|
409
|
+
if run.status in [wt.WorkflowStatus.FAILURE, wt.WorkflowStatus.CANCELLED]:
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
workflow_def = await self._resolve_run_definition(run)
|
|
413
|
+
if not workflow_def:
|
|
414
|
+
await self._emit_engine_alert(
|
|
415
|
+
run_id=run_id,
|
|
416
|
+
node_id=node_id,
|
|
417
|
+
loop_index=loop_index,
|
|
418
|
+
reason=WorkflowEngineAlertReason.WORKFLOW_DEFINITION_MISSING,
|
|
419
|
+
detail=wt.WorkflowEngineAlertDetail(workflow_id=str(run.workflow_id)),
|
|
420
|
+
)
|
|
421
|
+
await workflow_op.update_workflow_run_status(run_id, wt.WorkflowStatus.FAILURE)
|
|
422
|
+
return
|
|
423
|
+
raw_node_config = workflow_def.nodes_config.get(node_id)
|
|
424
|
+
if not raw_node_config:
|
|
425
|
+
logger.error(f"Node {node_id} not found in definition {workflow_def.id}")
|
|
426
|
+
await self._emit_engine_alert(
|
|
427
|
+
run_id=run_id,
|
|
428
|
+
node_id=node_id,
|
|
429
|
+
loop_index=loop_index,
|
|
430
|
+
reason=WorkflowEngineAlertReason.NODE_CONFIG_MISSING,
|
|
431
|
+
detail=wt.WorkflowEngineAlertDetail(
|
|
432
|
+
workflow_id=str(workflow_def.id),
|
|
433
|
+
node_id=node_id,
|
|
434
|
+
),
|
|
435
|
+
)
|
|
436
|
+
await workflow_op.update_workflow_run_status(run_id, wt.WorkflowStatus.FAILURE)
|
|
437
|
+
return
|
|
438
|
+
node_config = wt.NodeConfig.model_validate(raw_node_config)
|
|
439
|
+
|
|
440
|
+
# 2. 准备节点实例与执行上下文 / Prepare node instance + execution context snapshot
|
|
441
|
+
all_instances = await workflow_op.get_node_instances_by_run_id(run_id)
|
|
442
|
+
instance = await workflow_op.upsert_node_instance(
|
|
443
|
+
run_id=run_id,
|
|
444
|
+
node_id=node_id,
|
|
445
|
+
loop_index=loop_index,
|
|
446
|
+
)
|
|
447
|
+
nodes_map = {
|
|
448
|
+
inst.node_id: wt.NodeOutput(output=inst.final_output, status=inst.status)
|
|
449
|
+
for inst in all_instances
|
|
450
|
+
}
|
|
451
|
+
nodes_map[instance.node_id] = wt.NodeOutput(output=instance.final_output, status=instance.status)
|
|
452
|
+
|
|
453
|
+
callback_url = None
|
|
454
|
+
if self._requires_async_callback(node_config):
|
|
455
|
+
callback_url = self._build_callback_url(instance.id)
|
|
456
|
+
|
|
457
|
+
context = wt.WorkflowExecutionContext(
|
|
458
|
+
run_id=run_id,
|
|
459
|
+
global_context=run.global_context,
|
|
460
|
+
nodes=nodes_map,
|
|
461
|
+
loop_index=loop_index,
|
|
462
|
+
callback_url=callback_url,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# 3. 创建执行日志骨架 / Create execution log skeleton
|
|
466
|
+
log_entry = await workflow_op.create_execution_log(
|
|
467
|
+
node_instance_id=instance.id,
|
|
468
|
+
attempt_no=instance.attempt_count,
|
|
469
|
+
request_snapshot=run.global_context,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
# 4. 调用领域引擎执行节点 / Run node handler via workflow_engine
|
|
474
|
+
output, status, next_node_ids, new_loop_index = await workflow_engine.process_node(
|
|
475
|
+
workflow_def,
|
|
476
|
+
node_config,
|
|
477
|
+
context,
|
|
478
|
+
)
|
|
479
|
+
serialized_output = self._serialize_output_payload(output)
|
|
480
|
+
|
|
481
|
+
# 5. 持久化节点结果 / Persist node + log result (if not pending)
|
|
482
|
+
if status != wt.NodeStatus.PENDING:
|
|
483
|
+
await workflow_op.update_node_instance_result(
|
|
484
|
+
instance_id=instance.id,
|
|
485
|
+
status=status,
|
|
486
|
+
final_output=serialized_output,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
updated_context = self._merge_runtime_context(
|
|
490
|
+
current_context=run.global_context,
|
|
491
|
+
node_id=node_id,
|
|
492
|
+
node_status=status,
|
|
493
|
+
output=serialized_output,
|
|
494
|
+
)
|
|
495
|
+
updated_context = self._apply_action_context_mappings(
|
|
496
|
+
current_context=updated_context,
|
|
497
|
+
node_config=node_config,
|
|
498
|
+
output=serialized_output,
|
|
499
|
+
)
|
|
500
|
+
updated_run = await workflow_op.update_workflow_run_global_context(
|
|
501
|
+
run_id=run_id,
|
|
502
|
+
global_context=updated_context,
|
|
503
|
+
)
|
|
504
|
+
if updated_run:
|
|
505
|
+
run = updated_run
|
|
506
|
+
|
|
507
|
+
await workflow_op.update_execution_log_result(
|
|
508
|
+
log_id=log_entry.id,
|
|
509
|
+
status=ExecutionLogStatus.SUCCESS,
|
|
510
|
+
response_snapshot=serialized_output,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if status == wt.NodeStatus.FAILURE:
|
|
514
|
+
await workflow_op.update_workflow_run_status(run_id, wt.WorkflowStatus.FAILURE)
|
|
515
|
+
|
|
516
|
+
# 6. 派发下一批节点 / Dispatch downstream nodes
|
|
517
|
+
for next_id in next_node_ids:
|
|
518
|
+
enqueue_node_execution(run_id_str, next_id, new_loop_index)
|
|
519
|
+
|
|
520
|
+
if node_config.type == NodeType.END and status == wt.NodeStatus.SUCCESS:
|
|
521
|
+
# 结束节点成功意味着流程整体完成
|
|
522
|
+
# Successful END node completion marks the entire run as SUCCESS
|
|
523
|
+
await workflow_op.update_workflow_run_status(run_id, wt.WorkflowStatus.SUCCESS)
|
|
524
|
+
elif status == wt.NodeStatus.SUCCESS and not next_node_ids:
|
|
525
|
+
loop_reentry_node_id = self._resolve_loop_reentry_controller(
|
|
526
|
+
workflow_def=workflow_def,
|
|
527
|
+
current_node_id=node_id,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
if loop_reentry_node_id:
|
|
531
|
+
loop_controller_raw = workflow_def.nodes_config.get(loop_reentry_node_id)
|
|
532
|
+
if loop_controller_raw:
|
|
533
|
+
loop_controller = wt.NodeConfig.model_validate(loop_controller_raw)
|
|
534
|
+
else:
|
|
535
|
+
loop_controller = None
|
|
536
|
+
|
|
537
|
+
loop_controller_config = loop_controller.config if loop_controller else None
|
|
538
|
+
if isinstance(loop_controller_config, wt.LoopNodeConfig) and loop_controller is not None:
|
|
539
|
+
if new_loop_index >= loop_controller_config.max_iterations:
|
|
540
|
+
exit_targets = []
|
|
541
|
+
if loop_controller_config.exit_node_id:
|
|
542
|
+
exit_targets.append(loop_controller_config.exit_node_id)
|
|
543
|
+
elif loop_controller.next_node_ids:
|
|
544
|
+
exit_targets.extend(loop_controller.next_node_ids)
|
|
545
|
+
|
|
546
|
+
if exit_targets:
|
|
547
|
+
for exit_node_id in exit_targets:
|
|
548
|
+
enqueue_node_execution(run_id_str, exit_node_id, new_loop_index)
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
# LOOP 循环体末端节点允许省略显式回边,此处自动回派 LOOP 控制节点
|
|
552
|
+
# Loop body tail nodes can omit explicit back-edge; auto-dispatch loop control node
|
|
553
|
+
enqueue_node_execution(run_id_str, loop_reentry_node_id, new_loop_index + 1)
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
# 非 END 节点执行成功但无下游节点,视为编排断点并终止流程
|
|
557
|
+
# Successful non-END node without downstream nodes is treated as an orchestration dead-end
|
|
558
|
+
await self._emit_engine_alert(
|
|
559
|
+
run_id=run_id,
|
|
560
|
+
node_id=node_id,
|
|
561
|
+
loop_index=loop_index,
|
|
562
|
+
reason=WorkflowEngineAlertReason.NODE_TERMINATED_WITHOUT_DOWNSTREAM,
|
|
563
|
+
detail=wt.WorkflowEngineAlertDetail(
|
|
564
|
+
run_id=str(run_id),
|
|
565
|
+
workflow_id=str(workflow_def.id),
|
|
566
|
+
node_id=node_id,
|
|
567
|
+
error="Node completed successfully but no downstream nodes were dispatched",
|
|
568
|
+
),
|
|
569
|
+
severity=WorkflowEngineAlertSeverity.MAJOR,
|
|
570
|
+
category=WorkflowEngineAlertCategory.ORCHESTRATOR_GUARD,
|
|
571
|
+
)
|
|
572
|
+
await workflow_op.update_workflow_run_status(run_id, wt.WorkflowStatus.FAILURE)
|
|
573
|
+
|
|
574
|
+
except Exception as exc:
|
|
575
|
+
logger.error(
|
|
576
|
+
"Node execution failed for instance %s: %s",
|
|
577
|
+
instance.id,
|
|
578
|
+
exc,
|
|
579
|
+
exc_info=True,
|
|
580
|
+
)
|
|
581
|
+
await self._emit_engine_alert(
|
|
582
|
+
run_id=run_id,
|
|
583
|
+
node_id=node_id,
|
|
584
|
+
loop_index=loop_index,
|
|
585
|
+
reason=WorkflowEngineAlertReason.NODE_EXECUTION_EXCEPTION,
|
|
586
|
+
detail=wt.WorkflowEngineAlertDetail(
|
|
587
|
+
run_id=str(run_id),
|
|
588
|
+
node_id=node_id,
|
|
589
|
+
error=str(exc),
|
|
590
|
+
),
|
|
591
|
+
severity=WorkflowEngineAlertSeverity.MAJOR,
|
|
592
|
+
category=WorkflowEngineAlertCategory.TRANSPORT_PIPELINE,
|
|
593
|
+
)
|
|
594
|
+
error_payload = {"error": str(exc)}
|
|
595
|
+
await workflow_op.update_node_instance_result(
|
|
596
|
+
instance_id=instance.id,
|
|
597
|
+
status=wt.NodeStatus.FAILURE,
|
|
598
|
+
final_output=error_payload,
|
|
599
|
+
error_msg=str(exc),
|
|
600
|
+
)
|
|
601
|
+
await workflow_op.update_execution_log_result(
|
|
602
|
+
log_id=log_entry.id,
|
|
603
|
+
status=ExecutionLogStatus.FAILURE,
|
|
604
|
+
response_snapshot=error_payload,
|
|
605
|
+
error_detail=str(exc),
|
|
606
|
+
)
|
|
607
|
+
await workflow_op.update_workflow_run_status(run_id, wt.WorkflowStatus.FAILURE)
|
|
608
|
+
|
|
609
|
+
async def create_workflow_definition(self, data: wt.WorkflowDefinitionCreate) -> wt.WorkflowDefinition:
|
|
610
|
+
"""创建工作流定义 / Persist workflow definition metadata."""
|
|
611
|
+
|
|
612
|
+
if not data.name or not data.name.strip():
|
|
613
|
+
raise ParamError("Workflow name cannot be empty")
|
|
614
|
+
return await workflow_op.create_workflow_definition(data)
|
|
615
|
+
|
|
616
|
+
async def list_workflow_definitions(self, page: int = 1, page_size: int = 20) -> wt.WorkflowListResponse:
|
|
617
|
+
"""分页列出工作流定义 / List workflow definitions with pagination metadata."""
|
|
618
|
+
|
|
619
|
+
limit = page_size
|
|
620
|
+
offset = (page - 1) * page_size
|
|
621
|
+
workflows, total = await workflow_op.list_workflow_definitions(limit, offset)
|
|
622
|
+
return wt.WorkflowListResponse(
|
|
623
|
+
list=workflows,
|
|
624
|
+
total=total,
|
|
625
|
+
page=page,
|
|
626
|
+
page_size=page_size,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
async def get_workflow_definition(self, workflow_id_str: str) -> wt.WorkflowDefinition:
|
|
630
|
+
"""查询工作流定义详情 / Fetch workflow definition by ID."""
|
|
631
|
+
|
|
632
|
+
try:
|
|
633
|
+
workflow_uuid = uuid.UUID(workflow_id_str)
|
|
634
|
+
except ValueError:
|
|
635
|
+
raise ParamError(f"Invalid workflow ID format: {workflow_id_str}")
|
|
636
|
+
|
|
637
|
+
workflow = await workflow_op.get_workflow_definition_by_id(workflow_uuid)
|
|
638
|
+
if not workflow:
|
|
639
|
+
raise HTTPException(status_code=404, msg="Workflow not found")
|
|
640
|
+
return workflow
|
|
641
|
+
|
|
642
|
+
async def get_scheduled_workflows(self) -> List[wt.WorkflowDefinition]:
|
|
643
|
+
"""获取所有启用的定时工作流 / Return active scheduled workflows."""
|
|
644
|
+
|
|
645
|
+
return await workflow_op.get_scheduled_workflows()
|
|
646
|
+
|
|
647
|
+
async def list_workflow_versions(self, workflow_id_str: str, limit: int = 50) -> wt.WorkflowVersionListResponse:
|
|
648
|
+
"""列出工作流版本历史
|
|
649
|
+
List workflow definition versions with optional limit."""
|
|
650
|
+
|
|
651
|
+
try:
|
|
652
|
+
workflow_uuid = uuid.UUID(workflow_id_str)
|
|
653
|
+
except ValueError:
|
|
654
|
+
raise ParamError(f"Invalid workflow ID format: {workflow_id_str}")
|
|
655
|
+
|
|
656
|
+
versions = await workflow_op.list_workflow_versions(workflow_uuid, limit=limit)
|
|
657
|
+
return wt.WorkflowVersionListResponse(workflow_id=workflow_uuid, versions=versions)
|
|
658
|
+
|
|
659
|
+
async def list_workflow_runs(self, query: wt.WorkflowRunQuery) -> wt.WorkflowRunListResponse:
|
|
660
|
+
"""查询运行实例 / List workflow runs with optional filtering and pagination."""
|
|
661
|
+
|
|
662
|
+
workflow_id = None
|
|
663
|
+
if query.workflow_id:
|
|
664
|
+
try:
|
|
665
|
+
workflow_id = uuid.UUID(query.workflow_id)
|
|
666
|
+
except ValueError:
|
|
667
|
+
raise ParamError(f"Invalid workflow ID format: {query.workflow_id}")
|
|
668
|
+
|
|
669
|
+
runs, total = await workflow_op.list_workflow_runs(
|
|
670
|
+
workflow_id=workflow_id,
|
|
671
|
+
reqid=query.reqid,
|
|
672
|
+
limit=query.page_size,
|
|
673
|
+
offset=(query.page - 1) * query.page_size,
|
|
674
|
+
status=query.status,
|
|
675
|
+
date_from=query.date_from,
|
|
676
|
+
date_to=query.date_to,
|
|
677
|
+
)
|
|
678
|
+
return wt.WorkflowRunListResponse(
|
|
679
|
+
list=runs,
|
|
680
|
+
total=total,
|
|
681
|
+
page=query.page,
|
|
682
|
+
page_size=query.page_size,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
async def trigger_workflow(
|
|
686
|
+
self,
|
|
687
|
+
workflow_id_str: str,
|
|
688
|
+
payload: wt.WorkflowTriggerRequest,
|
|
689
|
+
trigger_type: wt.TriggerType = wt.TriggerType.API,
|
|
690
|
+
) -> wt.WorkflowTriggerResponse:
|
|
691
|
+
"""触发工作流执行 / Trigger a workflow run and enqueue start nodes."""
|
|
692
|
+
|
|
693
|
+
workflow = await self.get_workflow_definition(workflow_id_str)
|
|
694
|
+
definition_snapshot = self._build_definition_snapshot(workflow)
|
|
695
|
+
normalized_trigger_context = self._normalize_trigger_context(payload.trigger_context)
|
|
696
|
+
run = await workflow_op.create_workflow_run(
|
|
697
|
+
wt.WorkflowRunCreate(
|
|
698
|
+
workflow_id=workflow.id,
|
|
699
|
+
trigger_type=trigger_type,
|
|
700
|
+
trigger_context=normalized_trigger_context,
|
|
701
|
+
global_context=dict(normalized_trigger_context),
|
|
702
|
+
definition_version=workflow.version,
|
|
703
|
+
definition_snapshot=definition_snapshot,
|
|
704
|
+
)
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
dispatch_status = wt.TriggerDispatchStatus.QUEUED_NO_ENTRY
|
|
708
|
+
start_node: Optional[wt.NodeConfig] = None
|
|
709
|
+
|
|
710
|
+
for cfg in workflow.nodes_config.values():
|
|
711
|
+
candidate = wt.NodeConfig.model_validate(cfg)
|
|
712
|
+
if candidate.type == NodeType.START:
|
|
713
|
+
start_node = candidate
|
|
714
|
+
break
|
|
715
|
+
|
|
716
|
+
if start_node:
|
|
717
|
+
next_ids = start_node.next_node_ids or []
|
|
718
|
+
if next_ids:
|
|
719
|
+
# 标记运行已开始,便于前端展示实时状态
|
|
720
|
+
# Mark run as RUNNING so UI reflects in-progress execution
|
|
721
|
+
await workflow_op.update_workflow_run_status(run.id, wt.WorkflowStatus.RUNNING)
|
|
722
|
+
for nid in next_ids:
|
|
723
|
+
enqueue_node_execution(str(run.id), nid, 1)
|
|
724
|
+
if next_ids:
|
|
725
|
+
dispatch_status = wt.TriggerDispatchStatus.DISPATCHED
|
|
726
|
+
|
|
727
|
+
return wt.WorkflowTriggerResponse(run_id=str(run.id), dispatch_status=dispatch_status)
|
|
728
|
+
|
|
729
|
+
async def get_run_graph_status(self, run_id_str: str) -> wt.RunGraphResponse:
|
|
730
|
+
"""查询运行图谱状态 / Return DAG visualization payload for a workflow run."""
|
|
731
|
+
|
|
732
|
+
try:
|
|
733
|
+
run_id = uuid.UUID(run_id_str)
|
|
734
|
+
except ValueError:
|
|
735
|
+
raise ParamError(f"Invalid run ID format: {run_id_str}")
|
|
736
|
+
|
|
737
|
+
run = await workflow_op.get_workflow_run_by_id(run_id)
|
|
738
|
+
if not run:
|
|
739
|
+
raise HTTPException(status_code=404, msg="Run not found")
|
|
740
|
+
|
|
741
|
+
workflow_def = await self._resolve_run_definition(run)
|
|
742
|
+
if not workflow_def:
|
|
743
|
+
raise HTTPException(status_code=404, msg="Workflow definition snapshot missing")
|
|
744
|
+
graph_data = workflow_def.graph_data or wt.WorkflowGraphData()
|
|
745
|
+
instances = await workflow_op.get_node_instances_by_run_id(run_id)
|
|
746
|
+
instance_map = {inst.node_id: inst for inst in instances}
|
|
747
|
+
|
|
748
|
+
def _to_plain_dict(raw_config: Any) -> Optional[Dict[str, Any]]:
|
|
749
|
+
if raw_config is None:
|
|
750
|
+
return None
|
|
751
|
+
if isinstance(raw_config, dict):
|
|
752
|
+
return raw_config
|
|
753
|
+
if isinstance(raw_config, wt.NodeConfig):
|
|
754
|
+
return raw_config.model_dump(mode="json")
|
|
755
|
+
return None
|
|
756
|
+
|
|
757
|
+
graph_nodes: List[wt.RunGraphNode] = []
|
|
758
|
+
for node in graph_data.nodes:
|
|
759
|
+
inst = instance_map.get(node.node_id)
|
|
760
|
+
node_config_snapshot = _to_plain_dict(workflow_def.nodes_config.get(node.node_id))
|
|
761
|
+
graph_nodes.append(
|
|
762
|
+
wt.RunGraphNode(
|
|
763
|
+
node_id=node.node_id,
|
|
764
|
+
label=node.label,
|
|
765
|
+
type=node.type,
|
|
766
|
+
status=inst.status if inst else wt.NodeStatus.PENDING,
|
|
767
|
+
loop_index=inst.loop_index if inst else 1,
|
|
768
|
+
position=node.position,
|
|
769
|
+
ui=node.ui,
|
|
770
|
+
last_output=inst.final_output if inst else None,
|
|
771
|
+
node_config_snapshot=node_config_snapshot,
|
|
772
|
+
)
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
# Include orphan node instances (if they no longer exist in graph metadata)
|
|
776
|
+
for node_id, inst in instance_map.items():
|
|
777
|
+
if any(existing.node_id == node_id for existing in graph_nodes):
|
|
778
|
+
continue
|
|
779
|
+
node_config_snapshot = _to_plain_dict(workflow_def.nodes_config.get(node_id))
|
|
780
|
+
graph_nodes.append(
|
|
781
|
+
wt.RunGraphNode(
|
|
782
|
+
node_id=node_id,
|
|
783
|
+
label=node_id,
|
|
784
|
+
type=wt.NodeType.ACTION,
|
|
785
|
+
status=inst.status,
|
|
786
|
+
loop_index=inst.loop_index,
|
|
787
|
+
position=None,
|
|
788
|
+
ui={},
|
|
789
|
+
last_output=inst.final_output,
|
|
790
|
+
node_config_snapshot=node_config_snapshot,
|
|
791
|
+
)
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
return wt.RunGraphResponse(
|
|
795
|
+
run_id=str(run.id),
|
|
796
|
+
status=run.status,
|
|
797
|
+
graph_version=graph_data.version,
|
|
798
|
+
nodes=graph_nodes,
|
|
799
|
+
edges=graph_data.edges,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
async def get_node_history(self, run_id_str: str, node_id: str) -> wt.NodeExecutionHistoryResponse:
|
|
803
|
+
"""查询单个节点的执行历史 / Fetch node execution history for a run."""
|
|
804
|
+
|
|
805
|
+
try:
|
|
806
|
+
run_id = uuid.UUID(run_id_str)
|
|
807
|
+
except ValueError:
|
|
808
|
+
raise ParamError(f"Invalid run ID format: {run_id_str}")
|
|
809
|
+
|
|
810
|
+
instances = await workflow_op.get_node_instances_by_run_and_node_id(run_id, node_id)
|
|
811
|
+
history: List[wt.NodeExecutionHistory] = []
|
|
812
|
+
for inst in instances:
|
|
813
|
+
logs = await workflow_op.get_execution_logs_by_node_instance_id(inst.id)
|
|
814
|
+
history.append(
|
|
815
|
+
wt.NodeExecutionHistory(
|
|
816
|
+
node_id=inst.node_id,
|
|
817
|
+
loop_index=inst.loop_index,
|
|
818
|
+
status=inst.status,
|
|
819
|
+
output=inst.final_output,
|
|
820
|
+
logs=logs,
|
|
821
|
+
)
|
|
822
|
+
)
|
|
823
|
+
return wt.NodeExecutionHistoryResponse(histories=history)
|
|
824
|
+
|
|
825
|
+
async def handle_external_callback(
|
|
826
|
+
self,
|
|
827
|
+
node_instance_id_str: str,
|
|
828
|
+
payload: Dict[str, Any],
|
|
829
|
+
) -> wt.CallbackAckResponse:
|
|
830
|
+
"""处理异步回调并继续运行 / Persist async callback payload then resume downstream nodes."""
|
|
831
|
+
|
|
832
|
+
node_instance_id = uuid.UUID(node_instance_id_str)
|
|
833
|
+
instance = await workflow_op.get_node_instance_by_id(node_instance_id)
|
|
834
|
+
if not instance or instance.status != wt.NodeStatus.SUSPENDED:
|
|
835
|
+
logger.warning("Callback received for non-suspended instance: %s", node_instance_id)
|
|
836
|
+
return wt.CallbackAckResponse(status=CallbackAckStatus.IGNORED)
|
|
837
|
+
|
|
838
|
+
await workflow_op.update_node_instance_result(
|
|
839
|
+
node_instance_id,
|
|
840
|
+
wt.NodeStatus.SUCCESS,
|
|
841
|
+
final_output=self._serialize_output_payload(payload),
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
run = await workflow_op.get_workflow_run_by_id(instance.run_id)
|
|
845
|
+
if not run:
|
|
846
|
+
logger.error("Run %s not found while handling callback", instance.run_id)
|
|
847
|
+
return wt.CallbackAckResponse(status=CallbackAckStatus.IGNORED)
|
|
848
|
+
|
|
849
|
+
run_reqid = self._extract_reqid_from_run(run)
|
|
850
|
+
set_request_id(run_reqid or None)
|
|
851
|
+
|
|
852
|
+
workflow_def = await self._resolve_run_definition(run)
|
|
853
|
+
if not workflow_def:
|
|
854
|
+
logger.error("Workflow definition missing for run %s during callback", run.id)
|
|
855
|
+
return wt.CallbackAckResponse(status=CallbackAckStatus.IGNORED)
|
|
856
|
+
raw_node_config = workflow_def.nodes_config.get(instance.node_id)
|
|
857
|
+
if not raw_node_config:
|
|
858
|
+
logger.error("Node %s not found in workflow %s during callback", instance.node_id, workflow_def.id)
|
|
859
|
+
return wt.CallbackAckResponse(status=CallbackAckStatus.IGNORED)
|
|
860
|
+
node_config = wt.NodeConfig.model_validate(raw_node_config)
|
|
861
|
+
|
|
862
|
+
for nid in node_config.next_node_ids or []:
|
|
863
|
+
enqueue_node_execution(str(run.id), nid, instance.loop_index)
|
|
864
|
+
|
|
865
|
+
return wt.CallbackAckResponse(status=CallbackAckStatus.ACCEPTED)
|
|
866
|
+
|
|
867
|
+
async def get_workflow_stats(self) -> wt.WorkflowStats:
|
|
868
|
+
"""获取工作流统计数据 / Get workflow statistics."""
|
|
869
|
+
return await workflow_op.get_workflow_stats()
|
|
870
|
+
|
|
871
|
+
async def update_workflow_definition(
|
|
872
|
+
self,
|
|
873
|
+
workflow_id_str: str,
|
|
874
|
+
data: wt.WorkflowDefinitionUpdate
|
|
875
|
+
) -> wt.WorkflowDefinition:
|
|
876
|
+
"""
|
|
877
|
+
更新工作流定义(通过 domain 层,带分布式锁保护)
|
|
878
|
+
Update workflow definition (via domain layer with distributed lock protection).
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
workflow_id_str: 工作流唯一标识符 / Workflow unique identifier.
|
|
882
|
+
data: 更新数据 / Update data.
|
|
883
|
+
|
|
884
|
+
Returns:
|
|
885
|
+
更新后的工作流定义 / The updated workflow definition.
|
|
886
|
+
"""
|
|
887
|
+
try:
|
|
888
|
+
workflow_uuid = uuid.UUID(workflow_id_str)
|
|
889
|
+
except ValueError:
|
|
890
|
+
raise ParamError(f"Invalid workflow ID format: {workflow_id_str}")
|
|
891
|
+
|
|
892
|
+
# 通过 domain 层调用,自动获取分布式锁
|
|
893
|
+
# Call via domain layer, automatically acquires distributed lock
|
|
894
|
+
updated = await workflow_domain.update_workflow_definition(workflow_uuid, data)
|
|
895
|
+
if not updated:
|
|
896
|
+
raise HTTPException(status_code=404, msg="Workflow not found")
|
|
897
|
+
|
|
898
|
+
return updated
|
|
899
|
+
|
|
900
|
+
async def delete_workflow_definition(self, workflow_id_str: str) -> None:
|
|
901
|
+
"""
|
|
902
|
+
删除工作流定义(通过 domain 层,带分布式锁保护)
|
|
903
|
+
Delete workflow definition (via domain layer with distributed lock protection).
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
workflow_id_str: 工作流唯一标识符 / Workflow unique identifier.
|
|
907
|
+
"""
|
|
908
|
+
try:
|
|
909
|
+
workflow_uuid = uuid.UUID(workflow_id_str)
|
|
910
|
+
except ValueError:
|
|
911
|
+
raise ParamError(f"Invalid workflow ID format: {workflow_id_str}")
|
|
912
|
+
|
|
913
|
+
# 软删除:通过 domain 层设置 flag=1,自动获取分布式锁
|
|
914
|
+
# Soft delete: set flag=1 via domain layer, automatically acquires distributed lock
|
|
915
|
+
await workflow_domain.update_workflow_definition(
|
|
916
|
+
workflow_uuid,
|
|
917
|
+
wt.WorkflowDefinitionUpdate(flag=1)
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
async def rollback_workflow_definition(
|
|
921
|
+
self,
|
|
922
|
+
workflow_id_str: str,
|
|
923
|
+
request: wt.WorkflowRollbackRequest,
|
|
924
|
+
) -> wt.WorkflowDefinition:
|
|
925
|
+
"""回滚工作流定义到指定版本
|
|
926
|
+
Roll back a workflow definition to a target version."""
|
|
927
|
+
|
|
928
|
+
try:
|
|
929
|
+
workflow_uuid = uuid.UUID(workflow_id_str)
|
|
930
|
+
except ValueError:
|
|
931
|
+
raise ParamError(f"Invalid workflow ID format: {workflow_id_str}")
|
|
932
|
+
|
|
933
|
+
updated = await workflow_domain.rollback_workflow_definition(
|
|
934
|
+
workflow_uuid,
|
|
935
|
+
target_version=request.target_version,
|
|
936
|
+
reason=request.reason,
|
|
937
|
+
)
|
|
938
|
+
if not updated:
|
|
939
|
+
raise HTTPException(status_code=404, msg="Workflow version not found")
|
|
940
|
+
return updated
|
|
941
|
+
|
|
942
|
+
async def list_engine_alerts(
|
|
943
|
+
self,
|
|
944
|
+
query: wt.WorkflowEngineAlertQuery,
|
|
945
|
+
) -> wt.WorkflowEngineAlertListResponse:
|
|
946
|
+
"""查询工作流引擎告警 / List workflow engine alerts for operators."""
|
|
947
|
+
|
|
948
|
+
return await workflow_op.list_engine_alerts(query)
|
|
949
|
+
|
|
950
|
+
async def acknowledge_engine_alert(
|
|
951
|
+
self,
|
|
952
|
+
alert_id_str: str,
|
|
953
|
+
payload: wt.WorkflowEngineAlertAckRequest,
|
|
954
|
+
) -> wt.WorkflowEngineAlert:
|
|
955
|
+
"""标记引擎告警为已知晓 / Acknowledge an engine alert."""
|
|
956
|
+
|
|
957
|
+
try:
|
|
958
|
+
alert_id = uuid.UUID(alert_id_str)
|
|
959
|
+
except ValueError:
|
|
960
|
+
raise ParamError(f"Invalid alert ID format: {alert_id_str}")
|
|
961
|
+
|
|
962
|
+
operator = (payload.operator or "ops_console").strip() or "ops_console"
|
|
963
|
+
updated = await workflow_op.acknowledge_engine_alert(alert_id, operator, payload.note)
|
|
964
|
+
if not updated:
|
|
965
|
+
raise HTTPException(status_code=404, msg="Alert not found or already acknowledged")
|
|
966
|
+
return updated
|
|
967
|
+
|
|
968
|
+
async def resolve_engine_alert(
|
|
969
|
+
self,
|
|
970
|
+
alert_id_str: str,
|
|
971
|
+
payload: wt.WorkflowEngineAlertAckRequest,
|
|
972
|
+
) -> wt.WorkflowEngineAlert:
|
|
973
|
+
"""标记引擎告警为已解决 / Resolve an engine alert."""
|
|
974
|
+
|
|
975
|
+
try:
|
|
976
|
+
alert_id = uuid.UUID(alert_id_str)
|
|
977
|
+
except ValueError:
|
|
978
|
+
raise ParamError(f"Invalid alert ID format: {alert_id_str}")
|
|
979
|
+
|
|
980
|
+
operator = (payload.operator or "ops_console").strip() or "ops_console"
|
|
981
|
+
updated = await workflow_op.resolve_engine_alert(alert_id, operator, payload.note)
|
|
982
|
+
if not updated:
|
|
983
|
+
raise HTTPException(status_code=404, msg="Alert not found or already resolved")
|
|
984
|
+
return updated
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
workflow_app = WorkflowApp()
|
|
988
|
+
|
|
989
|
+
# Register inline orchestrator to avoid circular imports.
|
|
990
|
+
# 注册内联 orchestrator,避免 workflow_queue -> workflow_app 的循环依赖。
|
|
991
|
+
register_inline_orchestrator(workflow_app._execute_node_orchestrator)
|