create-windy 0.3.19 → 0.4.1

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 (234) hide show
  1. package/README.md +28 -13
  2. package/dist/cli.js +788 -280
  3. package/package.json +1 -1
  4. package/template/.agents/skills/windy-business-module/SKILL.md +34 -10
  5. package/template/.windy-template.json +2 -2
  6. package/template/AGENTS.md +18 -8
  7. package/template/README.md +32 -22
  8. package/template/apps/server/Dockerfile +1 -0
  9. package/template/apps/server/package.json +1 -0
  10. package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
  11. package/template/apps/server/scripts/validate-module-composition.ts +28 -0
  12. package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
  13. package/template/apps/server/src/agent/routes.test.ts +20 -7
  14. package/template/apps/server/src/agent/routes.ts +14 -0
  15. package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
  16. package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
  17. package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
  18. package/template/apps/server/src/app/server-composition.ts +56 -0
  19. package/template/apps/server/src/application-services.ts +2 -0
  20. package/template/apps/server/src/bulk-data/service.ts +14 -0
  21. package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
  22. package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
  23. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
  24. package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
  25. package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
  26. package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
  27. package/template/apps/server/src/feature/routes.ts +33 -15
  28. package/template/apps/server/src/foundation-activation.test.ts +60 -0
  29. package/template/apps/server/src/foundation.ts +64 -5
  30. package/template/apps/server/src/guards.test.ts +23 -0
  31. package/template/apps/server/src/guards.ts +12 -0
  32. package/template/apps/server/src/index.ts +28 -34
  33. package/template/apps/server/src/installed-business-modules.ts +13 -24
  34. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
  35. package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
  36. package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
  37. package/template/apps/server/src/license/activation.test.ts +30 -0
  38. package/template/apps/server/src/license/activation.ts +26 -0
  39. package/template/apps/server/src/license/restriction.test.ts +16 -0
  40. package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
  41. package/template/apps/server/src/license/routes.ts +20 -4
  42. package/template/apps/server/src/license/runtime-service.test.ts +10 -2
  43. package/template/apps/server/src/module-bootstrap.ts +14 -23
  44. package/template/apps/server/src/module-composition.test.ts +43 -2
  45. package/template/apps/server/src/module-composition.ts +29 -0
  46. package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
  47. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
  48. package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
  49. package/template/apps/server/src/module-host/durable-context.ts +42 -2
  50. package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
  51. package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
  52. package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
  53. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  54. package/template/apps/server/src/module-host/initialize.test.ts +78 -2
  55. package/template/apps/server/src/module-host/initialize.ts +15 -4
  56. package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
  57. package/template/apps/server/src/module-host/project-attachments.ts +58 -0
  58. package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
  59. package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
  60. package/template/apps/server/src/module-host/project-composition.ts +311 -0
  61. package/template/apps/server/src/module-host/request-context.ts +3 -0
  62. package/template/apps/server/src/module-host/route-declaration.ts +15 -6
  63. package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
  64. package/template/apps/server/src/module-host/route-installer.ts +12 -2
  65. package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
  66. package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
  67. package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
  68. package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
  69. package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
  70. package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
  71. package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
  72. package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
  73. package/template/apps/server/src/module-host/task-definitions.ts +1 -0
  74. package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
  75. package/template/apps/server/src/module-host.test.ts +2 -0
  76. package/template/apps/server/src/module-preflight.test.ts +289 -0
  77. package/template/apps/server/src/module-preflight.ts +96 -0
  78. package/template/apps/server/src/module-runtime-validation.ts +1 -0
  79. package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
  80. package/template/apps/server/src/platform-http-runtime.ts +32 -7
  81. package/template/apps/server/src/route-guards.test.ts +22 -0
  82. package/template/apps/server/src/route-guards.ts +10 -3
  83. package/template/apps/server/src/runtime-audit-query.ts +50 -0
  84. package/template/apps/server/src/runtime-feature.test.ts +46 -0
  85. package/template/apps/server/src/runtime-feature.ts +5 -0
  86. package/template/apps/server/src/runtime.ts +10 -49
  87. package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
  88. package/template/apps/server/src/scheduler/audit.ts +1 -1
  89. package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
  90. package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
  91. package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
  92. package/template/apps/server/src/scheduler/service.test.ts +1 -0
  93. package/template/apps/server/src/scheduler/service.ts +18 -21
  94. package/template/apps/server/src/scheduler/types.ts +5 -1
  95. package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
  96. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
  97. package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
  98. package/template/apps/server/src/settings/watermark-routes.ts +29 -1
  99. package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
  100. package/template/apps/server/src/startup-composition-validation.ts +54 -0
  101. package/template/apps/server/src/startup-preflight.test.ts +36 -0
  102. package/template/apps/server/src/startup-preflight.ts +37 -0
  103. package/template/apps/server/src/system/audit-routes.test.ts +9 -3
  104. package/template/apps/server/src/system/department-routes.test.ts +5 -3
  105. package/template/apps/server/src/system/feature-routes.test.ts +4 -3
  106. package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
  107. package/template/apps/server/src/system/interface-catalog.ts +3 -0
  108. package/template/apps/server/src/system/menu-routes.test.ts +5 -3
  109. package/template/apps/server/src/system/route-test-runtime.ts +24 -0
  110. package/template/apps/server/src/system/routes-validation.test.ts +7 -3
  111. package/template/apps/server/src/system/routes.test.ts +21 -11
  112. package/template/apps/server/src/system/routes.ts +11 -2
  113. package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
  114. package/template/apps/server/src/work-order/routes.test.ts +8 -0
  115. package/template/apps/server/src/work-order/task.test.ts +6 -1
  116. package/template/apps/web/Dockerfile +5 -0
  117. package/template/apps/web/package.json +1 -0
  118. package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
  119. package/template/apps/web/src/app/web-composition.ts +24 -0
  120. package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
  121. package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
  122. package/template/apps/web/src/composables/watermark-settings.ts +40 -11
  123. package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
  124. package/template/apps/web/src/layout/AppSidebar.vue +8 -1
  125. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
  126. package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
  127. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
  128. package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
  129. package/template/apps/web/src/layout/navigation.ts +29 -4
  130. package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
  131. package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
  132. package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
  133. package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
  134. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
  135. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
  136. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
  137. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
  138. package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
  139. package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
  140. package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
  141. package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
  142. package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
  143. package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
  144. package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
  145. package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
  146. package/template/apps/web/src/router/business-routes.ts +32 -45
  147. package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
  148. package/template/apps/web/src/router/index.ts +64 -54
  149. package/template/apps/web/src/router/manifest-routes.ts +27 -7
  150. package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
  151. package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
  152. package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
  153. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
  154. package/template/apps/web/src/router/web-composition-host.ts +20 -0
  155. package/template/apps/web/src/services/work-order-api.ts +16 -0
  156. package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
  157. package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
  158. package/template/apps/web/vite.config.ts +22 -1
  159. package/template/docs/architecture/migration-bundle.md +10 -7
  160. package/template/docs/architecture/module-manifest-composition.md +85 -3
  161. package/template/docs/architecture/search-provider-protocol.md +6 -1
  162. package/template/docs/architecture/server-module-interface.md +70 -7
  163. package/template/docs/development/business-web-routes.md +53 -50
  164. package/template/docs/development/module-lifecycle.md +48 -54
  165. package/template/docs/platform/navigation-registry.md +5 -4
  166. package/template/docs/platform/platform-shell-settings.md +15 -10
  167. package/template/docs/platform/web-system-crud.md +1 -1
  168. package/template/package.json +1 -1
  169. package/template/packages/config/package.json +1 -1
  170. package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
  171. package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
  172. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  173. package/template/packages/database/package.json +1 -1
  174. package/template/packages/database/src/schema/platform-settings.ts +3 -0
  175. package/template/packages/example-work-order/package.json +6 -0
  176. package/template/packages/example-work-order/src/admin-page.ts +78 -0
  177. package/template/packages/example-work-order/src/admin.ts +35 -0
  178. package/template/packages/example-work-order/src/manifest.test.ts +62 -5
  179. package/template/packages/example-work-order/src/manifest.ts +54 -7
  180. package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
  181. package/template/packages/example-work-order/src/server-module.ts +2 -2
  182. package/template/packages/jobs/package.json +1 -1
  183. package/template/packages/modules/index.ts +4 -0
  184. package/template/packages/modules/package.json +3 -2
  185. package/template/packages/modules/src/admin-ui-entry.ts +8 -0
  186. package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
  187. package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
  188. package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
  189. package/template/packages/modules/src/capability-audit-validation.ts +38 -0
  190. package/template/packages/modules/src/capability-guard-validation.ts +148 -0
  191. package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
  192. package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
  193. package/template/packages/modules/src/composition.test.ts +74 -9
  194. package/template/packages/modules/src/composition.ts +8 -0
  195. package/template/packages/modules/src/manifest.ts +1 -0
  196. package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
  197. package/template/packages/modules/src/module-host-range.test.ts +57 -0
  198. package/template/packages/modules/src/module-host-range.ts +125 -0
  199. package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
  200. package/template/packages/modules/src/project-module-assembly.ts +126 -0
  201. package/template/packages/modules/src/system-admin-routes.ts +6 -5
  202. package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
  203. package/template/packages/modules/src/system-agent-tools.ts +2 -2
  204. package/template/packages/modules/src/system-api-binding.ts +43 -9
  205. package/template/packages/modules/src/system-api-control-plane.ts +202 -0
  206. package/template/packages/modules/src/system-api-data-governance.ts +4 -2
  207. package/template/packages/modules/src/system-api-operational.ts +57 -8
  208. package/template/packages/modules/src/system-api-permissions.ts +115 -21
  209. package/template/packages/modules/src/system-api-watermark.ts +3 -0
  210. package/template/packages/modules/src/system-audit-actions.ts +7 -0
  211. package/template/packages/modules/src/system-data-governance.ts +5 -2
  212. package/template/packages/modules/src/system-features.ts +31 -0
  213. package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
  214. package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
  215. package/template/packages/modules/src/system-module-catalog.ts +68 -7
  216. package/template/packages/modules/src/system-module-tasks.ts +8 -1
  217. package/template/packages/modules/src/system-modules-access.test.ts +4 -0
  218. package/template/packages/modules/src/system-modules.test.ts +10 -2
  219. package/template/packages/modules/src/system-modules.ts +6 -5
  220. package/template/packages/modules/src/system-permissions.ts +4 -0
  221. package/template/packages/modules/user/module.ts +19 -2
  222. package/template/packages/server-sdk/index.ts +8 -0
  223. package/template/packages/server-sdk/package.json +2 -1
  224. package/template/packages/server-sdk/src/durable-handler.ts +16 -1
  225. package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
  226. package/template/packages/server-sdk/src/module-adapter.ts +59 -0
  227. package/template/packages/server-sdk/src/route-definition.ts +3 -2
  228. package/template/packages/shared/package.json +1 -1
  229. package/template/packages/shared/src/audit.test.ts +7 -0
  230. package/template/packages/shared/src/audit.ts +16 -0
  231. package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
  232. package/template/packages/shared/src/platform-settings.ts +17 -0
  233. package/template/packages/shared/src/rbac.ts +2 -0
  234. package/template/apps/web/src/app/business-routes.ts +0 -22
