create-windy 0.2.34 → 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 (237) hide show
  1. package/dist/cli.js +167 -17
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/AGENTS.md +5 -0
  5. package/template/README.md +3 -1
  6. package/template/apps/agent-server/Dockerfile +1 -0
  7. package/template/apps/agent-server/README.md +18 -5
  8. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  9. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  10. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  11. package/template/apps/agent-server/pyproject.toml +1 -1
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/app.py +1 -2
  17. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  18. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  19. package/template/apps/server/package.json +4 -1
  20. package/template/apps/server/src/agent/run-facade.ts +2 -7
  21. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  22. package/template/apps/server/src/application-services.ts +50 -0
  23. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  24. package/template/apps/server/src/audit/search-summary.ts +2 -6
  25. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  26. package/template/apps/server/src/auth/password.ts +30 -21
  27. package/template/apps/server/src/auth/routes.test.ts +44 -3
  28. package/template/apps/server/src/auth/routes.ts +6 -1
  29. package/template/apps/server/src/auth/runtime.ts +4 -1
  30. package/template/apps/server/src/auth/service.ts +25 -7
  31. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  32. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  33. package/template/apps/server/src/configuration/definition.ts +242 -12
  34. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  35. package/template/apps/server/src/configuration/repository.ts +27 -2
  36. package/template/apps/server/src/configuration/routes.ts +4 -1
  37. package/template/apps/server/src/configuration/service.test.ts +100 -0
  38. package/template/apps/server/src/configuration/service.ts +50 -10
  39. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  40. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  41. package/template/apps/server/src/example-modules.ts +4 -1
  42. package/template/apps/server/src/guards.ts +18 -5
  43. package/template/apps/server/src/http/request-source.test.ts +85 -0
  44. package/template/apps/server/src/http/request-source.ts +90 -0
  45. package/template/apps/server/src/index.ts +23 -6
  46. package/template/apps/server/src/license/routes.ts +2 -0
  47. package/template/apps/server/src/license/runtime-service.ts +10 -11
  48. package/template/apps/server/src/module-bootstrap.ts +2 -2
  49. package/template/apps/server/src/module-host/request-context.ts +6 -6
  50. package/template/apps/server/src/module-host/storage-scope.ts +2 -9
  51. package/template/apps/server/src/module-host/user-directory-port.ts +2 -6
  52. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  53. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  54. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  55. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  56. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  57. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  58. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  59. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  60. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  61. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  62. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  63. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  64. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  65. package/template/apps/server/src/notification/memory-support.ts +72 -0
  66. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  67. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  68. package/template/apps/server/src/notification/repository.test.ts +171 -0
  69. package/template/apps/server/src/notification/repository.ts +179 -32
  70. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  71. package/template/apps/server/src/notification/route-support.ts +225 -0
  72. package/template/apps/server/src/notification/routes.test.ts +182 -0
  73. package/template/apps/server/src/notification/routes.ts +225 -132
  74. package/template/apps/server/src/notification/service.test.ts +104 -0
  75. package/template/apps/server/src/notification/service.ts +140 -0
  76. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  77. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  78. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  79. package/template/apps/server/src/route-guards.ts +1 -0
  80. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  81. package/template/apps/server/src/runtime-development.ts +1 -0
  82. package/template/apps/server/src/runtime.test.ts +23 -9
  83. package/template/apps/server/src/runtime.ts +39 -2
  84. package/template/apps/server/src/scheduler/audit.ts +2 -11
  85. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  86. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  87. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  88. package/template/apps/server/src/scheduler/routes.ts +2 -7
  89. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  90. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  91. package/template/apps/server/src/search/service.ts +2 -6
  92. package/template/apps/server/src/settings/routes.ts +15 -3
  93. package/template/apps/server/src/settings/runtime.ts +10 -1
  94. package/template/apps/server/src/storage/runtime.ts +13 -0
  95. package/template/apps/server/src/system/audit-query.ts +33 -4
  96. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  97. package/template/apps/server/src/system/audit-routes.ts +6 -5
  98. package/template/apps/server/src/system/department-organization.ts +320 -0
  99. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  100. package/template/apps/server/src/system/department-routes.ts +114 -33
  101. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  102. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  103. package/template/apps/server/src/system/drizzle-system.ts +37 -9
  104. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  105. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  106. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  107. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  108. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  109. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  110. package/template/apps/server/src/system/route-helpers.ts +2 -5
  111. package/template/apps/server/src/system/route-types.ts +12 -3
  112. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  113. package/template/apps/server/src/system/routes.test.ts +10 -2
  114. package/template/apps/server/src/system/routes.ts +70 -36
  115. package/template/apps/server/src/system/seed.ts +7 -0
  116. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  117. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  118. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  119. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  120. package/template/apps/server/src/work-order/service.ts +42 -10
  121. package/template/apps/web/src/app/error-pages.ts +9 -0
  122. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  123. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  124. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  125. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  126. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  127. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  128. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  129. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  130. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  131. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  132. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  133. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  134. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  135. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  136. package/template/apps/web/src/lib/date-time.ts +22 -10
  137. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  138. package/template/apps/web/src/main.ts +14 -1
  139. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  140. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  141. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  142. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  143. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  144. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  145. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  146. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  147. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  148. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  149. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  150. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  151. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  152. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  153. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  154. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  155. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  156. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  157. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  158. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  159. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  160. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  161. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  162. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  163. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
  164. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  165. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  166. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  167. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  168. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  169. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  170. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  171. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  172. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  173. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  174. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  175. package/template/apps/web/src/router/error-navigation.ts +50 -0
  176. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  177. package/template/apps/web/src/router/index.ts +71 -0
  178. package/template/apps/web/src/router/index.webtest.ts +29 -0
  179. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  180. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  181. package/template/apps/web/src/services/auth-api.ts +5 -0
  182. package/template/apps/web/src/services/http.ts +43 -6
  183. package/template/apps/web/src/services/http.webtest.ts +70 -0
  184. package/template/apps/web/src/services/notification-api.ts +85 -2
  185. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  186. package/template/apps/web/src/services/search-api.ts +39 -0
  187. package/template/apps/web/src/services/system-api.ts +18 -0
  188. package/template/apps/web/vitest.config.ts +1 -0
  189. package/template/docker-compose.yml +66 -0
  190. package/template/docs/architecture/ai-runtime.md +14 -5
  191. package/template/docs/architecture/search-provider-protocol.md +38 -2
  192. package/template/docs/development/custom-error-pages.md +78 -0
  193. package/template/docs/development/custom-login-page.md +3 -3
  194. package/template/docs/platform/agent-runtime.md +32 -6
  195. package/template/docs/platform/audit-reliability.md +46 -2
  196. package/template/docs/platform/module-configuration.md +13 -4
  197. package/template/docs/platform/notifications.md +67 -11
  198. package/template/docs/platform/organization-membership.md +17 -3
  199. package/template/docs/platform/system-crud-api.md +16 -0
  200. package/template/docs/platform/web-system-crud.md +6 -2
  201. package/template/package.json +4 -0
  202. package/template/packages/config/index.test.ts +45 -0
  203. package/template/packages/config/package.json +1 -1
  204. package/template/packages/config/src/platform.ts +169 -48
  205. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  206. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  207. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  208. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  209. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  210. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  211. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  212. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  213. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  214. package/template/packages/database/package.json +1 -1
  215. package/template/packages/database/src/schema/identity.ts +7 -0
  216. package/template/packages/database/src/schema/notifications.ts +135 -0
  217. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  218. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  219. package/template/packages/modules/package.json +1 -1
  220. package/template/packages/modules/src/system-api-permissions.ts +54 -0
  221. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  222. package/template/packages/modules/src/system-features.ts +1 -1
  223. package/template/packages/modules/src/system-module-catalog.ts +2 -1
  224. package/template/packages/modules/src/system-modules.test.ts +13 -1
  225. package/template/packages/modules/src/system-modules.ts +12 -0
  226. package/template/packages/modules/src/system-permissions.ts +9 -5
  227. package/template/packages/server-sdk/package.json +1 -1
  228. package/template/packages/shared/package.json +1 -1
  229. package/template/packages/shared/src/audit.ts +25 -0
  230. package/template/packages/shared/src/module-configuration.ts +5 -0
  231. package/template/packages/shared/src/notification.ts +92 -1
  232. package/template/packages/shared/src/password.ts +7 -2
  233. package/template/packages/shared/src/platform-settings.ts +2 -0
  234. package/template/packages/storage/package.json +1 -1
  235. package/template/packages/storage/src/upload-service.test.ts +16 -0
  236. package/template/packages/storage/src/upload-service.ts +5 -1
  237. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -81,6 +81,106 @@ describe("模块配置版本服务", () => {
81
81
  expect(serialized).not.toContain("secret-value-marker");
82
82
  expect(serialized).not.toContain("SecretRef");
83
83
  });
