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.
- package/README.md +27 -9
- package/dist/cli.js +825 -117
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +16 -3
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +4 -0
- package/template/README.md +18 -11
- package/template/_gitignore +1 -0
- package/template/apps/agent-server/README.md +2 -0
- package/template/apps/agent-server/pyproject.toml +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +87 -10
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +18 -3
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +78 -11
- package/template/apps/agent-server/src/southwind_agent_server/app.py +120 -115
- package/template/apps/agent-server/src/southwind_agent_server/artifact_input.py +153 -0
- package/template/apps/agent-server/src/southwind_agent_server/config.py +29 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +81 -1
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +28 -0
- package/template/apps/agent-server/src/southwind_agent_server/fingerprint.py +41 -0
- package/template/apps/agent-server/src/southwind_agent_server/http_helpers.py +62 -0
- package/template/apps/agent-server/src/southwind_agent_server/lease.py +15 -0
- package/template/apps/agent-server/src/southwind_agent_server/multimodal.py +76 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +112 -20
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +14 -1
- package/template/apps/agent-server/src/southwind_agent_server/postgres_store.py +329 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_store_backend.py +332 -0
- package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +64 -3
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +68 -0
- package/template/apps/agent-server/src/southwind_agent_server/service.py +218 -56
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +36 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_catalog.py +107 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_schema.py +65 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_serialization.py +16 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +89 -57
- package/template/apps/agent-server/src/southwind_agent_server/store.py +105 -2
- package/template/apps/agent-server/src/southwind_agent_server/store_cipher.py +40 -0
- package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +89 -0
- package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +72 -0
- package/template/apps/agent-server/tests/test_adk_adapter.py +6 -2
- package/template/apps/agent-server/tests/test_api.py +24 -1
- package/template/apps/agent-server/tests/test_artifact_input.py +110 -0
- package/template/apps/agent-server/tests/test_config.py +4 -0
- package/template/apps/agent-server/tests/test_fingerprint.py +10 -0
- package/template/apps/agent-server/tests/test_multimodal.py +51 -0
- package/template/apps/agent-server/tests/test_openai_provider.py +6 -2
- package/template/apps/agent-server/tests/test_postgres_store.py +131 -0
- package/template/apps/agent-server/tests/test_provider_governance.py +61 -0
- package/template/apps/agent-server/tests/test_sqlite_store.py +95 -10
- package/template/apps/agent-server/tests/test_telemetry.py +25 -0
- package/template/apps/agent-server/tests/test_tool_approval.py +28 -0
- package/template/apps/agent-server/uv.lock +165 -0
- package/template/apps/document-server/Dockerfile +19 -0
- package/template/apps/document-server/pyproject.toml +43 -0
- package/template/apps/document-server/src/southwind_document_server/__init__.py +1 -0
- package/template/apps/document-server/src/southwind_document_server/adapter.py +337 -0
- package/template/apps/document-server/src/southwind_document_server/app.py +151 -0
- package/template/apps/document-server/src/southwind_document_server/config.py +94 -0
- package/template/apps/document-server/src/southwind_document_server/contracts.py +103 -0
- package/template/apps/document-server/src/southwind_document_server/service.py +149 -0
- package/template/apps/document-server/src/southwind_document_server/store.py +185 -0
- package/template/apps/document-server/tests/__init__.py +1 -0
- package/template/apps/document-server/tests/test_config.py +42 -0
- package/template/apps/document-server/tests/test_service.py +140 -0
- package/template/apps/document-server/uv.lock +430 -0
- package/template/apps/embedding-server/Dockerfile +17 -0
- package/template/apps/embedding-server/pyproject.toml +44 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/__init__.py +1 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/adapter.py +254 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/app.py +119 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/config.py +120 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +106 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/error.py +6 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/image_input.py +87 -0
- package/template/apps/embedding-server/tests/__init__.py +1 -0
- package/template/apps/embedding-server/tests/test_adapter.py +92 -0
- package/template/apps/embedding-server/tests/test_config.py +38 -0
- package/template/apps/embedding-server/uv.lock +449 -0
- package/template/apps/server/src/agent/background-operation-port.ts +37 -0
- package/template/apps/server/src/agent/background-operation.test.ts +231 -0
- package/template/apps/server/src/agent/background-operation.ts +252 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +114 -0
- package/template/apps/server/src/agent/execution-grants.ts +7 -1
- package/template/apps/server/src/agent/fake-runtime.test-support.ts +43 -0
- package/template/apps/server/src/agent/idempotency-key.test.ts +28 -0
- package/template/apps/server/src/agent/idempotency-key.ts +11 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +115 -1
- package/template/apps/server/src/agent/remote-runtime.ts +173 -5
- package/template/apps/server/src/agent/run-authorization.test-support.ts +9 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +126 -0
- package/template/apps/server/src/agent/run-authorization.ts +180 -0
- package/template/apps/server/src/agent/run-binding-repository.ts +93 -0
- package/template/apps/server/src/agent/run-contracts.ts +88 -1
- package/template/apps/server/src/agent/run-definition-support.ts +58 -0
- package/template/apps/server/src/agent/run-errors.ts +25 -0
- package/template/apps/server/src/agent/run-facade.test.ts +57 -46
- package/template/apps/server/src/agent/run-facade.ts +185 -65
- package/template/apps/server/src/agent/run-release.ts +78 -0
- package/template/apps/server/src/agent/run-route-input.ts +169 -0
- package/template/apps/server/src/agent/run-routes.ts +63 -76
- package/template/apps/server/src/agent/runtime-bootstrap.ts +169 -13
- package/template/apps/server/src/agent/sandbox-client.ts +200 -0
- package/template/apps/server/src/agent/sandbox-protocol.ts +76 -0
- package/template/apps/server/src/agent/sandbox-registry.ts +205 -0
- package/template/apps/server/src/agent/sandbox-runtime.test.ts +137 -0
- package/template/apps/server/src/agent/sandbox-worker-server.ts +195 -0
- package/template/apps/server/src/agent/sandbox-worker.ts +12 -0
- package/template/apps/server/src/agent/tool-approval.test.ts +75 -0
- package/template/apps/server/src/agent/tool-approval.ts +115 -0
- package/template/apps/server/src/agent/tool-audit.ts +62 -0
- package/template/apps/server/src/agent/tool-host.test.ts +39 -11
- package/template/apps/server/src/agent/tool-host.ts +184 -17
- package/template/apps/server/src/agent/tool-registry.ts +52 -47
- package/template/apps/server/src/agent/vision-facade.test.ts +256 -0
- package/template/apps/server/src/agent/vision-input.ts +120 -0
- package/template/apps/server/src/ai-artifact/codec.test.ts +44 -0
- package/template/apps/server/src/ai-artifact/codec.ts +137 -0
- package/template/apps/server/src/ai-artifact/routes.ts +37 -0
- package/template/apps/server/src/ai-artifact/runtime.ts +205 -0
- package/template/apps/server/src/application-services.ts +62 -2
- package/template/apps/server/src/auth/reauthentication.test.ts +40 -0
- package/template/apps/server/src/auth/reauthentication.ts +19 -0
- package/template/apps/server/src/auth/runtime.ts +9 -1
- package/template/apps/server/src/data-access/context.ts +6 -4
- package/template/apps/server/src/data-access/scope-snapshot.ts +27 -0
- package/template/apps/server/src/data-governance/export/approval-port.ts +6 -3
- package/template/apps/server/src/data-governance/export/authorization-port.test.ts +23 -4
- package/template/apps/server/src/data-governance/export/authorization-port.ts +89 -25
- package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
- package/template/apps/server/src/data-governance/export/job-runtime.test.ts +15 -4
- package/template/apps/server/src/data-governance/export/job-runtime.ts +68 -15
- package/template/apps/server/src/data-governance/export/ndjson-stream.ts +25 -6
- package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +4 -2
- package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
- package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
- package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
- package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
- package/template/apps/server/src/data-governance/export/routes.ts +11 -1
- package/template/apps/server/src/data-governance/export/scope-snapshot.ts +6 -18
- package/template/apps/server/src/data-governance/export/service-support.ts +188 -0
- package/template/apps/server/src/data-governance/export/service.ts +90 -113
- package/template/apps/server/src/data-governance/export/source.ts +101 -0
- package/template/apps/server/src/data-governance/export/types.ts +27 -0
- package/template/apps/server/src/document/contracts.ts +76 -0
- package/template/apps/server/src/document/facade.ts +339 -0
- package/template/apps/server/src/document/remote-runtime.test.ts +96 -0
- package/template/apps/server/src/document/remote-runtime.ts +223 -0
- package/template/apps/server/src/document/routes.ts +193 -0
- package/template/apps/server/src/embedding/contracts.ts +83 -0
- package/template/apps/server/src/embedding/error.ts +10 -0
- package/template/apps/server/src/embedding/facade.ts +217 -0
- package/template/apps/server/src/embedding/input.ts +172 -0
- package/template/apps/server/src/embedding/remote-runtime.test.ts +66 -0
- package/template/apps/server/src/embedding/remote-runtime.ts +218 -0
- package/template/apps/server/src/embedding/routes.ts +152 -0
- package/template/apps/server/src/index.ts +40 -43
- package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
- package/template/apps/server/src/jobs/artifact-service.ts +10 -0
- package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
- package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
- package/template/apps/server/src/jobs/drizzle-mappers.ts +4 -1
- package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
- package/template/apps/server/src/jobs/drizzle-repository.ts +1 -0
- package/template/apps/server/src/jobs/execution-repository.ts +17 -0
- package/template/apps/server/src/jobs/execution.test.ts +34 -0
- package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
- package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +5 -2
- package/template/apps/server/src/jobs/repository.ts +3 -0
- package/template/apps/server/src/jobs/worker.ts +80 -41
- package/template/apps/server/src/module-host/ai-operation-port.ts +170 -0
- package/template/apps/server/src/module-host/durable-context.test.ts +312 -0
- package/template/apps/server/src/module-host/durable-context.ts +124 -0
- package/template/apps/server/src/module-host/durable-handler-registration.ts +72 -0
- package/template/apps/server/src/module-host/governed-export-route-support.ts +171 -0
- package/template/apps/server/src/module-host/governed-export-routes.test.ts +327 -0
- package/template/apps/server/src/module-host/governed-export-routes.ts +238 -0
- package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
- package/template/apps/server/src/module-host/governed-resource-routes.ts +331 -0
- package/template/apps/server/src/module-host/governed-resources.test.ts +344 -0
- package/template/apps/server/src/module-host/governed-resources.ts +203 -0
- package/template/apps/server/src/module-host/http-runtime.ts +95 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
- package/template/apps/server/src/module-host/initialize.test.ts +49 -1
- package/template/apps/server/src/module-host/initialize.ts +39 -16
- package/template/apps/server/src/module-host/job-port.test.ts +262 -0
- package/template/apps/server/src/module-host/job-port.ts +334 -0
- package/template/apps/server/src/module-host/request-context.test.ts +88 -2
- package/template/apps/server/src/module-host/request-context.ts +56 -22
- package/template/apps/server/src/module-host/route-installer.test.ts +10 -0
- package/template/apps/server/src/module-host/route-installer.ts +36 -1
- package/template/apps/server/src/module-host/sandbox-tool-registration.ts +42 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +67 -31
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/module-runtime-validation.ts +7 -3
- package/template/apps/server/src/speech/contracts.ts +76 -0
- package/template/apps/server/src/speech/facade.ts +316 -0
- package/template/apps/server/src/speech/remote-runtime.test.ts +88 -0
- package/template/apps/server/src/speech/remote-runtime.ts +227 -0
- package/template/apps/server/src/speech/routes.ts +180 -0
- package/template/apps/server/src/system/actor.ts +39 -0
- package/template/apps/server/src/system/route-helpers.ts +1 -0
- package/template/apps/server/src/workflow/service.ts +2 -1
- package/template/apps/speech-server/Dockerfile +22 -0
- package/template/apps/speech-server/pyproject.toml +43 -0
- package/template/apps/speech-server/src/southwind_speech_server/__init__.py +1 -0
- package/template/apps/speech-server/src/southwind_speech_server/adapter.py +249 -0
- package/template/apps/speech-server/src/southwind_speech_server/app.py +183 -0
- package/template/apps/speech-server/src/southwind_speech_server/config.py +100 -0
- package/template/apps/speech-server/src/southwind_speech_server/contracts.py +103 -0
- package/template/apps/speech-server/src/southwind_speech_server/errors.py +6 -0
- package/template/apps/speech-server/src/southwind_speech_server/normalizer.py +108 -0
- package/template/apps/speech-server/src/southwind_speech_server/output_validation.py +128 -0
- package/template/apps/speech-server/src/southwind_speech_server/service.py +162 -0
- package/template/apps/speech-server/src/southwind_speech_server/store.py +209 -0
- package/template/apps/speech-server/tests/__init__.py +1 -0
- package/template/apps/speech-server/tests/test_config.py +33 -0
- package/template/apps/speech-server/tests/test_service.py +154 -0
- package/template/apps/speech-server/uv.lock +430 -0
- package/template/apps/web/src/app/business-routes.ts +22 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
- package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
- package/template/apps/web/src/components/common/FormDialog.vue +6 -0
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +13 -0
- package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
- package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
- package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
- package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
- package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
- package/template/apps/web/src/layout/AppShell.vue +28 -25
- package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
- package/template/apps/web/src/router/business-routes.ts +68 -0
- package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
- package/template/apps/web/src/router/index.ts +7 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/access-api.ts +5 -1
- package/template/apps/web/src/services/access-api.webtest.ts +35 -0
- package/template/docker-compose.yml +127 -0
- package/template/docs/architecture/ai-egress.md +12 -9
- package/template/docs/architecture/ai-runtime.md +23 -11
- package/template/docs/architecture/durable-jobs.md +25 -3
- package/template/docs/architecture/foundation-infrastructure.md +163 -0
- package/template/docs/architecture/module-host.md +115 -0
- package/template/docs/architecture/server-module-interface.md +39 -0
- package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
- package/template/docs/development/business-web-routes.md +66 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/development/module-lifecycle.md +71 -0
- package/template/docs/operations/observability.md +5 -1
- package/template/docs/platform/agent-runtime.md +21 -11
- package/template/docs/platform/agent-tool-sandbox.md +58 -0
- package/template/docs/platform/agent-tools.md +5 -0
- package/template/docs/platform/document-ai-runtime.md +204 -0
- package/template/docs/platform/governed-export-renderers.md +132 -0
- package/template/docs/platform/navigation-registry.md +5 -0
- package/template/docs/platform/organization-membership.md +2 -1
- package/template/docs/platform/speech-ai-runtime.md +156 -0
- package/template/docs/platform/system-crud-api.md +7 -1
- package/template/docs/platform/vision-ai-runtime.md +97 -0
- package/template/docs/platform/web-system-crud.md +6 -0
- package/template/package.json +16 -2
- package/template/packages/ai-client/index.ts +4 -0
- package/template/packages/ai-client/package.json +5 -2
- package/template/packages/ai-client/src/agent/client/artifact.ts +26 -0
- package/template/packages/ai-client/src/agent/client/attach.test.ts +124 -0
- package/template/packages/ai-client/src/agent/client/client.ts +119 -7
- package/template/packages/ai-client/src/agent/client/run.ts +14 -0
- package/template/packages/ai-client/src/agent/client/types.ts +18 -0
- package/template/packages/ai-client/src/agent/client/vision.test.ts +53 -0
- package/template/packages/ai-client/src/agent/contracts/contracts.test.ts +1 -0
- package/template/packages/ai-client/src/agent/contracts/events.ts +33 -0
- package/template/packages/ai-client/src/agent/contracts/runs.ts +71 -0
- package/template/packages/ai-client/src/agent/index.ts +1 -0
- package/template/packages/ai-client/src/artifact-ref.ts +8 -0
- package/template/packages/ai-client/src/document/client.test.ts +114 -0
- package/template/packages/ai-client/src/document/client.ts +134 -0
- package/template/packages/ai-client/src/document/contracts.ts +152 -0
- package/template/packages/ai-client/src/document/error.ts +43 -0
- package/template/packages/ai-client/src/document/index.ts +3 -0
- package/template/packages/ai-client/src/document/run.ts +104 -0
- package/template/packages/ai-client/src/document/validation.ts +115 -0
- package/template/packages/ai-client/src/embedding/client.test.ts +77 -0
- package/template/packages/ai-client/src/embedding/client.ts +96 -0
- package/template/packages/ai-client/src/embedding/contracts.ts +46 -0
- package/template/packages/ai-client/src/embedding/error.ts +61 -0
- package/template/packages/ai-client/src/embedding/index.ts +3 -0
- package/template/packages/ai-client/src/embedding/validation.ts +117 -0
- package/template/packages/ai-client/src/speech/client.test.ts +135 -0
- package/template/packages/ai-client/src/speech/client.ts +104 -0
- package/template/packages/ai-client/src/speech/contracts.ts +129 -0
- package/template/packages/ai-client/src/speech/error.ts +43 -0
- package/template/packages/ai-client/src/speech/index.ts +3 -0
- package/template/packages/ai-client/src/speech/run.ts +133 -0
- package/template/packages/ai-client/src/speech/validation.ts +132 -0
- package/template/packages/config/package.json +2 -2
- package/template/packages/database/drizzle/0039_early_ezekiel_stane.sql +1 -0
- package/template/packages/database/drizzle/0040_colossal_baron_strucker.sql +63 -0
- package/template/packages/database/drizzle/0041_yummy_prowler.sql +21 -0
- package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
- package/template/packages/database/drizzle/meta/0039_snapshot.json +7755 -0
- package/template/packages/database/drizzle/meta/0040_snapshot.json +8150 -0
- package/template/packages/database/drizzle/meta/0041_snapshot.json +8323 -0
- package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +2 -2
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +149 -0
- package/template/packages/database/src/schema/common.ts +2 -14
- package/template/packages/database/src/schema/durable-jobs.ts +5 -0
- package/template/packages/database/src/schema/governed-exports.ts +17 -5
- package/template/packages/database/src/schema/index.ts +3 -0
- package/template/packages/database/src/schema-durable-recovery.test.ts +31 -0
- package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
- package/template/packages/database/src/schema.test.ts +6 -23
- package/template/packages/jobs/package.json +3 -2
- package/template/packages/jobs/src/handlers.test.ts +13 -0
- package/template/packages/jobs/src/handlers.ts +7 -1
- package/template/packages/jobs/src/types.ts +4 -0
- package/template/packages/modules/package.json +2 -2
- package/template/packages/modules/src/ai-operation-composition.test-support.ts +165 -0
- package/template/packages/modules/src/ai-operation-composition.test.ts +91 -127
- package/template/packages/modules/src/ai-operation-composition.ts +59 -1
- package/template/packages/modules/src/manifest.ts +25 -1
- package/template/packages/modules/src/tool-composition.ts +18 -0
- package/template/packages/server-sdk/index.ts +50 -0
- package/template/packages/server-sdk/package.json +10 -4
- package/template/packages/server-sdk/src/ai-artifact-port.ts +34 -0
- package/template/packages/server-sdk/src/ai-operation-port.ts +65 -0
- package/template/packages/server-sdk/src/ai-operation-registration.ts +42 -1
- package/template/packages/server-sdk/src/background-ai-port.ts +43 -0
- package/template/packages/server-sdk/src/data-governance-registration.ts +116 -0
- package/template/packages/server-sdk/src/durable-handler.ts +29 -0
- package/template/packages/server-sdk/src/jobs-port.ts +69 -0
- package/template/packages/server-sdk/src/module-host.ts +16 -2
- package/template/packages/server-sdk/src/request-context.ts +6 -2
- package/template/packages/server-sdk/src/sandbox-tool.ts +21 -0
- package/template/packages/server-sdk/src/tool-registration.ts +2 -0
- package/template/packages/shared/package.json +1 -1
- 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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
285
|
+
const { toolHandlers, aiArtifacts, aiOperations } =
|
|
286
|
+
registerAgentPlatformRuntime({
|
|
287
|
+
app,
|
|
288
|
+
platformConfig,
|
|
289
|
+
modules: foundationModules,
|
|
290
|
+
hosts: moduleHosts,
|
|
284
291
|
runtime,
|
|
285
|
-
|
|
286
|
-
// @windy-module system.storage begin
|
|
292
|
+
systemRepositories,
|
|
287
293
|
storage: storageRuntime,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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 {
|
|
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) {
|