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,249 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .config import SpeechRuntimeConfig
|
|
12
|
+
from .contracts import JsonValue, StartSpeechRunRequest
|
|
13
|
+
from .errors import SafeSpeechError
|
|
14
|
+
from .normalizer import AudioNormalizer, NormalizationError
|
|
15
|
+
from .output_validation import canonicalize_output
|
|
16
|
+
|
|
17
|
+
SUPPORTED_MEDIA_TYPES = frozenset(
|
|
18
|
+
{
|
|
19
|
+
"audio/mpeg",
|
|
20
|
+
"audio/mp4",
|
|
21
|
+
"audio/ogg",
|
|
22
|
+
"audio/wav",
|
|
23
|
+
"audio/x-wav",
|
|
24
|
+
"audio/flac",
|
|
25
|
+
"audio/webm",
|
|
26
|
+
"video/mp4",
|
|
27
|
+
"video/webm",
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SpeechAdapter:
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
config: SpeechRuntimeConfig,
|
|
36
|
+
normalizer: AudioNormalizer | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.config = config
|
|
39
|
+
self.normalizer = normalizer or AudioNormalizer()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def available(self) -> bool:
|
|
43
|
+
return self.config.adapter_url is not None and self.normalizer.available
|
|
44
|
+
|
|
45
|
+
async def execute(self, request: StartSpeechRunRequest) -> JsonValue:
|
|
46
|
+
if not self.config.adapter_url:
|
|
47
|
+
raise SafeSpeechError(
|
|
48
|
+
"CAPABILITY_UNAVAILABLE",
|
|
49
|
+
"Speech Adapter 未配置",
|
|
50
|
+
True,
|
|
51
|
+
)
|
|
52
|
+
if request.source.media_type.lower() not in SUPPORTED_MEDIA_TYPES:
|
|
53
|
+
raise SafeSpeechError(
|
|
54
|
+
"INVALID_INPUT",
|
|
55
|
+
"音频或视频格式不受支持",
|
|
56
|
+
False,
|
|
57
|
+
)
|
|
58
|
+
missing = set(request.required_capabilities) - set(self.config.capabilities)
|
|
59
|
+
if missing:
|
|
60
|
+
raise SafeSpeechError(
|
|
61
|
+
"CAPABILITY_UNAVAILABLE",
|
|
62
|
+
"Speech Adapter 缺少所需能力",
|
|
63
|
+
True,
|
|
64
|
+
)
|
|
65
|
+
self._authorize_deployment(request)
|
|
66
|
+
enforce_runtime_quotas(request, self.config)
|
|
67
|
+
source = await self._download(request)
|
|
68
|
+
normalized = Path(f"{source}.wav")
|
|
69
|
+
try:
|
|
70
|
+
try:
|
|
71
|
+
audio = await self.normalizer.normalize(
|
|
72
|
+
source,
|
|
73
|
+
normalized,
|
|
74
|
+
request.limits.max_media_duration_seconds,
|
|
75
|
+
request.limits.max_duration_seconds,
|
|
76
|
+
)
|
|
77
|
+
except NormalizationError as error:
|
|
78
|
+
raise SafeSpeechError(
|
|
79
|
+
error.code,
|
|
80
|
+
str(error),
|
|
81
|
+
error.code == "CAPABILITY_UNAVAILABLE",
|
|
82
|
+
) from error
|
|
83
|
+
return await self._invoke(request, audio.path, audio.duration_ms)
|
|
84
|
+
finally:
|
|
85
|
+
await remove(source)
|
|
86
|
+
await remove(normalized)
|
|
87
|
+
|
|
88
|
+
async def _download(self, request: StartSpeechRunRequest) -> Path:
|
|
89
|
+
digest = hashlib.sha256()
|
|
90
|
+
total = 0
|
|
91
|
+
with tempfile.NamedTemporaryFile(delete=False) as target:
|
|
92
|
+
path = Path(target.name)
|
|
93
|
+
try:
|
|
94
|
+
async with httpx.AsyncClient(
|
|
95
|
+
timeout=httpx.Timeout(
|
|
96
|
+
self.config.request_timeout_seconds,
|
|
97
|
+
connect=self.config.connect_timeout_seconds,
|
|
98
|
+
)
|
|
99
|
+
) as client:
|
|
100
|
+
async with client.stream(
|
|
101
|
+
"GET",
|
|
102
|
+
self.config.artifact_url,
|
|
103
|
+
headers={
|
|
104
|
+
"authorization": f"Bearer {self.config.internal_token}",
|
|
105
|
+
"x-ai-artifact-ref": request.source.value,
|
|
106
|
+
},
|
|
107
|
+
) as response:
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
async for chunk in response.aiter_bytes():
|
|
110
|
+
total += len(chunk)
|
|
111
|
+
if total > min(
|
|
112
|
+
request.source.byte_size,
|
|
113
|
+
request.limits.max_input_bytes,
|
|
114
|
+
):
|
|
115
|
+
raise SafeSpeechError(
|
|
116
|
+
"BUDGET_EXCEEDED",
|
|
117
|
+
"Speech 输入超过声明上限",
|
|
118
|
+
False,
|
|
119
|
+
)
|
|
120
|
+
digest.update(chunk)
|
|
121
|
+
target.write(chunk)
|
|
122
|
+
except SafeSpeechError:
|
|
123
|
+
await remove(path)
|
|
124
|
+
raise
|
|
125
|
+
except Exception as error:
|
|
126
|
+
await remove(path)
|
|
127
|
+
raise SafeSpeechError(
|
|
128
|
+
"ARTIFACT_UNAVAILABLE",
|
|
129
|
+
"Speech Runtime 无法读取 Artifact",
|
|
130
|
+
True,
|
|
131
|
+
) from error
|
|
132
|
+
if (
|
|
133
|
+
total != request.source.byte_size
|
|
134
|
+
or digest.hexdigest() != request.source.sha256
|
|
135
|
+
):
|
|
136
|
+
await remove(path)
|
|
137
|
+
raise SafeSpeechError(
|
|
138
|
+
"ARTIFACT_UNAVAILABLE",
|
|
139
|
+
"Artifact 完整性校验失败",
|
|
140
|
+
False,
|
|
141
|
+
)
|
|
142
|
+
return path
|
|
143
|
+
|
|
144
|
+
async def _invoke(
|
|
145
|
+
self,
|
|
146
|
+
request: StartSpeechRunRequest,
|
|
147
|
+
path: Path,
|
|
148
|
+
duration_ms: int,
|
|
149
|
+
) -> JsonValue:
|
|
150
|
+
adapter_url = self.config.adapter_url
|
|
151
|
+
if not adapter_url:
|
|
152
|
+
raise SafeSpeechError(
|
|
153
|
+
"CAPABILITY_UNAVAILABLE", "Speech Adapter 未配置", True
|
|
154
|
+
)
|
|
155
|
+
headers = (
|
|
156
|
+
{"authorization": f"Bearer {self.config.adapter_token}"}
|
|
157
|
+
if self.config.adapter_token
|
|
158
|
+
else {}
|
|
159
|
+
)
|
|
160
|
+
try:
|
|
161
|
+
async with httpx.AsyncClient(
|
|
162
|
+
timeout=httpx.Timeout(
|
|
163
|
+
self.config.request_timeout_seconds,
|
|
164
|
+
connect=self.config.connect_timeout_seconds,
|
|
165
|
+
)
|
|
166
|
+
) as client:
|
|
167
|
+
with path.open("rb") as source:
|
|
168
|
+
response = await client.post(
|
|
169
|
+
f"{adapter_url.rstrip('/')}/v1/speech/transcribe",
|
|
170
|
+
headers=headers,
|
|
171
|
+
files={"file": ("source.wav", source, "audio/wav")},
|
|
172
|
+
data={
|
|
173
|
+
"operationKey": request.operation_key,
|
|
174
|
+
"durationMs": str(duration_ms),
|
|
175
|
+
"dataClassification": request.data_classification,
|
|
176
|
+
"options": encode(request.options),
|
|
177
|
+
"providerEgress": request.provider_egress.model_dump_json(
|
|
178
|
+
by_alias=True
|
|
179
|
+
),
|
|
180
|
+
"providerDataPolicy": (
|
|
181
|
+
request.provider_data_policy.model_dump_json(
|
|
182
|
+
by_alias=True
|
|
183
|
+
)
|
|
184
|
+
if request.provider_data_policy
|
|
185
|
+
else "null"
|
|
186
|
+
),
|
|
187
|
+
"limits": request.limits.model_dump_json(by_alias=True),
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
response.raise_for_status()
|
|
191
|
+
if len(response.content) > request.limits.max_output_bytes:
|
|
192
|
+
raise SafeSpeechError(
|
|
193
|
+
"BUDGET_EXCEEDED",
|
|
194
|
+
"Speech 输出超过上限",
|
|
195
|
+
False,
|
|
196
|
+
)
|
|
197
|
+
value: JsonValue = response.json()
|
|
198
|
+
return canonicalize_output(
|
|
199
|
+
value,
|
|
200
|
+
duration_ms,
|
|
201
|
+
request.limits.max_units,
|
|
202
|
+
)
|
|
203
|
+
except SafeSpeechError:
|
|
204
|
+
raise
|
|
205
|
+
except Exception as error:
|
|
206
|
+
raise SafeSpeechError(
|
|
207
|
+
"RUNTIME_UNAVAILABLE",
|
|
208
|
+
"Speech Adapter 执行失败",
|
|
209
|
+
True,
|
|
210
|
+
) from error
|
|
211
|
+
|
|
212
|
+
def _authorize_deployment(self, request: StartSpeechRunRequest) -> None:
|
|
213
|
+
if request.provider_egress.mode == "forbidden":
|
|
214
|
+
return
|
|
215
|
+
if not request.provider_data_policy:
|
|
216
|
+
raise SafeSpeechError(
|
|
217
|
+
"FORBIDDEN",
|
|
218
|
+
"Speech Operation 缺少 Provider 字段治理策略",
|
|
219
|
+
False,
|
|
220
|
+
)
|
|
221
|
+
if (
|
|
222
|
+
not self.config.deployment_key
|
|
223
|
+
or request.provider_egress.deployment_key != self.config.deployment_key
|
|
224
|
+
):
|
|
225
|
+
raise SafeSpeechError(
|
|
226
|
+
"FORBIDDEN",
|
|
227
|
+
"Speech Operation 未授权当前 Deployment",
|
|
228
|
+
False,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def enforce_runtime_quotas(
|
|
233
|
+
request: StartSpeechRunRequest,
|
|
234
|
+
config: SpeechRuntimeConfig,
|
|
235
|
+
) -> None:
|
|
236
|
+
if request.source.byte_size > request.limits.max_input_bytes:
|
|
237
|
+
raise SafeSpeechError("BUDGET_EXCEEDED", "Speech Artifact 超过输入上限", False)
|
|
238
|
+
if request.limits.max_units > config.max_units:
|
|
239
|
+
raise SafeSpeechError("BUDGET_EXCEEDED", "转写分段上限超过 Runtime 配置", False)
|
|
240
|
+
if request.limits.max_media_duration_seconds > config.max_media_duration_seconds:
|
|
241
|
+
raise SafeSpeechError("BUDGET_EXCEEDED", "媒体时长上限超过 Runtime 配置", False)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def encode(value: object) -> str:
|
|
245
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def remove(path: Path) -> None:
|
|
249
|
+
await asyncio.to_thread(path.unlink, missing_ok=True)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hmac
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
|
|
9
|
+
from fastapi import Depends, FastAPI, Header, HTTPException, Query
|
|
10
|
+
from fastapi.exceptions import RequestValidationError
|
|
11
|
+
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
|
12
|
+
|
|
13
|
+
from .adapter import SpeechAdapter
|
|
14
|
+
from .config import SpeechRuntimeConfig
|
|
15
|
+
from .contracts import ErrorResponse, StartRunResponse, StartSpeechRunRequest
|
|
16
|
+
from .errors import SafeSpeechError
|
|
17
|
+
from .service import SpeechRunService
|
|
18
|
+
from .store import RunConflictError, SpeechRunStore
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_app(config: SpeechRuntimeConfig) -> FastAPI:
|
|
22
|
+
app = FastAPI(title="Southwind Speech Runtime", docs_url=None, redoc_url=None)
|
|
23
|
+
store = SpeechRunStore(config.store_path)
|
|
24
|
+
service = SpeechRunService(SpeechAdapter(config), store)
|
|
25
|
+
|
|
26
|
+
def authorize(authorization: str | None = Header(default=None)) -> None:
|
|
27
|
+
provided = (authorization or "").removeprefix("Bearer ")
|
|
28
|
+
if not provided or not hmac.compare_digest(provided, config.internal_token):
|
|
29
|
+
raise HTTPException(status_code=403, detail="FORBIDDEN")
|
|
30
|
+
|
|
31
|
+
@app.get("/health/ready")
|
|
32
|
+
async def ready(_: None = Depends(authorize)) -> JSONResponse:
|
|
33
|
+
if not service.adapter.available:
|
|
34
|
+
return JSONResponse(
|
|
35
|
+
{"status": "unavailable", "capabilities": []},
|
|
36
|
+
status_code=503,
|
|
37
|
+
)
|
|
38
|
+
return JSONResponse(
|
|
39
|
+
{
|
|
40
|
+
"status": "ready",
|
|
41
|
+
"capabilities": list(config.capabilities),
|
|
42
|
+
"limits": {
|
|
43
|
+
"maxUnits": config.max_units,
|
|
44
|
+
"maxMediaDurationSeconds": config.max_media_duration_seconds,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@app.post("/api/speech/runs", status_code=202)
|
|
50
|
+
async def start(
|
|
51
|
+
request: StartSpeechRunRequest,
|
|
52
|
+
idempotency_key: str = Header(alias="idempotency-key"),
|
|
53
|
+
_: None = Depends(authorize),
|
|
54
|
+
) -> StartRunResponse:
|
|
55
|
+
if not idempotency_key.strip() or len(idempotency_key) > 160:
|
|
56
|
+
raise HTTPException(status_code=400, detail="INVALID_INPUT")
|
|
57
|
+
try:
|
|
58
|
+
record = await service.start(request, idempotency_key)
|
|
59
|
+
except RunConflictError:
|
|
60
|
+
raise HTTPException(status_code=409, detail="RUN_CONFLICT") from None
|
|
61
|
+
return StartRunResponse(
|
|
62
|
+
run_id=record.run_id,
|
|
63
|
+
status=record.status,
|
|
64
|
+
events_url=f"/api/speech/runs/{record.run_id}/events",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@app.get("/api/speech/runs/{run_id}/events")
|
|
68
|
+
async def events(
|
|
69
|
+
run_id: str,
|
|
70
|
+
after: int = Query(default=0, ge=0),
|
|
71
|
+
_: None = Depends(authorize),
|
|
72
|
+
) -> StreamingResponse:
|
|
73
|
+
service.require(run_id)
|
|
74
|
+
|
|
75
|
+
async def stream() -> AsyncIterator[str]:
|
|
76
|
+
cursor = after
|
|
77
|
+
for _attempt in range(30):
|
|
78
|
+
current = service.require(run_id)
|
|
79
|
+
events = store.events_after(run_id, cursor)
|
|
80
|
+
for event in events:
|
|
81
|
+
sequence = event.get("sequence")
|
|
82
|
+
if not isinstance(sequence, int):
|
|
83
|
+
raise RuntimeError("Speech Event sequence 无效")
|
|
84
|
+
cursor = sequence
|
|
85
|
+
yield f"data: {encode_event(event)}\n\n"
|
|
86
|
+
if current.terminal:
|
|
87
|
+
return
|
|
88
|
+
if not events:
|
|
89
|
+
yield ": heartbeat\n\n"
|
|
90
|
+
await asyncio.sleep(1)
|
|
91
|
+
|
|
92
|
+
return StreamingResponse(stream(), media_type="text/event-stream")
|
|
93
|
+
|
|
94
|
+
@app.get("/api/speech/runs/{run_id}/result")
|
|
95
|
+
async def result(run_id: str, _: None = Depends(authorize)) -> JSONResponse:
|
|
96
|
+
record = service.require(run_id)
|
|
97
|
+
if record.status != "completed" or not isinstance(record.output, dict):
|
|
98
|
+
return JSONResponse({"code": "RUN_NOT_READY"}, status_code=409)
|
|
99
|
+
artifact = store.artifact(run_id)
|
|
100
|
+
return JSONResponse(
|
|
101
|
+
{
|
|
102
|
+
"runId": run_id,
|
|
103
|
+
"status": "completed",
|
|
104
|
+
"output": {
|
|
105
|
+
**record.output,
|
|
106
|
+
"artifact": {
|
|
107
|
+
"mediaType": artifact.media_type,
|
|
108
|
+
"byteSize": artifact.byte_size,
|
|
109
|
+
"sha256": artifact.sha256,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@app.get("/api/speech/runs/{run_id}/artifact")
|
|
116
|
+
async def artifact(run_id: str, _: None = Depends(authorize)) -> Response:
|
|
117
|
+
record = service.require(run_id)
|
|
118
|
+
if record.status != "completed":
|
|
119
|
+
return JSONResponse(
|
|
120
|
+
encode_error("RUN_NOT_READY", "Speech 结果尚未就绪", False),
|
|
121
|
+
status_code=409,
|
|
122
|
+
)
|
|
123
|
+
result = store.artifact(run_id)
|
|
124
|
+
return Response(
|
|
125
|
+
content=result.body,
|
|
126
|
+
media_type=result.media_type,
|
|
127
|
+
headers={
|
|
128
|
+
"content-length": str(result.byte_size),
|
|
129
|
+
"x-content-sha256": result.sha256,
|
|
130
|
+
"cache-control": "private, no-store",
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@app.post("/api/speech/runs/{run_id}/cancel")
|
|
135
|
+
async def cancel(run_id: str, _: None = Depends(authorize)) -> JSONResponse:
|
|
136
|
+
record = await service.cancel(run_id)
|
|
137
|
+
return JSONResponse({"runId": run_id, "status": record.status})
|
|
138
|
+
|
|
139
|
+
@app.exception_handler(KeyError)
|
|
140
|
+
async def missing(_request: object, _error: KeyError) -> JSONResponse:
|
|
141
|
+
return safe_error("RUN_NOT_FOUND", "Speech Run 不存在", 404, False)
|
|
142
|
+
|
|
143
|
+
@app.exception_handler(HTTPException)
|
|
144
|
+
async def http_error(_request: object, error: HTTPException) -> JSONResponse:
|
|
145
|
+
code = str(error.detail)
|
|
146
|
+
return safe_error(code, "Speech 请求被拒绝", error.status_code, False)
|
|
147
|
+
|
|
148
|
+
@app.exception_handler(RequestValidationError)
|
|
149
|
+
async def validation_error(
|
|
150
|
+
_request: object, _error: RequestValidationError
|
|
151
|
+
) -> JSONResponse:
|
|
152
|
+
return safe_error("INVALID_INPUT", "Speech 请求格式无效", 400, False)
|
|
153
|
+
|
|
154
|
+
@app.exception_handler(SafeSpeechError)
|
|
155
|
+
async def safe_speech_error(
|
|
156
|
+
_request: object, error: SafeSpeechError
|
|
157
|
+
) -> JSONResponse:
|
|
158
|
+
status = 403 if error.code == "FORBIDDEN" else 400
|
|
159
|
+
return safe_error(error.code, error.safe_message, status, error.retryable)
|
|
160
|
+
|
|
161
|
+
return app
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def safe_error(code: str, message: str, status: int, retryable: bool) -> JSONResponse:
|
|
165
|
+
return JSONResponse(encode_error(code, message, retryable), status_code=status)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def encode_error(code: str, message: str, retryable: bool) -> dict[str, object]:
|
|
169
|
+
return ErrorResponse(
|
|
170
|
+
code=code,
|
|
171
|
+
message=message,
|
|
172
|
+
request_id=f"speech_{uuid.uuid4().hex}",
|
|
173
|
+
retryable=retryable,
|
|
174
|
+
).model_dump(by_alias=True)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def encode_event(event: dict[str, object]) -> str:
|
|
178
|
+
import json
|
|
179
|
+
|
|
180
|
+
return json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
app = create_app(SpeechRuntimeConfig.from_environment(dict(os.environ)))
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class SpeechRuntimeConfig:
|
|
10
|
+
internal_token: str
|
|
11
|
+
store_path: str
|
|
12
|
+
artifact_url: str
|
|
13
|
+
adapter_url: str | None
|
|
14
|
+
adapter_token: str | None
|
|
15
|
+
deployment_key: str | None
|
|
16
|
+
capabilities: tuple[str, ...]
|
|
17
|
+
max_units: int = 100_000
|
|
18
|
+
max_media_duration_seconds: int = 24 * 60 * 60
|
|
19
|
+
connect_timeout_seconds: float = 5
|
|
20
|
+
request_timeout_seconds: float = 3600
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_environment(cls, environment: dict[str, str]) -> SpeechRuntimeConfig:
|
|
24
|
+
artifact_url = environment.get(
|
|
25
|
+
"SPEECH_ARTIFACT_URL",
|
|
26
|
+
"http://server:9747/api/internal/ai/artifacts",
|
|
27
|
+
).strip()
|
|
28
|
+
validate_intranet_url(artifact_url)
|
|
29
|
+
adapter_url = environment.get("SPEECH_ADAPTER_URL", "").strip() or None
|
|
30
|
+
if adapter_url:
|
|
31
|
+
validate_intranet_url(adapter_url)
|
|
32
|
+
capabilities = tuple(
|
|
33
|
+
sorted(
|
|
34
|
+
{
|
|
35
|
+
item.strip()
|
|
36
|
+
for item in environment.get(
|
|
37
|
+
"SPEECH_ADAPTER_CAPABILITIES",
|
|
38
|
+
(
|
|
39
|
+
"speech.transcribe.batch,speech.normalize,"
|
|
40
|
+
"speech.vad,speech.timestamps"
|
|
41
|
+
),
|
|
42
|
+
).split(",")
|
|
43
|
+
if item.strip()
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return cls(
|
|
48
|
+
internal_token=required(environment, "SPEECH_SERVER_INTERNAL_TOKEN"),
|
|
49
|
+
store_path=environment.get(
|
|
50
|
+
"SPEECH_RUN_STORE_PATH", "/app/data/speech/runs.sqlite3"
|
|
51
|
+
).strip(),
|
|
52
|
+
artifact_url=artifact_url,
|
|
53
|
+
adapter_url=adapter_url,
|
|
54
|
+
adapter_token=environment.get("SPEECH_ADAPTER_TOKEN", "").strip() or None,
|
|
55
|
+
deployment_key=environment.get("SPEECH_DEPLOYMENT_KEY", "").strip() or None,
|
|
56
|
+
capabilities=capabilities,
|
|
57
|
+
max_units=positive_int(environment, "SPEECH_MAX_UNITS", 100_000),
|
|
58
|
+
max_media_duration_seconds=positive_int(
|
|
59
|
+
environment,
|
|
60
|
+
"SPEECH_MAX_MEDIA_DURATION_SECONDS",
|
|
61
|
+
24 * 60 * 60,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def required(environment: dict[str, str], key: str) -> str:
|
|
67
|
+
value = environment.get(key, "").strip()
|
|
68
|
+
if not value:
|
|
69
|
+
raise ValueError(f"缺少 {key}")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def positive_int(environment: dict[str, str], key: str, default: int) -> int:
|
|
74
|
+
raw = environment.get(key, str(default)).strip()
|
|
75
|
+
try:
|
|
76
|
+
value = int(raw)
|
|
77
|
+
except ValueError:
|
|
78
|
+
raise ValueError(f"{key} 必须是正整数") from None
|
|
79
|
+
if value <= 0:
|
|
80
|
+
raise ValueError(f"{key} 必须是正整数")
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def validate_intranet_url(raw: str) -> None:
|
|
85
|
+
parsed = urlparse(raw)
|
|
86
|
+
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
|
87
|
+
raise ValueError("Speech 内部 URL 无效")
|
|
88
|
+
if parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
89
|
+
raise ValueError("Speech 内部 URL 不允许凭据、查询串或片段")
|
|
90
|
+
host = parsed.hostname.lower()
|
|
91
|
+
if host == "localhost" or host.endswith(".internal") or "." not in host:
|
|
92
|
+
return
|
|
93
|
+
try:
|
|
94
|
+
address = ipaddress.ip_address(host)
|
|
95
|
+
except ValueError:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"Speech 内部 URL 必须使用 localhost、内部服务名、私网 IP 或 .internal"
|
|
98
|
+
) from None
|
|
99
|
+
if not (address.is_private or address.is_loopback):
|
|
100
|
+
raise ValueError("Speech 内部 URL 禁止使用公网地址")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Self
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
6
|
+
|
|
7
|
+
type JsonValue = (
|
|
8
|
+
str | int | float | bool | None | list[JsonValue] | dict[str, JsonValue]
|
|
9
|
+
)
|
|
10
|
+
type RunStatus = Literal["accepted", "running", "completed", "failed", "cancelled"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def to_camel(value: str) -> str:
|
|
14
|
+
first, *rest = value.split("_")
|
|
15
|
+
return first + "".join(part.capitalize() for part in rest)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CanonicalModel(BaseModel):
|
|
19
|
+
model_config = ConfigDict(
|
|
20
|
+
alias_generator=to_camel,
|
|
21
|
+
populate_by_name=True,
|
|
22
|
+
extra="forbid",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ArtifactInput(CanonicalModel):
|
|
27
|
+
value: str = Field(min_length=1, max_length=4096)
|
|
28
|
+
media_type: str = Field(min_length=1, max_length=160)
|
|
29
|
+
byte_size: int = Field(ge=1, le=2 * 1024 * 1024 * 1024)
|
|
30
|
+
sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RuntimeLimits(CanonicalModel):
|
|
34
|
+
max_duration_seconds: int = Field(ge=1, le=3600)
|
|
35
|
+
max_input_bytes: int = Field(ge=1, le=2 * 1024 * 1024 * 1024)
|
|
36
|
+
max_output_bytes: int = Field(ge=1024, le=64 * 1024 * 1024)
|
|
37
|
+
max_units: int = Field(ge=1, le=100_000)
|
|
38
|
+
max_media_duration_seconds: int = Field(ge=1, le=24 * 60 * 60)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProviderEgress(CanonicalModel):
|
|
42
|
+
mode: Literal["forbidden", "configured-endpoint"]
|
|
43
|
+
deployment_key: str | None = None
|
|
44
|
+
|
|
45
|
+
@model_validator(mode="after")
|
|
46
|
+
def validate_deployment(self) -> Self:
|
|
47
|
+
if self.mode == "configured-endpoint":
|
|
48
|
+
if not self.deployment_key or not self.deployment_key.strip():
|
|
49
|
+
raise ValueError("Provider Deployment Key 缺失")
|
|
50
|
+
elif self.deployment_key is not None:
|
|
51
|
+
raise ValueError("禁止出网时不能指定 Provider Deployment")
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ProviderFieldPolicy(CanonicalModel):
|
|
56
|
+
field: Literal["artifact-content", "provider-output"]
|
|
57
|
+
classification: Literal["public", "internal", "sensitive"]
|
|
58
|
+
dlp_action: Literal["reject", "redact"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ProviderInputFieldPolicy(CanonicalModel):
|
|
62
|
+
json_pointer: str = Field(pattern=r"^/[^/]+(?:/[^/]+)*$")
|
|
63
|
+
classification: Literal["public", "internal", "sensitive"]
|
|
64
|
+
dlp_action: Literal["reject", "redact"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ProviderDataPolicy(CanonicalModel):
|
|
68
|
+
input_fields: list[ProviderInputFieldPolicy] = Field(min_length=1)
|
|
69
|
+
envelope_fields: list[ProviderFieldPolicy] = Field(min_length=2)
|
|
70
|
+
|
|
71
|
+
@model_validator(mode="after")
|
|
72
|
+
def validate_fields(self) -> Self:
|
|
73
|
+
fields = [rule.field for rule in self.envelope_fields]
|
|
74
|
+
if set(fields) != {"artifact-content", "provider-output"}:
|
|
75
|
+
raise ValueError("Speech Provider 字段治理策略不完整")
|
|
76
|
+
if len(fields) != len(set(fields)):
|
|
77
|
+
raise ValueError("Speech Provider 字段治理策略重复")
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class StartSpeechRunRequest(CanonicalModel):
|
|
82
|
+
kind: Literal["speech"]
|
|
83
|
+
operation_key: str = Field(min_length=1, max_length=160)
|
|
84
|
+
options: JsonValue
|
|
85
|
+
source: ArtifactInput
|
|
86
|
+
required_capabilities: list[str] = Field(min_length=1, max_length=32)
|
|
87
|
+
data_classification: Literal["public", "internal", "sensitive"]
|
|
88
|
+
limits: RuntimeLimits
|
|
89
|
+
provider_egress: ProviderEgress
|
|
90
|
+
provider_data_policy: ProviderDataPolicy | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class StartRunResponse(CanonicalModel):
|
|
94
|
+
run_id: str
|
|
95
|
+
status: RunStatus
|
|
96
|
+
events_url: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ErrorResponse(CanonicalModel):
|
|
100
|
+
code: str
|
|
101
|
+
message: str
|
|
102
|
+
request_id: str
|
|
103
|
+
retryable: bool
|