84
+
85
+ test("重启字段保存为等待状态并在下一次启动提升为生效版本", async () => {
86
+ const repository = new InMemoryModuleConfigVersionRepository();
87
+ const current = { host: "old.smtp.local" };
88
+ const definition: ManagedModuleConfigDefinition = {
89
+ moduleKey: "mail.runtime",
90
+ label: "邮件传输配置",
91
+ fields: [
92
+ {
93
+ path: "host",
94
+ label: "SMTP 主机",
95
+ description: "重启后应用",
96
+ input: "text",
97
+ applyMode: "restart",
98
+ },
99
+ ],
100
+ validate(values) {
101
+ const input = values as Record<string, unknown>;
102
+ return { host: String(input.host || current.host) };
103
+ },
104
+ apply(values) {
105
+ current.host = values.host as string;
106
+ },
107
+ baseValues: () => ({ ...current }),
108
+ };
109
+ const service = new ModuleConfigurationService(repository, [definition]);
110
+
111
+ const created = await service.create(
112
+ "mail.runtime",
113
+ { host: "new.smtp.local" },
114
+ "user-1",
115
+ );
116
+
117
+ expect(created).toMatchObject({
118
+ ok: true,
119
+ version: { applyStatus: "pending-restart" },
120
+ });
121
+ expect(current.host).toBe("old.smtp.local");
122
+
123
+ const replacement = await service.create(
124
+ "mail.runtime",
125
+ { host: "corrected.smtp.local" },
126
+ "user-1",
127
+ );
128
+ expect(replacement).toMatchObject({
129
+ ok: true,
130
+ version: { version: 2, applyStatus: "pending-restart" },
131
+ });
132
+ expect((await service.summaries())[0]?.pendingValues).toEqual({
133
+ host: "corrected.smtp.local",
134
+ });
135
+ expect(
136
+ (await service.list("mail.runtime"))?.map((item) => item.applyStatus),
137
+ ).toEqual(["pending-restart", "superseded"]);
138
+
139
+ await new ModuleConfigurationService(repository, [definition]).initialize();
140
+
141
+ expect(current.host).toBe("corrected.smtp.local");
142
+ expect((await repository.latest("mail.runtime"))?.applyStatus).toBe(
143
+ "effective",
144
+ );
145
+ });
146
+
147
+ test("旧版本缺少新增字段时使用当前基线补齐", async () => {
148
+ const current = { enabled: false, timeoutMs: 300 };
149
+ const schema = z.strictObject({
150
+ enabled: z.boolean(),
151
+ timeoutMs: z.number().int(),
152
+ });
153
+ const definition: ManagedModuleConfigDefinition = {
154
+ moduleKey: "compatible.runtime",
155
+ label: "兼容配置",
156
+ fields: [],
157
+ validate(values) {
158
+ return schema.parse({
159
+ ...current,
160
+ ...(values as Record<string, unknown>),
161
+ });
162
+ },
163
+ apply(values) {
164
+ current.enabled = values.enabled as boolean;
165
+ current.timeoutMs = values.timeoutMs as number;
166
+ },
167
+ baseValues: () => ({ ...current }),
168
+ };
169
+ const repository = new InMemoryModuleConfigVersionRepository();
170
+ const version = await repository.begin({
171
+ moduleKey: definition.moduleKey,
172
+ values: { enabled: true },
173
+ diff: [],
174
+ actorId: "legacy",
175
+ });
176
+ await repository.markEffective(version.id, new Date());
177
+ const service = new ModuleConfigurationService(repository, [definition]);
178
+
179
+ await service.initialize();
180
+
181
+ expect(current).toEqual({ enabled: true, timeoutMs: 300 });
182
+ expect((await service.summaries())[0]?.effectiveValues).toEqual(current);
183
+ });
84
184
  });
