create-windy 0.3.2 → 0.3.5

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 (343) hide show
  1. package/README.md +27 -9
  2. package/dist/cli.js +825 -117
  3. package/package.json +1 -1
  4. package/template/.agents/skills/windy-business-module/SKILL.md +16 -3
  5. package/template/.windy-template.json +2 -2
  6. package/template/AGENTS.md +4 -0
  7. package/template/README.md +18 -11
  8. package/template/_gitignore +1 -0
  9. package/template/apps/agent-server/README.md +2 -0
  10. package/template/apps/agent-server/pyproject.toml +8 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +87 -10
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +18 -3
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +78 -11
  14. package/template/apps/agent-server/src/southwind_agent_server/app.py +120 -115
  15. package/template/apps/agent-server/src/southwind_agent_server/artifact_input.py +153 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/config.py +29 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +81 -1
  18. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +8 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +28 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/fingerprint.py +41 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/http_helpers.py +62 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/lease.py +15 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/multimodal.py +76 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +112 -20
  25. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +14 -1
  26. package/template/apps/agent-server/src/southwind_agent_server/postgres_store.py +329 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/postgres_store_backend.py +332 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +64 -3
  29. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +68 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/service.py +218 -56
  31. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +36 -0
  32. package/template/apps/agent-server/src/southwind_agent_server/sqlite_catalog.py +107 -0
  33. package/template/apps/agent-server/src/southwind_agent_server/sqlite_schema.py +65 -0
  34. package/template/apps/agent-server/src/southwind_agent_server/sqlite_serialization.py +16 -0
  35. package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +89 -57
  36. package/template/apps/agent-server/src/southwind_agent_server/store.py +105 -2
  37. package/template/apps/agent-server/src/southwind_agent_server/store_cipher.py +40 -0
  38. package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +89 -0
  39. package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +72 -0
  40. package/template/apps/agent-server/tests/test_adk_adapter.py +6 -2
  41. package/template/apps/agent-server/tests/test_api.py +24 -1
  42. package/template/apps/agent-server/tests/test_artifact_input.py +110 -0
  43. package/template/apps/agent-server/tests/test_config.py +4 -0
  44. package/template/apps/agent-server/tests/test_fingerprint.py +10 -0
  45. package/template/apps/agent-server/tests/test_multimodal.py +51 -0
  46. package/template/apps/agent-server/tests/test_openai_provider.py +6 -2
  47. package/template/apps/agent-server/tests/test_postgres_store.py +131 -0
  48. package/template/apps/agent-server/tests/test_provider_governance.py +61 -0
  49. package/template/apps/agent-server/tests/test_sqlite_store.py +95 -10
  50. package/template/apps/agent-server/tests/test_telemetry.py +25 -0
  51. package/template/apps/agent-server/tests/test_tool_approval.py +28 -0
  52. package/template/apps/agent-server/uv.lock +165 -0
  53. package/template/apps/document-server/Dockerfile +19 -0
  54. package/template/apps/document-server/pyproject.toml +43 -0
  55. package/template/apps/document-server/src/southwind_document_server/__init__.py +1 -0
  56. package/template/apps/document-server/src/southwind_document_server/adapter.py +337 -0
  57. package/template/apps/document-server/src/southwind_document_server/app.py +151 -0
  58. package/template/apps/document-server/src/southwind_document_server/config.py +94 -0
  59. package/template/apps/document-server/src/southwind_document_server/contracts.py +103 -0
  60. package/template/apps/document-server/src/southwind_document_server/service.py +149 -0
  61. package/template/apps/document-server/src/southwind_document_server/store.py +185 -0
  62. package/template/apps/document-server/tests/__init__.py +1 -0
  63. package/template/apps/document-server/tests/test_config.py +42 -0
  64. package/template/apps/document-server/tests/test_service.py +140 -0
  65. package/template/apps/document-server/uv.lock +430 -0
  66. package/template/apps/embedding-server/Dockerfile +17 -0
  67. package/template/apps/embedding-server/pyproject.toml +44 -0
  68. package/template/apps/embedding-server/src/southwind_embedding_server/__init__.py +1 -0
  69. package/template/apps/embedding-server/src/southwind_embedding_server/adapter.py +254 -0
  70. package/template/apps/embedding-server/src/southwind_embedding_server/app.py +119 -0
  71. package/template/apps/embedding-server/src/southwind_embedding_server/config.py +120 -0
  72. package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +106 -0
  73. package/template/apps/embedding-server/src/southwind_embedding_server/error.py +6 -0
  74. package/template/apps/embedding-server/src/southwind_embedding_server/image_input.py +87 -0
  75. package/template/apps/embedding-server/tests/__init__.py +1 -0
  76. package/template/apps/embedding-server/tests/test_adapter.py +92 -0
  77. package/template/apps/embedding-server/tests/test_config.py +38 -0
  78. package/template/apps/embedding-server/uv.lock +449 -0
  79. package/template/apps/server/src/agent/background-operation-port.ts +37 -0
  80. package/template/apps/server/src/agent/background-operation.test.ts +231 -0
  81. package/template/apps/server/src/agent/background-operation.ts +252 -0
  82. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +114 -0
  83. package/template/apps/server/src/agent/execution-grants.ts +7 -1
  84. package/template/apps/server/src/agent/fake-runtime.test-support.ts +43 -0
  85. package/template/apps/server/src/agent/idempotency-key.test.ts +28 -0
  86. package/template/apps/server/src/agent/idempotency-key.ts +11 -0
  87. package/template/apps/server/src/agent/remote-runtime.test.ts +115 -1
  88. package/template/apps/server/src/agent/remote-runtime.ts +173 -5
  89. package/template/apps/server/src/agent/run-authorization.test-support.ts +9 -0
  90. package/template/apps/server/src/agent/run-authorization.test.ts +126 -0
  91. package/template/apps/server/src/agent/run-authorization.ts +180 -0
  92. package/template/apps/server/src/agent/run-binding-repository.ts +93 -0
  93. package/template/apps/server/src/agent/run-contracts.ts +88 -1
  94. package/template/apps/server/src/agent/run-definition-support.ts +58 -0
  95. package/template/apps/server/src/agent/run-errors.ts +25 -0
  96. package/template/apps/server/src/agent/run-facade.test.ts +57 -46
  97. package/template/apps/server/src/agent/run-facade.ts +185 -65
  98. package/template/apps/server/src/agent/run-release.ts +78 -0
  99. package/template/apps/server/src/agent/run-route-input.ts +169 -0
  100. package/template/apps/server/src/agent/run-routes.ts +63 -76
  101. package/template/apps/server/src/agent/runtime-bootstrap.ts +169 -13
  102. package/template/apps/server/src/agent/sandbox-client.ts +200 -0
  103. package/template/apps/server/src/agent/sandbox-protocol.ts +76 -0
  104. package/template/apps/server/src/agent/sandbox-registry.ts +205 -0
  105. package/template/apps/server/src/agent/sandbox-runtime.test.ts +137 -0
  106. package/template/apps/server/src/agent/sandbox-worker-server.ts +195 -0
  107. package/template/apps/server/src/agent/sandbox-worker.ts +12 -0
  108. package/template/apps/server/src/agent/tool-approval.test.ts +75 -0
  109. package/template/apps/server/src/agent/tool-approval.ts +115 -0
  110. package/template/apps/server/src/agent/tool-audit.ts +62 -0
  111. package/template/apps/server/src/agent/tool-host.test.ts +39 -11
  112. package/template/apps/server/src/agent/tool-host.ts +184 -17
  113. package/template/apps/server/src/agent/tool-registry.ts +52 -47
  114. package/template/apps/server/src/agent/vision-facade.test.ts +256 -0
  115. package/template/apps/server/src/agent/vision-input.ts +120 -0
  116. package/template/apps/server/src/ai-artifact/codec.test.ts +44 -0
  117. package/template/apps/server/src/ai-artifact/codec.ts +137 -0
  118. package/template/apps/server/src/ai-artifact/routes.ts +37 -0
  119. package/template/apps/server/src/ai-artifact/runtime.ts +205 -0
  120. package/template/apps/server/src/application-services.ts +62 -2
  121. package/template/apps/server/src/auth/reauthentication.test.ts +40 -0
  122. package/template/apps/server/src/auth/reauthentication.ts +19 -0
  123. package/template/apps/server/src/auth/runtime.ts +9 -1
  124. package/template/apps/server/src/data-access/context.ts +6 -4
  125. package/template/apps/server/src/data-access/scope-snapshot.ts +27 -0
  126. package/template/apps/server/src/data-governance/export/approval-port.ts +6 -3
  127. package/template/apps/server/src/data-governance/export/authorization-port.test.ts +23 -4
  128. package/template/apps/server/src/data-governance/export/authorization-port.ts +89 -25
  129. package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
  130. package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
  131. package/template/apps/server/src/data-governance/export/job-runtime.test.ts +15 -4
  132. package/template/apps/server/src/data-governance/export/job-runtime.ts +68 -15
  133. package/template/apps/server/src/data-governance/export/ndjson-stream.ts +25 -6
  134. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +4 -2
  135. package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
  136. package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
  137. package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
  138. package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
  139. package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
  140. package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
  141. package/template/apps/server/src/data-governance/export/routes.ts +11 -1
  142. package/template/apps/server/src/data-governance/export/scope-snapshot.ts +6 -18
  143. package/template/apps/server/src/data-governance/export/service-support.ts +188 -0
  144. package/template/apps/server/src/data-governance/export/service.ts +90 -113
  145. package/template/apps/server/src/data-governance/export/source.ts +101 -0
  146. package/template/apps/server/src/data-governance/export/types.ts +27 -0
  147. package/template/apps/server/src/document/contracts.ts +76 -0
  148. package/template/apps/server/src/document/facade.ts +339 -0
  149. package/template/apps/server/src/document/remote-runtime.test.ts +96 -0
  150. package/template/apps/server/src/document/remote-runtime.ts +223 -0
  151. package/template/apps/server/src/document/routes.ts +193 -0
  152. package/template/apps/server/src/embedding/contracts.ts +83 -0
  153. package/template/apps/server/src/embedding/error.ts +10 -0
  154. package/template/apps/server/src/embedding/facade.ts +217 -0
  155. package/template/apps/server/src/embedding/input.ts +172 -0
  156. package/template/apps/server/src/embedding/remote-runtime.test.ts +66 -0
  157. package/template/apps/server/src/embedding/remote-runtime.ts +218 -0
  158. package/template/apps/server/src/embedding/routes.ts +152 -0
  159. package/template/apps/server/src/index.ts +40 -43
  160. package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
  161. package/template/apps/server/src/jobs/artifact-service.ts +10 -0
  162. package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
  163. package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
  164. package/template/apps/server/src/jobs/drizzle-mappers.ts +4 -1
  165. package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
  166. package/template/apps/server/src/jobs/drizzle-repository.ts +1 -0
  167. package/template/apps/server/src/jobs/execution-repository.ts +17 -0
  168. package/template/apps/server/src/jobs/execution.test.ts +34 -0
  169. package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
  170. package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
  171. package/template/apps/server/src/jobs/legacy-service-runtime.ts +5 -2
  172. package/template/apps/server/src/jobs/repository.ts +3 -0
  173. package/template/apps/server/src/jobs/worker.ts +80 -41
  174. package/template/apps/server/src/module-host/ai-operation-port.ts +170 -0
  175. package/template/apps/server/src/module-host/durable-context.test.ts +312 -0
  176. package/template/apps/server/src/module-host/durable-context.ts +124 -0
  177. package/template/apps/server/src/module-host/durable-handler-registration.ts +72 -0
  178. package/template/apps/server/src/module-host/governed-export-route-support.ts +171 -0
  179. package/template/apps/server/src/module-host/governed-export-routes.test.ts +327 -0
  180. package/template/apps/server/src/module-host/governed-export-routes.ts +238 -0
  181. package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
  182. package/template/apps/server/src/module-host/governed-resource-routes.ts +331 -0
  183. package/template/apps/server/src/module-host/governed-resources.test.ts +344 -0
  184. package/template/apps/server/src/module-host/governed-resources.ts +203 -0
  185. package/template/apps/server/src/module-host/http-runtime.ts +95 -0
  186. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  187. package/template/apps/server/src/module-host/initialize.test.ts +49 -1
  188. package/template/apps/server/src/module-host/initialize.ts +39 -16
  189. package/template/apps/server/src/module-host/job-port.test.ts +262 -0
  190. package/template/apps/server/src/module-host/job-port.ts +334 -0
  191. package/template/apps/server/src/module-host/request-context.test.ts +88 -2
  192. package/template/apps/server/src/module-host/request-context.ts +56 -22
  193. package/template/apps/server/src/module-host/route-installer.test.ts +10 -0
  194. package/template/apps/server/src/module-host/route-installer.ts +36 -1
  195. package/template/apps/server/src/module-host/sandbox-tool-registration.ts +42 -0
  196. package/template/apps/server/src/module-host/tool-handlers.ts +67 -31
  197. package/template/apps/server/src/module-host.ts +1 -0
  198. package/template/apps/server/src/module-runtime-validation.ts +7 -3
  199. package/template/apps/server/src/speech/contracts.ts +76 -0
  200. package/template/apps/server/src/speech/facade.ts +316 -0
  201. package/template/apps/server/src/speech/remote-runtime.test.ts +88 -0
  202. package/template/apps/server/src/speech/remote-runtime.ts +227 -0
  203. package/template/apps/server/src/speech/routes.ts +180 -0
  204. package/template/apps/server/src/system/actor.ts +39 -0
  205. package/template/apps/server/src/system/route-helpers.ts +1 -0
  206. package/template/apps/server/src/workflow/service.ts +2 -1
  207. package/template/apps/speech-server/Dockerfile +22 -0
  208. package/template/apps/speech-server/pyproject.toml +43 -0
  209. package/template/apps/speech-server/src/southwind_speech_server/__init__.py +1 -0
  210. package/template/apps/speech-server/src/southwind_speech_server/adapter.py +249 -0
  211. package/template/apps/speech-server/src/southwind_speech_server/app.py +183 -0
  212. package/template/apps/speech-server/src/southwind_speech_server/config.py +100 -0
  213. package/template/apps/speech-server/src/southwind_speech_server/contracts.py +103 -0
  214. package/template/apps/speech-server/src/southwind_speech_server/errors.py +6 -0
  215. package/template/apps/speech-server/src/southwind_speech_server/normalizer.py +108 -0
  216. package/template/apps/speech-server/src/southwind_speech_server/output_validation.py +128 -0
  217. package/template/apps/speech-server/src/southwind_speech_server/service.py +162 -0
  218. package/template/apps/speech-server/src/southwind_speech_server/store.py +209 -0
  219. package/template/apps/speech-server/tests/__init__.py +1 -0
  220. package/template/apps/speech-server/tests/test_config.py +33 -0
  221. package/template/apps/speech-server/tests/test_service.py +154 -0
  222. package/template/apps/speech-server/uv.lock +430 -0
  223. package/template/apps/web/src/app/business-routes.ts +22 -0
  224. package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
  225. package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
  226. package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
  227. package/template/apps/web/src/components/common/FormDialog.vue +6 -0
  228. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +13 -0
  229. package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
  230. package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
  231. package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
  232. package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
  233. package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
  234. package/template/apps/web/src/layout/AppShell.vue +28 -25
  235. package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
  236. package/template/apps/web/src/router/business-routes.ts +68 -0
  237. package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
  238. package/template/apps/web/src/router/index.ts +7 -0
  239. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  240. package/template/apps/web/src/services/access-api.ts +5 -1
  241. package/template/apps/web/src/services/access-api.webtest.ts +35 -0
  242. package/template/docker-compose.yml +127 -0
  243. package/template/docs/architecture/ai-egress.md +12 -9
  244. package/template/docs/architecture/ai-runtime.md +23 -11
  245. package/template/docs/architecture/durable-jobs.md +25 -3
  246. package/template/docs/architecture/foundation-infrastructure.md +163 -0
  247. package/template/docs/architecture/module-host.md +115 -0
  248. package/template/docs/architecture/server-module-interface.md +39 -0
  249. package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
  250. package/template/docs/development/business-web-routes.md +66 -0
  251. package/template/docs/development/custom-login-page.md +3 -3
  252. package/template/docs/development/module-lifecycle.md +71 -0
  253. package/template/docs/operations/observability.md +5 -1
  254. package/template/docs/platform/agent-runtime.md +21 -11
  255. package/template/docs/platform/agent-tool-sandbox.md +58 -0
  256. package/template/docs/platform/agent-tools.md +5 -0
  257. package/template/docs/platform/document-ai-runtime.md +204 -0
  258. package/template/docs/platform/governed-export-renderers.md +132 -0
  259. package/template/docs/platform/navigation-registry.md +5 -0
  260. package/template/docs/platform/organization-membership.md +2 -1
  261. package/template/docs/platform/speech-ai-runtime.md +156 -0
  262. package/template/docs/platform/system-crud-api.md +7 -1
  263. package/template/docs/platform/vision-ai-runtime.md +97 -0
  264. package/template/docs/platform/web-system-crud.md +6 -0
  265. package/template/package.json +16 -2
  266. package/template/packages/ai-client/index.ts +4 -0
  267. package/template/packages/ai-client/package.json +5 -2
  268. package/template/packages/ai-client/src/agent/client/artifact.ts +26 -0
  269. package/template/packages/ai-client/src/agent/client/attach.test.ts +124 -0
  270. package/template/packages/ai-client/src/agent/client/client.ts +119 -7
  271. package/template/packages/ai-client/src/agent/client/run.ts +14 -0
  272. package/template/packages/ai-client/src/agent/client/types.ts +18 -0
  273. package/template/packages/ai-client/src/agent/client/vision.test.ts +53 -0
  274. package/template/packages/ai-client/src/agent/contracts/contracts.test.ts +1 -0
  275. package/template/packages/ai-client/src/agent/contracts/events.ts +33 -0
  276. package/template/packages/ai-client/src/agent/contracts/runs.ts +71 -0
  277. package/template/packages/ai-client/src/agent/index.ts +1 -0
  278. package/template/packages/ai-client/src/artifact-ref.ts +8 -0
  279. package/template/packages/ai-client/src/document/client.test.ts +114 -0
  280. package/template/packages/ai-client/src/document/client.ts +134 -0
  281. package/template/packages/ai-client/src/document/contracts.ts +152 -0
  282. package/template/packages/ai-client/src/document/error.ts +43 -0
  283. package/template/packages/ai-client/src/document/index.ts +3 -0
  284. package/template/packages/ai-client/src/document/run.ts +104 -0
  285. package/template/packages/ai-client/src/document/validation.ts +115 -0
  286. package/template/packages/ai-client/src/embedding/client.test.ts +77 -0
  287. package/template/packages/ai-client/src/embedding/client.ts +96 -0
  288. package/template/packages/ai-client/src/embedding/contracts.ts +46 -0
  289. package/template/packages/ai-client/src/embedding/error.ts +61 -0
  290. package/template/packages/ai-client/src/embedding/index.ts +3 -0
  291. package/template/packages/ai-client/src/embedding/validation.ts +117 -0
  292. package/template/packages/ai-client/src/speech/client.test.ts +135 -0
  293. package/template/packages/ai-client/src/speech/client.ts +104 -0
  294. package/template/packages/ai-client/src/speech/contracts.ts +129 -0
  295. package/template/packages/ai-client/src/speech/error.ts +43 -0
  296. package/template/packages/ai-client/src/speech/index.ts +3 -0
  297. package/template/packages/ai-client/src/speech/run.ts +133 -0
  298. package/template/packages/ai-client/src/speech/validation.ts +132 -0
  299. package/template/packages/config/package.json +2 -2
  300. package/template/packages/database/drizzle/0039_early_ezekiel_stane.sql +1 -0
  301. package/template/packages/database/drizzle/0040_colossal_baron_strucker.sql +63 -0
  302. package/template/packages/database/drizzle/0041_yummy_prowler.sql +21 -0
  303. package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
  304. package/template/packages/database/drizzle/meta/0039_snapshot.json +7755 -0
  305. package/template/packages/database/drizzle/meta/0040_snapshot.json +8150 -0
  306. package/template/packages/database/drizzle/meta/0041_snapshot.json +8323 -0
  307. package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
  308. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  309. package/template/packages/database/package.json +2 -2
  310. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  311. package/template/packages/database/src/schema/agent-runtime.ts +149 -0
  312. package/template/packages/database/src/schema/common.ts +2 -14
  313. package/template/packages/database/src/schema/durable-jobs.ts +5 -0
  314. package/template/packages/database/src/schema/governed-exports.ts +17 -5
  315. package/template/packages/database/src/schema/index.ts +3 -0
  316. package/template/packages/database/src/schema-durable-recovery.test.ts +31 -0
  317. package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
  318. package/template/packages/database/src/schema.test.ts +6 -23
  319. package/template/packages/jobs/package.json +3 -2
  320. package/template/packages/jobs/src/handlers.test.ts +13 -0
  321. package/template/packages/jobs/src/handlers.ts +7 -1
  322. package/template/packages/jobs/src/types.ts +4 -0
  323. package/template/packages/modules/package.json +2 -2
  324. package/template/packages/modules/src/ai-operation-composition.test-support.ts +165 -0
  325. package/template/packages/modules/src/ai-operation-composition.test.ts +91 -127
  326. package/template/packages/modules/src/ai-operation-composition.ts +59 -1
  327. package/template/packages/modules/src/manifest.ts +25 -1
  328. package/template/packages/modules/src/tool-composition.ts +18 -0
  329. package/template/packages/server-sdk/index.ts +50 -0
  330. package/template/packages/server-sdk/package.json +10 -4
  331. package/template/packages/server-sdk/src/ai-artifact-port.ts +34 -0
  332. package/template/packages/server-sdk/src/ai-operation-port.ts +65 -0
  333. package/template/packages/server-sdk/src/ai-operation-registration.ts +42 -1
  334. package/template/packages/server-sdk/src/background-ai-port.ts +43 -0
  335. package/template/packages/server-sdk/src/data-governance-registration.ts +116 -0
  336. package/template/packages/server-sdk/src/durable-handler.ts +29 -0
  337. package/template/packages/server-sdk/src/jobs-port.ts +69 -0
  338. package/template/packages/server-sdk/src/module-host.ts +16 -2
  339. package/template/packages/server-sdk/src/request-context.ts +6 -2
  340. package/template/packages/server-sdk/src/sandbox-tool.ts +21 -0
  341. package/template/packages/server-sdk/src/tool-registration.ts +2 -0
  342. package/template/packages/shared/package.json +1 -1
  343. package/template/packages/shared/src/rbac.ts +30 -0
