create-windy 0.2.33 → 0.3.0

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 (284) hide show
  1. package/dist/cli.js +173 -17
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +10 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +9 -0
  6. package/template/README.md +3 -1
  7. package/template/apps/agent-server/Dockerfile +1 -0
  8. package/template/apps/agent-server/README.md +28 -5
  9. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  10. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  11. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  12. package/template/apps/agent-server/pyproject.toml +1 -1
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/app.py +63 -3
  18. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
  21. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
  22. package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
  25. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  26. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  27. package/template/apps/agent-server/tests/test_api.py +16 -1
  28. package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
  29. package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
  30. package/template/apps/server/package.json +4 -1
  31. package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
  32. package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
  33. package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
  34. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  35. package/template/apps/server/src/agent/run-contracts.ts +2 -0
  36. package/template/apps/server/src/agent/run-facade.test.ts +52 -2
  37. package/template/apps/server/src/agent/run-facade.ts +38 -9
  38. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  39. package/template/apps/server/src/application-services.ts +50 -0
  40. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  41. package/template/apps/server/src/audit/search-summary.ts +2 -6
  42. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  43. package/template/apps/server/src/auth/password.ts +30 -21
  44. package/template/apps/server/src/auth/routes.test.ts +44 -3
  45. package/template/apps/server/src/auth/routes.ts +6 -1
  46. package/template/apps/server/src/auth/runtime.ts +4 -1
  47. package/template/apps/server/src/auth/service.ts +25 -7
  48. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  49. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  50. package/template/apps/server/src/configuration/definition.ts +242 -12
  51. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  52. package/template/apps/server/src/configuration/repository.ts +27 -2
  53. package/template/apps/server/src/configuration/routes.ts +4 -1
  54. package/template/apps/server/src/configuration/service.test.ts +100 -0
  55. package/template/apps/server/src/configuration/service.ts +50 -10
  56. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  57. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  58. package/template/apps/server/src/example-modules.ts +4 -1
  59. package/template/apps/server/src/guards.ts +18 -5
  60. package/template/apps/server/src/http/request-source.test.ts +85 -0
  61. package/template/apps/server/src/http/request-source.ts +90 -0
  62. package/template/apps/server/src/index.ts +45 -45
  63. package/template/apps/server/src/license/routes.ts +2 -0
  64. package/template/apps/server/src/license/runtime-service.ts +10 -11
  65. package/template/apps/server/src/module-bootstrap.ts +2 -2
  66. package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
  67. package/template/apps/server/src/module-host/initialize.ts +30 -44
  68. package/template/apps/server/src/module-host/request-context.ts +29 -23
  69. package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
  70. package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
  71. package/template/apps/server/src/module-host/route-declaration.ts +45 -0
  72. package/template/apps/server/src/module-host/route-installer.ts +27 -3
  73. package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
  74. package/template/apps/server/src/module-host/storage-port.ts +134 -1
  75. package/template/apps/server/src/module-host/storage-scope.ts +89 -0
  76. package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
  77. package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
  78. package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
  79. package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
  80. package/template/apps/server/src/module-host/user-directory-port.ts +139 -0
  81. package/template/apps/server/src/module-host.ts +4 -0
  82. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  83. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  84. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  85. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  86. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  87. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  88. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  89. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  90. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  91. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  92. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  93. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  94. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  95. package/template/apps/server/src/notification/memory-support.ts +72 -0
  96. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  97. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  98. package/template/apps/server/src/notification/repository.test.ts +171 -0
  99. package/template/apps/server/src/notification/repository.ts +179 -32
  100. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  101. package/template/apps/server/src/notification/route-support.ts +225 -0
  102. package/template/apps/server/src/notification/routes.test.ts +182 -0
  103. package/template/apps/server/src/notification/routes.ts +225 -132
  104. package/template/apps/server/src/notification/service.test.ts +104 -0
  105. package/template/apps/server/src/notification/service.ts +140 -0
  106. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  107. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  108. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  109. package/template/apps/server/src/route-guards.ts +1 -0
  110. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  111. package/template/apps/server/src/runtime-development.ts +2 -0
  112. package/template/apps/server/src/runtime.test.ts +23 -9
  113. package/template/apps/server/src/runtime.ts +39 -2
  114. package/template/apps/server/src/scheduler/audit.ts +2 -11
  115. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  116. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  117. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  118. package/template/apps/server/src/scheduler/routes.ts +2 -7
  119. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  120. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  121. package/template/apps/server/src/search/service.ts +2 -6
  122. package/template/apps/server/src/settings/routes.ts +15 -3
  123. package/template/apps/server/src/settings/runtime.ts +10 -1
  124. package/template/apps/server/src/storage/runtime.ts +13 -0
  125. package/template/apps/server/src/system/audit-query.ts +33 -4
  126. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  127. package/template/apps/server/src/system/audit-routes.ts +6 -5
  128. package/template/apps/server/src/system/built-in-roles.ts +1 -0
  129. package/template/apps/server/src/system/department-organization.ts +320 -0
  130. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  131. package/template/apps/server/src/system/department-routes.ts +114 -33
  132. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  133. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  134. package/template/apps/server/src/system/drizzle-system.ts +37 -9
  135. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  136. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  137. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  138. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  139. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  140. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  141. package/template/apps/server/src/system/route-helpers.ts +2 -5
  142. package/template/apps/server/src/system/route-types.ts +12 -3
  143. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  144. package/template/apps/server/src/system/routes.test.ts +10 -2
  145. package/template/apps/server/src/system/routes.ts +70 -36
  146. package/template/apps/server/src/system/seed.ts +7 -0
  147. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  148. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  149. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  150. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  151. package/template/apps/server/src/work-order/service.ts +42 -10
  152. package/template/apps/web/src/app/error-pages.ts +9 -0
  153. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  154. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  155. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  156. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  157. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  158. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  159. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  160. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  161. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  162. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  163. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  164. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  165. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  166. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  167. package/template/apps/web/src/lib/date-time.ts +22 -10
  168. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  169. package/template/apps/web/src/main.ts +14 -1
  170. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  171. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  172. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  173. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  174. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  175. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  176. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  177. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  178. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  179. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  180. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  181. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  182. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  183. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  184. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  185. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  186. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  187. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  188. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  189. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  190. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  191. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  192. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  193. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  194. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
  195. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  196. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  197. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  198. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  199. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  200. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  201. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  202. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  203. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  204. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  205. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  206. package/template/apps/web/src/router/error-navigation.ts +50 -0
  207. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  208. package/template/apps/web/src/router/index.ts +71 -0
  209. package/template/apps/web/src/router/index.webtest.ts +29 -0
  210. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  211. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  212. package/template/apps/web/src/services/auth-api.ts +5 -0
  213. package/template/apps/web/src/services/http.ts +43 -6
  214. package/template/apps/web/src/services/http.webtest.ts +70 -0
  215. package/template/apps/web/src/services/notification-api.ts +85 -2
  216. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  217. package/template/apps/web/src/services/search-api.ts +39 -0
  218. package/template/apps/web/src/services/system-api.ts +18 -0
  219. package/template/apps/web/vitest.config.ts +1 -0
  220. package/template/docker-compose.yml +68 -0
  221. package/template/docs/architecture/ai-egress.md +49 -7
  222. package/template/docs/architecture/ai-runtime.md +34 -6
  223. package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
  224. package/template/docs/architecture/object-storage.md +22 -2
  225. package/template/docs/architecture/search-provider-protocol.md +38 -2
  226. package/template/docs/development/custom-error-pages.md +78 -0
  227. package/template/docs/development/custom-login-page.md +3 -3
  228. package/template/docs/platform/agent-runtime.md +76 -7
  229. package/template/docs/platform/audit-reliability.md +46 -2
  230. package/template/docs/platform/business-user-directory.md +82 -0
  231. package/template/docs/platform/module-configuration.md +13 -4
  232. package/template/docs/platform/notifications.md +67 -11
  233. package/template/docs/platform/organization-membership.md +17 -3
  234. package/template/docs/platform/system-crud-api.md +16 -0
  235. package/template/docs/platform/web-system-crud.md +6 -2
  236. package/template/package.json +4 -0
  237. package/template/packages/config/index.test.ts +45 -0
  238. package/template/packages/config/package.json +1 -1
  239. package/template/packages/config/src/platform.ts +169 -48
  240. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  241. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  242. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  243. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  244. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  245. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  246. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  247. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  248. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  249. package/template/packages/database/package.json +1 -1
  250. package/template/packages/database/src/schema/identity.ts +7 -0
  251. package/template/packages/database/src/schema/notifications.ts +135 -0
  252. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  253. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  254. package/template/packages/modules/package.json +1 -1
  255. package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
  256. package/template/packages/modules/src/ai-operation-composition.ts +115 -0
  257. package/template/packages/modules/src/manifest.ts +26 -0
  258. package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
  259. package/template/packages/modules/src/system-api-permissions.ts +54 -0
  260. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  261. package/template/packages/modules/src/system-features.ts +6 -1
  262. package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
  263. package/template/packages/modules/src/system-module-catalog.ts +28 -4
  264. package/template/packages/modules/src/system-modules.test.ts +13 -1
  265. package/template/packages/modules/src/system-modules.ts +12 -0
  266. package/template/packages/modules/src/system-permissions.ts +11 -5
  267. package/template/packages/server-sdk/index.ts +11 -0
  268. package/template/packages/server-sdk/package.json +3 -2
  269. package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
  270. package/template/packages/server-sdk/src/module-host.ts +8 -0
  271. package/template/packages/server-sdk/src/request-context.ts +2 -0
  272. package/template/packages/server-sdk/src/storage-port.ts +66 -0
  273. package/template/packages/server-sdk/src/tool-registration.ts +6 -0
  274. package/template/packages/server-sdk/src/user-directory-port.ts +42 -0
  275. package/template/packages/shared/package.json +1 -1
  276. package/template/packages/shared/src/audit.ts +25 -0
  277. package/template/packages/shared/src/module-configuration.ts +5 -0
  278. package/template/packages/shared/src/notification.ts +92 -1
  279. package/template/packages/shared/src/password.ts +7 -2
  280. package/template/packages/shared/src/platform-settings.ts +2 -0
  281. package/template/packages/storage/package.json +1 -1
  282. package/template/packages/storage/src/upload-service.test.ts +16 -0
  283. package/template/packages/storage/src/upload-service.ts +5 -1
  284. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { Transporter } from "nodemailer";
