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
@@ -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
+ });
@@ -0,0 +1,145 @@
1
+ export interface OpenSearchClientConfig {
2
+ url: string;
3
+ username?: string;
4
+ password?: string;
5
+ requestTimeoutMs: number;
6
+ }
7
+
8
+ export interface OpenSearchHealth {
9
+ status: "green" | "yellow" | "red";
10
+ }
11
+
12
+ export type OpenSearchFetcher = (
13
+ input: string | URL | Request,
14
+ init?: RequestInit,
15
+ ) => Promise<Response>;
16
+
17
+ export class OpenSearchClient {
18
+ private readonly baseUrl: URL;
19
+ private readonly authorization?: string;
20
+
21
+ constructor(
22
+ private readonly config: OpenSearchClientConfig,
23
+ private readonly fetcher: OpenSearchFetcher = fetch,
24
+ ) {
25
+ this.baseUrl = parseBaseUrl(config.url);
26
+ if (Boolean(config.username) !== Boolean(config.password)) {
27
+ throw new Error(
28
+ "OPENSEARCH_USERNAME 与 OPENSEARCH_PASSWORD 必须同时配置",
29
+ );
30
+ }
31
+ this.authorization = config.username
32
+ ? `Basic ${btoa(`${config.username}:${config.password}`)}`
33
+ : undefined;
34
+ }
35
+
36
+ async request<T>(
37
+ method: string,
38
+ path: string,
39
+ body: unknown,
40
+ signal?: AbortSignal,
41
+ contentType = "application/json",
42
+ ): Promise<T> {
43
+ const controller = new AbortController();
44
+ const abort = () => controller.abort();
45
+ signal?.addEventListener("abort", abort, { once: true });
46
+ const timer = setTimeout(
47
+ () => controller.abort(),
48
+ this.config.requestTimeoutMs,
49
+ );
50
+ try {
51
+ const headers = new Headers({ accept: "application/json" });
52
+ if (body !== undefined) headers.set("content-type", contentType);
53
+ if (this.authorization) headers.set("authorization", this.authorization);
54
+ const response = await this.fetcher(new URL(path, this.baseUrl), {
55
+ method,
56
+ headers,
57
+ body:
58
+ body === undefined
59
+ ? undefined
60
+ : typeof body === "string"
61
+ ? body
62
+ : JSON.stringify(body),
63
+ signal: controller.signal,
64
+ });
65
+ if (!response.ok) {
66
+ throw new OpenSearchRequestError(response.status);
67
+ }
68
+ if (response.status === 204 || method === "HEAD") return undefined as T;
69
+ return response.json() as Promise<T>;
70
+ } finally {
71
+ clearTimeout(timer);
72
+ signal?.removeEventListener("abort", abort);
73
+ }
74
+ }
75
+
76
+ health(signal?: AbortSignal) {
77
+ return this.request<OpenSearchHealth>(
78
+ "GET",
79
+ "/_cluster/health?local=true",
80
+ undefined,
81
+ signal,
82
+ );
83
+ }
84
+ }
85
+
86
+ export class OpenSearchRequestError extends Error {
87
+ constructor(readonly status: number) {
88
+ super(`OpenSearch 请求失败(HTTP ${status})`);
89
+ this.name = "OpenSearchRequestError";
90
+ }
91
+ }
92
+
93
+ export function readOpenSearchConfig(
94
+ environment: Record<string, string | undefined>,
95
+ ): OpenSearchClientConfig | undefined {
96
+ const url = environment.OPENSEARCH_URL?.trim();
97
+ if (!url) return undefined;
98
+ return {
99
+ url,
100
+ username: optional(environment.OPENSEARCH_USERNAME),
101
+ password: optional(environment.OPENSEARCH_PASSWORD),
102
+ requestTimeoutMs: boundedInteger(
103
+ "OPENSEARCH_REQUEST_TIMEOUT_MS",
104
+ environment.OPENSEARCH_REQUEST_TIMEOUT_MS,
105
+ 100,
106
+ 30_000,
107
+ 3_000,
108
+ ),
109
+ };
110
+ }
111
+
112
+ function parseBaseUrl(value: string): URL {
113
+ const url = new URL(value);
114
+ if (
115
+ !["http:", "https:"].includes(url.protocol) ||
116
+ url.username ||
117
+ url.password ||
118
+ (url.pathname !== "/" && url.pathname !== "") ||
119
+ url.search ||
120
+ url.hash
121
+ ) {
122
+ throw new Error("OPENSEARCH_URL 必须是不含凭据、路径和参数的 HTTP(S) 地址");
123
+ }
124
+ return url;
125
+ }
126
+
127
+ function optional(value: string | undefined): string | undefined {
128
+ return value?.trim() || undefined;
129
+ }
130
+
131
+ function boundedInteger(
132
+ name: string,
133
+ value: string | undefined,
134
+ minimum: number,
135
+ maximum: number,
136
+ fallback: number,
137
+ ): number {
138
+ if (value === undefined) return fallback;
139
+ if (!/^\d+$/.test(value)) throw new Error(`${name} 必须是整数`);
140
+ const parsed = Number(value);
141
+ if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
142
+ throw new Error(`${name} 必须位于 ${minimum}-${maximum}`);
143
+ }
144
+ return parsed;
145
+ }
@@ -3,7 +3,7 @@ import type { ModuleManifest } from "@southwind-ai/modules";
3
3
  import type { AuditEvent } from "@southwind-ai/shared";