85
185
 
86
186
  function createFixture(failApply = false) {
@@ -36,14 +36,21 @@ export class ModuleConfigurationService {
36
36
 
37
37
  async initialize(): Promise<void> {
38
38
  for (const definition of this.definitions.values()) {
39
- const effective = await this.repository.effective(definition.moduleKey);
40
- if (!effective) continue;
39
+ const latest = await this.repository.latest(definition.moduleKey);
40
+ const candidate =
41
+ latest?.applyStatus === "pending-restart"
42
+ ? latest
43
+ : await this.repository.effective(definition.moduleKey);
44
+ if (!candidate) continue;
41
45
  try {
42
- const values = definition.validate(effective.values);
46
+ const values = definition.validate(candidate.values);
43
47
  await definition.apply(values);
48
+ if (candidate.applyStatus === "pending-restart") {
49
+ await this.repository.markEffective(candidate.id, new Date());
50
+ }
44
51
  } catch (error) {
45
52
  await this.repository.markFailed(
46
- effective.id,
53
+ candidate.id,
47
54
  sanitizeApplyError(error),
48
55
  new Date(),
49
56
  );
@@ -62,9 +69,13 @@ export class ModuleConfigurationService {
62
69
  moduleKey: definition.moduleKey,
63
70
  label: definition.label,
64
71
  fields: definition.fields.map((field) => ({ ...field })),
65
- effectiveValues: {
66
- ...(effective?.values || definition.baseValues()),
67
- },
72
+ effectiveValues: definition.validate(
73
+ effective?.values || definition.baseValues(),
74
+ ),
75
+ pendingValues:
76
+ latest?.applyStatus === "pending-restart"
77
+ ? definition.validate(latest.values)
78
+ : undefined,
68
79
  effectiveSource: effective ? "version" : "environment",
69
80
  effectiveVersion: effective?.version,
70
81
  latestVersion: latest?.version,
@@ -129,16 +140,33 @@ export class ModuleConfigurationService {
129
140
  reason?: string,
130
141
  rollbackOfVersion?: number,
131
142
  ): Promise<ConfigVersionResult> {
132
- const effective = await this.repository.effective(definition.moduleKey);
133
- const before = effective?.values || definition.baseValues();
143
+ const [effective, latest] = await Promise.all([
144
+ this.repository.effective(definition.moduleKey),
145
+ this.repository.latest(definition.moduleKey),
146
+ ]);
147
+ const before = definition.validate(
148
+ latest?.applyStatus === "pending-restart"
149
+ ? latest.values
150
+ : effective?.values || definition.baseValues(),
151
+ );
152
+ const diff = configDiff(before, values);
134
153
  const version = await this.repository.begin({
135
154
  moduleKey: definition.moduleKey,
136
155
  values,
137
- diff: configDiff(before, values),
156
+ diff,
138
157
  rollbackOfVersion,
139
158
  reason,
140
159
  actorId,
141
160
  });
161
+ if (
162
+ latest?.applyStatus === "pending-restart" ||
163
+ requiresRestart(definition, diff)
164
+ ) {
165
+ return {
166
+ ok: true,
167
+ version: await this.repository.markPendingRestart(version.id),
168
+ };
169
+ }
142
170
  try {
143
171
  await definition.apply(values);
144
172
  return {
@@ -158,6 +186,18 @@ export class ModuleConfigurationService {
158
186
  }
159
187
  }
160
188
 
189
+ function requiresRestart(
190
+ definition: ManagedModuleConfigDefinition,
191
+ diff: ModuleConfigDiffEntry[],
192
+ ): boolean {
193
+ const restartPaths = new Set(
194
+ definition.fields
195
+ .filter((field) => field.applyMode === "restart")
196
+ .map((field) => field.path),
197
+ );
198
+ return diff.some((entry) => restartPaths.has(entry.path));
199
+ }
200
+
161
201
  export function configDiff(
162
202
  before: ModuleConfigValues,
163
203
  after: ModuleConfigValues,
@@ -133,9 +133,10 @@ function createRepositories(pool: Pool) {
133
133
  async function seedDepartments(
134
134
  repository: ReturnType<typeof createRepositories>["departments"],
135
135
  ) {
136
- await repository.create(department("dept_a"), setupActor);
136
+ await repository.create(department("org_root"), setupActor);
137
+ await repository.create(department("dept_a", "org_root"), setupActor);
137
138
  await repository.create(department("dept_a_child", "dept_a"), setupActor);
138
- await repository.create(department("dept_b"), setupActor);
139
+ await repository.create(department("dept_b", "org_root"), setupActor);
139
140
  }
140
141
 
141
142
  async function seedUsers(
@@ -1,6 +1,6 @@
1
1
  import type { GovernedExportQuerySnapshot } from "@southwind-ai/database";
2
2
  import type { AuditEvent } from "@southwind-ai/shared";
3
- import type { RequestGuardContext } from "../../guards.js";
3
+ import { auditActor, type RequestGuardContext } from "../../guards.js";
4
4
  import {
5
5
  DurableArtifactError,
6
6
  type DurableArtifactService,
@@ -310,11 +310,7 @@ function event(
310
310
  id: Bun.randomUUIDv7(),
311
311
  action,
312
312
  outcome: "success",
313
- actor: {
314
- id: context.actor.id,
315
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
316
- name: context.actor.name,
317
- },
313
+ actor: auditActor(context),
318
314
  target: { type: "system.user.governed-export", id },
319
315
  requestId: context.requestId,
320
316
  metadata,
@@ -17,6 +17,7 @@ import { createWorkOrderRepository } from "./work-order/repository.js";
17
17
  import { createDrizzleWorkOrderRepository } from "./work-order/drizzle-repository.js";
18
18
  import { createWorkOrderRouteDefinitions } from "./work-order/routes.js";
19
19
  import { WorkOrderSearchProvider } from "./work-order/search-provider.js";
20
+ import { createWorkOrderOpenSearchIndex } from "./work-order/opensearch-index.js";
20
21
  import { WorkOrderService } from "./work-order/service.js";
21
22
 
22
23
  export interface WorkOrderModuleDependencies {
@@ -42,6 +43,7 @@ export interface WorkOrderModule {
42
43
  */
43
44
  export function createWorkOrderModule(): WorkOrderModule {
44
45
  let service: WorkOrderService | undefined;
46
+ const searchIndex = createWorkOrderOpenSearchIndex(process.env);
45
47
  const requireService = () => {
46
48
  if (!service) throw new Error("示例工单模块尚未挂载");
47
49
  return service;
@@ -52,7 +54,7 @@ export function createWorkOrderModule(): WorkOrderModule {
52
54
  requireService().operationalSnapshot(
53
55
  platformDataActor("task_work_order_snapshot", "工单运营快照任务"),
54
56
  ),
55
- searchProvider: new WorkOrderSearchProvider(requireService),
57
+ searchProvider: new WorkOrderSearchProvider(requireService, searchIndex),
56
58
  });
57
59
  return {
58
60
  ...serverModule,
@@ -67,6 +69,7 @@ export function createWorkOrderModule(): WorkOrderModule {
67
69
  ? createDrizzleWorkOrderRepository(dependencies.db)
68
70
  : createWorkOrderRepository()),
69
71
  dependencies.departments,
72
+ searchIndex,
70
73
  );
71
74
  },
72
75
  };
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  ApiPermissionBinding,
3
+ AuditActor,
3
4
  AuditEvent,
4
5
  EntitlementKey,
5
6
  FeatureFlagDefinition,
@@ -33,6 +34,7 @@ export interface RequestGuardContext {
33
34
  actor: RequestActor;
34
35
  startedAt: string;
35
36
  clientIp?: string;
37
+ userAgent?: string;
36
38
  license?: {
37
39
  versionKey?: LicenseVersionKey;
38
40
  /** V1 兼容字段。新 License 不再包含 Entitlement。 */
@@ -61,6 +63,7 @@ export interface GuardDecision {
61
63
 
62
64
  export interface RequestGuardContextOptions {
63
65
  clientIp?: string;
66
+ userAgent?: string;
64
67
  resolveActor?: (request: Request) => RequestActor | undefined;
65
68
  resolveLicense?: (
66
69
  request: Request,
@@ -86,10 +89,24 @@ export function createRequestGuardContext(
86
89
  actor: options.resolveActor?.(request) || anonymousActor(),
87
90
  startedAt: new Date().toISOString(),
88
91
  clientIp: options.clientIp,
92
+ userAgent: options.userAgent,
89
93
  license: options.resolveLicense?.(request),
90
94
  };
91
95
  }
92
96
 
97
+ export function auditActor(
98
+ context: Pick<RequestGuardContext, "actor" | "clientIp" | "userAgent">,
99
+ ): AuditActor {
100
+ return {
101
+ id: context.actor.id,
102
+ type: context.actor.type === "anonymous" ? "user" : context.actor.type,
103
+ name: context.actor.name,
104
+ roleCodes: context.actor.roleCodes,
105
+ ip: context.clientIp,
106
+ userAgent: context.userAgent,
107
+ };
108
+ }
109
+
93
110
  export function evaluatePermissionGuard(
94
111
  context: RequestGuardContext,
95
112
  binding: PermissionGuardRequirement,
@@ -207,11 +224,7 @@ function deniedDecision(
207
224
  id: randomUUIDv7(),
208
225
  action: "security.denied",
209
226
  outcome: "denied",
210
- actor: {
211
- id: context.actor.id,
212
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
213
- name: context.actor.name,
214
- },
227
+ actor: auditActor(context),
215
228
  target: {
216
229
  type: "guard",
217
230
  id: targetId,
@@ -0,0 +1,85 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ requestSourcePolicyFromEnv,
4
+ resolveRequestSource,
5
+ } from "./request-source.js";
6
+
7
+ describe("平台请求来源信任边界", () => {
8
+ test("默认使用直接对端并忽略客户端伪造的转发头", () => {
9
+ const source = resolveRequestSource(
10
+ request("203.0.113.66"),
11
+ "198.51.100.10",
12
+ requestSourcePolicyFromEnv({}),
13
+ );
14
+
15
+ expect(source).toEqual({
16
+ clientIp: "198.51.100.10",
17
+ userAgent: "Windy-Test/1.0",
18
+ });
19
+ });
20
+
21
+ test("仅显式启用且直接对端受信时采用单一合法转发来源", () => {
22
+ const policy = requestSourcePolicyFromEnv({
23
+ WINDY_TRUST_PROXY: "true",
24
+ WINDY_TRUSTED_PROXY_IPS: "10.0.0.8",
25
+ });
26
+
27
+ expect(
28
+ resolveRequestSource(request("203.0.113.66"), "::1", policy).clientIp,
29
+ ).toBe("203.0.113.66");
30
+ expect(
31
+ resolveRequestSource(request("203.0.113.67"), "10.0.0.8", policy)
32
+ .clientIp,
33
+ ).toBe("203.0.113.67");
34
+ expect(
35
+ resolveRequestSource(request("203.0.113.68"), "10.0.0.9", policy)
36
+ .clientIp,
37
+ ).toBe("10.0.0.9");
38
+ });
39
+
40
+ test("多段或非法转发来源退回直接对端", () => {
41
+ const policy = requestSourcePolicyFromEnv({
42
+ WINDY_TRUST_PROXY: "true",
43
+ });
44
+ for (const forwardedFor of [
45
+ "203.0.113.66, 198.51.100.9",
46
+ "not-an-ip",
47
+ "203.0.113.66:443",
48
+ ]) {
49
+ expect(
50
+ resolveRequestSource(request(forwardedFor), "127.0.0.1", policy)
51
+ .clientIp,
52
+ ).toBe("127.0.0.1");
53
+ }
54
+ });
55
+
56
+ test("代理配置默认关闭且错误配置 fail closed", () => {
57
+ expect(requestSourcePolicyFromEnv({})).toEqual({
58
+ trustProxy: false,
59
+ trustedProxyIps: [],
60
+ });
61
+ expect(() =>
62
+ requestSourcePolicyFromEnv({
63
+ WINDY_TRUSTED_PROXY_IPS: "10.0.0.8",
64
+ }),
65
+ ).toThrow("WINDY_TRUST_PROXY=true");
66
+ expect(() =>
67
+ requestSourcePolicyFromEnv({ WINDY_TRUST_PROXY: "yes" }),
68
+ ).toThrow("只允许 true 或 false");
69
+ expect(() =>
70
+ requestSourcePolicyFromEnv({
71
+ WINDY_TRUST_PROXY: "true",
72
+ WINDY_TRUSTED_PROXY_IPS: "10.0.0.999",
73
+ }),
74
+ ).toThrow("无效 IP");
75
+ });
76
+ });
77
+
78
+ function request(forwardedFor: string): Request {
79
+ return new Request("https://windy.example.test/api/system/users", {
80
+ headers: {
81
+ "user-agent": "Windy-Test/1.0",
82
+ "x-forwarded-for": forwardedFor,
83
+ },
84
+ });
85
+ }
@@ -0,0 +1,90 @@
1
+ import { isIP } from "node:net";
2
+
3
+ export interface RequestSourcePolicy {
4
+ trustProxy: boolean;
5
+ trustedProxyIps: readonly string[];
6
+ }
7
+
8
+ export interface RequestSource {
9
+ clientIp?: string;
10
+ userAgent?: string;
11
+ }
12
+
13
+ export function requestSourcePolicyFromEnv(
14
+ env: Record<string, string | undefined>,
15
+ ): RequestSourcePolicy {
16
+ const trustProxy = readBoolean(env.WINDY_TRUST_PROXY);
17
+ const configuredIps = (env.WINDY_TRUSTED_PROXY_IPS || "")
18
+ .split(",")
19
+ .map((value) => value.trim())
20
+ .filter(Boolean);
21
+ if (!trustProxy && configuredIps.length) {
22
+ throw new Error(
23
+ "配置 WINDY_TRUSTED_PROXY_IPS 时必须显式设置 WINDY_TRUST_PROXY=true",
24
+ );
25
+ }
26
+ return {
27
+ trustProxy,
28
+ trustedProxyIps: configuredIps.map((value) => {
29
+ const normalized = normalizeIp(value);
30
+ if (!normalized) {
31
+ throw new Error(`WINDY_TRUSTED_PROXY_IPS 包含无效 IP:${value}`);
32
+ }
33
+ return normalized;
34
+ }),
35
+ };
36
+ }
37
+
38
+ export function resolveRequestSource(
39
+ request: Request,
40
+ peerIp: string | undefined,
41
+ policy: RequestSourcePolicy,
42
+ ): RequestSource {
43
+ const normalizedPeerIp = normalizeIp(peerIp);
44
+ const forwardedIp = resolveForwardedIp(request, normalizedPeerIp, policy);
45
+ return {
46
+ clientIp: forwardedIp || normalizedPeerIp,
47
+ userAgent: normalizeUserAgent(request.headers.get("user-agent")),
48
+ };
49
+ }
50
+
51
+ function resolveForwardedIp(
52
+ request: Request,
53
+ peerIp: string | undefined,
54
+ policy: RequestSourcePolicy,
55
+ ): string | undefined {
56
+ if (!policy.trustProxy || !peerIp || !isTrustedProxy(peerIp, policy)) {
57
+ return undefined;
58
+ }
59
+ const header = request.headers.get("x-forwarded-for")?.trim();
60
+ if (!header || header.includes(",")) return undefined;
61
+ return normalizeIp(header);
62
+ }
63
+
64
+ function isTrustedProxy(peerIp: string, policy: RequestSourcePolicy): boolean {
65
+ if (isLoopback(peerIp)) return true;
66
+ return policy.trustedProxyIps.some((value) => normalizeIp(value) === peerIp);
67
+ }
68
+
69
+ function isLoopback(ip: string): boolean {
70
+ return ip === "::1" || (isIP(ip) === 4 && ip.startsWith("127."));
71
+ }
72
+
73
+ function normalizeIp(value: string | undefined): string | undefined {
74
+ const candidate = value?.trim().toLowerCase();
75
+ if (!candidate) return undefined;
76
+ const mapped = candidate.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
77
+ const normalized = mapped && isIP(mapped) === 4 ? mapped : candidate;
78
+ return isIP(normalized) ? normalized : undefined;
79
+ }
80
+
81
+ function normalizeUserAgent(value: string | null): string | undefined {
82
+ const normalized = value?.trim();
83
+ return normalized ? normalized.slice(0, 1_024) : undefined;
84
+ }
85
+
86
+ function readBoolean(value: string | undefined): boolean {
87
+ if (value === undefined || value === "" || value === "false") return false;
88
+ if (value === "true") return true;
89
+ throw new Error("WINDY_TRUST_PROXY 只允许 true 或 false");
90
+ }
@@ -141,7 +141,11 @@ platformLicenseRuntime.setAuditSink(runtime.auditSink);
141
141
  // @windy-module system.license end
142
142
  const { authRepository, authService } = createAuthRuntime({
143
143
  repository: persistence?.authRepository,
144
- sessionTtlSeconds: platformConfig.security.sessionTtlSeconds,
144
+ sessionTtlSeconds: () => platformConfig.security.sessionTtlSeconds,
145
+ passwordPolicy: () => ({
146
+ minLength: platformConfig.security.passwordMinLength,
147
+ maxLength: platformConfig.security.passwordMaxLength,
148
+ }),
145
149
  auditWriter: (event) => runtime.auditSink.recordCriticalEvent(event),
146
150
  // @windy-module system.audit begin
147
151
  auditReader: persistence?.auditReader,
@@ -153,12 +157,17 @@ const storageRuntime = createStorageRuntime(
153
157
  persistence?.db,
154
158
  moduleHosts.uploadPolicies,
155
159
  );
160
+ configurationRuntime.bindStorage(storageRuntime.uploads);
156
161
  // @windy-module system.storage end
157
162
  // @windy-module system.settings begin
158
- const platformSettings = createPlatformSettingsRuntime(persistence?.db, {
159
- softwareName: platformConfig.name,
160
- logoUrl: platformConfig.logoUrl,
161
- });
163
+ const platformSettings = createPlatformSettingsRuntime(
164
+ persistence?.db,
165
+ {
166
+ softwareName: platformConfig.name,
167
+ logoUrl: platformConfig.logoUrl,
168
+ },
169
+ () => ({ timeZone: platformConfig.regional.timeZone }),
170
+ );
162
171
  // @windy-module system.settings end
163
172
  // @windy-module system.search begin
164
173
  const searchHost = createSearchHost(
@@ -177,6 +186,7 @@ const {
177
186
  scheduler,
178
187
  taskDefinitions: scheduledTaskDefinitions,
179
188
  notifications: notificationRepository,
189
+ notificationService,
180
190
  workflow: workflowService,
181
191
  governedExportService,
182
192
  durable: durableJobRuntime,
@@ -188,6 +198,7 @@ const {
188
198
  storage: storageRuntime,
189
199
  moduleTasks: moduleTaskDefinitions,
190
200
  bulkRetentionDays: readPositiveInt(process.env.BULK_DATA_RETENTION_DAYS, 7),
201
+ mailConfig: platformConfig.mail,
191
202
  installModuleDurableHandlers: moduleHosts.installDurableHandlers,
192
203
  });
193
204
  const schedulerWorker = new SchedulerWorker(
@@ -220,7 +231,13 @@ registerPlatformLicenseRoutes(app, platformLicenseRuntime);
220
231
  platformSettings.register(app, runtime, features);
221
232
  // @windy-module system.settings end
222
233
  // @windy-module system.notification begin
223
- registerNotificationRoutes(app, notificationRepository, runtime, features);
234
+ registerNotificationRoutes(
235
+ app,
236
+ notificationRepository,
237
+ runtime,
238
+ features,
239
+ notificationService,
240
+ );
224
241
  // @windy-module system.notification end
225
242
  // @windy-module system.workflow begin
226
243
  registerWorkflowRoutes(app, workflowService, runtime);
@@ -52,6 +52,8 @@ export function registerPlatformLicenseRoutes(
52
52
  const installed = await licenseRuntime.install(rawLicense, {
53
53
  actor: context.guardContext.actor,
54
54
  requestId: context.guardContext.requestId,
55
+ clientIp: context.guardContext.clientIp,
56
+ userAgent: context.guardContext.userAgent,
55
57
  });
56
58
  if (installed.result.status === "rejected") {
57
59
  return Response.json(
@@ -17,7 +17,11 @@ import type {
17
17
  LicenseFailureReason,
18
18
  SignedLicense,
19
19
  } from "@southwind-ai/shared";
20
- import type { RequestActor } from "../guards.js";
20
+ import {
21
+ auditActor,
22
+ type RequestActor,
23
+ type RequestGuardContext,
24
+ } from "../guards.js";
21
25
  import type { InMemoryAuditSink } from "../runtime.js";
22
26
  import type {
23
27
  LicenseEntitlementResolver,
@@ -57,10 +61,10 @@ export interface PlatformLicenseRuntimeOptions {
57
61
  now?: () => Date;
58
62
  }
59
63
 
60
- export interface LicenseInstallActorContext {
61
- actor: RequestActor;
62
- requestId: string;
63
- }
64
+ export type LicenseInstallActorContext = Pick<
65
+ RequestGuardContext,
66
+ "actor" | "requestId" | "clientIp" | "userAgent"
67
+ >;
64
68
 
65
69
  export class PlatformLicenseRuntime implements LicenseEntitlementResolver {
66
70
  private readonly stateStorage: LicenseStorage;
@@ -285,12 +289,7 @@ function installAuditEvent(
285
289
  id: crypto.randomUUID(),
286
290
  action: "license.update",
287
291
  outcome: success ? "success" : "failure",
288
- actor: {
289
- id: context.actor.id,
290
- type: "user",
291
- name: context.actor.name,
292
- roleCodes: context.actor.roleCodes,
293
- },
292
+ actor: auditActor(context),
294
293
  target: {
295
294
  type: "runtime-license",
296
295
  id: result.license?.claims.licenseId || candidate?.claims.licenseId,
@@ -17,7 +17,7 @@ import {
17
17
  } from "./module-host.js";
18
18
  import type { ServerPersistence } from "./persistence.js";
19
19
  // @windy-module system.storage begin
20
- import { platformUploadPolicies } from "./storage/runtime.js";
20
+ import { createPlatformUploadPolicies } from "./storage/runtime.js";
21
21
  // @windy-module system.storage end
22
22
  import {
23
23
  createSystemRepositories,
@@ -51,7 +51,7 @@ export async function bootstrapServerModules(
51
51
  installations: installedBusinessModules,
52
52
  baseUploadPolicies: [
53
53
  // @windy-module system.storage begin
54
- ...platformUploadPolicies,
54
+ ...createPlatformUploadPolicies(platformConfig),
55
55
  // @windy-module system.storage end
56
56
  ],
57
57
  });
@@ -15,7 +15,11 @@ import {
15
15
  } from "@southwind-ai/server-sdk";
16
16
  import type { AuditEvent } from "@southwind-ai/shared";
17
17
  import type { ModuleManifest } from "@southwind-ai/modules";
18
- import type { RequestActor, RequestGuardContext } from "../guards.js";
18
+ import {
19
+ auditActor,
20
+ type RequestActor,
21
+ type RequestGuardContext,
22
+ } from "../guards.js";
19
23
  import type { createServerRuntime } from "../runtime.js";
20
24
  import { unavailableModuleStoragePort } from "./storage-port.js";
21
25
 
@@ -130,11 +134,7 @@ function buildAuditEvent(
130
134
  id: randomUUIDv7(),
131
135
  action,
132
136
  outcome: "success",
133
- actor: {
134
- id: context.actor.id,
135
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
136
- name: context.actor.name,
137
- },
137
+ actor: auditActor(context),
138
138
  target: input.target,
139
139
  requestId: context.requestId,
140
140
  metadata: input.metadata,