3
+ import {
4
+ NotificationChannelError,
5
+ type NotificationChannelMessage,
6
+ } from "./channel-adapter.js";
7
+ import { SmtpNotificationChannelAdapter } from "./smtp-adapter.js";
8
+
9
+ describe("SmtpNotificationChannelAdapter", () => {
10
+ test("使用类型化 mail 配置发件且不向消息暴露 smtpUrl", async () => {
11
+ const sent: unknown[] = [];
12
+ const transporter = {
13
+ async sendMail(message: unknown) {
14
+ sent.push(message);
15
+ return { messageId: "mail-1" };
16
+ },
17
+ } as unknown as Transporter;
18
+ const adapter = new SmtpNotificationChannelAdapter(config, transporter);
19
+ expect(await adapter.deliver(message)).toEqual({
20
+ providerMessageId: "mail-1",
21
+ });
22
+ expect(sent[0]).toMatchObject({
23
+ from: "notify@example.com",
24
+ to: "user@example.com",
25
+ subject: "测试通知",
26
+ text: "正文",
27
+ });
28
+ expect(JSON.stringify(sent[0])).not.toContain("smtp://");
29
+ });
30
+
31
+ test("认证错误标记为不可重试且只保留标准化错误码", async () => {
32
+ const transporter = {
33
+ async sendMail() {
34
+ throw Object.assign(new Error("包含服务端敏感响应"), { code: "EAUTH" });
35
+ },
36
+ } as unknown as Transporter;
37
+ const adapter = new SmtpNotificationChannelAdapter(config, transporter);
38
+ try {
39
+ await adapter.deliver(message);
40
+ throw new Error("expected rejection");
41
+ } catch (error) {
42
+ expect(error).toBeInstanceOf(NotificationChannelError);
43
+ expect(error).toMatchObject({ code: "EAUTH", retryable: false });
44
+ expect(String(error)).not.toContain("敏感响应");
45
+ }
46
+ });
47
+ });
48
+
49
+ const config = {
50
+ enabled: true,
51
+ host: "smtp.example.com",
52
+ port: 587,
53
+ secure: false,
54
+ fromAddress: "notify@example.com",
55
+ connectTimeoutMs: 5_000,
56
+ smtpUrl: "smtp://secret@example.com",
57
+ };
58
+
59
+ const message: NotificationChannelMessage = {
60
+ deliveryId: "delivery-1",
61
+ userId: "user-1",
62
+ address: "user@example.com",
63
+ subject: "测试通知",
64
+ content: "正文",
65
+ };
@@ -0,0 +1,68 @@
1
+ import nodemailer from "nodemailer";
2
+ import type { Transporter } from "nodemailer";
3
+ import type { PlatformConfig } from "@southwind-ai/config";
4
+ import {
5
+ NotificationChannelError,
6
+ type NotificationChannelAdapter,
7
+ type NotificationChannelMessage,
8
+ } from "./channel-adapter.js";
9
+
10
+ export class SmtpNotificationChannelAdapter implements NotificationChannelAdapter {
11
+ readonly channel = "email" as const;
12
+ private readonly transporter: Transporter;
13
+
14
+ constructor(
15
+ private readonly config: PlatformConfig["mail"],
16
+ transporter?: Transporter,
17
+ ) {
18
+ if (!config.enabled || !config.fromAddress) {
19
+ throw new Error("SMTP 通知渠道未启用或未配置发件地址");
20
+ }
21
+ this.transporter =
22
+ transporter ||
23
+ nodemailer.createTransport(
24
+ config.smtpUrl || {
25
+ host: config.host,
26
+ port: config.port,
27
+ secure: config.secure,
28
+ connectionTimeout: config.connectTimeoutMs,
29
+ },
30
+ );
31
+ }
32
+
33
+ async deliver(message: NotificationChannelMessage) {
34
+ try {
35
+ const result = await this.transporter.sendMail({
36
+ from: this.config.fromAddress,
37
+ to: message.address,
38
+ subject: message.subject,
39
+ text: message.content,
40
+ headers: { "X-Windy-Delivery-Id": message.deliveryId },
41
+ });
42
+ return { providerMessageId: result.messageId || undefined };
43
+ } catch (error) {
44
+ const code = smtpErrorCode(error);
45
+ throw new NotificationChannelError(code, isRetryableSmtpError(error));
46
+ }
47
+ }
48
+ }
49
+
50
+ function smtpErrorCode(error: unknown) {
51
+ if (!error || typeof error !== "object") return "SMTP_DELIVERY_FAILED";
52
+ const value = error as { code?: unknown; responseCode?: unknown };
53
+ const code =
54
+ typeof value.code === "string" ? value.code.toUpperCase() : undefined;
55
+ const responseCode =
56
+ typeof value.responseCode === "number" ? value.responseCode : undefined;
57
+ return (
58
+ code || (responseCode ? `SMTP_${responseCode}` : "SMTP_DELIVERY_FAILED")
59
+ );
60
+ }
61
+
62
+ function isRetryableSmtpError(error: unknown) {
63
+ if (!error || typeof error !== "object") return true;
64
+ const value = error as { code?: unknown; responseCode?: unknown };
65
+ if (typeof value.responseCode === "number")
66
+ return value.responseCode >= 400 && value.responseCode < 500;
67
+ return !["EAUTH", "EENVELOPE", "EMESSAGE"].includes(String(value.code || ""));
68
+ }
@@ -4,7 +4,10 @@ import { resolve } from "node:path";
4
4
  import { drizzle } from "drizzle-orm/node-postgres";
