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,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NormalizationError(Exception):
|
|
11
|
+
def __init__(self, code: str, message: str) -> None:
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.code = code
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class NormalizedAudio:
|
|
18
|
+
path: Path
|
|
19
|
+
duration_ms: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AudioNormalizer:
|
|
23
|
+
@property
|
|
24
|
+
def available(self) -> bool:
|
|
25
|
+
return bool(shutil.which("ffmpeg") and shutil.which("ffprobe"))
|
|
26
|
+
|
|
27
|
+
async def normalize(
|
|
28
|
+
self,
|
|
29
|
+
source: Path,
|
|
30
|
+
target: Path,
|
|
31
|
+
max_duration_seconds: int,
|
|
32
|
+
timeout_seconds: int,
|
|
33
|
+
) -> NormalizedAudio:
|
|
34
|
+
if not self.available:
|
|
35
|
+
raise NormalizationError("CAPABILITY_UNAVAILABLE", "音频归一化工具不可用")
|
|
36
|
+
duration_ms = await self._duration(source, timeout_seconds)
|
|
37
|
+
if duration_ms > max_duration_seconds * 1000:
|
|
38
|
+
raise NormalizationError("BUDGET_EXCEEDED", "音频时长超过 Operation 配额")
|
|
39
|
+
await run_process(
|
|
40
|
+
[
|
|
41
|
+
"ffmpeg",
|
|
42
|
+
"-nostdin",
|
|
43
|
+
"-v",
|
|
44
|
+
"error",
|
|
45
|
+
"-i",
|
|
46
|
+
str(source),
|
|
47
|
+
"-vn",
|
|
48
|
+
"-ac",
|
|
49
|
+
"1",
|
|
50
|
+
"-ar",
|
|
51
|
+
"16000",
|
|
52
|
+
"-c:a",
|
|
53
|
+
"pcm_s16le",
|
|
54
|
+
"-y",
|
|
55
|
+
str(target),
|
|
56
|
+
],
|
|
57
|
+
timeout_seconds,
|
|
58
|
+
)
|
|
59
|
+
if not await asyncio.to_thread(valid_output, target):
|
|
60
|
+
raise NormalizationError("INVALID_INPUT", "音频归一化输出无效")
|
|
61
|
+
return NormalizedAudio(target, duration_ms)
|
|
62
|
+
|
|
63
|
+
async def _duration(self, source: Path, timeout_seconds: int) -> int:
|
|
64
|
+
output = await run_process(
|
|
65
|
+
[
|
|
66
|
+
"ffprobe",
|
|
67
|
+
"-v",
|
|
68
|
+
"error",
|
|
69
|
+
"-show_entries",
|
|
70
|
+
"format=duration",
|
|
71
|
+
"-of",
|
|
72
|
+
"json",
|
|
73
|
+
str(source),
|
|
74
|
+
],
|
|
75
|
+
timeout_seconds,
|
|
76
|
+
)
|
|
77
|
+
try:
|
|
78
|
+
value = json.loads(output)
|
|
79
|
+
seconds = float(value["format"]["duration"])
|
|
80
|
+
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
|
|
81
|
+
raise NormalizationError("INVALID_INPUT", "无法读取音频时长") from None
|
|
82
|
+
if seconds <= 0:
|
|
83
|
+
raise NormalizationError("INVALID_INPUT", "音频时长无效")
|
|
84
|
+
return round(seconds * 1000)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def run_process(arguments: list[str], timeout_seconds: int) -> str:
|
|
88
|
+
process = await asyncio.create_subprocess_exec(
|
|
89
|
+
*arguments,
|
|
90
|
+
stdout=asyncio.subprocess.PIPE,
|
|
91
|
+
stderr=asyncio.subprocess.PIPE,
|
|
92
|
+
)
|
|
93
|
+
try:
|
|
94
|
+
stdout, _stderr = await asyncio.wait_for(
|
|
95
|
+
process.communicate(),
|
|
96
|
+
timeout=max(1, timeout_seconds),
|
|
97
|
+
)
|
|
98
|
+
except TimeoutError:
|
|
99
|
+
process.kill()
|
|
100
|
+
await process.wait()
|
|
101
|
+
raise NormalizationError("BUDGET_EXCEEDED", "音频归一化超时") from None
|
|
102
|
+
if process.returncode != 0:
|
|
103
|
+
raise NormalizationError("INVALID_INPUT", "音频格式不受支持或内容损坏")
|
|
104
|
+
return stdout.decode("utf-8", errors="strict")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def valid_output(target: Path) -> bool:
|
|
108
|
+
return target.is_file() and target.stat().st_size > 44
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .contracts import JsonValue
|
|
4
|
+
from .errors import SafeSpeechError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def canonicalize_output(
|
|
8
|
+
value: JsonValue,
|
|
9
|
+
duration_ms: int,
|
|
10
|
+
max_units: int,
|
|
11
|
+
) -> JsonValue:
|
|
12
|
+
if not isinstance(value, dict):
|
|
13
|
+
raise invalid_output("Speech 输出缺少分段")
|
|
14
|
+
segments = value.get("segments")
|
|
15
|
+
if not isinstance(segments, list):
|
|
16
|
+
raise invalid_output("Speech 输出缺少分段")
|
|
17
|
+
if len(segments) > max_units:
|
|
18
|
+
raise invalid_output("Speech 输出分段超过上限")
|
|
19
|
+
canonical: list[JsonValue] = []
|
|
20
|
+
previous_end = 0
|
|
21
|
+
for index, segment in enumerate(segments, 1):
|
|
22
|
+
if not isinstance(segment, dict):
|
|
23
|
+
raise invalid_output("Speech 分段结构无效")
|
|
24
|
+
start = integer(segment.get("startMs"))
|
|
25
|
+
end = integer(segment.get("endMs"))
|
|
26
|
+
text = segment.get("text")
|
|
27
|
+
if (
|
|
28
|
+
start is None
|
|
29
|
+
or end is None
|
|
30
|
+
or start < previous_end
|
|
31
|
+
or end <= start
|
|
32
|
+
or end > duration_ms + 1000
|
|
33
|
+
or not isinstance(text, str)
|
|
34
|
+
or not text.strip()
|
|
35
|
+
):
|
|
36
|
+
raise invalid_output("Speech 分段时间戳或文本无效")
|
|
37
|
+
item: dict[str, JsonValue] = {
|
|
38
|
+
"id": f"segment-{index:06d}",
|
|
39
|
+
"text": text,
|
|
40
|
+
"startMs": start,
|
|
41
|
+
"endMs": end,
|
|
42
|
+
}
|
|
43
|
+
confidence = probability(segment.get("confidence"))
|
|
44
|
+
if confidence is not None:
|
|
45
|
+
item["confidence"] = confidence
|
|
46
|
+
language = segment.get("language")
|
|
47
|
+
if language is not None:
|
|
48
|
+
if not isinstance(language, str) or not language.strip():
|
|
49
|
+
raise invalid_output("Speech 分段语言无效")
|
|
50
|
+
item["language"] = language
|
|
51
|
+
words = segment.get("words")
|
|
52
|
+
if words is not None:
|
|
53
|
+
item["words"] = canonicalize_words(words, start, end)
|
|
54
|
+
canonical.append(item)
|
|
55
|
+
previous_end = end
|
|
56
|
+
metadata = value.get("metadata", {})
|
|
57
|
+
if not isinstance(metadata, dict) or any(
|
|
58
|
+
not isinstance(item, str | int | float | bool) for item in metadata.values()
|
|
59
|
+
):
|
|
60
|
+
raise invalid_output("Speech 元数据无效")
|
|
61
|
+
result: dict[str, JsonValue] = {
|
|
62
|
+
"durationMs": duration_ms,
|
|
63
|
+
"segments": canonical,
|
|
64
|
+
"metadata": {**metadata, "normalizedFormat": "pcm_s16le/16000/mono"},
|
|
65
|
+
}
|
|
66
|
+
language = value.get("language")
|
|
67
|
+
if language is not None:
|
|
68
|
+
if not isinstance(language, str) or not language.strip():
|
|
69
|
+
raise invalid_output("Speech 语言无效")
|
|
70
|
+
result["language"] = language
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def canonicalize_words(
|
|
75
|
+
value: JsonValue, segment_start: int, segment_end: int
|
|
76
|
+
) -> JsonValue:
|
|
77
|
+
if not isinstance(value, list):
|
|
78
|
+
raise invalid_output("Speech 词级时间戳无效")
|
|
79
|
+
result: list[JsonValue] = []
|
|
80
|
+
previous_end = segment_start
|
|
81
|
+
for word in value:
|
|
82
|
+
if not isinstance(word, dict):
|
|
83
|
+
raise invalid_output("Speech 词级时间戳无效")
|
|
84
|
+
start = integer(word.get("startMs"))
|
|
85
|
+
end = integer(word.get("endMs"))
|
|
86
|
+
text = word.get("text")
|
|
87
|
+
if (
|
|
88
|
+
start is None
|
|
89
|
+
or end is None
|
|
90
|
+
or start < previous_end
|
|
91
|
+
or end <= start
|
|
92
|
+
or end > segment_end
|
|
93
|
+
or not isinstance(text, str)
|
|
94
|
+
or not text
|
|
95
|
+
):
|
|
96
|
+
raise invalid_output("Speech 词级时间戳无效")
|
|
97
|
+
item: dict[str, JsonValue] = {
|
|
98
|
+
"text": text,
|
|
99
|
+
"startMs": start,
|
|
100
|
+
"endMs": end,
|
|
101
|
+
}
|
|
102
|
+
confidence = probability(word.get("confidence"))
|
|
103
|
+
if confidence is not None:
|
|
104
|
+
item["confidence"] = confidence
|
|
105
|
+
result.append(item)
|
|
106
|
+
previous_end = end
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def integer(value: object) -> int | None:
|
|
111
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def probability(value: object) -> float | int | None:
|
|
115
|
+
if value is None:
|
|
116
|
+
return None
|
|
117
|
+
if (
|
|
118
|
+
not isinstance(value, int | float)
|
|
119
|
+
or isinstance(value, bool)
|
|
120
|
+
or value < 0
|
|
121
|
+
or value > 1
|
|
122
|
+
):
|
|
123
|
+
raise invalid_output("Speech 置信度无效")
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def invalid_output(message: str) -> SafeSpeechError:
|
|
128
|
+
return SafeSpeechError("OUTPUT_VALIDATION_FAILED", message, False)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from .adapter import SpeechAdapter
|
|
9
|
+
from .contracts import ErrorResponse, StartSpeechRunRequest
|
|
10
|
+
from .errors import SafeSpeechError
|
|
11
|
+
from .store import RunRecord, SpeechRunStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SpeechRunService:
|
|
15
|
+
def __init__(self, adapter: SpeechAdapter, store: SpeechRunStore) -> None:
|
|
16
|
+
self.adapter = adapter
|
|
17
|
+
self.store = store
|
|
18
|
+
self.tasks: dict[str, asyncio.Task[None]] = {}
|
|
19
|
+
|
|
20
|
+
async def start(
|
|
21
|
+
self, request: StartSpeechRunRequest, idempotency_key: str
|
|
22
|
+
) -> RunRecord:
|
|
23
|
+
validate_options(request)
|
|
24
|
+
fingerprint = hashlib.sha256(
|
|
25
|
+
json.dumps(
|
|
26
|
+
{
|
|
27
|
+
"operationKey": request.operation_key,
|
|
28
|
+
"sourceSha256": request.source.sha256,
|
|
29
|
+
"options": request.options,
|
|
30
|
+
"capabilities": sorted(request.required_capabilities),
|
|
31
|
+
},
|
|
32
|
+
ensure_ascii=False,
|
|
33
|
+
separators=(",", ":"),
|
|
34
|
+
sort_keys=True,
|
|
35
|
+
).encode()
|
|
36
|
+
).hexdigest()
|
|
37
|
+
record, created = self.store.create(
|
|
38
|
+
f"speech_{uuid.uuid4().hex}",
|
|
39
|
+
request.operation_key,
|
|
40
|
+
idempotency_key,
|
|
41
|
+
fingerprint,
|
|
42
|
+
)
|
|
43
|
+
if created:
|
|
44
|
+
task = asyncio.create_task(self._execute(record.run_id, request))
|
|
45
|
+
self.tasks[record.run_id] = task
|
|
46
|
+
task.add_done_callback(lambda _task: self.tasks.pop(record.run_id, None))
|
|
47
|
+
return record
|
|
48
|
+
|
|
49
|
+
async def cancel(self, run_id: str) -> RunRecord:
|
|
50
|
+
record = self.require(run_id)
|
|
51
|
+
if not record.terminal:
|
|
52
|
+
self.store.append(run_id, "run.cancelled")
|
|
53
|
+
task = self.tasks.get(run_id)
|
|
54
|
+
if task:
|
|
55
|
+
task.cancel()
|
|
56
|
+
return self.require(run_id)
|
|
57
|
+
|
|
58
|
+
def require(self, run_id: str) -> RunRecord:
|
|
59
|
+
record = self.store.get(run_id)
|
|
60
|
+
if not record:
|
|
61
|
+
raise KeyError(run_id)
|
|
62
|
+
return record
|
|
63
|
+
|
|
64
|
+
async def _execute(self, run_id: str, request: StartSpeechRunRequest) -> None:
|
|
65
|
+
self.store.append(run_id, "run.started")
|
|
66
|
+
try:
|
|
67
|
+
async with asyncio.timeout(request.limits.max_duration_seconds):
|
|
68
|
+
output = await self.adapter.execute(request)
|
|
69
|
+
if self.require(run_id).status == "cancelled":
|
|
70
|
+
return
|
|
71
|
+
segments = output.get("segments") if isinstance(output, dict) else None
|
|
72
|
+
if not isinstance(segments, list):
|
|
73
|
+
raise SafeSpeechError(
|
|
74
|
+
"OUTPUT_VALIDATION_FAILED",
|
|
75
|
+
"Speech 输出缺少稳定分段",
|
|
76
|
+
False,
|
|
77
|
+
)
|
|
78
|
+
total = len(segments)
|
|
79
|
+
for index, segment in enumerate(segments, 1):
|
|
80
|
+
if not isinstance(segment, dict):
|
|
81
|
+
raise SafeSpeechError(
|
|
82
|
+
"OUTPUT_VALIDATION_FAILED",
|
|
83
|
+
"Speech 分段无效",
|
|
84
|
+
False,
|
|
85
|
+
)
|
|
86
|
+
end_ms = segment.get("endMs")
|
|
87
|
+
if not isinstance(end_ms, int):
|
|
88
|
+
raise SafeSpeechError(
|
|
89
|
+
"OUTPUT_VALIDATION_FAILED",
|
|
90
|
+
"Speech 分段结束时间无效",
|
|
91
|
+
False,
|
|
92
|
+
)
|
|
93
|
+
self.store.append(
|
|
94
|
+
run_id,
|
|
95
|
+
"speech.segment.completed",
|
|
96
|
+
{
|
|
97
|
+
"completedSegments": index,
|
|
98
|
+
"totalSegments": total,
|
|
99
|
+
"endMs": end_ms,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
self.store.complete(run_id, output)
|
|
103
|
+
except TimeoutError:
|
|
104
|
+
self._fail(run_id, "RUN_TIMEOUT", "Speech 任务超时", True)
|
|
105
|
+
except SafeSpeechError as error:
|
|
106
|
+
self._fail(
|
|
107
|
+
run_id,
|
|
108
|
+
error.code,
|
|
109
|
+
error.safe_message,
|
|
110
|
+
error.retryable,
|
|
111
|
+
)
|
|
112
|
+
except Exception:
|
|
113
|
+
self._fail(run_id, "RUNTIME_UNAVAILABLE", "Speech Runtime 执行失败", True)
|
|
114
|
+
|
|
115
|
+
def _fail(self, run_id: str, code: str, message: str, retryable: bool) -> None:
|
|
116
|
+
self.store.append(
|
|
117
|
+
run_id,
|
|
118
|
+
"run.failed",
|
|
119
|
+
{
|
|
120
|
+
"error": ErrorResponse(
|
|
121
|
+
code=code,
|
|
122
|
+
message=message,
|
|
123
|
+
request_id=f"speech_{uuid.uuid4().hex}",
|
|
124
|
+
retryable=retryable,
|
|
125
|
+
).model_dump(by_alias=True)
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def validate_options(request: StartSpeechRunRequest) -> None:
|
|
131
|
+
options = request.options
|
|
132
|
+
if not isinstance(options, dict):
|
|
133
|
+
raise SafeSpeechError("INVALID_INPUT", "Speech options 必须是对象", False)
|
|
134
|
+
allowed = {"language", "vad", "timestamps"}
|
|
135
|
+
if any(key not in allowed for key in options):
|
|
136
|
+
raise SafeSpeechError("INVALID_INPUT", "Speech options 含未知字段", False)
|
|
137
|
+
language = options.get("language")
|
|
138
|
+
if language is not None and (
|
|
139
|
+
not isinstance(language, str) or not language.strip() or len(language) > 35
|
|
140
|
+
):
|
|
141
|
+
raise SafeSpeechError("INVALID_INPUT", "Speech language 无效", False)
|
|
142
|
+
timestamps = options.get("timestamps")
|
|
143
|
+
if timestamps is not None and timestamps not in {"segment", "word"}:
|
|
144
|
+
raise SafeSpeechError("INVALID_INPUT", "Speech timestamps 无效", False)
|
|
145
|
+
vad = options.get("vad")
|
|
146
|
+
if vad is None:
|
|
147
|
+
return
|
|
148
|
+
if not isinstance(vad, dict) or any(
|
|
149
|
+
key not in {"enabled", "minSpeechMs", "maxSilenceMs"} for key in vad
|
|
150
|
+
):
|
|
151
|
+
raise SafeSpeechError("INVALID_INPUT", "Speech VAD 配置无效", False)
|
|
152
|
+
if not isinstance(vad.get("enabled"), bool):
|
|
153
|
+
raise SafeSpeechError("INVALID_INPUT", "Speech VAD enabled 无效", False)
|
|
154
|
+
for key in ("minSpeechMs", "maxSilenceMs"):
|
|
155
|
+
value = vad.get(key)
|
|
156
|
+
if value is not None and (
|
|
157
|
+
not isinstance(value, int)
|
|
158
|
+
or isinstance(value, bool)
|
|
159
|
+
or value < 0
|
|
160
|
+
or value > 60_000
|
|
161
|
+
):
|
|
162
|
+
raise SafeSpeechError("INVALID_INPUT", f"Speech VAD {key} 无效", False)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import sqlite3
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import cast
|
|
10
|
+
|
|
11
|
+
from .contracts import JsonValue, RunStatus
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RunRecord:
|
|
16
|
+
run_id: str
|
|
17
|
+
operation_key: str
|
|
18
|
+
idempotency_key: str
|
|
19
|
+
fingerprint: str
|
|
20
|
+
status: RunStatus
|
|
21
|
+
output: JsonValue = None
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def terminal(self) -> bool:
|
|
25
|
+
return self.status in {"completed", "failed", "cancelled"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ResultArtifact:
|
|
30
|
+
body: bytes
|
|
31
|
+
media_type: str
|
|
32
|
+
byte_size: int
|
|
33
|
+
sha256: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RunConflictError(Exception):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SpeechRunStore:
|
|
41
|
+
def __init__(self, path: str) -> None:
|
|
42
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
self.database = sqlite3.connect(path, check_same_thread=False)
|
|
44
|
+
self.database.row_factory = sqlite3.Row
|
|
45
|
+
self._initialize()
|
|
46
|
+
self._recover()
|
|
47
|
+
|
|
48
|
+
def create(
|
|
49
|
+
self,
|
|
50
|
+
run_id: str,
|
|
51
|
+
operation_key: str,
|
|
52
|
+
idempotency_key: str,
|
|
53
|
+
fingerprint: str,
|
|
54
|
+
) -> tuple[RunRecord, bool]:
|
|
55
|
+
existing = self.database.execute(
|
|
56
|
+
"select run_id from speech_runs where idempotency_key = ?",
|
|
57
|
+
(idempotency_key,),
|
|
58
|
+
).fetchone()
|
|
59
|
+
if existing:
|
|
60
|
+
record = self.get(str(existing["run_id"]))
|
|
61
|
+
if not record or record.fingerprint != fingerprint:
|
|
62
|
+
raise RunConflictError
|
|
63
|
+
return record, False
|
|
64
|
+
now = time.time()
|
|
65
|
+
self.database.execute(
|
|
66
|
+
"""
|
|
67
|
+
insert into speech_runs values (?, ?, ?, ?, 'accepted', 'null', ?, ?)
|
|
68
|
+
""",
|
|
69
|
+
(run_id, operation_key, idempotency_key, fingerprint, now, now),
|
|
70
|
+
)
|
|
71
|
+
self.database.commit()
|
|
72
|
+
return self.get(run_id) or _missing(), True
|
|
73
|
+
|
|
74
|
+
def get(self, run_id: str) -> RunRecord | None:
|
|
75
|
+
row = self.database.execute(
|
|
76
|
+
"select * from speech_runs where run_id = ?", (run_id,)
|
|
77
|
+
).fetchone()
|
|
78
|
+
if not row:
|
|
79
|
+
return None
|
|
80
|
+
return RunRecord(
|
|
81
|
+
run_id=str(row["run_id"]),
|
|
82
|
+
operation_key=str(row["operation_key"]),
|
|
83
|
+
idempotency_key=str(row["idempotency_key"]),
|
|
84
|
+
fingerprint=str(row["fingerprint"]),
|
|
85
|
+
status=cast(RunStatus, str(row["status"])),
|
|
86
|
+
output=json.loads(str(row["output_json"])),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def append(
|
|
90
|
+
self, run_id: str, event_type: str, fields: dict[str, object] | None = None
|
|
91
|
+
) -> None:
|
|
92
|
+
record = self.get(run_id)
|
|
93
|
+
if not record or record.terminal:
|
|
94
|
+
return
|
|
95
|
+
sequence = (
|
|
96
|
+
int(
|
|
97
|
+
self.database.execute(
|
|
98
|
+
"select count(*) from speech_events where run_id = ?", (run_id,)
|
|
99
|
+
).fetchone()[0]
|
|
100
|
+
)
|
|
101
|
+
+ 1
|
|
102
|
+
)
|
|
103
|
+
event = {
|
|
104
|
+
"type": event_type,
|
|
105
|
+
"runId": run_id,
|
|
106
|
+
"sequence": sequence,
|
|
107
|
+
**(fields or {}),
|
|
108
|
+
}
|
|
109
|
+
status = {
|
|
110
|
+
"run.started": "running",
|
|
111
|
+
"run.completed": "completed",
|
|
112
|
+
"run.failed": "failed",
|
|
113
|
+
"run.cancelled": "cancelled",
|
|
114
|
+
}.get(event_type, record.status)
|
|
115
|
+
self.database.execute(
|
|
116
|
+
"insert into speech_events values (?, ?, ?)",
|
|
117
|
+
(run_id, sequence, encode(event)),
|
|
118
|
+
)
|
|
119
|
+
self.database.execute(
|
|
120
|
+
"update speech_runs set status = ?, updated_at = ? where run_id = ?",
|
|
121
|
+
(status, time.time(), run_id),
|
|
122
|
+
)
|
|
123
|
+
self.database.commit()
|
|
124
|
+
|
|
125
|
+
def complete(self, run_id: str, output: JsonValue) -> None:
|
|
126
|
+
self.database.execute(
|
|
127
|
+
"update speech_runs set output_json = ? where run_id = ?",
|
|
128
|
+
(encode(output), run_id),
|
|
129
|
+
)
|
|
130
|
+
self.database.commit()
|
|
131
|
+
self.append(run_id, "run.completed")
|
|
132
|
+
|
|
133
|
+
def artifact(self, run_id: str) -> ResultArtifact:
|
|
134
|
+
record = self.get(run_id)
|
|
135
|
+
if not record or record.status != "completed":
|
|
136
|
+
raise KeyError(run_id)
|
|
137
|
+
body = encode(record.output).encode()
|
|
138
|
+
return ResultArtifact(
|
|
139
|
+
body=body,
|
|
140
|
+
media_type="application/json",
|
|
141
|
+
byte_size=len(body),
|
|
142
|
+
sha256=hashlib.sha256(body).hexdigest(),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def events_after(self, run_id: str, after: int) -> list[dict[str, object]]:
|
|
146
|
+
return [
|
|
147
|
+
json.loads(str(row["event_json"]))
|
|
148
|
+
for row in self.database.execute(
|
|
149
|
+
"""
|
|
150
|
+
select event_json from speech_events
|
|
151
|
+
where run_id = ? and sequence > ? order by sequence
|
|
152
|
+
""",
|
|
153
|
+
(run_id, after),
|
|
154
|
+
)
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
def _initialize(self) -> None:
|
|
158
|
+
self.database.executescript(
|
|
159
|
+
"""
|
|
160
|
+
create table if not exists speech_runs (
|
|
161
|
+
run_id text primary key,
|
|
162
|
+
operation_key text not null,
|
|
163
|
+
idempotency_key text not null unique,
|
|
164
|
+
fingerprint text not null,
|
|
165
|
+
status text not null,
|
|
166
|
+
output_json text not null,
|
|
167
|
+
created_at real not null,
|
|
168
|
+
updated_at real not null
|
|
169
|
+
);
|
|
170
|
+
create table if not exists speech_events (
|
|
171
|
+
run_id text not null references speech_runs(run_id) on delete cascade,
|
|
172
|
+
sequence integer not null,
|
|
173
|
+
event_json text not null,
|
|
174
|
+
primary key (run_id, sequence)
|
|
175
|
+
);
|
|
176
|
+
"""
|
|
177
|
+
)
|
|
178
|
+
self.database.commit()
|
|
179
|
+
|
|
180
|
+
def _recover(self) -> None:
|
|
181
|
+
rows = self.database.execute(
|
|
182
|
+
"select run_id from speech_runs where status in ('accepted', 'running')"
|
|
183
|
+
).fetchall()
|
|
184
|
+
for row in rows:
|
|
185
|
+
self.append(
|
|
186
|
+
str(row["run_id"]),
|
|
187
|
+
"run.failed",
|
|
188
|
+
{
|
|
189
|
+
"error": {
|
|
190
|
+
"code": "RUNTIME_UNAVAILABLE",
|
|
191
|
+
"message": "Speech Runtime 重启中断了任务,请使用新幂等键重试",
|
|
192
|
+
"requestId": "speech-recovery",
|
|
193
|
+
"retryable": True,
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def encode(value: object) -> str:
|
|
200
|
+
return json.dumps(
|
|
201
|
+
value,
|
|
202
|
+
ensure_ascii=False,
|
|
203
|
+
separators=(",", ":"),
|
|
204
|
+
sort_keys=True,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _missing() -> RunRecord:
|
|
209
|
+
raise RuntimeError("Speech Run 创建后不可读")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Speech Runtime tests."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from southwind_speech_server.config import SpeechRuntimeConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_internal_endpoints_reject_public_hosts() -> None:
|
|
9
|
+
with pytest.raises(ValueError, match="禁止使用公网地址"):
|
|
10
|
+
SpeechRuntimeConfig.from_environment(
|
|
11
|
+
{
|
|
12
|
+
"SPEECH_SERVER_INTERNAL_TOKEN": "internal-token",
|
|
13
|
+
"SPEECH_ADAPTER_URL": "https://8.8.8.8",
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_internal_endpoints_reject_public_dns_names() -> None:
|
|
19
|
+
with pytest.raises(ValueError, match="内部服务名"):
|
|
20
|
+
SpeechRuntimeConfig.from_environment(
|
|
21
|
+
{
|
|
22
|
+
"SPEECH_SERVER_INTERNAL_TOKEN": "internal-token",
|
|
23
|
+
"SPEECH_ADAPTER_URL": "https://public.example.com",
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_unconfigured_adapter_keeps_runtime_bootable() -> None:
|
|
29
|
+
config = SpeechRuntimeConfig.from_environment(
|
|
30
|
+
{"SPEECH_SERVER_INTERNAL_TOKEN": "internal-token"}
|
|
31
|
+
)
|
|
32
|
+
assert config.adapter_url is None
|
|
33
|
+
assert config.artifact_url == "http://server:9747/api/internal/ai/artifacts"
|