@@ -1,7 +1,7 @@
1
1
  import { randomUUIDv7 } from "bun";
2
2
  import type { SchedulerOperationsSnapshot } from "@southwind-ai/shared";
3
3
  import { runGuardedBackgroundTask } from "../background/task-guard.js";
4
- import type { RequestGuardContext } from "../guards.js";
4
+ import type { RequestActor, RequestGuardContext } from "../guards.js";
5
5
  import type { LegacyDurableAdapter } from "../jobs/legacy-adapter.js";
6
6
  import type { createServerRuntime } from "../runtime.js";
7
7
  import type { SchedulerRepository } from "./repository.js";
@@ -41,6 +41,9 @@ export class SchedulerService {
41
41
  private readonly runtime: ServerRuntime,
42
42
  private readonly clock: SchedulerClock = systemClock,
43
43
  private readonly durable?: LegacyDurableAdapter,
44
+ private readonly resolveActor?: (
45
+ actorId: string,
46
+ ) => Promise<RequestActor | undefined>,
44
47
  ) {
45
48
  this.recorder = new SchedulerExecutionRecorder(repository, runtime, clock);
46
49
  validateTaskDefinitions(definitions);
@@ -196,13 +199,14 @@ export class SchedulerService {
196
199
  await this.executeScheduled(run, false);
197
200
  return;
198
201
  }
199
- await this.execute(
200
- run,
201
- definition,
202
- manualResumeContext(run, definition),
203
- false,
204
- false,
202
+ const actor = await this.resolveActor?.(run.requestedBy);
203
+ const context = await this.runtime.createGuardContextForActor(
204
+ new Request(
205
+ `http://scheduler.local/tasks/${encodeURIComponent(run.taskKey)}/resume`,
206
+ ),
207
+ actor ?? deniedResumeActor(run.requestedBy),
205
208
  );
209
+ await this.execute(run, definition, context, true, false);
206
210
  }
207
211
 
208
212
  private async executeScheduled(run: ScheduledTaskRun, useDurable = true) {
@@ -262,7 +266,7 @@ export class SchedulerService {
262
266
  finishedAt: this.clock.now(),
263
267
  error: guarded.reason,
264
268
  });
265
- this.recorder.denied(context, blocked, guarded.reason);
269
+ this.recorder.denied(context, blocked, definition, guarded.reason);
266
270
  return blocked;
267
271
  }