5
5
  import { migrate } from "drizzle-orm/node-postgres/migrator";
6
6
  import { Pool } from "pg";
7
- import { DrizzleAuditLogWriter } from "./audit/drizzle-log-store.js";
7
+ import {
8
+ DrizzleAuditLogReader,
9
+ DrizzleAuditLogWriter,
10
+ } from "./audit/drizzle-log-store.js";
8
11
 
9
12
  const databaseUrl = process.env.AUDIT_TEST_DATABASE_URL;
10
13
  const describePostgres = databaseUrl ? describe : describe.skip;
@@ -12,6 +15,7 @@ const describePostgres = databaseUrl ? describe : describe.skip;
12
15
  describePostgres("PostgreSQL audit writer", () => {
13
16
  let pool: Pool;
14
17
  let writer: DrizzleAuditLogWriter;
18
+ let reader: DrizzleAuditLogReader;
15
19
 
16
20
  beforeAll(async () => {
17
21
  pool = new Pool({ connectionString: databaseUrl });
@@ -25,10 +29,13 @@ describePostgres("PostgreSQL audit writer", () => {
25
29
  migrationsTable: "__audit01_test_migrations",
26
30
  });
27
31
  writer = new DrizzleAuditLogWriter(db);
32
+ reader = new DrizzleAuditLogReader(db);
28
33
  });
29
34
 
30
35
  afterAll(async () => {
31
- await pool?.query("delete from audit_logs where id = 'audit_pg_1'");
36
+ await pool?.query(
37
+ "delete from audit_logs where id in ('audit_pg_1', 'audit_pg_search_1')",
38
+ );
32
39
  await pool?.end();
33
40
  });
34
41
 
@@ -51,4 +58,36 @@ describePostgres("PostgreSQL audit writer", () => {
51
58
  );
52
59
  expect(result.rows[0]?.count).toBe("1");
53
60
  });
61
+
62
+ test("关键字与分钟范围在 PostgreSQL Reader 中共同生效", async () => {
63
+ await writer.write({
64
+ id: "audit_pg_search_1",
65
+ action: "case.review",
66
+ outcome: "success",
67
+ actor: {
68
+ id: "user_1",
69
+ type: "user",
70
+ name: "审计员",
71
+ ip: "203.0.113.8",
72
+ },
73
+ target: { type: "case", id: "case_1", label: "案件甲" },
74
+ module: "case-workbench",
75
+ requestId: "request_pg_1",
76
+ message: "完成复核",
77
+ metadata: { note: "线索已确认" },
78
+ occurredAt: "2026-07-20T01:15:30.000Z",
79
+ });
80
+
81
+ const result = await reader.list({
82
+ page: 1,
83
+ pageSize: 10,
84
+ q: "案件甲",
85
+ actorId: "user_1",
86
+ action: "case.review",
87
+ occurredAfter: "2026-07-20T01:15:00.000Z",
88
+ occurredBefore: "2026-07-20T01:15:59.999Z",
89
+ });
90
+
91
+ expect(result.items.map(({ id }) => id)).toContain("audit_pg_search_1");
92
+ });
54
93
  });