4
4
  import { evaluateAccessGuards } from "../access-guards.js";
5
5
  import { isServerScopeContext } from "../data-access/context.js";
6
- import type { RequestGuardContext } from "../guards.js";
6
+ import { auditActor, type RequestGuardContext } from "../guards.js";
7
7
  import type { createServerRuntime } from "../runtime.js";
8
8
  import { decodeSearchCursor, encodeSearchCursor } from "./cursor.js";
9
9
  import { SearchProtocolError } from "./errors.js";
@@ -232,11 +232,7 @@ function searchEvent(
232
232
  id: randomUUIDv7(),
233
233
  action,
234
234
  outcome: "success",
235
- actor: {
236
- id: context.actor.id,
237
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
238
- name: context.actor.name,
239
- },
235
+ actor: auditActor(context),
240
236
  target: { type: "search-provider" },
241
237
  requestId: context.requestId,
242
238
  metadata,
@@ -22,15 +22,27 @@ export function registerBrandingSettingsRoutes(
22
22
  repository: BrandingSettingsRepository,
23
23
  runtime: ServerRuntime,
24
24
  features: FeatureFlagDefinition[],
25
+ runtimeSettings: () => Pick<
26
+ PlatformBrandingSettings,
27
+ "timeZone"
28
+ > = () => ({}),
25
29
  ): void {
26
30
  const feature = findFeature(features, "system.settings");
27
31
 
28
- app.get("/platform/branding", () => repository.get());
32
+ app.get("/platform/branding", async () => ({
33
+ ...(await repository.get()),
34
+ ...runtimeSettings(),
35
+ }));
29
36
 
30
37
  app.get("/system/settings/branding", async (rawContext) => {
31
38
  const { guardContext } = routeContext(rawContext);
32
39
  const denied = await guard(runtime, guardContext, READ_BINDING, feature);
33
- return denied || repository.get();
40
+ return (
41
+ denied || {
42
+ ...(await repository.get()),
43
+ ...runtimeSettings(),
44
+ }
45
+ );
34
46
  });
35
47
 
36
48
  app.put("/system/settings/branding", async (rawContext) => {
@@ -61,7 +73,7 @@ export function registerBrandingSettingsRoutes(
61
73
  { changedFields: changedFields(parsed.value) },
62
74
  ),
63
75
  );
64
- return updated;
76
+ return { ...updated, ...runtimeSettings() };
65
77
  });
66
78
  }
67
79
 
@@ -16,6 +16,9 @@ import { registerWatermarkPolicyRoutes } from "./watermark-routes.js";
16
16
  export function createPlatformSettingsRuntime(
17
17
  db: NodePgDatabase | undefined,
18
18
  defaults: PlatformBrandingSettings,
19
+ runtimeSettings: () => Pick<PlatformBrandingSettings, "timeZone"> = () => ({
20
+ timeZone: defaults.timeZone,
21
+ }),
19
22
  ) {
20
23
  const branding = db
21
24
  ? new DrizzleBrandingSettingsRepository(db, defaults)
@@ -33,7 +36,13 @@ export function createPlatformSettingsRuntime(
33
36
  server: ReturnType<typeof createServerRuntime>,
34
37
  features: FeatureFlagDefinition[],
35
38
  ) {
36
- registerBrandingSettingsRoutes(app, branding, server, features);
39
+ registerBrandingSettingsRoutes(
40
+ app,
41
+ branding,
42
+ server,
43
+ features,
44
+ runtimeSettings,
45
+ );
37
46
  registerWatermarkPolicyRoutes(app, watermark, server, features);
38
47
  },
39
48
  };
