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.
Files changed (440) hide show
  1. cancan_microstack/__init__.py +14 -0
  2. cancan_microstack/__version__.py +10 -0
  3. cancan_microstack/assets/__init__.py +6 -0
  4. cancan_microstack/assets/builds/caddy/Caddyfile +187 -0
  5. cancan_microstack/assets/builds/caddy/DEPLOYMENT.md +303 -0
  6. cancan_microstack/assets/builds/caddy/Dockerfile +46 -0
  7. cancan_microstack/assets/builds/caddy/README.md +343 -0
  8. cancan_microstack/assets/builds/caddy/geoip/README.md +5 -0
  9. cancan_microstack/assets/builds/caddy/start.sh +78 -0
  10. cancan_microstack/assets/builds/caddy/waf/coraza.conf +179 -0
  11. cancan_microstack/assets/builds/service/Dockerfile +59 -0
  12. cancan_microstack/assets/builds/service/README.md +13 -0
  13. cancan_microstack/assets/ddl/create_db.sql +22 -0
  14. cancan_microstack/assets/ddl/infra/execution_log_tbl.sql +46 -0
  15. cancan_microstack/assets/ddl/infra/node_instance_tbl.sql +56 -0
  16. cancan_microstack/assets/ddl/infra/service_action_log_tbl.sql +36 -0
  17. cancan_microstack/assets/ddl/infra/service_config_tbl.sql +26 -0
  18. cancan_microstack/assets/ddl/infra/service_info_tbl.sql +45 -0
  19. cancan_microstack/assets/ddl/infra/service_instance_tbl.sql +54 -0
  20. cancan_microstack/assets/ddl/infra/service_operation_tbl.sql +47 -0
  21. cancan_microstack/assets/ddl/infra/workflow_definition_tbl.sql +60 -0
  22. cancan_microstack/assets/ddl/infra/workflow_definition_version_tbl.sql +35 -0
  23. cancan_microstack/assets/ddl/infra/workflow_engine_alert_tbl.sql +34 -0
  24. cancan_microstack/assets/ddl/infra/workflow_run_tbl.sql +52 -0
  25. cancan_microstack/assets/ddl/ops/admin_user_tbl.sql +34 -0
  26. cancan_microstack/assets/ddl/ops/caddy_access_log_tbl.sql +91 -0
  27. cancan_microstack/assets/ddl/ops/caddy_certificate_tbl.sql +59 -0
  28. cancan_microstack/assets/ddl/ops/caddy_rate_limit_tbl.sql +64 -0
  29. cancan_microstack/assets/ddl/ops/caddy_route_tbl.sql +63 -0
  30. cancan_microstack/assets/ddl/ops/caddy_stats_tbl.sql +77 -0
  31. cancan_microstack/assets/ddl/trigger.sql +21 -0
  32. cancan_microstack/assets/docker/docker-compose.infra.yml +401 -0
  33. cancan_microstack/assets/scripts/README.md +195 -0
  34. cancan_microstack/assets/scripts/docker/build_images.sh +44 -0
  35. cancan_microstack/assets/scripts/docker/force_rebuild_images.sh +38 -0
  36. cancan_microstack/assets/scripts/docker/rebuild_all.sh +34 -0
  37. cancan_microstack/assets/scripts/docker/rebuild_compose.sh +61 -0
  38. cancan_microstack/assets/scripts/docker/restart.sh +35 -0
  39. cancan_microstack/assets/scripts/docker/restart_compose.sh +35 -0
  40. cancan_microstack/assets/scripts/docker/start.sh +78 -0
  41. cancan_microstack/assets/scripts/docker/start_all.sh +46 -0
  42. cancan_microstack/assets/scripts/docker/start_compose.sh +66 -0
  43. cancan_microstack/assets/scripts/docker/stop.sh +67 -0
  44. cancan_microstack/assets/scripts/docker/stop_all.sh +38 -0
  45. cancan_microstack/assets/scripts/docker/stop_compose.sh +38 -0
  46. cancan_microstack/assets/scripts/podman/build_images_podman.sh +59 -0
  47. cancan_microstack/assets/scripts/podman/cleanup_podman.sh +25 -0
  48. cancan_microstack/assets/scripts/podman/force_rebuild_images_podman.sh +56 -0
  49. cancan_microstack/assets/scripts/podman/rebuild_all_podman.sh +37 -0
  50. cancan_microstack/assets/scripts/podman/rebuild_compose_podman.sh +60 -0
  51. cancan_microstack/assets/scripts/podman/restart_compose_podman.sh +73 -0
  52. cancan_microstack/assets/scripts/podman/start_all_podman.sh +66 -0
  53. cancan_microstack/assets/scripts/podman/start_compose_podman.sh +80 -0
  54. cancan_microstack/assets/scripts/podman/start_podman.sh +91 -0
  55. cancan_microstack/assets/scripts/podman/stop.sh +73 -0
  56. cancan_microstack/assets/scripts/podman/stop_all_podman.sh +34 -0
  57. cancan_microstack/assets/scripts/podman/stop_compose_podman.sh +58 -0
  58. cancan_microstack/assets/scripts/start_controllersrv.sh +9 -0
  59. cancan_microstack/assets/scripts/utils/check_all_db_tables.sh +104 -0
  60. cancan_microstack/assets/scripts/utils/check_env.sh +177 -0
  61. cancan_microstack/assets/scripts/utils/check_service_management_deployment.sh +225 -0
  62. cancan_microstack/assets/scripts/utils/deploy_service_management.sh +176 -0
  63. cancan_microstack/assets/scripts/utils/force_reload_infrasrv.sh +52 -0
  64. cancan_microstack/assets/scripts/utils/monitor_service_management.sh +187 -0
  65. cancan_microstack/assets/scripts/utils/reset_postgres_volume.sh +68 -0
  66. cancan_microstack/assets/scripts/utils/test_async_operations.sh +141 -0
  67. cancan_microstack/assets/scripts/utils/verify_real_operations.sh +76 -0
  68. cancan_microstack/assets/service/Dockerfile +65 -0
  69. cancan_microstack/assets/www/adminops/assets/AppEmpty.vue_vue_type_script_setup_true_lang-BOKUurnM.js +1 -0
  70. cancan_microstack/assets/www/adminops/assets/ConfigManage-DKV5YOUz.js +1 -0
  71. cancan_microstack/assets/www/adminops/assets/ConfigManage-Y5bhy7wG.css +1 -0
  72. cancan_microstack/assets/www/adminops/assets/ConsoleManage-8ljYvCW2.js +1 -0
  73. cancan_microstack/assets/www/adminops/assets/ConsoleManage-BWpyqbuQ.css +1 -0
  74. cancan_microstack/assets/www/adminops/assets/DashboardNew-B9Nf1OPl.js +1 -0
  75. cancan_microstack/assets/www/adminops/assets/DashboardNew-DYWZKQ1V.css +1 -0
  76. cancan_microstack/assets/www/adminops/assets/LogSearch-CA0Jhe78.js +1 -0
  77. cancan_microstack/assets/www/adminops/assets/LogSearch-CCZfTNPF.css +1 -0
  78. cancan_microstack/assets/www/adminops/assets/LoginView-BId3kP3M.css +1 -0
  79. cancan_microstack/assets/www/adminops/assets/LoginView-BQZTV_Qy.js +1 -0
  80. cancan_microstack/assets/www/adminops/assets/OperationProgressDialog-BdEYwqFq.js +1 -0
  81. cancan_microstack/assets/www/adminops/assets/OperationProgressDialog-D-pASR8G.css +1 -0
  82. cancan_microstack/assets/www/adminops/assets/PageContainer-Byss-yUC.js +1 -0
  83. cancan_microstack/assets/www/adminops/assets/PageContainer-C3nSZwM7.css +1 -0
  84. cancan_microstack/assets/www/adminops/assets/RateLimitManage-BDI8jLpC.css +1 -0
  85. cancan_microstack/assets/www/adminops/assets/RateLimitManage-DJY4NiF-.js +1 -0
  86. cancan_microstack/assets/www/adminops/assets/RouteManage-DaUQ4QLw.css +1 -0
  87. cancan_microstack/assets/www/adminops/assets/RouteManage-w9XCU0UA.js +1 -0
  88. cancan_microstack/assets/www/adminops/assets/ServiceCard-BFzHe6Tw.css +1 -0
  89. cancan_microstack/assets/www/adminops/assets/ServiceCard-BJUhWnA-.js +1 -0
  90. cancan_microstack/assets/www/adminops/assets/ServiceDetail-Cw24WuKp.js +1 -0
  91. cancan_microstack/assets/www/adminops/assets/ServiceDetail-Yum47zdB.css +1 -0
  92. cancan_microstack/assets/www/adminops/assets/ServiceList-C7ryvbhE.js +1 -0
  93. cancan_microstack/assets/www/adminops/assets/ServiceList-Cgd01fUx.css +1 -0
  94. cancan_microstack/assets/www/adminops/assets/ServiceLogs-COpG9H0h.js +1 -0
  95. cancan_microstack/assets/www/adminops/assets/ServiceLogs-H_Alq0cf.css +1 -0
  96. cancan_microstack/assets/www/adminops/assets/StatsOverview-D0TwMQkA.js +39 -0
  97. cancan_microstack/assets/www/adminops/assets/StatsOverview-lqAN6pqM.css +1 -0
  98. cancan_microstack/assets/www/adminops/assets/TotpBindView-CWlAmzFt.js +1 -0
  99. cancan_microstack/assets/www/adminops/assets/TotpBindView-HoQC1lhx.css +1 -0
  100. cancan_microstack/assets/www/adminops/assets/TotpVerifyView-BHN1VtX1.css +1 -0
  101. cancan_microstack/assets/www/adminops/assets/TotpVerifyView-D3w_lZk8.js +1 -0
  102. cancan_microstack/assets/www/adminops/assets/WorkflowCenter-DU_mpIA0.css +1 -0
  103. cancan_microstack/assets/www/adminops/assets/WorkflowCenter-i50rZyxN.js +1 -0
  104. cancan_microstack/assets/www/adminops/assets/WorkflowDesigner-CnHokPL9.js +1 -0
  105. cancan_microstack/assets/www/adminops/assets/WorkflowDesigner-DaZaZpLd.css +1 -0
  106. cancan_microstack/assets/www/adminops/assets/WorkflowRuns-B09hK48c.js +1 -0
  107. cancan_microstack/assets/www/adminops/assets/WorkflowRuns-wGutKIIU.css +1 -0
  108. cancan_microstack/assets/www/adminops/assets/caddy-nnCKf8fG.js +1 -0
  109. cancan_microstack/assets/www/adminops/assets/format-Cuzxgna9.js +1 -0
  110. cancan_microstack/assets/www/adminops/assets/index-CiFlm8oc.js +64 -0
  111. cancan_microstack/assets/www/adminops/assets/index-UW0T1Dkc.css +1 -0
  112. cancan_microstack/assets/www/adminops/assets/service-BYlgGPs_.js +1 -0
  113. cancan_microstack/assets/www/adminops/assets/service-operation-6GzLw2Z1.js +1 -0
  114. cancan_microstack/assets/www/adminops/assets/style-CcIXnQ5y.css +1 -0
  115. cancan_microstack/assets/www/adminops/assets/style-lRnStdGu.js +39 -0
  116. cancan_microstack/assets/www/adminops/assets/useDebounce-BRlqfXqf.js +1 -0
  117. cancan_microstack/assets/www/adminops/assets/workflow-CUXs39Ac.js +1 -0
  118. cancan_microstack/assets/www/adminops/index.html +16 -0
  119. cancan_microstack/assets/www/adminops/vite.svg +1 -0
  120. cancan_microstack/cli/__init__.py +14 -0
  121. cancan_microstack/cli/__main__.py +9 -0
  122. cancan_microstack/cli/main.py +552 -0
  123. cancan_microstack/cmd/__init__.py +54 -0
  124. cancan_microstack/cmd/cancan/__init__.py +12 -0
  125. cancan_microstack/cmd/cancan/run.py +395 -0
  126. cancan_microstack/cmd/controllersrv/__init__.py +0 -0
  127. cancan_microstack/cmd/controllersrv/run.py +131 -0
  128. cancan_microstack/cmd/infrasrv/__init__.py +5 -0
  129. cancan_microstack/cmd/infrasrv/run.py +100 -0
  130. cancan_microstack/cmd/opsbffsrv/__init__.py +5 -0
  131. cancan_microstack/cmd/opsbffsrv/run.py +96 -0
  132. cancan_microstack/core/__init__.py +5 -0
  133. cancan_microstack/core/assets.py +123 -0
  134. cancan_microstack/core/compose_builder.py +102 -0
  135. cancan_microstack/core/doctor.py +152 -0
  136. cancan_microstack/core/microstack.py +71 -0
  137. cancan_microstack/core/runner.py +56 -0
  138. cancan_microstack/core/stack_manager.py +186 -0
  139. cancan_microstack/public/__init__.py +7 -0
  140. cancan_microstack/public/api/__init__.py +1 -0
  141. cancan_microstack/public/api/controllersrv_client.py +277 -0
  142. cancan_microstack/public/api/infrasrv_client.py +404 -0
  143. cancan_microstack/public/const/__init__.py +1 -0
  144. cancan_microstack/public/const/action_consts.py +18 -0
  145. cancan_microstack/public/const/app_consts.py +42 -0
  146. cancan_microstack/public/const/caddy_consts.py +22 -0
  147. cancan_microstack/public/const/controllersrv_consts.py +163 -0
  148. cancan_microstack/public/const/docker_consts.py +15 -0
  149. cancan_microstack/public/const/error.py +56 -0
  150. cancan_microstack/public/const/health_consts.py +52 -0
  151. cancan_microstack/public/const/hook_enums.py +56 -0
  152. cancan_microstack/public/const/logging_enums.py +13 -0
  153. cancan_microstack/public/const/metrics_enums.py +36 -0
  154. cancan_microstack/public/const/monitor_enums.py +26 -0
  155. cancan_microstack/public/const/operation_consts.py +53 -0
  156. cancan_microstack/public/const/opsbffsrv_error.py +92 -0
  157. cancan_microstack/public/const/overrides_consts.py +13 -0
  158. cancan_microstack/public/const/redis.py +17 -0
  159. cancan_microstack/public/const/service_consts.py +15 -0
  160. cancan_microstack/public/const/workflow_consts.py +65 -0
  161. cancan_microstack/public/error.py +41 -0
  162. cancan_microstack/public/logging/__init__.py +0 -0
  163. cancan_microstack/public/logging/initializer.py +109 -0
  164. cancan_microstack/public/logging/mq_handler.py +279 -0
  165. cancan_microstack/public/schemas/__init__.py +1 -0
  166. cancan_microstack/public/schemas/caddy/__init__.py +381 -0
  167. cancan_microstack/public/schemas/caddy/analysis.py +90 -0
  168. cancan_microstack/public/schemas/caddy/route.py +18 -0
  169. cancan_microstack/public/schemas/common.py +79 -0
  170. cancan_microstack/public/schemas/controllersrv/__init__.py +3 -0
  171. cancan_microstack/public/schemas/controllersrv/async_requests.py +30 -0
  172. cancan_microstack/public/schemas/controllersrv/compose_models.py +47 -0
  173. cancan_microstack/public/schemas/controllersrv/const.py +24 -0
  174. cancan_microstack/public/schemas/controllersrv/docker_models.py +45 -0
  175. cancan_microstack/public/schemas/controllersrv/docker_responses.py +104 -0
  176. cancan_microstack/public/schemas/controllersrv/requests.py +54 -0
  177. cancan_microstack/public/schemas/controllersrv/responses.py +124 -0
  178. cancan_microstack/public/schemas/controllersrv/task_models.py +102 -0
  179. cancan_microstack/public/schemas/controllersrv/validation.py +23 -0
  180. cancan_microstack/public/schemas/hook_metrics.py +124 -0
  181. cancan_microstack/public/schemas/hooks.py +39 -0
  182. cancan_microstack/public/schemas/infra/__init__.py +0 -0
  183. cancan_microstack/public/schemas/infra/cleanup.py +25 -0
  184. cancan_microstack/public/schemas/infra/container.py +74 -0
  185. cancan_microstack/public/schemas/infra/enums.py +135 -0
  186. cancan_microstack/public/schemas/infra/health_check.py +42 -0
  187. cancan_microstack/public/schemas/infra/hook_log.py +42 -0
  188. cancan_microstack/public/schemas/infra/operation.py +90 -0
  189. cancan_microstack/public/schemas/infra/overview.py +25 -0
  190. cancan_microstack/public/schemas/infra/push.py +33 -0
  191. cancan_microstack/public/schemas/infra/service_action_log.py +47 -0
  192. cancan_microstack/public/schemas/infra/service_config.py +10 -0
  193. cancan_microstack/public/schemas/infra/service_info.py +69 -0
  194. cancan_microstack/public/schemas/infra/service_instance.py +93 -0
  195. cancan_microstack/public/schemas/infra/service_management.py +152 -0
  196. cancan_microstack/public/schemas/infra/service_operation.py +79 -0
  197. cancan_microstack/public/schemas/infra/service_registry.py +158 -0
  198. cancan_microstack/public/schemas/infra/status_types.py +19 -0
  199. cancan_microstack/public/schemas/infra/workflow.py +566 -0
  200. cancan_microstack/public/schemas/logging/__init__.py +1 -0
  201. cancan_microstack/public/schemas/logging/log_event.py +121 -0
  202. cancan_microstack/public/schemas/opsbffsrv/__init__.py +1 -0
  203. cancan_microstack/public/schemas/opsbffsrv/async_ops.py +17 -0
  204. cancan_microstack/public/schemas/opsbffsrv/db_admin.py +147 -0
  205. cancan_microstack/public/schemas/opsbffsrv/db_init.py +48 -0
  206. cancan_microstack/public/schemas/opsbffsrv/service_config.py +89 -0
  207. cancan_microstack/public/schemas/opsbffsrv/service_logs.py +54 -0
  208. cancan_microstack/public/schemas/service_operation.py +24 -0
  209. cancan_microstack/public/schemas/service_registry.py +40 -0
  210. cancan_microstack/public/types/__init__.py +7 -0
  211. cancan_microstack/public/web/__init__.py +0 -0
  212. cancan_microstack/public/web/config_value.py +105 -0
  213. cancan_microstack/public/web/server.py +385 -0
  214. cancan_microstack/py.typed +0 -0
  215. cancan_microstack/runtime/__init__.py +0 -0
  216. cancan_microstack/runtime/compose_cmd.py +228 -0
  217. cancan_microstack/runtime/host_daemon.py +318 -0
  218. cancan_microstack/runtime/overrides.py +103 -0
  219. cancan_microstack/runtime/resources.py +25 -0
  220. cancan_microstack/runtime/workspace.py +94 -0
  221. cancan_microstack/services/__init__.py +0 -0
  222. cancan_microstack/services/controllersrv/__init__.py +8 -0
  223. cancan_microstack/services/controllersrv/application/__init__.py +0 -0
  224. cancan_microstack/services/controllersrv/application/docker_compose_app.py +427 -0
  225. cancan_microstack/services/controllersrv/conf/__init__.py +0 -0
  226. cancan_microstack/services/controllersrv/conf/config.py +76 -0
  227. cancan_microstack/services/controllersrv/conf/settings.py +54 -0
  228. cancan_microstack/services/controllersrv/domain/__init__.py +0 -0
  229. cancan_microstack/services/controllersrv/domain/docker_compose/__init__.py +0 -0
  230. cancan_microstack/services/controllersrv/domain/docker_compose/docker_compose_domain.py +278 -0
  231. cancan_microstack/services/controllersrv/domain/service_validator.py +327 -0
  232. cancan_microstack/services/controllersrv/domain/task/__init__.py +17 -0
  233. cancan_microstack/services/controllersrv/domain/task/task_queue.py +286 -0
  234. cancan_microstack/services/controllersrv/domain/task/task_worker.py +495 -0
  235. cancan_microstack/services/controllersrv/infrastructure/__init__.py +0 -0
  236. cancan_microstack/services/controllersrv/interface/__init__.py +0 -0
  237. cancan_microstack/services/controllersrv/interface/api/__init__.py +0 -0
  238. cancan_microstack/services/controllersrv/interface/api/docker_control_api.py +470 -0
  239. cancan_microstack/services/controllersrv/router.py +132 -0
  240. cancan_microstack/services/infrasrv/__init__.py +4 -0
  241. cancan_microstack/services/infrasrv/application/__init__.py +0 -0
  242. cancan_microstack/services/infrasrv/application/health_check_app.py +24 -0
  243. cancan_microstack/services/infrasrv/application/logging/__init__.py +1 -0
  244. cancan_microstack/services/infrasrv/application/logging/log_ingestion_service.py +183 -0
  245. cancan_microstack/services/infrasrv/application/service_config.py +22 -0
  246. cancan_microstack/services/infrasrv/application/service_logs_app.py +53 -0
  247. cancan_microstack/services/infrasrv/application/service_management_app.py +689 -0
  248. cancan_microstack/services/infrasrv/application/service_operation_tracker.py +251 -0
  249. cancan_microstack/services/infrasrv/application/service_registry.py +53 -0
  250. cancan_microstack/services/infrasrv/application/workflow/__init__.py +0 -0
  251. cancan_microstack/services/infrasrv/application/workflow/workflow_app.py +991 -0
  252. cancan_microstack/services/infrasrv/application/workflow/workflow_queue.py +302 -0
  253. cancan_microstack/services/infrasrv/application/workflow/workflow_tasks.py +46 -0
  254. cancan_microstack/services/infrasrv/application/workflow/workflow_worker_runtime.py +122 -0
  255. cancan_microstack/services/infrasrv/conf/__init__.py +0 -0
  256. cancan_microstack/services/infrasrv/conf/config.py +98 -0
  257. cancan_microstack/services/infrasrv/domain/__init__.py +0 -0
  258. cancan_microstack/services/infrasrv/domain/health_check/__init__.py +3 -0
  259. cancan_microstack/services/infrasrv/domain/health_check/health_check_domain.py +576 -0
  260. cancan_microstack/services/infrasrv/domain/hooks/__init__.py +19 -0
  261. cancan_microstack/services/infrasrv/domain/hooks/builtin_hooks.py +308 -0
  262. cancan_microstack/services/infrasrv/domain/hooks/hook_registry.py +43 -0
  263. cancan_microstack/services/infrasrv/domain/hooks/hooks_log_utils.py +275 -0
  264. cancan_microstack/services/infrasrv/domain/hooks/init.py +17 -0
  265. cancan_microstack/services/infrasrv/domain/hooks/metrics.py +205 -0
  266. cancan_microstack/services/infrasrv/domain/hooks/pre_registration_hooks.py +490 -0
  267. cancan_microstack/services/infrasrv/domain/registry/__init__.py +0 -0
  268. cancan_microstack/services/infrasrv/domain/registry/service_registry.py +509 -0
  269. cancan_microstack/services/infrasrv/domain/service_config/__init__.py +0 -0
  270. cancan_microstack/services/infrasrv/domain/service_config/service_config.py +50 -0
  271. cancan_microstack/services/infrasrv/domain/service_logs/__init__.py +0 -0
  272. cancan_microstack/services/infrasrv/domain/service_logs/service_logs_domain.py +51 -0
  273. cancan_microstack/services/infrasrv/domain/workflow/__init__.py +4 -0
  274. cancan_microstack/services/infrasrv/domain/workflow/engine.py +159 -0
  275. cancan_microstack/services/infrasrv/domain/workflow/node_handlers.py +509 -0
  276. cancan_microstack/services/infrasrv/domain/workflow/workflow_domain.py +164 -0
  277. cancan_microstack/services/infrasrv/infrastructure/__init__.py +0 -0
  278. cancan_microstack/services/infrasrv/infrastructure/api/__init__.py +0 -0
  279. cancan_microstack/services/infrasrv/infrastructure/api/controllersrv_api.py +165 -0
  280. cancan_microstack/services/infrasrv/infrastructure/cache/__init__.py +0 -0
  281. cancan_microstack/services/infrasrv/infrastructure/cache/service_registry_cache.py +174 -0
  282. cancan_microstack/services/infrasrv/infrastructure/db/__init__.py +0 -0
  283. cancan_microstack/services/infrasrv/infrastructure/db/model/__init__.py +0 -0
  284. cancan_microstack/services/infrasrv/infrastructure/db/model/execution_log_tbl.py +53 -0
  285. cancan_microstack/services/infrasrv/infrastructure/db/model/node_instance_tbl.py +55 -0
  286. cancan_microstack/services/infrasrv/infrastructure/db/model/service_action_log_tbl.py +44 -0
  287. cancan_microstack/services/infrasrv/infrastructure/db/model/service_config_tbl.py +30 -0
  288. cancan_microstack/services/infrasrv/infrastructure/db/model/service_info_tbl.py +59 -0
  289. cancan_microstack/services/infrasrv/infrastructure/db/model/service_instance_tbl.py +88 -0
  290. cancan_microstack/services/infrasrv/infrastructure/db/model/service_operation_tbl.py +73 -0
  291. cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_definition_tbl.py +55 -0
  292. cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_definition_version_tbl.py +43 -0
  293. cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_engine_alert_tbl.py +57 -0
  294. cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_run_tbl.py +56 -0
  295. cancan_microstack/services/infrasrv/infrastructure/db/operate/__init__.py +0 -0
  296. cancan_microstack/services/infrasrv/infrastructure/db/operate/service_action_log_op.py +239 -0
  297. cancan_microstack/services/infrasrv/infrastructure/db/operate/service_config.py +80 -0
  298. cancan_microstack/services/infrasrv/infrastructure/db/operate/service_config_manager.py +198 -0
  299. cancan_microstack/services/infrasrv/infrastructure/db/operate/service_info_op.py +297 -0
  300. cancan_microstack/services/infrasrv/infrastructure/db/operate/service_instance_op.py +688 -0
  301. cancan_microstack/services/infrasrv/infrastructure/db/operate/service_operation_op.py +387 -0
  302. cancan_microstack/services/infrasrv/infrastructure/db/operate/service_registry.py +124 -0
  303. cancan_microstack/services/infrasrv/infrastructure/db/operate/workflow_op.py +804 -0
  304. cancan_microstack/services/infrasrv/infrastructure/ddl_manager.py +31 -0
  305. cancan_microstack/services/infrasrv/infrastructure/mongo/__init__.py +1 -0
  306. cancan_microstack/services/infrasrv/infrastructure/mongo/log_repository.py +129 -0
  307. cancan_microstack/services/infrasrv/interface/__init__.py +0 -0
  308. cancan_microstack/services/infrasrv/interface/api/__init__.py +0 -0
  309. cancan_microstack/services/infrasrv/interface/api/health_check_api.py +29 -0
  310. cancan_microstack/services/infrasrv/interface/api/hooks.py +284 -0
  311. cancan_microstack/services/infrasrv/interface/api/internal.py +49 -0
  312. cancan_microstack/services/infrasrv/interface/api/internal_instance_api.py +265 -0
  313. cancan_microstack/services/infrasrv/interface/api/internal_operation_api.py +206 -0
  314. cancan_microstack/services/infrasrv/interface/api/service_config.py +50 -0
  315. cancan_microstack/services/infrasrv/interface/api/service_logs_api.py +49 -0
  316. cancan_microstack/services/infrasrv/interface/api/service_management_api.py +113 -0
  317. cancan_microstack/services/infrasrv/interface/api/service_registry.py +117 -0
  318. cancan_microstack/services/infrasrv/interface/api/workflow_api.py +303 -0
  319. cancan_microstack/services/infrasrv/interface/schedule/__init__.py +0 -0
  320. cancan_microstack/services/infrasrv/interface/schedule/cleanup.py +13 -0
  321. cancan_microstack/services/infrasrv/interface/schedule/health_check.py +27 -0
  322. cancan_microstack/services/infrasrv/interface/schedule/log_cleanup.py +26 -0
  323. cancan_microstack/services/infrasrv/interface/schedule/operation_tracker.py +25 -0
  324. cancan_microstack/services/infrasrv/interface/schedule/scheduler.py +39 -0
  325. cancan_microstack/services/infrasrv/interface/schedule/workflow_scheduler.py +115 -0
  326. cancan_microstack/services/infrasrv/router.py +341 -0
  327. cancan_microstack/services/opsbffsrv/__init__.py +4 -0
  328. cancan_microstack/services/opsbffsrv/application/__init__.py +0 -0
  329. cancan_microstack/services/opsbffsrv/application/async_operation_app.py +150 -0
  330. cancan_microstack/services/opsbffsrv/application/auth_app.py +285 -0
  331. cancan_microstack/services/opsbffsrv/application/caddy/__init__.py +0 -0
  332. cancan_microstack/services/opsbffsrv/application/caddy/access_log_analysis_app.py +344 -0
  333. cancan_microstack/services/opsbffsrv/application/caddy/access_log_ingestion_service.py +169 -0
  334. cancan_microstack/services/opsbffsrv/application/caddy/certificate_management_app.py +355 -0
  335. cancan_microstack/services/opsbffsrv/application/caddy/rate_limit_management_app.py +496 -0
  336. cancan_microstack/services/opsbffsrv/application/caddy/route_management_app.py +401 -0
  337. cancan_microstack/services/opsbffsrv/application/caddy/stats_aggregation_app.py +364 -0
  338. cancan_microstack/services/opsbffsrv/application/db_admin_app.py +103 -0
  339. cancan_microstack/services/opsbffsrv/application/db_init_app.py +283 -0
  340. cancan_microstack/services/opsbffsrv/application/logging/__init__.py +1 -0
  341. cancan_microstack/services/opsbffsrv/application/logging/log_query_app.py +28 -0
  342. cancan_microstack/services/opsbffsrv/application/service_config.py +158 -0
  343. cancan_microstack/services/opsbffsrv/application/service_logs_app.py +74 -0
  344. cancan_microstack/services/opsbffsrv/application/service_registry.py +36 -0
  345. cancan_microstack/services/opsbffsrv/application/workflow_ops_app.py +730 -0
  346. cancan_microstack/services/opsbffsrv/conf/__init__.py +0 -0
  347. cancan_microstack/services/opsbffsrv/conf/config.py +224 -0
  348. cancan_microstack/services/opsbffsrv/domain/__init__.py +0 -0
  349. cancan_microstack/services/opsbffsrv/domain/auth/__init__.py +0 -0
  350. cancan_microstack/services/opsbffsrv/domain/auth/admin_init.py +38 -0
  351. cancan_microstack/services/opsbffsrv/domain/auth/auth_domain.py +108 -0
  352. cancan_microstack/services/opsbffsrv/domain/caddy/__init__.py +0 -0
  353. cancan_microstack/services/opsbffsrv/domain/caddy/access_log_analysis.py +358 -0
  354. cancan_microstack/services/opsbffsrv/domain/caddy/certificate_management.py +325 -0
  355. cancan_microstack/services/opsbffsrv/domain/caddy/default_routes.py +53 -0
  356. cancan_microstack/services/opsbffsrv/domain/caddy/rate_limit_management.py +308 -0
  357. cancan_microstack/services/opsbffsrv/domain/caddy/route_management.py +279 -0
  358. cancan_microstack/services/opsbffsrv/domain/caddy/stats_aggregation.py +654 -0
  359. cancan_microstack/services/opsbffsrv/domain/db_admin/__init__.py +0 -0
  360. cancan_microstack/services/opsbffsrv/domain/db_admin/db_admin_domain.py +118 -0
  361. cancan_microstack/services/opsbffsrv/domain/db_init/__init__.py +3 -0
  362. cancan_microstack/services/opsbffsrv/domain/db_init/db_init_domain.py +358 -0
  363. cancan_microstack/services/opsbffsrv/domain/logging/__init__.py +1 -0
  364. cancan_microstack/services/opsbffsrv/domain/logging/log_query_domain.py +99 -0
  365. cancan_microstack/services/opsbffsrv/domain/service_config/__init__.py +0 -0
  366. cancan_microstack/services/opsbffsrv/domain/service_config/service_config.py +81 -0
  367. cancan_microstack/services/opsbffsrv/domain/service_registry/__init__.py +0 -0
  368. cancan_microstack/services/opsbffsrv/domain/service_registry/service_registry.py +292 -0
  369. cancan_microstack/services/opsbffsrv/infrastructure/__init__.py +0 -0
  370. cancan_microstack/services/opsbffsrv/infrastructure/api/__init__.py +0 -0
  371. cancan_microstack/services/opsbffsrv/infrastructure/api/infrasrv_api.py +242 -0
  372. cancan_microstack/services/opsbffsrv/infrastructure/auth/__init__.py +0 -0
  373. cancan_microstack/services/opsbffsrv/infrastructure/auth/captcha_service.py +67 -0
  374. cancan_microstack/services/opsbffsrv/infrastructure/auth/password_service.py +12 -0
  375. cancan_microstack/services/opsbffsrv/infrastructure/auth/redis_store.py +131 -0
  376. cancan_microstack/services/opsbffsrv/infrastructure/auth/totp_service.py +59 -0
  377. cancan_microstack/services/opsbffsrv/infrastructure/caddy/__init__.py +0 -0
  378. cancan_microstack/services/opsbffsrv/infrastructure/caddy/access_log_parser.py +307 -0
  379. cancan_microstack/services/opsbffsrv/infrastructure/caddy/admin_api_client.py +678 -0
  380. cancan_microstack/services/opsbffsrv/infrastructure/caddy/ip_geo_locator.py +176 -0
  381. cancan_microstack/services/opsbffsrv/infrastructure/db/__init__.py +0 -0
  382. cancan_microstack/services/opsbffsrv/infrastructure/db/model/__init__.py +0 -0
  383. cancan_microstack/services/opsbffsrv/infrastructure/db/model/admin_user_tbl.py +33 -0
  384. cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_access_log_tbl.py +90 -0
  385. cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_certificate_tbl.py +65 -0
  386. cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_rate_limit_tbl.py +69 -0
  387. cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_route_tbl.py +66 -0
  388. cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_stats_tbl.py +78 -0
  389. cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_action_log_tbl.py +44 -0
  390. cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_config_tbl.py +30 -0
  391. cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_info_tbl.py +51 -0
  392. cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_instance_tbl.py +68 -0
  393. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/__init__.py +0 -0
  394. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/admin_user_operate.py +59 -0
  395. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_access_log.py +531 -0
  396. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_certificate.py +451 -0
  397. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_rate_limit.py +360 -0
  398. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_route.py +271 -0
  399. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_stats.py +343 -0
  400. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_action_log_op.py +57 -0
  401. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_config.py +86 -0
  402. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_info_op.py +79 -0
  403. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_instance.py +58 -0
  404. cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_registry.py +138 -0
  405. cancan_microstack/services/opsbffsrv/infrastructure/ddl_manager.py +31 -0
  406. cancan_microstack/services/opsbffsrv/infrastructure/mongo/__init__.py +1 -0
  407. cancan_microstack/services/opsbffsrv/infrastructure/mongo/log_query_repository.py +87 -0
  408. cancan_microstack/services/opsbffsrv/interface/__init__.py +0 -0
  409. cancan_microstack/services/opsbffsrv/interface/api/__init__.py +0 -0
  410. cancan_microstack/services/opsbffsrv/interface/api/async_operation_api.py +137 -0
  411. cancan_microstack/services/opsbffsrv/interface/api/auth_api.py +113 -0
  412. cancan_microstack/services/opsbffsrv/interface/api/caddy/__init__.py +3 -0
  413. cancan_microstack/services/opsbffsrv/interface/api/caddy/access_log_api.py +174 -0
  414. cancan_microstack/services/opsbffsrv/interface/api/caddy/certificate_api.py +235 -0
  415. cancan_microstack/services/opsbffsrv/interface/api/caddy/rate_limit_api.py +302 -0
  416. cancan_microstack/services/opsbffsrv/interface/api/caddy/route_api.py +250 -0
  417. cancan_microstack/services/opsbffsrv/interface/api/caddy/stats_api.py +243 -0
  418. cancan_microstack/services/opsbffsrv/interface/api/db_admin_api.py +62 -0
  419. cancan_microstack/services/opsbffsrv/interface/api/db_init_api.py +109 -0
  420. cancan_microstack/services/opsbffsrv/interface/api/instance_management_api.py +165 -0
  421. cancan_microstack/services/opsbffsrv/interface/api/log_query_api.py +41 -0
  422. cancan_microstack/services/opsbffsrv/interface/api/mongo_express_proxy_api.py +181 -0
  423. cancan_microstack/services/opsbffsrv/interface/api/pgweb_proxy_api.py +154 -0
  424. cancan_microstack/services/opsbffsrv/interface/api/rabbitmq_mgmt_proxy_api.py +518 -0
  425. cancan_microstack/services/opsbffsrv/interface/api/redis_commander_proxy_api.py +133 -0
  426. cancan_microstack/services/opsbffsrv/interface/api/service_config.py +146 -0
  427. cancan_microstack/services/opsbffsrv/interface/api/service_logs_api.py +81 -0
  428. cancan_microstack/services/opsbffsrv/interface/api/service_registry.py +66 -0
  429. cancan_microstack/services/opsbffsrv/interface/api/workflow_ops_api.py +413 -0
  430. cancan_microstack/services/opsbffsrv/interface/middleware/__init__.py +0 -0
  431. cancan_microstack/services/opsbffsrv/interface/middleware/auth_middleware.py +52 -0
  432. cancan_microstack/services/opsbffsrv/router.py +901 -0
  433. cancan_microstack/utils/__init__.py +1 -0
  434. cancan_microstack/utils/container_env.py +218 -0
  435. cancan_microstack-0.0.1.dist-info/METADATA +155 -0
  436. cancan_microstack-0.0.1.dist-info/RECORD +440 -0
  437. cancan_microstack-0.0.1.dist-info/WHEEL +5 -0
  438. cancan_microstack-0.0.1.dist-info/entry_points.txt +2 -0
  439. cancan_microstack-0.0.1.dist-info/licenses/LICENSE +21 -0
  440. 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)