@@ -44,6 +44,7 @@ export function systemFeatureKeyForResource(resourceName: string): string {
44
44
  audit: "system.audit",
45
45
  license: "system.license",
46
46
  ops: "system.ops",
47
+ notification: "system.notification",
47
48
  scheduler: "system.scheduler",
48
49
  settings: "system.settings",
49
50
  };
@@ -95,4 +95,43 @@ describe("server runtime audit list", () => {
95
95
  expect(hidden.items.map(({ id }) => id)).toEqual(["audit_user"]);
96
96
  expect(included.total).toBe(2);
97
97
  });
98
+
99
+ test("无数据库时关键字覆盖动作、操作者、对象、IP 和已脱敏上下文", async () => {
100
+ const runtime = createServerRuntime();
101
+ runtime.auditSink.recordEvent({
102
+ id: "audit_keyword",
103
+ action: "case.review",
104
+ outcome: "success",
105
+ actor: {
106
+ id: "user_1",
107
+ type: "user",
108
+ name: "审计员",
109
+ ip: "203.0.113.8",
110
+ },
111
+ target: { type: "case", id: "case_1", label: "案件甲" },
112
+ module: "case-workbench",
113
+ requestId: "request_1",
114
+ message: "完成复核",
115
+ metadata: { note: "线索已确认", token: "[REDACTED]" },
116
+ occurredAt: "2026-07-14T01:00:00.000Z",
117
+ });
118
+
119
+ for (const q of [
120
+ "case.review",
121
+ "审计员",
122
+ "案件甲",
123
+ "203.0.113.8",
124
+ "完成复核",
125
+ "case-workbench",
126
+ "request_1",
127
+ "线索已确认",
128
+ ]) {
129
+ const result = await runtime.listAuditEvents({
130
+ page: 1,
131
+ pageSize: 10,
132
+ q,
133
+ });
134
+ expect(result.items.map(({ id }) => id)).toEqual(["audit_keyword"]);
135
+ }
136
+ });
98
137
  });