@@ -1,4 +1,5 @@
1
1
  import { resolve } from "node:path";
2
+ import type { PlatformConfig } from "@southwind-ai/config";
2
3
  import {
3
4
  FileService,
4
5
  InMemoryUploadRepository,
@@ -47,6 +48,18 @@ export const platformUploadPolicies: UploadPolicy[] = [
47
48
  },
48
49
  ];
49
50
 
51
+ export function createPlatformUploadPolicies(
52
+ config: Pick<PlatformConfig, "storage">,
53
+ ): UploadPolicy[] {
54
+ return platformUploadPolicies.map((policy) => ({
55
+ ...policy,
56
+ maxByteSize:
57
+ policy.purpose === "bulk-data"
58
+ ? config.storage.bulkDataMaxByteSize
59
+ : config.storage.brandingLogoMaxByteSize,
60
+ }));
61
+ }
62
+
50
63
  export function createStorageRuntime(
51
64
  env: Record<string, string | undefined>,
52
65
  db?: NodePgDatabase,
@@ -1,9 +1,21 @@
1
- export function validateAuditTimeRange(
2
- query: Record<string, string | undefined>,
3
- ) {
1
+ const ISO_TIME =
2
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/;
3
+ export const MAX_AUDIT_QUERY_LENGTH = 120;
4
+
5
+ export function validateAuditQuery(query: Record<string, string | undefined>) {
6
+ const normalizedQuery = normalizeAuditKeyword(query.q);
7
+ if (normalizedQuery && normalizedQuery.length > MAX_AUDIT_QUERY_LENGTH) {
8
+ return Response.json(
9
+ {
10
+ error: "INVALID_AUDIT_QUERY",
11
+ message: `q 最多 ${MAX_AUDIT_QUERY_LENGTH} 个字符。`,
12
+ },
13
+ { status: 400 },
14
+ );
15
+ }
4
16
  for (const key of ["occurredAfter", "occurredBefore"] as const) {
5
17
  const value = query[key];
6
- if (value && Number.isNaN(Date.parse(value))) {
18
+ if (value && (!ISO_TIME.test(value) || Number.isNaN(Date.parse(value)))) {
7
19
  return Response.json(
8
20
  {
9
21
  error: "INVALID_AUDIT_TIME_RANGE",
@@ -13,5 +25,22 @@ export function validateAuditTimeRange(
13
25
  );
14
26
  }
15
27
  }
28
+ if (
29
+ query.occurredAfter &&
30
+ query.occurredBefore &&
31
+ Date.parse(query.occurredAfter) > Date.parse(query.occurredBefore)
32
+ ) {
33
+ return Response.json(
34
+ {
35
+ error: "INVALID_AUDIT_TIME_RANGE",
36
+ message: "occurredAfter 不能晚于 occurredBefore。",
37
+ },
38
+ { status: 400 },
39
+ );
40
+ }
16
41
  return undefined;
17
42
  }
43
+
44
+ export function normalizeAuditKeyword(value: string | undefined) {
45
+ return value?.normalize("NFKC").trim().replace(/\s+/g, " ") || undefined;
46
+ }
@@ -1,9 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { createFoundationSnapshot } from "../foundation.js";
3
- import {
4
- createServerRuntime,
5
- type AuditEventListQuery,
6
- } from "../runtime.js";
3
+ import { createServerRuntime, type AuditEventListQuery } from "../runtime.js";
7
4
  import { FakeRouteApp } from "./route-test-app.js";
8
5
  import { registerSystemRoutes } from "./routes.js";
9
6
  import { createSystemRepositories } from "./seed.js";
@@ -73,9 +70,11 @@ describe("audit routes", () => {
73
70
  params: {},
74
71
  query: {
75
72
  pageSize: "5",
73
+ q: " 审计员 ",
76
74
  actorId: "user_1",
77
75
  resource: "users",
78
76
  occurredAfter: "2026-07-01T00:00:00.000Z",
77
+ occurredBefore: "2026-07-31T23:59:59.999Z",
79
78
  },
80
79
  })) as { total: number; pageSize: number; items: Array<{ id: string }> };
81
80
 
@@ -83,11 +82,66 @@ describe("audit routes", () => {
83
82
  expect(result.pageSize).toBe(5);
84
83
  expect(result.items[0]?.id).toBe("audit_from_db");
85
84
  expect(receivedAuditQuery).toMatchObject({
85
+ q: "审计员",
86
86
  occurredAfter: "2026-07-01T00:00:00.000Z",
87
+ occurredBefore: "2026-07-31T23:59:59.999Z",
87
88
  includeScheduler: false,
88
89
  });
89
90
  });
90
91
 
92
+ test("列表与导出原样使用关键字、操作者、动作和分钟范围", async () => {
93
+ const received: AuditEventListQuery[] = [];
94
+ const runtime = createServerRuntime(
95
+ { WINDY_DEV_ADMIN_TOKEN: "token_1" },
96
+ {
97
+ auditReader: {
98
+ async list(query) {
99
+ received.push(query);
100
+ return {
101
+ items: [],
102
+ total: 0,
103
+ page: query.page,
104
+ pageSize: query.pageSize,
105
+ };
106
+ },
107
+ },
108
+ },
109
+ );
110
+ const app = new FakeRouteApp();
111
+ registerSystemRoutes(
112
+ app,
113
+ createSystemRepositories(createFoundationSnapshot()),
114
+ runtime,
115
+ );
116
+ const guardContext = await runtime.createGuardContext(
117
+ new Request("http://localhost/api/system/audit-logs", {
118
+ headers: { authorization: "Bearer token_1" },
119
+ }),
120
+ );
121
+ const filters = {
122
+ q: "案件甲",
123
+ actorId: "user_1",
124
+ action: "case.review",
125
+ occurredAfter: "2026-07-20T01:15:00.000Z",
126
+ occurredBefore: "2026-07-20T01:20:59.999Z",
127
+ };
128
+
129
+ await app.invoke("GET", "/system/audit-logs", {
130
+ guardContext,
131
+ params: {},
132
+ query: filters,
133
+ });
134
+ await app.invoke("GET", "/system/audit-logs/export", {
135
+ guardContext,
136
+ params: {},
137
+ query: { ...filters, includeScheduler: "true" },
138
+ });
139
+
140
+ expect(received).toHaveLength(2);
141
+ expect(received[0]).toMatchObject({ ...filters, includeScheduler: false });
142
+ expect(received[1]).toMatchObject({ ...filters, includeScheduler: true });
143
+ });
144
+
91
145
  test("列表固定排除调度器,导出可显式包含", async () => {
92
146
  const { app, runtime, guardContext } = await auditFixture();
93
147
  const event = {
@@ -5,7 +5,7 @@ import { parsePagination } from "../pagination.js";
5
5
  import { resourceEvent, routeContext } from "./route-helpers.js";
6
6
  import type { RouteApp } from "./route-types.js";
7
7
  import { withPermission } from "./resource-route-support.js";
8
- import { validateAuditTimeRange } from "./audit-query.js";
8
+ import { normalizeAuditKeyword, validateAuditQuery } from "./audit-query.js";
9
9
 
10
10
  type ServerRuntime = ReturnType<typeof createServerRuntime>;
11
11
 
@@ -27,8 +27,8 @@ export function registerAuditRoutes(
27
27
  async () => {
28
28
  const pagination = parsePagination(query);
29
29
  if (pagination instanceof Response) return pagination;
30
- const invalidTimeRange = validateAuditTimeRange(query);
31
- if (invalidTimeRange) return invalidTimeRange;
30
+ const invalidQuery = validateAuditQuery(query);
31
+ if (invalidQuery) return invalidQuery;
32
32
  return runtime.listAuditEvents({
33
33
  ...pagination,
34
34
  ...auditFilters(query),
@@ -48,8 +48,8 @@ export function registerAuditRoutes(
48
48
  "read",
49
49
  feature,
50
50
  async () => {
51
- const invalidTimeRange = validateAuditTimeRange(query);
52
- if (invalidTimeRange) return invalidTimeRange;
51
+ const invalidQuery = validateAuditQuery(query);
52
+ if (invalidQuery) return invalidQuery;
53
53
  const includeScheduler = query.includeScheduler === "true";
54
54
  const items = await listAll(runtime, {
55
55
  ...auditFilters(query),
@@ -82,6 +82,7 @@ export function registerAuditRoutes(
82
82
 
83
83
  function auditFilters(query: Record<string, string | undefined>) {
84
84
  return {
85
+ q: normalizeAuditKeyword(query.q),
85
86
  actorId: query.actorId,
86
87
  action: query.action,
87
88
  resource: query.resource,