@@ -0,0 +1,152 @@
1
+ import type {
2
+ ModuleAiArtifactRef,
3
+ ModuleAiJsonValue,
4
+ } from "@southwind-ai/server-sdk";
5
+ import type { RequestGuardContext } from "../guards.js";
6
+ import type { RouteApp } from "../system/route-types.js";
7
+ import { EmbeddingFacadeError } from "./error.js";
8
+ import type { EmbeddingFacade } from "./facade.js";
9
+ import type { EmbeddingFacadeItem } from "./input.js";
10
+
11
+ interface RouteContext {
12
+ readonly body?: unknown;
13
+ readonly request: Request;
14
+ readonly guardContext: RequestGuardContext;
15
+ }
16
+
17
+ export function registerEmbeddingRoutes(
18
+ app: RouteApp,
19
+ facade: EmbeddingFacade,
20
+ ) {
21
+ app.post("/embeddings", async (raw) => {
22
+ const context = raw as RouteContext;
23
+ try {
24
+ const request = parseRequest(context.body);
25
+ const idempotencyKey = context.request.headers.get("idempotency-key");
26
+ if (!idempotencyKey?.trim()) throw invalid("幂等键不能为空");
27
+ return Response.json(
28
+ await facade.embed(
29
+ request.operationKey,
30
+ request.items,
31
+ request.options,
32
+ idempotencyKey,
33
+ context.guardContext,
34
+ context.request.signal,
35
+ ),
36
+ );
37
+ } catch (error) {
38
+ return knownError(error);
39
+ }
40
+ });
41
+ }
42
+
43
+ function parseRequest(value: unknown): {
44
+ operationKey: string;
45
+ items: readonly EmbeddingFacadeItem[];
46
+ options: ModuleAiJsonValue;
47
+ } {
48
+ if (
49
+ !isRecord(value) ||
50
+ Object.keys(value).some(
51
+ (key) => !["operationKey", "items", "options"].includes(key),
52
+ ) ||
53
+ typeof value.operationKey !== "string" ||
54
+ !value.operationKey.trim() ||
55
+ !Array.isArray(value.items) ||
56
+ !value.items.length ||
57
+ value.items.length > 256
58
+ ) {
59
+ throw invalid("Embedding 请求格式无效");
60
+ }
61
+ const ids = new Set<string>();
62
+ const items = value.items.map((item): EmbeddingFacadeItem => {
63
+ if (
64
+ !isRecord(item) ||
65
+ typeof item.id !== "string" ||
66
+ !item.id.trim() ||
67
+ ids.has(item.id)
68
+ ) {
69
+ throw invalid("Embedding 输入 ID 无效或重复");
70
+ }
71
+ ids.add(item.id);
72
+ if (
73
+ "text" in item &&
74
+ !("source" in item) &&
75
+ typeof item.text === "string" &&
76
+ item.text.trim()
77
+ ) {
78
+ return { id: item.id, text: item.text };
79
+ }
80
+ if (!("text" in item) && isRecord(item.source)) {
81
+ return { id: item.id, source: artifact(item.source) };
82
+ }
83
+ throw invalid("Embedding 输入必须且只能包含文本或 Artifact");
84
+ });
85
+ assertJson(value.options ?? {});
86
+ return {
87
+ operationKey: value.operationKey,
88
+ items,
89
+ options: (value.options ?? {}) as ModuleAiJsonValue,
90
+ };
91
+ }
92
+
93
+ function artifact(value: Record<string, unknown>): ModuleAiArtifactRef {
94
+ if (
95
+ typeof value.value !== "string" ||
96
+ !value.value ||
97
+ /^[a-z][a-z0-9+.-]*:\/\//i.test(value.value) ||
98
+ typeof value.mediaType !== "string" ||
99
+ !Number.isSafeInteger(value.byteSize) ||
100
+ Number(value.byteSize) < 1 ||
101
+ typeof value.sha256 !== "string" ||
102
+ !/^[a-f0-9]{64}$/.test(value.sha256)
103
+ ) {
104
+ throw invalid("Embedding ArtifactRef 无效");
105
+ }
106
+ return {
107
+ value: value.value,
108
+ mediaType: value.mediaType,
109
+ byteSize: Number(value.byteSize),
110
+ sha256: value.sha256,
111
+ expiresAt: typeof value.expiresAt === "string" ? value.expiresAt : "",
112
+ };
113
+ }
114
+
115
+ function assertJson(value: unknown) {
116
+ try {
117
+ if (JSON.stringify(value) === undefined) throw new Error();
118
+ } catch {
119
+ throw invalid("Embedding options 必须是 JSON");
120
+ }
121
+ }
122
+
123
+ function knownError(error: unknown) {
124
+ if (error instanceof EmbeddingFacadeError) {
125
+ return Response.json(
126
+ {
127
+ code: error.code,
128
+ message: error.message,
129
+ requestId: crypto.randomUUID(),
130
+ retryable: error.status >= 500,
131
+ },
132
+ { status: error.status },
133
+ );
134
+ }
135
+ return Response.json(
136
+ {
137
+ code: "RUNTIME_UNAVAILABLE",
138
+ message: "Embedding Runtime 暂不可用",
139
+ requestId: crypto.randomUUID(),
140
+ retryable: true,
141
+ },
142
+ { status: 503 },
143
+ );
144
+ }
145
+
146
+ function invalid(message: string) {
147
+ return new EmbeddingFacadeError("INVALID_INPUT", message, 400);
148
+ }
149
+
150
+ function isRecord(value: unknown): value is Record<string, unknown> {
151
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
152
+ }
@@ -17,14 +17,8 @@ import { createServerRuntime } from "./runtime.js";
17
17
  import { readPositiveInt } from "./runtime-config.js";