@@ -25,6 +25,7 @@ export function developmentAdminActor(): RequestActor {
25
25
  "system.menu.read",
26
26
  "system.user.read",
27
27
  "system.user.manage",
28
+ "system.user-directory.read",
28
29
  "system.role.read",
29
30
  "system.role.manage",
30
31
  "system.department.read",
@@ -51,6 +52,7 @@ export function developmentAdminActor(): RequestActor {
51
52
  "system.scheduler.read",
52
53
  "system.scheduler.manage",
53
54
  "system.notification.read",
55
+ "system.notification.update",
54
56
  "system.notification.manage",
55
57
  "system.bulk-data.manage",
56
58
  "system.storage.read",
@@ -7,6 +7,7 @@ import {
7
7
  type FeatureFlagDefinition,
8
8
  } from "@southwind-ai/shared";
9
9
  import { createFoundationSnapshot } from "./foundation.js";
10
+ import { auditActor } from "./guards.js";
10
11
  import { createServerRuntime } from "./runtime.js";
11
12
  import { AuditWriteUnavailableError } from "./audit/contracts.js";
12
13
 
@@ -136,30 +137,43 @@ describe("server runtime", () => {
136
137
  });
137
138
 
138
139
  test("外部 actor resolver 优先提供数据库角色权限来源", async () => {
140
+ const sessionActor = {
141
+ id: "user_from_db",
142
+ type: "user" as const,
143
+ name: "数据库用户",
144
+ roleCodes: ["operator"],
145
+ permissionKeys: ["system.audit.read"],
146
+ };
139
147
  const runtime = createServerRuntime(
140
148
  { WINDY_DEV_ADMIN_TOKEN: "token_1" },
141
149
  {
142
150
  actorResolver: {
143
151
  async resolve() {
144
- return {
145
- id: "user_from_db",
146
- type: "user",
147
- name: "数据库用户",
148
- roleCodes: ["operator"],
149
- permissionKeys: ["system.audit.read"],
150
- };
152
+ return sessionActor;
151
153
  },
152
154
  },
153
155
  },
154
156
  );
155
157
  const request = new Request("http://localhost/api/system/audit-logs", {
156
- headers: { authorization: "Bearer token_1" },
158
+ headers: {
159
+ authorization: "Bearer token_1",
160
+ "user-agent": "Windy-Audit-Test/1.0",
161
+ "x-forwarded-for": "203.0.113.99",
162
+ },
157
163
  });
158
164
 
159
- const context = await runtime.createGuardContext(request);
165
+ const context = await runtime.createGuardContext(request, {
166
+ clientIp: "198.51.100.10",
167
+ });
160
168
 
161
169
  expect(context.actor.id).toBe("user_from_db");
162
170
  expect(context.actor.permissionKeys).toEqual(["system.audit.read"]);
171
+ expect(auditActor(context)).toMatchObject({
172
+ ip: "198.51.100.10",
173
+ userAgent: "Windy-Audit-Test/1.0",
174
+ });
175
+ expect(sessionActor).not.toHaveProperty("ip");
176
+ expect(sessionActor).not.toHaveProperty("userAgent");
163
177
  });
164
178
 
165
179
  test("外部 license resolver 优先提供真实 License 授权状态", async () => {
@@ -27,6 +27,10 @@ import {
27
27
  type FeatureFlagRuntimeResolver,
28
28
  type ResolvedFeatureFlag,
29
29
  } from "./runtime-feature.js";
30
+ import {
31
+ requestSourcePolicyFromEnv,
32
+ resolveRequestSource,
33
+ } from "./http/request-source.js";
30
34
 
31
35
  export type {
32
36
  FeatureFlagRuntimeResolver,
@@ -53,6 +57,7 @@ export interface AuditEventWriter {
53
57
  export interface AuditEventListQuery {
54
58
  page: number;
55
59
  pageSize: number;
60
+ q?: string;
56
61
  actorId?: string;
57
62
  action?: string;
58
63
  resource?: string;
@@ -198,6 +203,7 @@ export function createServerRuntime(
198
203
  options.featureResolver,
199
204
  env.NODE_ENV === "production",
200
205
  );
206
+ const requestSourcePolicy = requestSourcePolicyFromEnv(env);
201
207
 
202
208
  return {
203
209
  auditSink,
@@ -205,13 +211,18 @@ export function createServerRuntime(
205
211
  request: Request,
206
212
  metadata: { clientIp?: string } = {},
207
213
  ) {
214
+ const requestSource = resolveRequestSource(
215
+ request,
216
+ metadata.clientIp,
217
+ requestSourcePolicy,
218
+ );
208
219
  const actor =
209
220
  (await options.actorResolver?.resolve(request)) ||
210
221
  sessionStore.resolve(request);
211
222
  const license =
212
223
  (await options.licenseResolver?.resolve()) || licenseStore.resolve();
213
224
  return createRequestGuardContext(request, {
214
- clientIp: metadata.clientIp,
225
+ ...requestSource,
215
226
  resolveActor: () => actor,
216
227
  resolveLicense: () => license,
217
228
  });
@@ -221,10 +232,15 @@ export function createServerRuntime(
221
232
  actor: RequestActor,
222
233
  metadata: { clientIp?: string } = {},
223
234
  ) {
235
+ const requestSource = resolveRequestSource(
236
+ request,
237
+ metadata.clientIp,
238
+ requestSourcePolicy,
239
+ );
224
240
  const license =
225
241
  (await options.licenseResolver?.resolve()) || licenseStore.resolve();
226
242
  return createRequestGuardContext(request, {
227
- clientIp: metadata.clientIp,
243
+ ...requestSource,
228
244
  resolveActor: () => actor,
229
245
  resolveLicense: () => license,
230
246
  });
@@ -268,6 +284,7 @@ function matchesAuditQuery(
268
284
  query: AuditEventListQuery,
269
285
  ): boolean {
270
286
  return (
287
+ (!query.q || matchesAuditKeyword(event, query.q)) &&
271
288
  (!query.actorId || event.actor.id === query.actorId) &&
272
289
  (!query.action || event.action === query.action) &&
273
290
  (!query.resource || event.target?.type === query.resource) &&
@@ -277,3 +294,23 @@ function matchesAuditQuery(
277
294
  (!query.occurredBefore || event.occurredAt <= query.occurredBefore)
278
295
  );
279
296
  }
297
+
298
+ function matchesAuditKeyword(event: AuditEvent, query: string): boolean {
299
+ const keyword = query.toLowerCase();
300
+ const values = [
301
+ event.action,
302
+ event.actor.id,
303
+ event.actor.name,
304
+ event.target?.type,
305
+ event.target?.id,
306
+ event.target?.label,
307
+ event.actor.ip,
308
+ event.message,
309
+ event.module,
310
+ event.requestId,
311
+ event.metadata ? JSON.stringify(event.metadata) : undefined,
312
+ ];
313
+ return values.some((value) =>
314
+ value === undefined ? false : String(value).toLowerCase().includes(keyword),
315
+ );
316
+ }
@@ -1,6 +1,6 @@
1
1
  import { randomUUIDv7 } from "bun";
2
2
  import type { AuditEvent } from "@southwind-ai/shared";
3
- import type { RequestActor, RequestGuardContext } from "../guards.js";
3
+ import { auditActor, type RequestGuardContext } from "../guards.js";
4
4
  import type { ScheduledTaskRun } from "./types.js";
5
5
 
6
6
  export function schedulerAuditEvent(
@@ -14,7 +14,7 @@ export function schedulerAuditEvent(
14
14
  id: randomUUIDv7(),
15
15
  action,
16
16
  outcome,
17
- actor: auditActor(context.actor),
17
+ actor: auditActor(context),
18
18
  target: { type: "scheduled-task", id: run.taskKey },
19
19
  requestId: context.requestId,
20
20
  metadata: {
@@ -27,12 +27,3 @@ export function schedulerAuditEvent(
27
27
  occurredAt: occurredAt.toISOString(),
28
28
  };
29
29
  }
30
-
31
- function auditActor(actor: RequestActor): AuditEvent["actor"] {
32
- return {
33
- id: actor.id,
34
- type: actor.type === "anonymous" ? "user" : actor.type,
35
- name: actor.name,
36
- roleCodes: actor.roleCodes,
37
- };
38
- }
@@ -13,6 +13,7 @@ describe("平台任务 Manifest Adapter", () => {
13
13
  storageCleanup: () => ({ deleted: 0 }),
14
14
  workflowTimeoutDispatch: () => ({ claimed: false }),
15
15
  governedExportDispatch: () => ({ claimed: false }),
16
+ notificationDispatch: () => ({ claimed: false }),
16
17
  },
17
18
  );
18
19
 
@@ -31,5 +32,10 @@ describe("平台任务 Manifest Adapter", () => {
31
32
  .find(({ key }) => key === "system.governed-export.dispatch")
32
33
  ?.isIdleResult?.({ claimed: false }),
33
34
  ).toBe(true);
35
+ expect(
36
+ definitions
37
+ .find(({ key }) => key === "system.notification.dispatch")
38
+ ?.isIdleResult?.({ claimed: false }),
39
+ ).toBe(true);
34
40
  });
35
41
  });
@@ -8,6 +8,7 @@ export interface PlatformTaskHandlers {
8
8
  storageCleanup?: ScheduledTaskDefinition["handler"];
9
9
  workflowTimeoutDispatch?: ScheduledTaskDefinition["handler"];
10
10
  governedExportDispatch?: ScheduledTaskDefinition["handler"];
11
+ notificationDispatch?: ScheduledTaskDefinition["handler"];
11
12
  }
12
13
 
13
14
  export function createPlatformTaskDefinitions(
@@ -33,6 +34,11 @@ export function createPlatformTaskDefinitions(
33
34
  "system.governed-export.dispatch",
34
35
  handlers.governedExportDispatch,
35
36
  );
37
+ if (handlers.notificationDispatch)
38
+ handlerByKey.set(
39
+ "system.notification.dispatch",
40
+ handlers.notificationDispatch,
41
+ );
36
42
 
37
43
  return systemModuleManifests()
38
44
  .flatMap((module) => module.tasks)
@@ -49,6 +55,7 @@ export function createPlatformTaskDefinitions(
49
55
  isIdleResult:
50
56
  task.key === "system.bulk-data.dispatch" ||
51
57
  task.key === "system.workflow.timeout-dispatch" ||
58
+ task.key === "system.notification.dispatch" ||
52
59
  task.key === "system.governed-export.dispatch"
53
60
  ? (value) => value?.claimed === false
54
61
  : undefined,
@@ -1,7 +1,7 @@
1
1
  import { randomUUIDv7 } from "bun";
2
2
  import type { AuditEvent } from "@southwind-ai/shared";
3
3
  import { runGuardedBackgroundTask } from "../background/task-guard.js";
4
- import type { RequestActor, RequestGuardContext } from "../guards.js";
4
+ import { auditActor, type RequestGuardContext } from "../guards.js";
5
5
  import type { createServerRuntime } from "../runtime.js";
6
6
  import type { SchedulerRepository } from "./repository.js";
7
7
  import { sanitizeRecoveryReason } from "./sensitive-text.js";
@@ -116,7 +116,7 @@ function auditEvent(
116
116
  id: randomUUIDv7(),
117
117
  action: "task.recover",
118
118
  outcome,
119
- actor: auditActor(context.actor),
119
+ actor: auditActor(context),
120
120
  target: { type: "scheduled-task-run", id: recovery.runId },
121
121
  requestId: context.requestId,
122
122
  message: phase === "intent" ? "人工恢复意图已确认" : "人工恢复状态转换结果",
@@ -141,12 +141,3 @@ function policy(definition: ScheduledTaskDefinition) {
141
141
  feature: definition.feature,
142
142
  };
143
143
  }
144
-
145
- function auditActor(actor: RequestActor): AuditEvent["actor"] {
146
- return {
147
- id: actor.id,
148
- type: actor.type === "anonymous" ? "user" : actor.type,
149
- name: actor.name,
150
- roleCodes: actor.roleCodes,
151
- };
152
- }
@@ -4,7 +4,7 @@ import type {
4
4
  FeatureFlagDefinition,
5
5
  HttpMethod,
6
6
  } from "@southwind-ai/shared";
7
- import type { RequestGuardContext } from "../guards.js";
7
+ import { auditActor, type RequestGuardContext } from "../guards.js";
8
8
  import { evaluateRouteGuards, findSystemFeature } from "../route-guards.js";
9
9
  import type { createServerRuntime } from "../runtime.js";
10
10
  import { routeContext } from "../system/route-helpers.js";
@@ -209,12 +209,7 @@ function readAudit(context: RequestGuardContext, resource: string): AuditEvent {
209
209
  id: randomUUIDv7(),
210
210
  action: "task.read",
211
211
  outcome: "success",
212
- actor: {
213
- id: context.actor.id,
214
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
215
- name: context.actor.name,
216
- roleCodes: context.actor.roleCodes,
217
- },
212
+ actor: auditActor(context),
218
213
  target: { type: resource },
219
214
  requestId: context.requestId,
220
215
  occurredAt: new Date().toISOString(),
@@ -0,0 +1,45 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { OpenSearchClient, readOpenSearchConfig } from "./opensearch-client.js";
3
+
4
+ describe("OpenSearch Client 配置", () => {
5
+ test("未配置 URL 时保持默认无外部搜索依赖", () => {
6
+ expect(readOpenSearchConfig({})).toBeUndefined();
7
+ });
8
+
9
+ test("拒绝 URL 内嵌凭据与不完整认证配置", () => {
10
+ const config = readOpenSearchConfig({
11
+ OPENSEARCH_URL: "http://user:secret@localhost:9200",
12
+ });
13
+ expect(() => new OpenSearchClient(config!)).toThrow("不含凭据、路径和参数");
14
+ expect(
15
+ () =>
16
+ new OpenSearchClient({
17
+ url: "http://localhost:9200",
18
+ username: "windy",
19
+ requestTimeoutMs: 1_000,
20
+ }),
21
+ ).toThrow("必须同时配置");
22
+ });
23
+
24
+ test("认证只进入 Server 到 OpenSearch 的请求头", async () => {
25
+ const requests: Request[] = [];
26
+ const client = new OpenSearchClient(
27
+ {
28
+ url: "http://opensearch:9200",
29
+ username: "windy",
30
+ password: "secret",
31
+ requestTimeoutMs: 1_000,
32
+ },
33
+ async (input, init) => {
34
+ requests.push(new Request(input, init));
35
+ return Response.json({ status: "green" });
36
+ },
37
+ );
38
+
39
+ expect(await client.health()).toEqual({ status: "green" });
40
+ expect(requests[0]?.url).toBe(
41
+ "http://opensearch:9200/_cluster/health?local=true",
42
+ );
43
+ expect(requests[0]?.headers.get("authorization")).toStartWith("Basic ");
44
+ });
45
+ });