268
272
  return this.recorder.success(run, definition, context, guarded.value);
@@ -321,19 +325,12 @@ export class SchedulerService {
321
325
  }
322
326
  }
323
327
 
324
- function manualResumeContext(
325
- run: ScheduledTaskRun,
326
- definition: ScheduledTaskDefinition,
327
- ): RequestGuardContext {
328
+ function deniedResumeActor(actorId: string): RequestActor {
328
329
  return {
329
- requestId: randomUUIDv7(),
330
- actor: {
331
- id: run.requestedBy,
332
- type: "user",
333
- name: run.requestedBy,
334
- roleCodes: [],
335
- permissionKeys: [definition.permissionKey],
336
- },
337
- startedAt: new Date().toISOString(),
330
+ id: actorId,
331
+ type: "user",
332
+ name: actorId,
333
+ roleCodes: [],
334
+ permissionKeys: [],
338
335
  };
339
336
  }
@@ -1,4 +1,7 @@
1
- import type { FeatureFlagDefinition } from "@southwind-ai/shared";
1
+ import type {
2
+ AuditActionType,
3
+ FeatureFlagDefinition,
4
+ } from "@southwind-ai/shared";
2
5
 
3
6
  export interface SchedulerClock {
4
7
  now(): Date;
@@ -20,6 +23,7 @@ export interface ScheduledTaskDefinition {
20
23
  maxAttempts: number;
21
24
  retryDelaySeconds: number;
22
25
  permissionKey: string;
26
+ auditAction: AuditActionType;
23
27
  feature: FeatureFlagDefinition;
24
28
  handler: ScheduledTaskHandler;
25
29
  isIdleResult?: (value: Record<string, unknown> | void) => boolean;
@@ -42,6 +42,8 @@ describePostgres("PostgreSQL watermark policy repository", () => {
42
42
  density: "high",
43
43
  contentFields: ["username", "phone"],
44
44
  customContent: "内部资料",
45
+ opacityPercent: 16,
46
+ fontSize: 20,
45
47
  },
46
48
  "user_settings",
47
49
  );
@@ -56,6 +58,8 @@ describePostgres("PostgreSQL watermark policy repository", () => {
56
58
  density: "high",
57
59
  contentFields: ["username", "phone"],
58
60
  customContent: "内部资料",
61
+ opacityPercent: 16,
62
+ fontSize: 20,
59
63
  });
60
64
  });
61
65
  });