18
18
  // @windy-module system.scheduler end
19
19
  // @windy-module system.agent begin
20
- import {
21
- createSystemToolHandlers,
22
- registerAgentRoutes,
23
- } from "./agent/routes.js";
24
20
  import { registerAgentPlatformRuntime } from "./agent/runtime-bootstrap.js";
25
- import { GovernedToolRegistry } from "./agent/tool-registry.js";
26
- import { PlatformMcpAdapter } from "./mcp/adapter.js";
27
- import { registerMcpRoutes } from "./mcp/routes.js";
21
+ import { DeferredAgentBackgroundOperationPortFactory } from "./agent/background-operation-port.js";
28
22
  // @windy-module system.agent end
29
23
  import { createAuthRuntime } from "./auth/runtime.js";
30
24
  import { createServerPersistence } from "./persistence.js";
@@ -56,16 +50,13 @@ import { createStorageRuntime } from "./storage/runtime.js";
56
50
  import { registerStorageRoutes } from "./storage/routes.js";
57
51
  // @windy-module system.storage end
58
52
  import { validateInstalledRuntime } from "./module-runtime-validation.js";
59
- import { installModuleRoutes } from "./module-host.js";
53
+ import { installModuleHttpRuntime } from "./module-host/http-runtime.js";
60
54
  // @windy-module system.search begin
