create-windy 0.3.2 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/README.md +27 -9
  2. package/dist/cli.js +825 -117
  3. package/package.json +1 -1
  4. package/template/.agents/skills/windy-business-module/SKILL.md +16 -3
  5. package/template/.windy-template.json +2 -2
  6. package/template/AGENTS.md +4 -0
  7. package/template/README.md +18 -11
  8. package/template/_gitignore +1 -0
  9. package/template/apps/agent-server/README.md +2 -0
  10. package/template/apps/agent-server/pyproject.toml +8 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +87 -10
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +18 -3
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +78 -11
  14. package/template/apps/agent-server/src/southwind_agent_server/app.py +120 -115
  15. package/template/apps/agent-server/src/southwind_agent_server/artifact_input.py +153 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/config.py +29 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +81 -1
  18. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +8 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +28 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/fingerprint.py +41 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/http_helpers.py +62 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/lease.py +15 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/multimodal.py +76 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +112 -20
  25. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +14 -1
  26. package/template/apps/agent-server/src/southwind_agent_server/postgres_store.py +329 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/postgres_store_backend.py +332 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +64 -3
  29. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +68 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/service.py +218 -56
  31. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +36 -0
  32. package/template/apps/agent-server/src/southwind_agent_server/sqlite_catalog.py +107 -0
  33. package/template/apps/agent-server/src/southwind_agent_server/sqlite_schema.py +65 -0
  34. package/template/apps/agent-server/src/southwind_agent_server/sqlite_serialization.py +16 -0
  35. package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +89 -57
  36. package/template/apps/agent-server/src/southwind_agent_server/store.py +105 -2
  37. package/template/apps/agent-server/src/southwind_agent_server/store_cipher.py +40 -0
  38. package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +89 -0
  39. package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +72 -0
  40. package/template/apps/agent-server/tests/test_adk_adapter.py +6 -2
  41. package/template/apps/agent-server/tests/test_api.py +24 -1
  42. package/template/apps/agent-server/tests/test_artifact_input.py +110 -0
  43. package/template/apps/agent-server/tests/test_config.py +4 -0
  44. package/template/apps/agent-server/tests/test_fingerprint.py +10 -0
  45. package/template/apps/agent-server/tests/test_multimodal.py +51 -0
  46. package/template/apps/agent-server/tests/test_openai_provider.py +6 -2
  47. package/template/apps/agent-server/tests/test_postgres_store.py +131 -0
  48. package/template/apps/agent-server/tests/test_provider_governance.py +61 -0
  49. package/template/apps/agent-server/tests/test_sqlite_store.py +95 -10
  50. package/template/apps/agent-server/tests/test_telemetry.py +25 -0
  51. package/template/apps/agent-server/tests/test_tool_approval.py +28 -0
  52. package/template/apps/agent-server/uv.lock +165 -0
  53. package/template/apps/document-server/Dockerfile +19 -0
  54. package/template/apps/document-server/pyproject.toml +43 -0
  55. package/template/apps/document-server/src/southwind_document_server/__init__.py +1 -0
  56. package/template/apps/document-server/src/southwind_document_server/adapter.py +337 -0
  57. package/template/apps/document-server/src/southwind_document_server/app.py +151 -0
  58. package/template/apps/document-server/src/southwind_document_server/config.py +94 -0
  59. package/template/apps/document-server/src/southwind_document_server/contracts.py +103 -0
  60. package/template/apps/document-server/src/southwind_document_server/service.py +149 -0
  61. package/template/apps/document-server/src/southwind_document_server/store.py +185 -0
  62. package/template/apps/document-server/tests/__init__.py +1 -0
  63. package/template/apps/document-server/tests/test_config.py +42 -0
  64. package/template/apps/document-server/tests/test_service.py +140 -0
  65. package/template/apps/document-server/uv.lock +430 -0
  66. package/template/apps/embedding-server/Dockerfile +17 -0
  67. package/template/apps/embedding-server/pyproject.toml +44 -0
  68. package/template/apps/embedding-server/src/southwind_embedding_server/__init__.py +1 -0
  69. package/template/apps/embedding-server/src/southwind_embedding_server/adapter.py +254 -0
  70. package/template/apps/embedding-server/src/southwind_embedding_server/app.py +119 -0
  71. package/template/apps/embedding-server/src/southwind_embedding_server/config.py +120 -0
  72. package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +106 -0
  73. package/template/apps/embedding-server/src/southwind_embedding_server/error.py +6 -0
  74. package/template/apps/embedding-server/src/southwind_embedding_server/image_input.py +87 -0
  75. package/template/apps/embedding-server/tests/__init__.py +1 -0
  76. package/template/apps/embedding-server/tests/test_adapter.py +92 -0
  77. package/template/apps/embedding-server/tests/test_config.py +38 -0
  78. package/template/apps/embedding-server/uv.lock +449 -0
  79. package/template/apps/server/src/agent/background-operation-port.ts +37 -0
  80. package/template/apps/server/src/agent/background-operation.test.ts +231 -0
  81. package/template/apps/server/src/agent/background-operation.ts +252 -0
  82. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +114 -0
  83. package/template/apps/server/src/agent/execution-grants.ts +7 -1
  84. package/template/apps/server/src/agent/fake-runtime.test-support.ts +43 -0
  85. package/template/apps/server/src/agent/idempotency-key.test.ts +28 -0
  86. package/template/apps/server/src/agent/idempotency-key.ts +11 -0
  87. package/template/apps/server/src/agent/remote-runtime.test.ts +115 -1
  88. package/template/apps/server/src/agent/remote-runtime.ts +173 -5
  89. package/template/apps/server/src/agent/run-authorization.test-support.ts +9 -0
  90. package/template/apps/server/src/agent/run-authorization.test.ts +126 -0
  91. package/template/apps/server/src/agent/run-authorization.ts +180 -0
  92. package/template/apps/server/src/agent/run-binding-repository.ts +93 -0
  93. package/template/apps/server/src/agent/run-contracts.ts +88 -1
  94. package/template/apps/server/src/agent/run-definition-support.ts +58 -0
  95. package/template/apps/server/src/agent/run-errors.ts +25 -0
  96. package/template/apps/server/src/agent/run-facade.test.ts +57 -46
  97. package/template/apps/server/src/agent/run-facade.ts +185 -65
  98. package/template/apps/server/src/agent/run-release.ts +78 -0
  99. package/template/apps/server/src/agent/run-route-input.ts +169 -0
  100. package/template/apps/server/src/agent/run-routes.ts +63 -76
  101. package/template/apps/server/src/agent/runtime-bootstrap.ts +169 -13
  102. package/template/apps/server/src/agent/sandbox-client.ts +200 -0
  103. package/template/apps/server/src/agent/sandbox-protocol.ts +76 -0
  104. package/template/apps/server/src/agent/sandbox-registry.ts +205 -0
  105. package/template/apps/server/src/agent/sandbox-runtime.test.ts +137 -0
  106. package/template/apps/server/src/agent/sandbox-worker-server.ts +195 -0
  107. package/template/apps/server/src/agent/sandbox-worker.ts +12 -0
  108. package/template/apps/server/src/agent/tool-approval.test.ts +75 -0
  109. package/template/apps/server/src/agent/tool-approval.ts +115 -0
  110. package/template/apps/server/src/agent/tool-audit.ts +62 -0
  111. package/template/apps/server/src/agent/tool-host.test.ts +39 -11
  112. package/template/apps/server/src/agent/tool-host.ts +184 -17
  113. package/template/apps/server/src/agent/tool-registry.ts +52 -47
  114. package/template/apps/server/src/agent/vision-facade.test.ts +256 -0
  115. package/template/apps/server/src/agent/vision-input.ts +120 -0
  116. package/template/apps/server/src/ai-artifact/codec.test.ts +44 -0
  117. package/template/apps/server/src/ai-artifact/codec.ts +137 -0
  118. package/template/apps/server/src/ai-artifact/routes.ts +37 -0
  119. package/template/apps/server/src/ai-artifact/runtime.ts +205 -0
  120. package/template/apps/server/src/application-services.ts +62 -2
  121. package/template/apps/server/src/auth/reauthentication.test.ts +40 -0
  122. package/template/apps/server/src/auth/reauthentication.ts +19 -0
  123. package/template/apps/server/src/auth/runtime.ts +9 -1
  124. package/template/apps/server/src/data-access/context.ts +6 -4
  125. package/template/apps/server/src/data-access/scope-snapshot.ts +27 -0
  126. package/template/apps/server/src/data-governance/export/approval-port.ts +6 -3
  127. package/template/apps/server/src/data-governance/export/authorization-port.test.ts +23 -4
  128. package/template/apps/server/src/data-governance/export/authorization-port.ts +89 -25
  129. package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
  130. package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
  131. package/template/apps/server/src/data-governance/export/job-runtime.test.ts +15 -4
  132. package/template/apps/server/src/data-governance/export/job-runtime.ts +68 -15
  133. package/template/apps/server/src/data-governance/export/ndjson-stream.ts +25 -6
  134. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +4 -2
  135. package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
  136. package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
  137. package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
  138. package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
  139. package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
  140. package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
  141. package/template/apps/server/src/data-governance/export/routes.ts +11 -1
  142. package/template/apps/server/src/data-governance/export/scope-snapshot.ts +6 -18
  143. package/template/apps/server/src/data-governance/export/service-support.ts +188 -0
  144. package/template/apps/server/src/data-governance/export/service.ts +90 -113
  145. package/template/apps/server/src/data-governance/export/source.ts +101 -0
  146. package/template/apps/server/src/data-governance/export/types.ts +27 -0
  147. package/template/apps/server/src/document/contracts.ts +76 -0
  148. package/template/apps/server/src/document/facade.ts +339 -0
  149. package/template/apps/server/src/document/remote-runtime.test.ts +96 -0
  150. package/template/apps/server/src/document/remote-runtime.ts +223 -0
  151. package/template/apps/server/src/document/routes.ts +193 -0
  152. package/template/apps/server/src/embedding/contracts.ts +83 -0
  153. package/template/apps/server/src/embedding/error.ts +10 -0
  154. package/template/apps/server/src/embedding/facade.ts +217 -0
  155. package/template/apps/server/src/embedding/input.ts +172 -0
  156. package/template/apps/server/src/embedding/remote-runtime.test.ts +66 -0
  157. package/template/apps/server/src/embedding/remote-runtime.ts +218 -0
  158. package/template/apps/server/src/embedding/routes.ts +152 -0
  159. package/template/apps/server/src/index.ts +40 -43
  160. package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
  161. package/template/apps/server/src/jobs/artifact-service.ts +10 -0
  162. package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
  163. package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
  164. package/template/apps/server/src/jobs/drizzle-mappers.ts +4 -1
  165. package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
  166. package/template/apps/server/src/jobs/drizzle-repository.ts +1 -0
  167. package/template/apps/server/src/jobs/execution-repository.ts +17 -0
  168. package/template/apps/server/src/jobs/execution.test.ts +34 -0
  169. package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
  170. package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
  171. package/template/apps/server/src/jobs/legacy-service-runtime.ts +5 -2
  172. package/template/apps/server/src/jobs/repository.ts +3 -0
  173. package/template/apps/server/src/jobs/worker.ts +80 -41
  174. package/template/apps/server/src/module-host/ai-operation-port.ts +170 -0
  175. package/template/apps/server/src/module-host/durable-context.test.ts +312 -0
  176. package/template/apps/server/src/module-host/durable-context.ts +124 -0
  177. package/template/apps/server/src/module-host/durable-handler-registration.ts +72 -0
  178. package/template/apps/server/src/module-host/governed-export-route-support.ts +171 -0
  179. package/template/apps/server/src/module-host/governed-export-routes.test.ts +327 -0
  180. package/template/apps/server/src/module-host/governed-export-routes.ts +238 -0
  181. package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
  182. package/template/apps/server/src/module-host/governed-resource-routes.ts +331 -0
  183. package/template/apps/server/src/module-host/governed-resources.test.ts +344 -0
  184. package/template/apps/server/src/module-host/governed-resources.ts +203 -0
  185. package/template/apps/server/src/module-host/http-runtime.ts +95 -0
  186. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  187. package/template/apps/server/src/module-host/initialize.test.ts +49 -1
  188. package/template/apps/server/src/module-host/initialize.ts +39 -16
  189. package/template/apps/server/src/module-host/job-port.test.ts +262 -0
  190. package/template/apps/server/src/module-host/job-port.ts +334 -0
  191. package/template/apps/server/src/module-host/request-context.test.ts +88 -2
  192. package/template/apps/server/src/module-host/request-context.ts +56 -22
  193. package/template/apps/server/src/module-host/route-installer.test.ts +10 -0
  194. package/template/apps/server/src/module-host/route-installer.ts +36 -1
  195. package/template/apps/server/src/module-host/sandbox-tool-registration.ts +42 -0
  196. package/template/apps/server/src/module-host/tool-handlers.ts +67 -31
  197. package/template/apps/server/src/module-host.ts +1 -0
  198. package/template/apps/server/src/module-runtime-validation.ts +7 -3
  199. package/template/apps/server/src/speech/contracts.ts +76 -0
  200. package/template/apps/server/src/speech/facade.ts +316 -0
  201. package/template/apps/server/src/speech/remote-runtime.test.ts +88 -0
  202. package/template/apps/server/src/speech/remote-runtime.ts +227 -0
  203. package/template/apps/server/src/speech/routes.ts +180 -0
  204. package/template/apps/server/src/system/actor.ts +39 -0
  205. package/template/apps/server/src/system/route-helpers.ts +1 -0
  206. package/template/apps/server/src/workflow/service.ts +2 -1
  207. package/template/apps/speech-server/Dockerfile +22 -0
  208. package/template/apps/speech-server/pyproject.toml +43 -0
  209. package/template/apps/speech-server/src/southwind_speech_server/__init__.py +1 -0
  210. package/template/apps/speech-server/src/southwind_speech_server/adapter.py +249 -0
  211. package/template/apps/speech-server/src/southwind_speech_server/app.py +183 -0
  212. package/template/apps/speech-server/src/southwind_speech_server/config.py +100 -0
  213. package/template/apps/speech-server/src/southwind_speech_server/contracts.py +103 -0
  214. package/template/apps/speech-server/src/southwind_speech_server/errors.py +6 -0
  215. package/template/apps/speech-server/src/southwind_speech_server/normalizer.py +108 -0
  216. package/template/apps/speech-server/src/southwind_speech_server/output_validation.py +128 -0
  217. package/template/apps/speech-server/src/southwind_speech_server/service.py +162 -0
  218. package/template/apps/speech-server/src/southwind_speech_server/store.py +209 -0
  219. package/template/apps/speech-server/tests/__init__.py +1 -0
  220. package/template/apps/speech-server/tests/test_config.py +33 -0
  221. package/template/apps/speech-server/tests/test_service.py +154 -0
  222. package/template/apps/speech-server/uv.lock +430 -0
  223. package/template/apps/web/src/app/business-routes.ts +22 -0
  224. package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
  225. package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
  226. package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
  227. package/template/apps/web/src/components/common/FormDialog.vue +6 -0
  228. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +13 -0
  229. package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
  230. package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
  231. package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
  232. package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
  233. package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
  234. package/template/apps/web/src/layout/AppShell.vue +28 -25
  235. package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
  236. package/template/apps/web/src/router/business-routes.ts +68 -0
  237. package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
  238. package/template/apps/web/src/router/index.ts +7 -0
  239. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  240. package/template/apps/web/src/services/access-api.ts +5 -1
  241. package/template/apps/web/src/services/access-api.webtest.ts +35 -0
  242. package/template/docker-compose.yml +127 -0
  243. package/template/docs/architecture/ai-egress.md +12 -9
  244. package/template/docs/architecture/ai-runtime.md +23 -11
  245. package/template/docs/architecture/durable-jobs.md +25 -3
  246. package/template/docs/architecture/foundation-infrastructure.md +163 -0
  247. package/template/docs/architecture/module-host.md +115 -0
  248. package/template/docs/architecture/server-module-interface.md +39 -0
  249. package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
  250. package/template/docs/development/business-web-routes.md +66 -0
  251. package/template/docs/development/custom-login-page.md +3 -3
  252. package/template/docs/development/module-lifecycle.md +71 -0
  253. package/template/docs/operations/observability.md +5 -1
  254. package/template/docs/platform/agent-runtime.md +21 -11
  255. package/template/docs/platform/agent-tool-sandbox.md +58 -0
  256. package/template/docs/platform/agent-tools.md +5 -0
  257. package/template/docs/platform/document-ai-runtime.md +204 -0
  258. package/template/docs/platform/governed-export-renderers.md +132 -0
  259. package/template/docs/platform/navigation-registry.md +5 -0
  260. package/template/docs/platform/organization-membership.md +2 -1
  261. package/template/docs/platform/speech-ai-runtime.md +156 -0
  262. package/template/docs/platform/system-crud-api.md +7 -1
  263. package/template/docs/platform/vision-ai-runtime.md +97 -0
  264. package/template/docs/platform/web-system-crud.md +6 -0
  265. package/template/package.json +16 -2
  266. package/template/packages/ai-client/index.ts +4 -0
  267. package/template/packages/ai-client/package.json +5 -2
  268. package/template/packages/ai-client/src/agent/client/artifact.ts +26 -0
  269. package/template/packages/ai-client/src/agent/client/attach.test.ts +124 -0
  270. package/template/packages/ai-client/src/agent/client/client.ts +119 -7
  271. package/template/packages/ai-client/src/agent/client/run.ts +14 -0
  272. package/template/packages/ai-client/src/agent/client/types.ts +18 -0
  273. package/template/packages/ai-client/src/agent/client/vision.test.ts +53 -0
  274. package/template/packages/ai-client/src/agent/contracts/contracts.test.ts +1 -0
  275. package/template/packages/ai-client/src/agent/contracts/events.ts +33 -0
  276. package/template/packages/ai-client/src/agent/contracts/runs.ts +71 -0
  277. package/template/packages/ai-client/src/agent/index.ts +1 -0
  278. package/template/packages/ai-client/src/artifact-ref.ts +8 -0
  279. package/template/packages/ai-client/src/document/client.test.ts +114 -0
  280. package/template/packages/ai-client/src/document/client.ts +134 -0
  281. package/template/packages/ai-client/src/document/contracts.ts +152 -0
  282. package/template/packages/ai-client/src/document/error.ts +43 -0
  283. package/template/packages/ai-client/src/document/index.ts +3 -0
  284. package/template/packages/ai-client/src/document/run.ts +104 -0
  285. package/template/packages/ai-client/src/document/validation.ts +115 -0
  286. package/template/packages/ai-client/src/embedding/client.test.ts +77 -0
  287. package/template/packages/ai-client/src/embedding/client.ts +96 -0
  288. package/template/packages/ai-client/src/embedding/contracts.ts +46 -0
  289. package/template/packages/ai-client/src/embedding/error.ts +61 -0
  290. package/template/packages/ai-client/src/embedding/index.ts +3 -0
  291. package/template/packages/ai-client/src/embedding/validation.ts +117 -0
  292. package/template/packages/ai-client/src/speech/client.test.ts +135 -0
  293. package/template/packages/ai-client/src/speech/client.ts +104 -0
  294. package/template/packages/ai-client/src/speech/contracts.ts +129 -0
  295. package/template/packages/ai-client/src/speech/error.ts +43 -0
  296. package/template/packages/ai-client/src/speech/index.ts +3 -0
  297. package/template/packages/ai-client/src/speech/run.ts +133 -0
  298. package/template/packages/ai-client/src/speech/validation.ts +132 -0
  299. package/template/packages/config/package.json +2 -2
  300. package/template/packages/database/drizzle/0039_early_ezekiel_stane.sql +1 -0
  301. package/template/packages/database/drizzle/0040_colossal_baron_strucker.sql +63 -0
  302. package/template/packages/database/drizzle/0041_yummy_prowler.sql +21 -0
  303. package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
  304. package/template/packages/database/drizzle/meta/0039_snapshot.json +7755 -0
  305. package/template/packages/database/drizzle/meta/0040_snapshot.json +8150 -0
  306. package/template/packages/database/drizzle/meta/0041_snapshot.json +8323 -0
  307. package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
  308. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  309. package/template/packages/database/package.json +2 -2
  310. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  311. package/template/packages/database/src/schema/agent-runtime.ts +149 -0
  312. package/template/packages/database/src/schema/common.ts +2 -14
  313. package/template/packages/database/src/schema/durable-jobs.ts +5 -0
  314. package/template/packages/database/src/schema/governed-exports.ts +17 -5
  315. package/template/packages/database/src/schema/index.ts +3 -0
  316. package/template/packages/database/src/schema-durable-recovery.test.ts +31 -0
  317. package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
  318. package/template/packages/database/src/schema.test.ts +6 -23
  319. package/template/packages/jobs/package.json +3 -2
  320. package/template/packages/jobs/src/handlers.test.ts +13 -0
  321. package/template/packages/jobs/src/handlers.ts +7 -1
  322. package/template/packages/jobs/src/types.ts +4 -0
  323. package/template/packages/modules/package.json +2 -2
  324. package/template/packages/modules/src/ai-operation-composition.test-support.ts +165 -0
  325. package/template/packages/modules/src/ai-operation-composition.test.ts +91 -127
  326. package/template/packages/modules/src/ai-operation-composition.ts +59 -1
  327. package/template/packages/modules/src/manifest.ts +25 -1
  328. package/template/packages/modules/src/tool-composition.ts +18 -0
  329. package/template/packages/server-sdk/index.ts +50 -0
  330. package/template/packages/server-sdk/package.json +10 -4
  331. package/template/packages/server-sdk/src/ai-artifact-port.ts +34 -0
  332. package/template/packages/server-sdk/src/ai-operation-port.ts +65 -0
  333. package/template/packages/server-sdk/src/ai-operation-registration.ts +42 -1
  334. package/template/packages/server-sdk/src/background-ai-port.ts +43 -0
  335. package/template/packages/server-sdk/src/data-governance-registration.ts +116 -0
  336. package/template/packages/server-sdk/src/durable-handler.ts +29 -0
  337. package/template/packages/server-sdk/src/jobs-port.ts +69 -0
  338. package/template/packages/server-sdk/src/module-host.ts +16 -2
  339. package/template/packages/server-sdk/src/request-context.ts +6 -2
  340. package/template/packages/server-sdk/src/sandbox-tool.ts +21 -0
  341. package/template/packages/server-sdk/src/tool-registration.ts +2 -0
  342. package/template/packages/shared/package.json +1 -1
  343. package/template/packages/shared/src/rbac.ts +30 -0