@@ -30,6 +30,7 @@ export class DrizzleWatermarkPolicyRepository implements WatermarkPolicyReposito
30
30
  .values({
31
31
  id: PLATFORM_WATERMARK_ID,
32
32
  ...policy,
33
+ businessPagesEnabled: policy.enabled,
33
34
  createdBy: actorId,
34
35
  updatedBy: actorId,
35
36
  })
@@ -37,10 +38,12 @@ export class DrizzleWatermarkPolicyRepository implements WatermarkPolicyReposito
37
38
  target: platformWatermarkPolicies.id,
38
39
  set: {
39
40
  enabled: policy.enabled,
40
- businessPagesEnabled: policy.businessPagesEnabled,
41
+ businessPagesEnabled: policy.enabled,
41
42
  density: policy.density,
42
43
  contentFields: policy.contentFields,
43
44
  customContent: policy.customContent,
45
+ opacityPercent: policy.opacityPercent,
46
+ fontSize: policy.fontSize,
44
47
  updatedAt: new Date(),
45
48
  updatedBy: actorId,
46
49
  },
@@ -57,12 +60,17 @@ function toPolicy(row: {
57
60
  density: PlatformWatermarkPolicy["density"];
58
61
  contentFields: PlatformWatermarkPolicy["contentFields"];
59
62
  customContent: string;
63
+ opacityPercent: number;
64
+ fontSize: number;
60
65
  }): PlatformWatermarkPolicy {
66
+ const enabled = row.enabled && row.businessPagesEnabled;
61
67
  return {
62
- enabled: row.enabled,
63
- businessPagesEnabled: row.businessPagesEnabled,
68
+ enabled,
69
+ businessPagesEnabled: enabled,
64
70
  density: row.density,
65
71
  contentFields: row.contentFields,
66
72
  customContent: row.customContent,
73
+ opacityPercent: row.opacityPercent,
74
+ fontSize: row.fontSize,
67
75
  };
68
76
  }
@@ -47,6 +47,8 @@ describe("平台水印策略路由", () => {
47
47
  density: "medium",
48
48
  contentFields: ["username", "displayName"],
49
49
  customContent: "",
50
+ opacityPercent: 8,
51
+ fontSize: 14,
50
52
  });
51
53
  });
52
54
 
@@ -58,6 +60,8 @@ describe("平台水印策略路由", () => {
58
60
  density: "high",
59
61
  contentFields: ["username", "phone", "custom"],
60
62
  customContent: "内部资料",
63
+ opacityPercent: 16,
64
+ fontSize: 20,
61
65
  };
62
66
 
63
67
  const updated = (await fixture.app.invoke(
@@ -83,7 +87,13 @@ describe("平台水印策略路由", () => {
83
87
  action: "config.update",
84
88
  target: { type: "platform-watermark", id: "platform" },
85
89
  metadata: expect.objectContaining({
86
- changedFields: ["density", "contentFields", "customContent"],
90
+ changedFields: [
91
+ "density",
92
+ "contentFields",
93
+ "customContent",
94
+ "opacityPercent",
95
+ "fontSize",
96
+ ],
87
97
  }),
88
98
  }),
89
99
  ]),
@@ -104,6 +114,8 @@ describe("平台水印策略路由", () => {
104
114
  density: "low",
105
115
  contentFields: ["username"],
106
116
  customContent: "",
117
+ opacityPercent: 8,
118
+ fontSize: 14,
107
119
  })) as Response;
108
120
 
109
121
  expect(readable.enabled).toBe(true);
@@ -139,6 +151,8 @@ describe("平台水印策略路由", () => {
139
151
  density: "medium",
140
152
  contentFields: ["username"],
141
153
  customContent: "x".repeat(81),
154
+ opacityPercent: 8,
155
+ fontSize: 14,
142
156
  })) as Response;
143
157
 
144
158
  expect(response.status).toBe(400);