61
55
  import { adaptModuleSearchProviders } from "./module-host.js";
62
56
  // @windy-module system.search end
63
57
  // @windy-module system.scheduler begin
64
58
  import { createModuleTaskDefinitions } from "./module-host.js";
65
59
  // @windy-module system.scheduler end
66
- // @windy-module system.agent begin
67
- import { createModuleToolHandlerMap } from "./module-host.js";
68
- // @windy-module system.agent end
69
60
  // @windy-module system.search begin
70
61
  import { createSearchHost } from "./search/host.js";
71
62
  // @windy-module system.search end
@@ -130,7 +121,13 @@ const observability = createHttpObservability(runtime, persistence, logger);
130
121
  // @windy-module system.license begin
131
122
  platformLicenseRuntime.setAuditSink(runtime.auditSink);
132
123
  // @windy-module system.license end
133
- const { authRepository, authService } = createAuthRuntime({
124
+ const {
125
+ authRepository,
126
+ authService,
127
+ // @windy-module system.data-governance begin
128
+ reauthentication,
129
+ // @windy-module system.data-governance end
130
+ } = createAuthRuntime({
134
131
  repository: persistence?.authRepository,
135
132
  sessionTtlSeconds: () => platformConfig.security.sessionTtlSeconds,
136
133
  passwordPolicy: () => ({
@@ -172,6 +169,9 @@ const moduleTaskDefinitions = createModuleTaskDefinitions(
172
169
  moduleHosts,
173
170
  features,
174
171
  );
172
+ // @windy-module system.agent begin
173
+ const moduleBackgroundAi = new DeferredAgentBackgroundOperationPortFactory();
174
+ // @windy-module system.agent end
175
175
  const {
176
176
  bulkDataService,
177
177
  scheduler,
@@ -190,7 +190,11 @@ const {
190
190
  moduleTasks: moduleTaskDefinitions,
191
191
  bulkRetentionDays: readPositiveInt(process.env.BULK_DATA_RETENTION_DAYS, 7),
192
192
  mailConfig: platformConfig.mail,
193
+ moduleHosts,
193
194
  installModuleDurableHandlers: moduleHosts.installDurableHandlers,
195
+ // @windy-module system.agent begin
196
+ backgroundAi: moduleBackgroundAi,
197
+ // @windy-module system.agent end
194
198
  });
195
199
  const schedulerWorker = new SchedulerWorker(
196
200
  scheduler,
@@ -278,31 +282,19 @@ registerSystemOperationRoutes(app, runtime, features, {
278
282
  });
279
283
  // @windy-module system.operations end
280
284
  // @windy-module system.agent begin
281
- const toolHandlers = new Map([
282
- ...createSystemToolHandlers(systemRepositories, runtime),
283
- ...createModuleToolHandlerMap(moduleHosts, {
285
+ const { toolHandlers, aiArtifacts, aiOperations } =
286
+ registerAgentPlatformRuntime({
287
+ app,
288
+ platformConfig,
289
+ modules: foundationModules,
290
+ hosts: moduleHosts,
284
291
  runtime,
285
- features: foundationSnapshot.features,
286
- // @windy-module system.storage begin
292
+ systemRepositories,
287
293
  storage: storageRuntime,
288
- // @windy-module system.storage end
289
- }),
290
- ]);
291
- const toolRegistry = new GovernedToolRegistry(
292
- foundationModules,
293
- toolHandlers,
294
- runtime,
295
- );
296
- registerAgentRoutes(app, toolRegistry);
297
- registerMcpRoutes(app, new PlatformMcpAdapter(toolRegistry, runtime, features));
298
- registerAgentPlatformRuntime({
299
- app,
300
- platformConfig,
301
- modules: foundationModules,
302
- hosts: moduleHosts,
303
- runtime,
304
- toolRegistry,
305
- });
294
+ workflow: workflowService,
295
+ db: persistence?.db,
296
+ backgroundAi: moduleBackgroundAi,
297
+ });
306
298
  // @windy-module system.agent end
307
299
  // @windy-module system.scheduler begin
308
300
  registerSchedulerRoutes(app, scheduler, runtime, features);
@@ -310,21 +302,26 @@ registerSchedulerRoutes(app, scheduler, runtime, features);
310
302
  // @windy-module system.bulk-data begin
311
303
  registerBulkDataRoutes(app, bulkDataService, runtime, features);
312
304
  // @windy-module system.bulk-data end
313
- installModuleRoutes(moduleHosts, app, {
305
+ installModuleHttpRuntime(moduleHosts, app, {
314
306
  runtime,
315
307
  features,
316
308
  // @windy-module system.scheduler begin
317
309
  jobs: durableJobRuntime.service,
310
+ jobWorker: durableJobRuntime.worker,
318
311
  // @windy-module system.scheduler end
319
312
  // @windy-module system.storage begin
320
- storage: {
321
- uploads: storageRuntime.uploads,
322
- files: storageRuntime.files,
323
- },
313
+ storage: storageRuntime,
324
314
  // @windy-module system.storage end
325
- userDirectory: {
326
- repositories: systemRepositories,
327
- },
315
+ systemRepositories,
316
+ // @windy-module system.agent begin
317
+ aiArtifacts,
318
+ aiOperations,
319
+ // @windy-module system.agent end
320
+ // @windy-module system.data-governance begin
321
+ reauthenticate: (actorId, password) =>
322
+ reauthentication.verify(actorId, password),
323
+ governedExportService,
324
+ // @windy-module system.data-governance end
328
325
  });
329
326
 
330
327
  validateInstalledRuntime({
@@ -1,13 +1,13 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { MemoryBlobStore } from "@southwind-ai/storage";
3
3
  import { DurableArtifactService } from "./artifact-service.js";
4
- import { InMemoryDurableJobRepository } from "./repository.js";
4
+ import { InMemoryDurableExecutionRepository } from "./in-memory-execution.js";
5
5
  import { DurableJobService } from "./service.js";
6
6
 
7
7
  describe("DurableArtifactService", () => {
8
8
  test("只有 Owner 或 manage 权限可读取且过期后统一拒绝", async () => {
9
9
  let now = new Date("2026-07-16T00:00:00.000Z");
10
- const repository = new InMemoryDurableJobRepository();
10
+ const repository = new InMemoryDurableExecutionRepository();
11
11
  const jobs = new DurableJobService(repository, () => now);
12
12
  const submitted = await jobs.submit({
13
13
  handlerKey: "test.artifact",
@@ -70,6 +70,18 @@ describe("DurableArtifactService", () => {
70
70
  ).rejects.toMatchObject({
71
71
  code: "ARTIFACT_EXPIRED",
72
72
  });
73
+
74
+ now = new Date("2026-07-16T12:00:00.000Z");
75
+ await repository.requestCancel({
76
+ jobId: submitted.job.id,
77
+ expectedStatus: "queued",
78
+ now,
79
+ });
80
+ await expect(
81
+ service.open(submitted.job.id, "result", "owner-1"),
82
+ ).rejects.toMatchObject({
83
+ code: "ARTIFACT_JOB_CANCELLED",
84
+ });
73
85
  });
74
86
  });
75
87
 
@@ -5,6 +5,7 @@ import type { DurableJobRepository } from "./repository.js";
5
5
  export type DurableArtifactErrorCode =
6
6
  | "ARTIFACT_NOT_FOUND"
7
7
  | "ARTIFACT_FORBIDDEN"
8
+ | "ARTIFACT_JOB_CANCELLED"
8
9
  | "ARTIFACT_EXPIRED"
9
10
  | "ARTIFACT_CONTENT_MISSING";
10
11
 
@@ -54,6 +55,15 @@ export class DurableArtifactService {
54
55
  if (artifact.ownerId !== actorId && !allowManage) {
55
56
  throw new DurableArtifactError("ARTIFACT_FORBIDDEN", "无权访问任务产物");
56
57
  }
58
+ if (
59
+ snapshot?.job.status === "cancelled" ||
60
+ snapshot?.job.cancelRequestedAt
61
+ ) {
62
+ throw new DurableArtifactError(
63
+ "ARTIFACT_JOB_CANCELLED",
64
+ "已取消任务的产物不可访问",
65
+ );
66
+ }
57
67
  if (artifact.expiresAt && artifact.expiresAt <= this.now().toISOString()) {
58
68
  throw new DurableArtifactError("ARTIFACT_EXPIRED", "任务产物已过期");
59
69
  }
@@ -11,6 +11,8 @@ import { and, eq, max, notInArray } from "drizzle-orm";
11
11
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
12
12
  import type {
13
13
  ClaimDurableJob,
14
+ CancelDurableJob,
15
+ DurableJobControlResult,
14
16
  DurableExecutionRepository,
15
17
  FailDurableJob,
16
18
  LeaseResult,
@@ -220,24 +222,21 @@ export class DrizzleDurableExecutionRepository implements DurableExecutionReposi
220
222
  });
221
223
  }
222
224
 
223
- async requestCancel(jobId: string, now: Date) {
224
- return this.db.transaction(async (tx) => {
225
- const [job] = await tx
226
- .select()
227
- .from(durableJobs)
228
- .where(eq(durableJobs.id, jobId))
229
- .for("update")
230
- .limit(1);
231
- if (!job || !["queued", "running"].includes(job.status)) return false;
232
- if (job.status === "queued") await cancelRemaining(tx, job.id, now);
233
- else {
234
- await tx
235
- .update(durableJobs)
236
- .set({ cancelRequestedAt: now, updatedAt: now })
237
- .where(eq(durableJobs.id, job.id));
238
- }
239
- return true;
240
- });
225
+ requestCancel(
226
+ input: CancelDurableJob,
227
+ ): Promise<DurableJobControlResult>;
228
+ requestCancel(jobId: string, now: Date): Promise<boolean>;
229
+ async requestCancel(
230
+ input: CancelDurableJob | string,
231
+ now?: Date,
232
+ ) {
233
+ const legacy = typeof input === "string";
234
+ const result = await this.recovery.requestCancel(
235
+ legacy ? { jobId: input, now: now! } : input,
236
+ );
237
+ return legacy
238
+ ? ["changed", "unchanged"].includes(result.outcome)
239
+ : result;
241
240
  }
242
241
 
243
242
  sweepExpired(now: Date, retryDelayMs: number) {
@@ -180,7 +180,20 @@ describePostgres("PostgreSQL durable execution", () => {
180
180
  const execution = new DrizzleDurableExecutionRepository(drizzle(pool));
181
181
  const claim = await execution.claim(claimInput("worker-a", base, 10_000));
182
182
  if (!claim) throw new Error("任务未领取");
183
- expect(await execution.requestCancel(submitted.job.id, base)).toBe(true);
183
+ const running = await service.get(submitted.job.id);
184
+ if (!running) throw new Error("运行任务不存在");
185
+ const cancelInput = {
186
+ jobId: submitted.job.id,
187
+ expectedStatus: running.job.status,
188
+ expectedVersion: running.job.updatedAt,
189
+ now: base,
190
+ } as const;
191
+ expect(await execution.requestCancel(cancelInput)).toEqual({
192
+ outcome: "changed",
193
+ });
194
+ expect(await execution.requestCancel(cancelInput)).toEqual({
195
+ outcome: "unchanged",
196
+ });
184
197
  expect(await execution.heartbeat(claim, base, 10_000)).toMatchObject({
185
198
  ok: true,
186
199
  cancelRequested: true,
@@ -190,11 +203,16 @@ describePostgres("PostgreSQL durable execution", () => {
190
203
  ).toBe(1);
191
204
  expect((await service.get(submitted.job.id))?.job.status).toBe("cancelled");
192
205
 
206
+ const cancelled = await service.get(submitted.job.id);
207
+ if (!cancelled) throw new Error("取消任务不存在");
193
208
  const recovery = {
194
209
  jobId: submitted.job.id,
195
210
  requestedBy: "ops_user",
196
211
  reason: "确认下游依赖已经恢复",
197
212
  idempotencyKey: "pg-recovery-1",
213
+ expectedAttempt: 1,
214
+ expectedStatus: "cancelled" as const,
215
+ expectedVersion: cancelled.job.updatedAt,
198
216
  now: new Date(base.getTime() + 2_000),
199
217
  };
200
218
  const results = await Promise.all([
@@ -26,6 +26,7 @@ export function mapJob(row: typeof durableJobs.$inferSelect): DurableJob {
26
26
  return {
27
27
  ...row,
28
28
  status: row.status as DurableJob["status"],
29
+ authorizationScope: row.authorizationScope ?? undefined,
29
30
  availableAt: row.availableAt.toISOString(),
30
31
  cancelRequestedAt: iso(row.cancelRequestedAt),
31
32
  startedAt: iso(row.startedAt),
@@ -89,10 +90,12 @@ export function mapProgress(
89
90
  export function mapArtifact(
90
91
  row: typeof durableJobArtifacts.$inferSelect,
91
92
  ): DurableJobArtifact {
93
+ const { metadata, ...artifact } = row;
92
94
  return {
93
- ...row,
95
+ ...artifact,
94
96
  stageId: row.stageId ?? undefined,
95
97
  artifactRef: artifactRef(row.artifactRef),
98
+ ...(metadata ? { metadata } : {}),
96
99
  expiresAt: iso(row.expiresAt),
97
100
  createdAt: row.createdAt.toISOString(),
98
101
  };
@@ -6,14 +6,48 @@ import {
6
6
  durableJobStages,
7
7
  durableJobs,
8
8
  } from "@southwind-ai/database";
9
- import { and, eq, lte, notInArray, or } from "drizzle-orm";
9
+ import { and, eq, lte, max, notInArray, or } from "drizzle-orm";
10
10
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
11
- import type { ManualJobRecovery } from "./execution-repository.js";
11
+ import type {
12
+ CancelDurableJob,
13
+ ManualJobRecovery,
14
+ } from "./execution-repository.js";
12
15
  import { mapRecovery } from "./drizzle-mappers.js";
13
16
 
14
17
  export class DrizzleDurableRecoveryRepository {
15
18
  constructor(private readonly db: NodePgDatabase) {}
16
19
 
20
+ async requestCancel(input: CancelDurableJob) {
21
+ return this.db.transaction(async (tx) => {
22
+ const [job] = await tx
23
+ .select()
24
+ .from(durableJobs)
25
+ .where(eq(durableJobs.id, input.jobId))
26
+ .for("update")
27
+ .limit(1);
28
+ if (!job) return { outcome: "not-found" } as const;
29
+ if (job.status === "cancelled" || job.cancelRequestedAt) {
30
+ return { outcome: "unchanged" } as const;
31
+ }
32
+ if (
33
+ !["queued", "running"].includes(job.status) ||
34
+ (input.expectedStatus && job.status !== input.expectedStatus) ||
35
+ (input.expectedVersion &&
36
+ job.updatedAt.toISOString() !== input.expectedVersion)
37
+ ) {
38
+ return { outcome: "conflict" } as const;
39
+ }
40
+ if (job.status === "queued") await cancelRemaining(tx, job.id, input.now);
41
+ else {
42
+ await tx
43
+ .update(durableJobs)
44
+ .set({ cancelRequestedAt: input.now, updatedAt: input.now })
45
+ .where(eq(durableJobs.id, job.id));
46
+ }
47
+ return { outcome: "changed" } as const;
48
+ });
49
+ }
50
+
17
51
  async sweepExpired(now: Date, retryDelayMs: number) {
18
52
  return this.db.transaction(async (tx) => {
19
53
  const rows = await tx
@@ -139,6 +173,19 @@ export class DrizzleDurableRecoveryRepository {
139
173
  if (!["failed", "cancelled"].includes(job.status)) {
140
174
  throw new Error("只有失败或已取消任务可以人工恢复");
141
175
  }
176
+ const [{ attempt = 0 } = { attempt: 0 }] = await tx
177
+ .select({ attempt: max(durableJobStages.currentAttempt) })
178
+ .from(durableJobStages)
179
+ .where(eq(durableJobStages.jobId, job.id));
180
+ if (
181
+ (input.expectedStatus && job.status !== input.expectedStatus) ||
182
+ (input.expectedVersion &&
183
+ job.updatedAt.toISOString() !== input.expectedVersion) ||
184
+ (input.expectedAttempt !== undefined &&
185
+ (attempt || 0) !== input.expectedAttempt)
186
+ ) {
187
+ throw new Error("任务状态已变化");
188
+ }
142
189
  const [activeLease] = await tx
143
190
  .select({ id: durableJobLeases.id })
144
191
  .from(durableJobLeases)
@@ -49,6 +49,7 @@ export class DrizzleDurableJobRepository implements DurableJobRepository {
49
49
  payload: input.payload,
50
50
  priority: input.priority || 0,
51
51
  requestedBy: input.requestedBy,
52
+ authorizationScope: input.authorizationScope,
52
53
  maxAttempts: input.maxAttempts || 3,
53
54
  timeoutSeconds: input.timeoutSeconds || 900,
54
55
  availableAt: input.availableAt || now,
@@ -2,6 +2,7 @@ import type {
2
2
  ClaimedDurableJob,
3
3
  DurableJobArtifact,
4
4
  DurableJobRecovery,
5
+ DurableJobStatus,
5
6
  DurableLeaseIdentity,
6
7
  } from "@southwind-ai/jobs";
7
8
 
@@ -32,6 +33,9 @@ export interface ManualJobRecovery {
32
33
  requestedBy: string;
33
34
  reason: string;
34
35
  idempotencyKey: string;
36
+ expectedAttempt?: number;
37
+ expectedStatus?: DurableJobStatus;
38
+ expectedVersion?: string;
35
39
  now: Date;
36
40
  }
37
41
 
@@ -40,6 +44,17 @@ export interface ManualJobRecoveryResult {
40
44
  recovered: boolean;
41
45
  }
42
46
 
47
+ export interface CancelDurableJob {
48
+ jobId: string;
49
+ expectedStatus?: DurableJobStatus;
50
+ expectedVersion?: string;
51
+ now: Date;
52
+ }
53
+
54
+ export type DurableJobControlResult =
55
+ | { outcome: "changed" | "unchanged" }
56
+ | { outcome: "not-found" | "conflict" };
57
+
43
58
  export interface DurableExecutionRepository {
44
59
  claim(input: ClaimDurableJob): Promise<ClaimedDurableJob | undefined>;
45
60
  heartbeat(
@@ -66,7 +81,9 @@ export interface DurableExecutionRepository {
66
81
  failure: FailDurableJob,
67
82
  now: Date,
68
83
  ): Promise<LeaseResult>;
84
+ /** @deprecated 兼容平台内部旧调用;模块 Port 必须使用 CAS 输入。 */
69
85
  requestCancel(jobId: string, now: Date): Promise<boolean>;
86
+ requestCancel(input: CancelDurableJob): Promise<DurableJobControlResult>;
70
87
  sweepExpired(now: Date, retryDelayMs: number): Promise<number>;
71
88
  recover(input: ManualJobRecovery): Promise<ManualJobRecoveryResult>;
72
89
  }
@@ -287,6 +287,40 @@ describe("durable job execution", () => {
287
287
  expect(observed).toEqual(["prepare:2", "apply:2"]);
288
288
  expect((await service.get(submitted.job.id))?.job.status).toBe("succeeded");
289
289
  });
290
+
291
+ test("模块 Handler 每次尝试都通过 contextualizer 重建执行上下文", async () => {
292
+ const repository = new InMemoryDurableExecutionRepository();
293
+ const service = new DurableJobService(repository, () => base);
294
+ await service.submit(jobInput("module-context"));
295
+ const observed: string[] = [];
296
+ const handlers = new DurableHandlerRegistry().register(
297
+ {
298
+ key: "test.handler",
299
+ payload: { parse: () => ({}) },
300
+ execute: (_payload, context) => {
301
+ observed.push(
302
+ `${(context as { requestId?: string }).requestId}:${context.attemptNumber}`,
303
+ );
304
+ },
305
+ },
306
+ { moduleName: "test" },
307
+ );
308
+ let rebuilds = 0;
309
+ const worker = new DurableJobWorker(repository, handlers, {
310
+ now: () => base,
311
+ contextualize: async ({ moduleName, context }) => {
312
+ rebuilds += 1;
313
+ expect(moduleName).toBe("test");
314
+ return { ...context, requestId: `request-${rebuilds}` };
315
+ },
316
+ });
317
+
318
+ expect(await worker.runNext("default", "worker-a")).toMatchObject({
319
+ status: "succeeded",
320
+ });
321
+ expect(observed).toEqual(["request-1:1"]);
322
+ expect(rebuilds).toBe(1);
323
+ });
290
324
  });
291
325
 
292
326
  function jobInput(idempotencyKey: string) {