@@ -0,0 +1,254 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import math
6
+
7
+ import httpx
8
+
9
+ from .config import EmbeddingConfig
10
+ from .contracts import (
11
+ EmbeddingItem,
12
+ EmbeddingMetadata,
13
+ EmbeddingRequest,
14
+ EmbeddingResponse,
15
+ EmbeddingVector,
16
+ JsonValue,
17
+ )
18
+ from .error import SafeEmbeddingError
19
+ from .image_input import load_image
20
+
21
+
22
+ class EmbeddingAdapter:
23
+ def __init__(self, config: EmbeddingConfig) -> None:
24
+ self.config = config
25
+
26
+ @property
27
+ def available(self) -> bool:
28
+ return self.config.adapter_url is not None
29
+
30
+ async def embed(self, request: EmbeddingRequest) -> EmbeddingResponse:
31
+ self._authorize(request)
32
+ if not self.config.adapter_url:
33
+ raise SafeEmbeddingError(
34
+ "CAPABILITY_UNAVAILABLE",
35
+ "Embedding Adapter 未配置",
36
+ True,
37
+ )
38
+ if len(request.items) > min(request.limits.max_units, self.config.max_units):
39
+ raise budget("Embedding 批量数量超过 Runtime 上限")
40
+ missing = set(request.required_capabilities) - set(self.config.capabilities)
41
+ if missing:
42
+ raise SafeEmbeddingError(
43
+ "CAPABILITY_UNAVAILABLE",
44
+ "Embedding Deployment 缺少 Operation 所需能力",
45
+ True,
46
+ )
47
+ prepared, input_hashes = await self._prepare(request)
48
+ raw = await self._invoke(request, prepared)
49
+ vectors = validate_vectors(
50
+ raw,
51
+ request.items,
52
+ input_hashes,
53
+ self.config.dimensions,
54
+ self.config.normalized,
55
+ )
56
+ return EmbeddingResponse(
57
+ batch_id=batch_id(request, input_hashes, self.config),
58
+ items=vectors,
59
+ metadata=EmbeddingMetadata(
60
+ deployment_alias=self.config.deployment_alias,
61
+ model=self.config.model,
62
+ version=self.config.version,
63
+ dimensions=self.config.dimensions,
64
+ normalized=self.config.normalized,
65
+ ),
66
+ )
67
+
68
+ async def _prepare(
69
+ self,
70
+ request: EmbeddingRequest,
71
+ ) -> tuple[list[dict[str, JsonValue]], list[str]]:
72
+ artifacts = sum(item.source is not None for item in request.items)
73
+ if artifacts > (request.limits.max_artifacts or 0):
74
+ raise budget("Embedding 图片数量超过 Operation 上限")
75
+ if artifacts and request.limits.max_pixels_per_artifact is None:
76
+ raise budget("Embedding Operation 未声明图片像素上限")
77
+ prepared: list[dict[str, JsonValue]] = []
78
+ hashes: list[str] = []
79
+ remaining = request.limits.max_input_bytes
80
+ for item in request.items:
81
+ if item.text is not None:
82
+ body = item.text.encode()
83
+ remaining -= len(body)
84
+ if remaining < 0:
85
+ raise budget("Embedding 文本超过输入上限")
86
+ hashes.append(hashlib.sha256(body).hexdigest())
87
+ prepared.append({"id": item.id, "text": item.text})
88
+ continue
89
+ if item.source is None or request.limits.max_pixels_per_artifact is None:
90
+ raise invalid("Embedding 图片输入无效")
91
+ encoded, digest = await load_image(
92
+ item.source,
93
+ self.config,
94
+ remaining,
95
+ request.limits.max_pixels_per_artifact,
96
+ )
97
+ remaining -= item.source.byte_size
98
+ hashes.append(digest)
99
+ prepared.append(
100
+ {
101
+ "id": item.id,
102
+ "image": {
103
+ "mediaType": "image/png",
104
+ "dataBase64": encoded,
105
+ },
106
+ }
107
+ )
108
+ return prepared, hashes
109
+
110
+ async def _invoke(
111
+ self,
112
+ request: EmbeddingRequest,
113
+ items: list[dict[str, JsonValue]],
114
+ ) -> object:
115
+ url = self.config.adapter_url
116
+ if not url:
117
+ raise SafeEmbeddingError(
118
+ "CAPABILITY_UNAVAILABLE",
119
+ "Embedding Adapter 未配置",
120
+ True,
121
+ )
122
+ headers = (
123
+ {"authorization": f"Bearer {self.config.adapter_token}"}
124
+ if self.config.adapter_token
125
+ else {}
126
+ )
127
+ try:
128
+ async with httpx.AsyncClient(
129
+ timeout=httpx.Timeout(
130
+ self.config.request_timeout_seconds,
131
+ connect=self.config.connect_timeout_seconds,
132
+ )
133
+ ) as client:
134
+ response = await client.post(
135
+ f"{url.rstrip('/')}/v1/embeddings",
136
+ headers=headers,
137
+ json={
138
+ "deploymentAlias": self.config.deployment_alias,
139
+ "model": self.config.model,
140
+ "version": self.config.version,
141
+ "dimensions": self.config.dimensions,
142
+ "items": items,
143
+ "options": request.options,
144
+ },
145
+ )
146
+ response.raise_for_status()
147
+ if len(response.content) > request.limits.max_output_bytes:
148
+ raise budget("Embedding 输出超过上限")
149
+ return response.json()
150
+ except SafeEmbeddingError:
151
+ raise
152
+ except Exception as error:
153
+ raise SafeEmbeddingError(
154
+ "RUNTIME_UNAVAILABLE",
155
+ "Embedding Adapter 执行失败",
156
+ True,
157
+ ) from error
158
+
159
+ def _authorize(self, request: EmbeddingRequest) -> None:
160
+ if request.provider_egress.mode == "forbidden":
161
+ return
162
+ fields = (
163
+ request.provider_data_policy.envelope_fields
164
+ if request.provider_data_policy
165
+ else []
166
+ )
167
+ if (
168
+ not request.provider_data_policy
169
+ or not self.config.deployment_key
170
+ or request.provider_egress.deployment_key != self.config.deployment_key
171
+ or not any(item.get("field") == "embedding-input" for item in fields)
172
+ ):
173
+ raise SafeEmbeddingError(
174
+ "FORBIDDEN",
175
+ "Embedding Operation 未授权当前 Deployment",
176
+ False,
177
+ )
178
+
179
+
180
+ def validate_vectors(
181
+ value: object,
182
+ inputs: list[EmbeddingItem],
183
+ hashes: list[str],
184
+ dimensions: int,
185
+ normalized: bool,
186
+ ) -> list[EmbeddingVector]:
187
+ vectors = value.get("vectors") if isinstance(value, dict) else None
188
+ if not isinstance(vectors, list) or len(vectors) != len(inputs):
189
+ raise invalid_output()
190
+ result: list[EmbeddingVector] = []
191
+ for index, (raw, expected) in enumerate(zip(vectors, inputs, strict=True)):
192
+ vector = raw.get("vector") if isinstance(raw, dict) else None
193
+ if (
194
+ not isinstance(raw, dict)
195
+ or raw.get("id") != expected.id
196
+ or not isinstance(vector, list)
197
+ or len(vector) != dimensions
198
+ or any(
199
+ not isinstance(item, int | float)
200
+ or isinstance(item, bool)
201
+ or not math.isfinite(item)
202
+ for item in vector
203
+ )
204
+ ):
205
+ raise invalid_output()
206
+ values = [float(item) for item in vector]
207
+ norm = math.sqrt(sum(item * item for item in values))
208
+ if normalized and abs(norm - 1) > 0.01:
209
+ raise invalid_output("Embedding 向量未按声明归一化")
210
+ result.append(
211
+ EmbeddingVector(
212
+ id=expected.id,
213
+ vector=values,
214
+ input_sha256=hashes[index],
215
+ )
216
+ )
217
+ return result
218
+
219
+
220
+ def batch_id(
221
+ request: EmbeddingRequest,
222
+ hashes: list[str],
223
+ config: EmbeddingConfig,
224
+ ) -> str:
225
+ payload = json.dumps(
226
+ {
227
+ "operationKey": request.operation_key,
228
+ "ids": [item.id for item in request.items],
229
+ "hashes": hashes,
230
+ "options": request.options,
231
+ "deploymentAlias": config.deployment_alias,
232
+ "model": config.model,
233
+ "version": config.version,
234
+ "dimensions": config.dimensions,
235
+ },
236
+ ensure_ascii=False,
237
+ separators=(",", ":"),
238
+ sort_keys=True,
239
+ ).encode()
240
+ return f"embed_{hashlib.sha256(payload).hexdigest()[:32]}"
241
+
242
+
243
+ def invalid(message: str) -> SafeEmbeddingError:
244
+ return SafeEmbeddingError("INVALID_INPUT", message, False)
245
+
246
+
247
+ def budget(message: str) -> SafeEmbeddingError:
248
+ return SafeEmbeddingError("BUDGET_EXCEEDED", message, False)
249
+
250
+
251
+ def invalid_output(
252
+ message: str = "Embedding Adapter 输出无效",
253
+ ) -> SafeEmbeddingError:
254
+ return SafeEmbeddingError("OUTPUT_VALIDATION_FAILED", message, False)
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import hmac
5
+ import os
6
+ import uuid
7
+
8
+ from fastapi import Depends, FastAPI, Header, HTTPException
9
+ from fastapi.exceptions import RequestValidationError
10
+ from fastapi.responses import JSONResponse
11
+
12
+ from .adapter import EmbeddingAdapter
13
+ from .config import EmbeddingConfig
14
+ from .contracts import EmbeddingRequest, EmbeddingResponse, ErrorResponse
15
+ from .error import SafeEmbeddingError
16
+
17
+
18
+ def create_app(
19
+ config: EmbeddingConfig,
20
+ adapter: EmbeddingAdapter | None = None,
21
+ ) -> FastAPI:
22
+ app = FastAPI(title="Southwind Embedding Runtime", docs_url=None, redoc_url=None)
23
+ runtime = adapter or EmbeddingAdapter(config)
24
+
25
+ def authorize(authorization: str | None = Header(default=None)) -> None:
26
+ provided = (authorization or "").removeprefix("Bearer ")
27
+ if not provided or not hmac.compare_digest(provided, config.internal_token):
28
+ raise HTTPException(status_code=403, detail="FORBIDDEN")
29
+
30
+ @app.get("/health/ready")
31
+ async def ready(_: None = Depends(authorize)) -> JSONResponse:
32
+ if not runtime.available:
33
+ return JSONResponse(
34
+ {"status": "unavailable", "capabilities": []},
35
+ status_code=503,
36
+ )
37
+ return JSONResponse(
38
+ {
39
+ "status": "ready",
40
+ "capabilities": list(config.capabilities),
41
+ "limits": {"maxUnits": config.max_units},
42
+ "deployment": {
43
+ "alias": config.deployment_alias,
44
+ "model": config.model,
45
+ "version": config.version,
46
+ "dimensions": config.dimensions,
47
+ "normalized": config.normalized,
48
+ },
49
+ }
50
+ )
51
+
52
+ @app.post("/api/embeddings")
53
+ async def embed(
54
+ request: EmbeddingRequest,
55
+ idempotency_key: str = Header(alias="idempotency-key"),
56
+ _: None = Depends(authorize),
57
+ ) -> EmbeddingResponse:
58
+ if not idempotency_key.strip() or len(idempotency_key) > 160:
59
+ raise HTTPException(status_code=400, detail="INVALID_INPUT")
60
+ try:
61
+ async with asyncio.timeout(request.limits.max_duration_seconds):
62
+ return await runtime.embed(request)
63
+ except TimeoutError as error:
64
+ raise SafeEmbeddingError(
65
+ "RUNTIME_UNAVAILABLE",
66
+ "Embedding 执行超时",
67
+ True,
68
+ ) from error
69
+
70
+ @app.exception_handler(HTTPException)
71
+ async def http_error(_request: object, error: HTTPException) -> JSONResponse:
72
+ code = str(error.detail)
73
+ return safe_error(code, "Embedding 请求被拒绝", error.status_code, False)
74
+
75
+ @app.exception_handler(RequestValidationError)
76
+ async def validation_error(
77
+ _request: object,
78
+ _error: RequestValidationError,
79
+ ) -> JSONResponse:
80
+ return safe_error("INVALID_INPUT", "Embedding 请求格式无效", 400, False)
81
+
82
+ @app.exception_handler(SafeEmbeddingError)
83
+ async def embedding_error(
84
+ _request: object,
85
+ error: SafeEmbeddingError,
86
+ ) -> JSONResponse:
87
+ return safe_error(
88
+ error.code,
89
+ error.safe_message,
90
+ error_status(error.code),
91
+ error.retryable,
92
+ )
93
+
94
+ return app
95
+
96
+
97
+ def error_status(code: str) -> int:
98
+ if code == "FORBIDDEN":
99
+ return 403
100
+ if code == "RUN_CONFLICT":
101
+ return 409
102
+ if code in {"CAPABILITY_UNAVAILABLE", "RUNTIME_UNAVAILABLE"}:
103
+ return 503
104
+ return 400
105
+
106
+
107
+ def safe_error(code: str, message: str, status: int, retryable: bool) -> JSONResponse:
108
+ return JSONResponse(
109
+ ErrorResponse(
110
+ code=code,
111
+ message=message,
112
+ request_id=f"embedding_{uuid.uuid4().hex}",
113
+ retryable=retryable,
114
+ ).model_dump(by_alias=True),
115
+ status_code=status,
116
+ )
117
+
118
+
119
+ app = create_app(EmbeddingConfig.from_environment(dict(os.environ)))
@@ -0,0 +1,120 @@
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 EmbeddingConfig:
10
+ internal_token: str
11
+ artifact_url: str
12
+ adapter_url: str | None
13
+ adapter_token: str | None
14
+ deployment_key: str | None
15
+ deployment_alias: str
16
+ model: str
17
+ version: str
18
+ dimensions: int
19
+ normalized: bool
20
+ capabilities: tuple[str, ...]
21
+ max_units: int = 256
22
+ connect_timeout_seconds: float = 5
23
+ request_timeout_seconds: float = 120
24
+
25
+ @classmethod
26
+ def from_environment(cls, environment: dict[str, str]) -> EmbeddingConfig:
27
+ artifact_url = environment.get(
28
+ "EMBEDDING_ARTIFACT_URL",
29
+ "http://server:9747/api/internal/ai/artifacts",
30
+ ).strip()
31
+ validate_intranet_url(artifact_url)
32
+ adapter_url = environment.get("EMBEDDING_ADAPTER_URL", "").strip() or None
33
+ if adapter_url:
34
+ validate_intranet_url(adapter_url)
35
+ return cls(
36
+ internal_token=required(environment, "EMBEDDING_SERVER_INTERNAL_TOKEN"),
37
+ artifact_url=artifact_url,
38
+ adapter_url=adapter_url,
39
+ adapter_token=environment.get("EMBEDDING_ADAPTER_TOKEN", "").strip()
40
+ or None,
41
+ deployment_key=environment.get("EMBEDDING_DEPLOYMENT_KEY", "").strip()
42
+ or None,
43
+ deployment_alias=required(environment, "EMBEDDING_DEPLOYMENT_ALIAS"),
44
+ model=required(environment, "EMBEDDING_MODEL"),
45
+ version=required(environment, "EMBEDDING_MODEL_VERSION"),
46
+ dimensions=required_positive_int(environment, "EMBEDDING_DIMENSIONS"),
47
+ normalized=boolean(environment, "EMBEDDING_NORMALIZED", True),
48
+ capabilities=required_capabilities(
49
+ tuple(
50
+ sorted(
51
+ {
52
+ item.strip()
53
+ for item in environment.get(
54
+ "EMBEDDING_CAPABILITIES",
55
+ "embedding.text",
56
+ ).split(",")
57
+ if item.strip()
58
+ }
59
+ )
60
+ )
61
+ ),
62
+ max_units=positive_int(environment, "EMBEDDING_MAX_UNITS", 256),
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 required_positive_int(environment: dict[str, str], key: str) -> int:
85
+ return positive_int(environment, key, 0)
86
+
87
+
88
+ def boolean(environment: dict[str, str], key: str, default: bool) -> bool:
89
+ raw = environment.get(key, str(default).lower()).strip().lower()
90
+ if raw not in {"true", "false"}:
91
+ raise ValueError(f"{key} 必须是 true 或 false")
92
+ return raw == "true"
93
+
94
+
95
+ def required_capabilities(value: tuple[str, ...]) -> tuple[str, ...]:
96
+ if not value:
97
+ raise ValueError("EMBEDDING_CAPABILITIES 不能为空")
98
+ return value
99
+
100
+
101
+ def validate_intranet_url(raw: str) -> None:
102
+ parsed = urlparse(raw)
103
+ if (
104
+ parsed.scheme not in {"http", "https"}
105
+ or not parsed.hostname
106
+ or parsed.username
107
+ or parsed.password
108
+ or parsed.query
109
+ or parsed.fragment
110
+ ):
111
+ raise ValueError("Embedding 内部 URL 无效")
112
+ host = parsed.hostname.lower()
113
+ if host == "localhost" or host.endswith(".internal") or "." not in host:
114
+ return
115
+ try:
116
+ address = ipaddress.ip_address(host)
117
+ except ValueError:
118
+ raise ValueError("Embedding 内部 URL 必须使用本地或私网地址") from None
119
+ if not (address.is_private or address.is_loopback):
120
+ raise ValueError("Embedding 内部 URL 禁止使用公网地址")
@@ -0,0 +1,106 @@
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
+
11
+
12
+ def to_camel(value: str) -> str:
13
+ first, *rest = value.split("_")
14
+ return first + "".join(part.capitalize() for part in rest)
15
+
16
+
17
+ class CanonicalModel(BaseModel):
18
+ model_config = ConfigDict(
19
+ alias_generator=to_camel,
20
+ populate_by_name=True,
21
+ extra="forbid",
22
+ )
23
+
24
+
25
+ class ArtifactInput(CanonicalModel):
26
+ value: str = Field(min_length=1)
27
+ media_type: str = Field(min_length=1)
28
+ byte_size: int = Field(ge=1)
29
+ sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
30
+
31
+
32
+ class EmbeddingItem(CanonicalModel):
33
+ id: str = Field(min_length=1, max_length=200)
34
+ text: str | None = None
35
+ source: ArtifactInput | None = None
36
+
37
+ @model_validator(mode="after")
38
+ def exactly_one_input(self) -> Self:
39
+ if (self.text is None) == (self.source is None):
40
+ raise ValueError("Embedding Item 必须且只能包含一种输入")
41
+ if self.text is not None and not self.text.strip():
42
+ raise ValueError("Embedding 文本不能为空")
43
+ return self
44
+
45
+
46
+ class RuntimeLimits(CanonicalModel):
47
+ max_duration_seconds: int = Field(ge=1, le=600)
48
+ max_input_bytes: int = Field(ge=1, le=268_435_456)
49
+ max_output_bytes: int = Field(ge=1, le=268_435_456)
50
+ max_units: int = Field(ge=1, le=256)
51
+ max_artifacts: int | None = Field(default=None, ge=1, le=16)
52
+ max_pixels_per_artifact: int | None = Field(
53
+ default=None,
54
+ ge=1,
55
+ le=100_000_000,
56
+ )
57
+
58
+
59
+ class ProviderEgress(CanonicalModel):
60
+ mode: Literal["forbidden", "configured-endpoint"]
61
+ deployment_key: str | None = None
62
+
63
+
64
+ class ProviderDataPolicy(CanonicalModel):
65
+ input_fields: list[dict[str, JsonValue]]
66
+ envelope_fields: list[dict[str, JsonValue]]
67
+
68
+
69
+ class EmbeddingRequest(CanonicalModel):
70
+ kind: Literal["embedding"]
71
+ operation_key: str = Field(min_length=1)
72
+ items: list[EmbeddingItem] = Field(min_length=1, max_length=256)
73
+ options: JsonValue
74
+ input_modalities: list[Literal["text", "image"]] = Field(min_length=1)
75
+ required_capabilities: list[str] = Field(min_length=1, max_length=32)
76
+ data_classification: Literal["public", "internal", "sensitive"]
77
+ limits: RuntimeLimits
78
+ provider_egress: ProviderEgress
79
+ provider_data_policy: ProviderDataPolicy | None = None
80
+
81
+
82
+ class EmbeddingVector(CanonicalModel):
83
+ id: str
84
+ vector: list[float]
85
+ input_sha256: str
86
+
87
+
88
+ class EmbeddingMetadata(CanonicalModel):
89
+ deployment_alias: str
90
+ model: str
91
+ version: str
92
+ dimensions: int
93
+ normalized: bool
94
+
95
+
96
+ class EmbeddingResponse(CanonicalModel):
97
+ batch_id: str
98
+ items: list[EmbeddingVector]
99
+ metadata: EmbeddingMetadata
100
+
101
+
102
+ class ErrorResponse(CanonicalModel):
103
+ code: str
104
+ message: str
105
+ request_id: str
106
+ retryable: bool
@@ -0,0 +1,6 @@
1
+ class SafeEmbeddingError(Exception):
2
+ def __init__(self, code: str, message: str, retryable: bool) -> None:
3
+ super().__init__(message)
4
+ self.code = code
5
+ self.safe_message = message
6
+ self.retryable = retryable
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import base64
5
+ import hashlib
6
+ from io import BytesIO
7
+
8
+ import httpx
9
+ from PIL import Image, UnidentifiedImageError
10
+
11
+ from .config import EmbeddingConfig
12
+ from .contracts import ArtifactInput
13
+ from .error import SafeEmbeddingError
14
+
15
+ _IMAGE_TYPES = frozenset({"image/jpeg", "image/png", "image/webp"})
16
+
17
+
18
+ async def load_image(
19
+ source: ArtifactInput,
20
+ config: EmbeddingConfig,
21
+ max_bytes: int,
22
+ max_pixels: int,
23
+ ) -> tuple[str, str]:
24
+ if source.media_type.lower() not in _IMAGE_TYPES:
25
+ raise invalid("Embedding 图片格式不受支持")
26
+ body = bytearray()
27
+ digest = hashlib.sha256()
28
+ try:
29
+ async with httpx.AsyncClient(
30
+ timeout=httpx.Timeout(
31
+ config.request_timeout_seconds,
32
+ connect=config.connect_timeout_seconds,
33
+ )
34
+ ) as client:
35
+ async with client.stream(
36
+ "GET",
37
+ config.artifact_url,
38
+ headers={
39
+ "authorization": f"Bearer {config.internal_token}",
40
+ "x-ai-artifact-ref": source.value,
41
+ },
42
+ ) as response:
43
+ response.raise_for_status()
44
+ async for chunk in response.aiter_bytes():
45
+ body.extend(chunk)
46
+ digest.update(chunk)
47
+ if len(body) > min(source.byte_size, max_bytes):
48
+ raise budget("Embedding 图片超过输入上限")
49
+ except SafeEmbeddingError:
50
+ raise
51
+ except Exception as error:
52
+ raise SafeEmbeddingError(
53
+ "ARTIFACT_UNAVAILABLE",
54
+ "Embedding Runtime 无法读取图片 Artifact",
55
+ True,
56
+ ) from error
57
+ if len(body) != source.byte_size or digest.hexdigest() != source.sha256:
58
+ raise invalid("Embedding Artifact 完整性校验失败")
59
+ encoded = await asyncio.to_thread(sanitize, bytes(body), max_pixels)
60
+ return encoded, source.sha256
61
+
62
+
63
+ def sanitize(raw: bytes, max_pixels: int) -> str:
64
+ try:
65
+ with Image.open(BytesIO(raw)) as source:
66
+ width, height = source.size
67
+ if width <= 0 or height <= 0 or width * height > max_pixels:
68
+ raise budget("Embedding 图片像素超过上限")
69
+ source.load()
70
+ normalized = source.convert(
71
+ "RGBA" if source.has_transparency_data else "RGB"
72
+ )
73
+ target = BytesIO()
74
+ normalized.save(target, format="PNG", optimize=False)
75
+ except SafeEmbeddingError:
76
+ raise
77
+ except (Image.DecompressionBombError, UnidentifiedImageError, OSError):
78
+ raise invalid("Embedding 图片内容损坏") from None
79
+ return base64.b64encode(target.getvalue()).decode()
80
+
81
+
82
+ def invalid(message: str) -> SafeEmbeddingError:
83
+ return SafeEmbeddingError("INVALID_INPUT", message, False)
84
+
85
+
86
+ def budget(message: str) -> SafeEmbeddingError:
87
+ return SafeEmbeddingError("BUDGET_EXCEEDED", message, False)