@@ -148,9 +162,48 @@ describe("平台水印策略路由", () => {
148
162
  density: "medium",
149
163
  contentFields: ["username", "displayName"],
150
164
  customContent: "",
165
+ opacityPercent: 8,
166
+ fontSize: 14,
151
167
  });
152
168
  expect(fixture.runtime.auditSink.list()).toHaveLength(0);
153
169
  });
170
+
171
+ test("旧业务范围状态会折叠到唯一开关", async () => {
172
+ const fixture = await createFixture(settingsActor(true));
173
+
174
+ const updated = (await update(fixture, {
175
+ enabled: true,
176
+ businessPagesEnabled: false,
177
+ density: "medium",
178
+ contentFields: ["email", "displayName"],
179
+ customContent: "",
180
+ opacityPercent: 8,
181
+ fontSize: 14,
182
+ })) as PlatformWatermarkPolicy;
183
+
184
+ expect(updated.enabled).toBe(true);
185
+ expect(updated.businessPagesEnabled).toBe(true);
186
+ expect(updated.contentFields).toEqual(["email", "displayName"]);
187
+ });
188
+
189
+ test("不透明度和字体大小超出范围时拒绝保存", async () => {
190
+ const fixture = await createFixture(settingsActor(true));
191
+
192
+ const response = (await update(fixture, {
193
+ enabled: true,
194
+ businessPagesEnabled: true,
195
+ density: "medium",
196
+ contentFields: ["username"],
197
+ customContent: "",
198
+ opacityPercent: 31,
199
+ fontSize: 14,
200
+ })) as Response;
201
+
202
+ expect(response.status).toBe(400);
203
+ await expect(response.json()).resolves.toMatchObject({
204
+ error: "INVALID_WATERMARK_POLICY",
205
+ });
206
+ });
154
207
  });
155
208
 
156
209
  async function update(
@@ -6,6 +6,8 @@ import type {
6
6
  import {
7
7
  WATERMARK_CONTENT_FIELDS,
8
8
  WATERMARK_DENSITIES,
9
+ WATERMARK_FONT_SIZE_RANGE,
10
+ WATERMARK_OPACITY_RANGE,
9
11
  } from "@southwind-ai/shared";
10
12
  import { evaluateRouteGuards, findFeature } from "../route-guards.js";
11
13
  import type { createServerRuntime } from "../runtime.js";
@@ -137,6 +139,28 @@ function parseWatermarkPolicy(body: unknown): WatermarkParseResult {
137
139
  if (customContent.length > 80) {
138
140
  return { ok: false, message: "自定义内容不能超过 80 个字符" };
139
141
  }
142
+ if (
143
+ typeof source.opacityPercent !== "number" ||
144
+ !Number.isInteger(source.opacityPercent) ||
145
+ source.opacityPercent < WATERMARK_OPACITY_RANGE.min ||
146
+ source.opacityPercent > WATERMARK_OPACITY_RANGE.max
147
+ ) {
148
+ return {
149
+ ok: false,
150
+ message: `水印不透明度必须是 ${WATERMARK_OPACITY_RANGE.min}% 到 ${WATERMARK_OPACITY_RANGE.max}% 的整数`,
151
+ };
152
+ }
153
+ if (
154
+ typeof source.fontSize !== "number" ||
155
+ !Number.isInteger(source.fontSize) ||
156
+ source.fontSize < WATERMARK_FONT_SIZE_RANGE.min ||
157
+ source.fontSize > WATERMARK_FONT_SIZE_RANGE.max
158
+ ) {
159
+ return {
160
+ ok: false,
161
+ message: `水印字体大小必须是 ${WATERMARK_FONT_SIZE_RANGE.min}px 到 ${WATERMARK_FONT_SIZE_RANGE.max}px 的整数`,
162
+ };
163
+ }
140
164
  const contentFields = [
141
165
  ...new Set(
142
166
  source.contentFields as PlatformWatermarkPolicy["contentFields"],
@@ -146,10 +170,12 @@ function parseWatermarkPolicy(body: unknown): WatermarkParseResult {
146
170
  ok: true,
147
171
  value: {
148
172
  enabled: source.enabled,
149
- businessPagesEnabled: source.businessPagesEnabled,
173
+ businessPagesEnabled: source.enabled,
150
174
  density: source.density as PlatformWatermarkPolicy["density"],
151
175
  contentFields,
152
176
  customContent,
177
+ opacityPercent: source.opacityPercent,
178
+ fontSize: source.fontSize,
153
179
  },
154
180
  };
155
181
  }
@@ -164,6 +190,8 @@ function changedFields(
164
190
  "density",
165
191
  "contentFields",
166
192
  "customContent",
193
+ "opacityPercent",
194
+ "fontSize",
167
195
  ].filter(
168
196
  (field) =>
169
197
  JSON.stringify(before[field as keyof PlatformWatermarkPolicy]) !==
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validateCompositionBeforeStartup } from "./startup-composition-validation.js";
3
+
4
+ describe("完整 Server Composition 启动前门禁", () => {
5
+ test("正常启动先执行无数据库验证子进程", () => {
6
+ let databaseUrlSeen = "not-called";
7
+ validateCompositionBeforeStartup(
8
+ ["bun", "src/index.ts"],
9
+ (_command, _args, options) => {
10
+ databaseUrlSeen = options.env.DATABASE_URL ?? "";
11
+ return { status: 0 };
12
+ },
13
+ );
14
+
15
+ expect(databaseUrlSeen).toBe("");
16
+ });
17
+
18
+ test("验证子进程自身不递归,并把失败传回启动进程", () => {
19
+ let calls = 0;
20
+ validateCompositionBeforeStartup(
21
+ ["bun", "src/index.ts", "--validate-module-composition"],
22
+ () => {
23
+ calls += 1;
24
+ return { status: 0 };
25
+ },
26
+ );
27
+ expect(calls).toBe(0);
28
+
29
+ expect(() =>
30
+ validateCompositionBeforeStartup(["bun", "src/index.ts"], () => ({
31
+ status: 1,
32
+ })),
33
+ ).toThrow("启动前校验失败");
34
+ });
35
+ });
@@ -0,0 +1,54 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const VALIDATION_ARGUMENT = "--validate-module-composition";
5
+ const validatorPath = fileURLToPath(
6
+ new URL("../scripts/validate-module-composition.ts", import.meta.url),
7
+ );
8
+ const serverRoot = fileURLToPath(new URL("..", import.meta.url));
9
+
10
+ interface ValidationProcessResult {
11
+ readonly status: number | null;
12
+ readonly error?: Error;
13
+ }
14
+
15
+ type SpawnValidationProcess = (
16
+ command: string,
17
+ args: readonly string[],
18
+ options: {
19
+ readonly cwd: string;
20
+ readonly env: NodeJS.ProcessEnv;
21
+ readonly stdio: "inherit";
22
+ },
23
+ ) => ValidationProcessResult;
24
+
25
+ /**
26
+ * 正常启动先用无数据库子进程走一遍与 build 相同的完整路由/任务/Provider 门禁。
27
+ */
28
+ export function validateCompositionBeforeStartup(
29
+ argv: readonly string[] = process.argv,
30
+ spawn: SpawnValidationProcess = spawnValidationProcess,
31
+ ): void {
32
+ if (argv.includes(VALIDATION_ARGUMENT)) return;
33
+ const env = { ...process.env };
34
+ delete env.DATABASE_URL;
35
+ const result = spawn(process.execPath, [validatorPath], {
36
+ cwd: serverRoot,
37
+ env,
38
+ stdio: "inherit",
39
+ });
40
+ if (result.error) throw result.error;
41
+ if (result.status !== 0) {
42
+ throw new Error(
43
+ `Server 完整 Module Composition 启动前校验失败(exit ${result.status ?? "unknown"})`,
44
+ );
45
+ }
46
+ }
47
+
48
+ function spawnValidationProcess(
49
+ command: string,
50
+ args: readonly string[],
51
+ options: Parameters<SpawnValidationProcess>[2],
52
+ ): ValidationProcessResult {
53
+ return spawnSync(command, [...args], options);
54
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { runPreflightBeforeRecovery } from "./startup-preflight.js";
3
+
4
+ describe("Server 启动前门禁顺序", () => {
5
+ test("先完成无持久化 preflight,再恢复审计队列", async () => {
6
+ const calls: string[] = [];
7
+ const result = await runPreflightBeforeRecovery(
8
+ () => {
9
+ calls.push("prepare");
10
+ return Promise.resolve("ready");
11
+ },
12
+ () => {
13
+ calls.push("recover");
14
+ return Promise.resolve();
15
+ },
16
+ );
17
+
18
+ expect(result).toBe("ready");
19
+ expect(calls).toEqual(["prepare", "recover"]);
20
+ });
21
+
22
+ test("preflight 失败时不触发恢复写入", async () => {
23
+ let recovered = false;
24
+ await expect(
25
+ runPreflightBeforeRecovery(
26
+ () => Promise.reject(new Error("invalid composition")),
27
+ () => {
28
+ recovered = true;
29
+ return Promise.resolve();
30
+ },
31
+ ),
32
+ ).rejects.toThrow("invalid composition");
33
+
34
+ expect(recovered).toBe(false);
35
+ });
36
+ });
@@ -0,0 +1,37 @@
1
+ import type { ModuleUploadPolicy } from "@southwind-ai/server-sdk";
2
+ import { prepareInstalledServerModules } from "./module-preflight.js";
3
+ import { createPlatformBootstrap } from "./platform-bootstrap.js";
4
+ // @windy-module system.storage begin
5
+ import { createPlatformUploadPolicies } from "./storage/runtime.js";
6
+ // @windy-module system.storage end
7
+
8
+ /**
9
+ * 在恢复审计队列、执行迁移或写入目录前,完成项目装配、Host 与 attachment 门禁。
10
+ */
11
+ export async function createPreparedPlatformBootstrap() {
12
+ const bootstrap = createPlatformBootstrap();
13
+ const baseUploadPolicies: ModuleUploadPolicy[] = [
14
+ // @windy-module system.storage begin
15
+ ...createPlatformUploadPolicies(bootstrap.platformConfig),
16
+ // @windy-module system.storage end
17
+ ];
18
+ const recoverAuditQueue = () =>
19
+ // @windy-module system.audit begin
20
+ bootstrap.persistence?.auditWriter.recover() ??
21
+ // @windy-module system.audit end
22
+ Promise.resolve();
23
+ const preparedModules = await runPreflightBeforeRecovery(
24
+ () => prepareInstalledServerModules(baseUploadPolicies),
25
+ recoverAuditQueue,
26
+ );
27
+ return { ...bootstrap, preparedModules };
28
+ }
29
+
30
+ export async function runPreflightBeforeRecovery<T>(
31
+ prepare: () => Promise<T>,
32
+ recover: () => Promise<void>,
33
+ ): Promise<T> {
34
+ const prepared = await prepare();
35
+ await recover();
36
+ return prepared;
37
+ }
@@ -6,12 +6,14 @@ import { registerSystemRoutes } from "./routes.js";
6
6
  import { createSystemRepositories } from "./seed.js";
7
7
 
8
8
  async function auditFixture() {
9
+ const snapshot = createFoundationSnapshot();
9
10
  const runtime = createServerRuntime({ WINDY_DEV_ADMIN_TOKEN: "token_1" });
10
11
  const app = new FakeRouteApp();
11
12
  registerSystemRoutes(
12
13
  app,
13
- createSystemRepositories(createFoundationSnapshot()),
14
+ createSystemRepositories(snapshot),
14
15
  runtime,
16
+ snapshot.features,
15
17
  );
16
18
  const guardContext = await runtime.createGuardContext(
17
19
  new Request("http://localhost/api/system/audit-logs", {
@@ -54,10 +56,12 @@ describe("audit routes", () => {
54
56
  },
55
57
  );
56
58
  const app = new FakeRouteApp();
59
+ const snapshot = createFoundationSnapshot();
57
60
  registerSystemRoutes(
58
61
  app,
59
- createSystemRepositories(createFoundationSnapshot()),
62
+ createSystemRepositories(snapshot),
60
63
  runtime,
64
+ snapshot.features,
61
65
  );
62
66
  const guardContext = await runtime.createGuardContext(
63
67
  new Request("http://localhost/api/system/audit-logs", {
@@ -108,10 +112,12 @@ describe("audit routes", () => {
108
112
  },
109
113
  );
110
114
  const app = new FakeRouteApp();
115
+ const snapshot = createFoundationSnapshot();
111
116
  registerSystemRoutes(
112
117
  app,
113
- createSystemRepositories(createFoundationSnapshot()),
118
+ createSystemRepositories(snapshot),
114
119
  runtime,
120
+ snapshot.features,
115
121
  );
116
122
  const guardContext = await runtime.createGuardContext(
117
123
  new Request("http://localhost/api/system/audit-logs", {
@@ -1,14 +1,16 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { createFoundationSnapshot } from "../foundation.js";
3
- import { createServerRuntime } from "../runtime.js";
4
3
  import { FakeRouteApp } from "./route-test-app.js";
4
+ import { createRouteTestRuntime } from "./route-test-runtime.js";
5
5
  import { registerSystemRoutes } from "./routes.js";
6
6
  import { createSystemRepositories } from "./seed.js";
7
7
 
8
8
  async function fixture() {
9
9
  const snapshot = createFoundationSnapshot();
10
10
  const repositories = createSystemRepositories(snapshot);
11
- const runtime = createServerRuntime({ WINDY_DEV_ADMIN_TOKEN: "token_1" });
11
+ const runtime = createRouteTestRuntime(snapshot, {
12
+ WINDY_DEV_ADMIN_TOKEN: "token_1",
13
+ });
12
14
  const app = new FakeRouteApp();
13
15
  registerSystemRoutes(app, repositories, runtime, snapshot.features);
14
16
  const guardContext = await runtime.createGuardContext(
@@ -27,7 +29,7 @@ async function fixture() {
27
29
  describe("部门组织路由", () => {
28
30
  test("部门处置缺少权限时经过统一 Guard 拒绝", async () => {
29
31
  const snapshot = createFoundationSnapshot();
30
- const runtime = createServerRuntime();
32
+ const runtime = createRouteTestRuntime(snapshot);
31
33
  const app = new FakeRouteApp();
32
34
  registerSystemRoutes(
33
35
  app,
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { createFoundationSnapshot } from "../foundation.js";
3
- import { createServerRuntime } from "../runtime.js";
4
3
  import { FakeRouteApp } from "./route-test-app.js";
4
+ import { createRouteTestRuntime } from "./route-test-runtime.js";
5
5
  import { registerSystemRoutes } from "./routes.js";
6
6
  import { createSystemRepositories } from "./seed.js";
7
7
 
@@ -10,7 +10,8 @@ describe("feature routes", () => {
10
10
  const snapshot = createFoundationSnapshot();
11
11
  const repositories = createSystemRepositories(snapshot);
12
12
  await repositories.features.syncManifest(snapshot.features);
13
- const runtime = createServerRuntime(
13
+ const runtime = createRouteTestRuntime(
14
+ snapshot,
14
15
  { WINDY_DEV_ADMIN_TOKEN: "token_1" },
15
16
  { featureResolver: repositories.features },
16
17
  );
@@ -46,7 +47,7 @@ describe("feature routes", () => {
46
47
  });
47
48
  expect(denied.status).toBe(403);
48
49
  await expect(denied.json()).resolves.toMatchObject({
49
- reason: "feature-disabled",
50
+ reason: "dependency-missing",
50
51
  });
51
52
  expect(app.handlers.has("POST /system/features")).toBe(false);
52
53
  expect(app.handlers.has("POST /system/features/import")).toBe(false);
@@ -15,7 +15,8 @@ describe("系统接口权限目录", () => {
15
15
  ),
16
16
  ).toMatchObject({
17
17
  permissionKey: "system.user.manage",
18
- featureKey: "system.rbac",
18
+ featureKey: "system.user",
19
+ auditAction: "crud.create",
19
20
  guardOrder: ["license", "feature", "permission"],
20
21
  source: "module-manifest",
21
22
  });
@@ -52,11 +53,12 @@ describe("系统接口权限目录", () => {
52
53
  ),
53
54
  ).toMatchObject({
54
55
  permissionKey: "system.feature.manage",
55
- guardOrder: ["license", "permission"],
56
+ featureKey: "system.feature",
57
+ guardOrder: ["license", "feature", "permission"],
56
58
  });
57
59
  expect(
58
60
  interfaces.find((item) => item.path === "/api/foundation"),
59
- ).toMatchObject({ anonymous: true });
61
+ ).toMatchObject({ anonymous: true, auditAction: undefined });
60
62
  // @windy-module system.bulk-data begin
61
63
  expect(
62
64
  interfaces.find(
@@ -1,5 +1,6 @@
1
1
  import type { ModuleManifest } from "@southwind-ai/modules";
2
2
  import type {
3
+ AuditActionType,
3
4
  EntitlementKey,
4
5
  HttpMethod,
5
6
  LicenseVersionKey,
@@ -19,6 +20,7 @@ export interface SystemInterfacePermissionDetail {
19
20
  permissionKey: string;
20
21
  anonymous: boolean;
21
22
  featureKey?: string;
23
+ auditAction?: AuditActionType;
22
24
  allowedLicenseVersions?: readonly LicenseVersionKey[];
23
25
  entitlementKey?: EntitlementKey;
24
26
  guardOrder: readonly GuardStep[];
@@ -56,6 +58,7 @@ export function collectSystemInterfacePermissions(
56
58
  permissionKey: binding.permissionKey,
57
59
  anonymous: binding.anonymous === true,
58
60
  featureKey: feature?.key,
61
+ auditAction: binding.auditAction,
59
62
  allowedLicenseVersions: feature?.allowedLicenseVersions,
60
63
  entitlementKey: feature?.requiredEntitlement,
61
64
  guardOrder: resolveGuardOrder(feature),
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { createFoundationSnapshot } from "../foundation.js";
3
- import { createServerRuntime } from "../runtime.js";
4
3
  import { FakeRouteApp } from "./route-test-app.js";
4
+ import { createRouteTestRuntime } from "./route-test-runtime.js";
5
5
  import { registerSystemRoutes } from "./routes.js";
6
6
  import { createSystemRepositories } from "./seed.js";
7
7
 
@@ -9,7 +9,9 @@ describe("菜单结构批量路由", () => {
9
9
  test("完整草稿通过 system.menu.manage Guard 后一次保存并写审计", async () => {
10
10
  const snapshot = createFoundationSnapshot();
11
11
  const repositories = createSystemRepositories(snapshot);
12
- const runtime = createServerRuntime({ WINDY_DEV_ADMIN_TOKEN: "token_1" });
12
+ const runtime = createRouteTestRuntime(snapshot, {
13
+ WINDY_DEV_ADMIN_TOKEN: "token_1",
14
+ });
13
15
  const app = new FakeRouteApp();
14
16
  registerSystemRoutes(app, repositories, runtime, snapshot.features);
15
17
  const guardContext = await runtime.createGuardContext(
@@ -49,7 +51,7 @@ describe("菜单结构批量路由", () => {
49
51
  test("缺少管理权限时拒绝保存菜单草稿", async () => {
50
52
  const snapshot = createFoundationSnapshot();
51
53
  const repositories = createSystemRepositories(snapshot);
52
- const runtime = createServerRuntime();
54
+ const runtime = createRouteTestRuntime(snapshot);
53
55
  const app = new FakeRouteApp();
54
56
  registerSystemRoutes(app, repositories, runtime, snapshot.features);
55
57
  const guardContext = await runtime.createGuardContext(