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
@@ -0,0 +1,24 @@
1
+ import type { FeatureFlagDefinition } from "@southwind-ai/shared";
2
+ import { createServerRuntime, type ServerRuntimeOptions } from "../runtime.js";
3
+
4
+ interface TestFeatureCatalog {
5
+ readonly features: readonly FeatureFlagDefinition[];
6
+ readonly disabledModuleNames?: readonly string[];
7
+ }
8
+
9
+ type TestRuntimeOptions = Omit<
10
+ ServerRuntimeOptions,
11
+ "featureCatalog" | "disabledModuleNames"
12
+ >;
13
+
14
+ export function createRouteTestRuntime(
15
+ catalog: TestFeatureCatalog,
16
+ env: Record<string, string | undefined> = {},
17
+ options: TestRuntimeOptions = {},
18
+ ) {
19
+ return createServerRuntime(env, {
20
+ ...options,
21
+ featureCatalog: catalog.features,
22
+ disabledModuleNames: new Set(catalog.disabledModuleNames),
23
+ });
24
+ }
@@ -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
 
@@ -71,12 +71,16 @@ describe("system route query validation", () => {
71
71
  });
72
72
 
73
73
  async function createFixture(path: string) {
74
- const runtime = createServerRuntime({ WINDY_DEV_ADMIN_TOKEN: "token_1" });
74
+ const snapshot = createFoundationSnapshot();
75
+ const runtime = createRouteTestRuntime(snapshot, {
76
+ WINDY_DEV_ADMIN_TOKEN: "token_1",
77
+ });
75
78
  const app = new FakeRouteApp();
76
79
  registerSystemRoutes(
77
80
  app,
78
- createSystemRepositories(createFoundationSnapshot()),
81
+ createSystemRepositories(snapshot),
79
82
  runtime,
83
+ snapshot.features,
80
84
  );
81
85
  const guardContext = await runtime.createGuardContext(
82
86
  new Request(`http://localhost${path}`, {
@@ -12,19 +12,21 @@ import { registerSystemRoutes } from "./routes.js";
12
12
  import { createSystemRepositories } from "./seed.js";
13
13
 
14
14
  async function routeFixture(
15
- features: FeatureFlagDefinition[] = [],
15
+ features?: FeatureFlagDefinition[],
16
16
  auditWriter?: AuditEventWriter,
17
17
  ) {
18
+ const snapshot = createFoundationSnapshot();
19
+ const activeFeatures = features ?? snapshot.features;
18
20
  const runtime = createServerRuntime(
19
21
  { WINDY_DEV_ADMIN_TOKEN: "token_1" },
20
- { auditWriter },
22
+ { auditWriter, featureCatalog: activeFeatures },
21
23
  );
22
24
  const app = new FakeRouteApp();
23
25
  registerSystemRoutes(
24
26
  app,
25
- createSystemRepositories(createFoundationSnapshot()),
27
+ createSystemRepositories(snapshot),
26
28
  runtime,
27
- features,
29
+ activeFeatures,
28
30
  );
29
31
  const guardContext = await runtime.createGuardContext(
30
32
  new Request("http://localhost/api/system/users", {
@@ -78,12 +80,17 @@ describe("system routes", () => {
78
80
  });
79
81
 
80
82
  test("缺少权限时返回 403 Response 并记录 security.denied", async () => {
81
- const runtime = createServerRuntime();
83
+ const snapshot = createFoundationSnapshot();
84
+ const runtime = createServerRuntime(
85
+ {},
86
+ { featureCatalog: snapshot.features },
87
+ );
82
88
  const app = new FakeRouteApp();
83
89
  registerSystemRoutes(
84
90
  app,
85
- createSystemRepositories(createFoundationSnapshot()),
91
+ createSystemRepositories(snapshot),
86
92
  runtime,
93
+ snapshot.features,
87
94
  );
88
95
  const guardContext = await runtime.createGuardContext(
89
96
  new Request("http://localhost/api/system/users"),
@@ -127,7 +134,7 @@ describe("system routes", () => {
127
134
 
128
135
  expect(hiddenResponse.status).toBe(403);
129
136
  await expect(hiddenResponse.json()).resolves.toMatchObject({
130
- reason: "feature-disabled",
137
+ reason: "dependency-missing",
131
138
  });
132
139
 
133
140
  // @windy-module system.license begin
@@ -209,7 +216,7 @@ describe("system routes", () => {
209
216
 
210
217
  test("用户与角色变更使用关键策略,普通资源变更保持降级可用", async () => {
211
218
  const policies: string[] = [];
212
- const { app, guardContext } = await routeFixture([], {
219
+ const { app, guardContext } = await routeFixture(undefined, {
213
220
  write: async (_event, policy) => void policies.push(policy || "ordinary"),
214
221
  });
215
222
 
@@ -234,7 +241,7 @@ describe("system routes", () => {
234
241
  });
235
242
 
236
243
  test("关键资源审计失败会拒绝请求,普通资源仍完成并报告降级", async () => {
237
- const { app, runtime, guardContext } = await routeFixture([], {
244
+ const { app, runtime, guardContext } = await routeFixture(undefined, {
238
245
  write: async () => {
239
246
  throw new Error("audit persistence unavailable");
240
247
  },
@@ -281,7 +288,10 @@ describe("system routes", () => {
281
288
  await repositories.features.syncManifest(snapshot.features);
282
289
  const runtime = createServerRuntime(
283
290
  { WINDY_DEV_ADMIN_TOKEN: "token_1" },
284
- { featureResolver: repositories.features },
291
+ {
292
+ featureResolver: repositories.features,
293
+ featureCatalog: snapshot.features,
294
+ },
285
295
  );
286
296
  const app = new FakeRouteApp();
287
297
  registerSystemRoutes(app, repositories, runtime, snapshot.features);
@@ -315,7 +325,7 @@ describe("system routes", () => {
315
325
  });
316
326
  expect(denied.status).toBe(403);
317
327
  await expect(denied.json()).resolves.toMatchObject({
318
- reason: "feature-disabled",
328
+ reason: "dependency-missing",
319
329
  });
320
330
  expect(app.handlers.has("POST /system/features")).toBe(false);
321
331
  expect(app.handlers.has("POST /system/features/import")).toBe(false);
@@ -27,7 +27,7 @@ export function registerSystemRoutes(
27
27
  app: RouteApp,
28
28
  repositories: SystemRepositories,
29
29
  runtime: ReturnType<typeof createServerRuntime>,
30
- features: FeatureFlagDefinition[] = [],
30
+ features: FeatureFlagDefinition[],
31
31
  options: {
32
32
  authRepository?: Pick<AuthRepository, "revokeUserSessions">;
33
33
  } = {},
@@ -98,7 +98,16 @@ export function registerSystemRoutes(
98
98
  ),
99
99
  runtime,
100
100
  );
101
- registerFeatureFlagRoutes(app, repositories.features, runtime);
101
+ const featureManagement = findSystemFeature(features, "feature");
102
+ if (!featureManagement) {
103
+ throw new Error("功能开关管理缺少 system.feature Feature");
104
+ }
105
+ registerFeatureFlagRoutes(
106
+ app,
107
+ repositories.features,
108
+ runtime,
109
+ featureManagement,
110
+ );
102
111
  // @windy-module system.license begin
103
112
  registerResourceRoutes(
104
113
  app,
@@ -43,7 +43,9 @@ describe("平台模块 composition root", () => {
43
43
  "advanced",
44
44
  ]);
45
45
  const workOrder = modules.find(({ name }) => name === "work-order");
46
- expect(workOrder?.adminRoutes).toEqual([]);
46
+ expect(workOrder?.adminRoutes.map(({ key }) => key)).toEqual([
47
+ "work-order.ticket.list",
48
+ ]);
47
49
  expect(workOrder?.apiPermissions).toHaveLength(5);
48
50
  expect(workOrder?.tasks.map(({ key }) => key)).toContain(
49
51
  "work-order.ticket.snapshot",
@@ -88,9 +90,9 @@ describe("平台模块 composition root", () => {
88
90
  "@southwind-ai/web",
89
91
  );
90
92
  expect(businessPackage.dependencies).not.toHaveProperty("vue");
91
- expect(webPackage.dependencies).not.toHaveProperty(
92
- "@southwind-ai/example-work-order",
93
- );
93
+ expect(webPackage.dependencies).toMatchObject({
94
+ "@southwind-ai/example-work-order": "workspace:*",
95
+ });
94
96
  });
95
97
 
96
98
  test("示例模块经 Host 声明式装配提供搜索 Provider", async () => {
@@ -22,6 +22,9 @@ describe("示例工单 routes", () => {
22
22
  items: WorkOrder[];
23
23
  };
24
24
  expect(listed.items.map(({ id }) => id)).toEqual(["ticket_self"]);
25
+ expect(fixture.runtime.auditSink.list()).toContainEqual(
26
+ expect.objectContaining({ action: "data.read" }),
27
+ );
25
28
 
26
29
  const denied = (await fixture.invoke("GET", "/work-orders/:id", {
27
30
  id: "ticket_other",
@@ -76,6 +79,11 @@ describe("示例工单 routes", () => {
76
79
  target: expect.objectContaining({ id: created.id, type: "work-order" }),
77
80
  }),
78
81
  );
82
+ expect(
83
+ fixture.runtime.auditSink
84
+ .list()
85
+ .filter(({ action }) => action === "crud.create"),
86
+ ).toHaveLength(1);
79
87
  });
80
88
  });
81
89
 
@@ -33,9 +33,14 @@ describe("示例工单后台任务", () => {
33
33
  workOrderDataScopeBinding,
34
34
  ),
35
35
  });
36
+ const definitions = createModuleTaskDefinitions(
37
+ hosts,
38
+ createFoundationSnapshot().features,
39
+ );
40
+ expect(definitions[0]?.auditAction).toBe("task.execute");
36
41
  const scheduler = new SchedulerService(
37
42
  new InMemorySchedulerRepository(),
38
- createModuleTaskDefinitions(hosts, createFoundationSnapshot().features),
43
+ definitions,
39
44
  runtime,
40
45
  );
41
46
  await scheduler.initialize();
@@ -4,6 +4,7 @@ FROM oven/bun:1.3.14-alpine AS deps
4
4
  WORKDIR /app
5
5
 
6
6
  COPY package.json bun.lock ./
7
+ COPY windy.modules.json windy.modules.json
7
8
  # windy-copy:deps begin
8
9
  COPY apps/license/package.json apps/license/package.json
9
10
  COPY apps/server/package.json apps/server/package.json
@@ -27,9 +28,13 @@ RUN --mount=type=cache,id=windy-bun-1.3.14,target=/root/.bun/install/cache,shari
27
28
  FROM deps AS build
28
29
  # windy-copy:build begin
29
30
  COPY apps/web apps/web
31
+ COPY packages/example-work-order packages/example-work-order
32
+ COPY packages/jobs packages/jobs
30
33
  COPY packages/license-sdk packages/license-sdk
31
34
  COPY packages/modules packages/modules
35
+ COPY packages/server-sdk packages/server-sdk
32
36
  COPY packages/shared packages/shared
37
+ COPY packages/storage packages/storage
33
38
  # windy-copy:build end
34
39
  RUN bun run --cwd apps/web build
35
40
 
@@ -18,6 +18,7 @@
18
18
  "@tanstack/vue-form": "^1.33.0",
19
19
  "@tanstack/vue-table": "^8.21.3",
20
20
  "@vueuse/core": "^14.3.0",
21
+ "@southwind-ai/example-work-order": "workspace:*",
21
22
  "@southwind-ai/license": "workspace:*",
22
23
  "@southwind-ai/shared": "workspace:*",
23
24
  "class-variance-authority": "^0.7.1",
@@ -0,0 +1,106 @@
1
+ import moduleAssembly from "../../../windy.modules.json";
2
+ import {
3
+ systemAdminUiManifests,
4
+ validateProjectModuleAssembly,
5
+ } from "@southwind-ai/modules/admin-ui";
6
+ import {
7
+ projectAdminUiManifest,
8
+ type AdminUiManifestSource,
9
+ } from "@southwind-ai/modules";
10
+ import { webComposition } from "../src/app/web-composition";
11
+ import {
12
+ resolveAdminUiComposition,
13
+ validateAdminRoutePathCollisions,
14
+ type AdminUiModule,
15
+ } from "../src/router/admin-ui-composition";
16
+ import { validatePlatformAdminRouteKeys } from "../src/router/manifest-routes";
17
+ import { platformAdminRouteBindings } from "../src/router/platform-admin-route-bindings";
18
+ import { reservedWebRoutePathList } from "../src/router/reserved-route-paths";
19
+
20
+ const assembly = validateProjectModuleAssembly(moduleAssembly);
21
+ const installedModules = await Promise.all(
22
+ assembly.modules
23
+ .filter(
24
+ (
25
+ entry,
26
+ ): entry is typeof entry & {
27
+ readonly package: string;
28
+ } => entry.enabled && entry.package !== null,
29
+ )
30
+ .map(async (entry) => {
31
+ const [adminLoaded, manifestLoaded]: readonly [unknown, unknown] =
32
+ await Promise.all([
33
+ import(`${entry.package}/admin`),
34
+ import(`${entry.package}/manifest`),
35
+ ]);
36
+ if (!isAdminUiModuleExport(adminLoaded)) {
37
+ throw new Error(
38
+ `Module ${entry.name} 缺少 windyAdminUiModule 标准导出`,
39
+ );
40
+ }
41
+ if (!isModuleManifestExport(manifestLoaded)) {
42
+ throw new Error(
43
+ `Module ${entry.name} 缺少 windyModuleManifest 标准导出`,
44
+ );
45
+ }
46
+ if (adminLoaded.windyAdminUiModule.manifest.name !== entry.name) {
47
+ throw new Error(
48
+ `Module package ${entry.package} 导出了错误 Admin UI:${adminLoaded.windyAdminUiModule.manifest.name}`,
49
+ );
50
+ }
51
+ const expectedAdminManifest = projectAdminUiManifest(
52
+ manifestLoaded.windyModuleManifest,
53
+ );
54
+ if (
55
+ JSON.stringify(adminLoaded.windyAdminUiModule.manifest) !==
56
+ JSON.stringify(expectedAdminManifest)
57
+ ) {
58
+ throw new Error(
59
+ `Module ${entry.name} 的 ./admin 与 ./manifest 投影不一致`,
60
+ );
61
+ }
62
+ return adminLoaded.windyAdminUiModule;
63
+ }),
64
+ );
65
+
66
+ const composition = resolveAdminUiComposition({
67
+ declaration: webComposition,
68
+ assembly,
69
+ platformModules: systemAdminUiManifests(),
70
+ installedModules,
71
+ });
72
+ validateAdminRoutePathCollisions(composition, reservedWebRoutePathList);
73
+ validatePlatformAdminRouteKeys(
74
+ platformAdminRouteBindings.map(({ routeKey }) => routeKey),
75
+ );
76
+
77
+ function isAdminUiModuleExport(
78
+ value: unknown,
79
+ ): value is { readonly windyAdminUiModule: AdminUiModule } {
80
+ if (!isRecord(value) || !isRecord(value.windyAdminUiModule)) return false;
81
+ const module = value.windyAdminUiModule;
82
+ return (
83
+ isRecord(module.manifest) &&
84
+ typeof module.manifest.name === "string" &&
85
+ typeof module.manifest.version === "string" &&
86
+ Array.isArray(module.manifest.adminRoutes) &&
87
+ typeof module.hostRange === "string" &&
88
+ Array.isArray(module.pages)
89
+ );
90
+ }
91
+
92
+ function isModuleManifestExport(
93
+ value: unknown,
94
+ ): value is { readonly windyModuleManifest: AdminUiManifestSource } {
95
+ if (!isRecord(value) || !isRecord(value.windyModuleManifest)) return false;
96
+ const manifest = value.windyModuleManifest;
97
+ return (
98
+ typeof manifest.name === "string" &&
99
+ typeof manifest.version === "string" &&
100
+ Array.isArray(manifest.adminRoutes)
101
+ );
102
+ }
103
+
104
+ function isRecord(value: unknown): value is Record<string, unknown> {
105
+ return typeof value === "object" && value !== null && !Array.isArray(value);
106
+ }
@@ -0,0 +1,24 @@
1
+ import { defineWebComposition } from "../router/admin-ui-composition";
2
+
3
+ /**
4
+ * 客户拥有的 Admin UI 覆盖声明。
5
+ *
6
+ * 模块的窄 Manifest 与默认页面来自 package 标准 `./admin` 导出;这里只覆盖客户
7
+ * 自己接管的页面 loader 与呈现偏好,不复制路径、标题、Feature 或 Permission。
8
+ */
9
+ export const webComposition = defineWebComposition({
10
+ overrides: [
11
+ // @windy-module work-order begin
12
+ {
13
+ moduleName: "work-order",
14
+ pages: [
15
+ {
16
+ routeKey: "work-order.ticket.list",
17
+ load: () => import("../pages/work-orders/WorkOrderListPage.vue"),
18
+ },
19
+ ],
20
+ },
21
+ // @windy-module work-order end
22
+ ],
23
+ presentation: [],
24
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { webComposition } from "./web-composition";
3
+
4
+ describe("project-owned Web 组合声明", () => {
5
+ test("示例工单只覆盖 moduleName 与 lazy page loader", () => {
6
+ const registration = webComposition.overrides.find(
7
+ ({ moduleName }) => moduleName === "work-order",
8
+ );
9
+
10
+ expect(registration?.moduleName).toBe("work-order");
11
+ expect(registration?.pages).toHaveLength(1);
12
+ expect(registration?.pages[0]?.routeKey).toBe("work-order.ticket.list");
13
+ expect(registration?.pages[0]?.load).toBeTypeOf("function");
14
+ });
15
+ });
@@ -30,6 +30,8 @@ describe("全局水印策略状态", () => {
30
30
  density: "high",
31
31
  contentFields: ["custom"],
32
32
  customContent: "全局审计",
33
+ opacityPercent: 12,
34
+ fontSize: 18,
33
35
  });
34
36
  const { useWatermarkSettings } = await import("./useWatermarkSettings");
35
37
  const watermark = useWatermarkSettings(shallowRef(undefined));
@@ -42,6 +44,8 @@ describe("全局水印策略状态", () => {
42
44
  density: "high",
43
45
  contentFields: ["custom"],
44
46
  customContent: "全局审计",
47
+ opacityPercent: 12,
48
+ fontSize: 18,
45
49
  });
46
50
  expect(localStorageRead).not.toHaveBeenCalled();
47
51
  expect(localStorageWrite).not.toHaveBeenCalled();
@@ -54,6 +58,8 @@ describe("全局水印策略状态", () => {
54
58
  density: "medium",
55
59
  contentFields: ["username", "displayName"],
56
60
  customContent: "",
61
+ opacityPercent: 8,
62
+ fontSize: 14,
57
63
  });
58
64
  api.update.mockResolvedValue({
59
65
  enabled: true,
@@ -61,6 +67,8 @@ describe("全局水印策略状态", () => {
61
67
  density: "high",
62
68
  contentFields: ["username", "custom"],
63
69
  customContent: " 内部资料 ",
70
+ opacityPercent: 16,
71
+ fontSize: 20,
64
72
  });
65
73
  const { useWatermarkSettings } = await import("./useWatermarkSettings");
66
74
  const actor = shallowRef({
@@ -80,6 +88,8 @@ describe("全局水印策略状态", () => {
80
88
  density: "high",
81
89
  contentFields: ["username", "custom"],
82
90
  customContent: "内部资料",
91
+ opacityPercent: 16,
92
+ fontSize: 20,
83
93
  });
84
94
 
85
95
  expect(api.update).toHaveBeenCalledWith({
@@ -88,6 +98,8 @@ describe("全局水印策略状态", () => {
88
98
  density: "high",
89
99
  contentFields: ["username", "custom"],
90
100
  customContent: "内部资料",
101
+ opacityPercent: 16,
102
+ fontSize: 20,
91
103
  });
92
104
  expect(second.policy.value).toEqual({
93
105
  enabled: true,
@@ -95,6 +107,8 @@ describe("全局水印策略状态", () => {
95
107
  density: "high",
96
108
  contentFields: ["username", "custom"],
97
109
  customContent: "内部资料",
110
+ opacityPercent: 16,
111
+ fontSize: 20,
98
112
  });
99
113
  });
100
114
  });
@@ -3,6 +3,8 @@ import {
3
3
  DEFAULT_PLATFORM_WATERMARK_POLICY,
4
4
  WATERMARK_CONTENT_FIELDS,
5
5
  WATERMARK_DENSITIES,
6
+ WATERMARK_FONT_SIZE_RANGE,
7
+ WATERMARK_OPACITY_RANGE,
6
8
  type PlatformWatermarkPolicy,
7
9
  type WatermarkContentField,
8
10
  type WatermarkDensity,
@@ -16,8 +18,6 @@ const DENSITY_GAPS: Record<
16
18
  medium: { horizontal: 72, vertical: 48 },
17
19
  high: { horizontal: 36, vertical: 24 },
18
20
  };
19
- const FONT_SIZE = 14;
20
- const LINE_HEIGHT = 22;
21
21
  const ROTATION_RADIANS = (22 * Math.PI) / 180;
22
22
  const MAX_LINE_WEIGHT = 32;
23
23
 
@@ -58,15 +58,26 @@ export function normalizeWatermarkPolicy(
58
58
  ),
59
59
  ),
60
60
  ];
61
+ const enabled = Boolean(value.enabled && value.businessPagesEnabled);
61
62
  return {
62
- enabled: value.enabled,
63
- businessPagesEnabled: value.businessPagesEnabled,
63
+ enabled,
64
+ businessPagesEnabled: enabled,
64
65
  density,
65
66
  contentFields:
66
67
  contentFields.length > 0
67
68
  ? contentFields
68
69
  : [...DEFAULT_PLATFORM_WATERMARK_POLICY.contentFields],
69
70
  customContent: value.customContent.trim().slice(0, 80),
71
+ opacityPercent: boundedInteger(
72
+ value.opacityPercent,
73
+ WATERMARK_OPACITY_RANGE,
74
+ DEFAULT_PLATFORM_WATERMARK_POLICY.opacityPercent,
75
+ ),
76
+ fontSize: boundedInteger(
77
+ value.fontSize,
78
+ WATERMARK_FONT_SIZE_RANGE,
79
+ DEFAULT_PLATFORM_WATERMARK_POLICY.fontSize,
80
+ ),
70
81
  };
71
82
  }
72
83
 
@@ -78,10 +89,19 @@ export interface WatermarkPattern {
78
89
  export function createWatermarkPattern(
79
90
  content: string,
80
91
  density: WatermarkDensity,
92
+ fontSize: number,
81
93
  ): WatermarkPattern {
94
+ const safeFontSize = boundedInteger(
95
+ fontSize,
96
+ WATERMARK_FONT_SIZE_RANGE,
97
+ DEFAULT_PLATFORM_WATERMARK_POLICY.fontSize,
98
+ );
99
+ const lineHeight = Math.ceil(safeFontSize * 1.57);
82
100
  const lines = wrapWatermarkContent(content);
83
- const textWidth = Math.max(...lines.map(estimatedTextWidth));
84
- const textHeight = lines.length * LINE_HEIGHT;
101
+ const textWidth = Math.max(
102
+ ...lines.map((line) => estimatedTextWidth(line, safeFontSize)),
103
+ );
104
+ const textHeight = lines.length * lineHeight;
85
105
  const rotatedWidth =
86
106
  textWidth * Math.cos(ROTATION_RADIANS) +
87
107
  textHeight * Math.sin(ROTATION_RADIANS);
@@ -91,17 +111,17 @@ export function createWatermarkPattern(
91
111
  const gap = DENSITY_GAPS[density];
92
112
  const width = Math.ceil(rotatedWidth + gap.horizontal);
93
113
  const height = Math.ceil(rotatedHeight + gap.vertical);
94
- const firstLineY = (height - textHeight) / 2 + FONT_SIZE;
114
+ const firstLineY = (height - textHeight) / 2 + safeFontSize;
95
115
  const tspans = lines
96
116
  .map(
97
117
  (line, index) =>
98
- `<tspan x="${width / 2}" y="${firstLineY + index * LINE_HEIGHT}">${escapeXml(line)}</tspan>`,
118
+ `<tspan x="${width / 2}" y="${firstLineY + index * lineHeight}">${escapeXml(line)}</tspan>`,
99
119
  )
100
120
  .join("");
101
121
  const svg =
102
122
  `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` +
103
123
  `<g transform="rotate(-22 ${width / 2} ${height / 2})">` +
104
- `<text text-anchor="middle" font-family="system-ui,sans-serif" font-size="${FONT_SIZE}" font-weight="500" fill="black">${tspans}</text>` +
124
+ `<text text-anchor="middle" font-family="system-ui,sans-serif" font-size="${safeFontSize}" font-weight="500" fill="black">${tspans}</text>` +
105
125
  "</g></svg>";
106
126
  return {
107
127
  image: `url("data:image/svg+xml,${encodeURIComponent(svg)}")`,
@@ -125,8 +145,8 @@ function wrapWatermarkContent(content: string): string[] {
125
145
  return lines.length > 0 ? lines : [""];
126
146
  }
127
147
 
128
- function estimatedTextWidth(value: string): number {
129
- return Math.max(1, textWeight(value)) * (FONT_SIZE * 0.56);
148
+ function estimatedTextWidth(value: string, fontSize: number): number {
149
+ return Math.max(1, textWeight(value)) * (fontSize * 0.56);
130
150
  }
131
151
 
132
152
  function textWeight(value: string): number {
@@ -145,3 +165,12 @@ function escapeXml(value: string): string {
145
165
  .replaceAll('"', "&quot;")
146
166
  .replaceAll("'", "&apos;");
147
167
  }
168
+
169
+ function boundedInteger(
170
+ value: number,
171
+ range: { readonly min: number; readonly max: number },
172
+ fallback: number,
173
+ ): number {
174
+ if (!Number.isFinite(value)) return fallback;
175
+ return Math.min(range.max, Math.max(range.min, Math.round(value)));
176
+ }
@@ -73,8 +73,8 @@ describe("水印设置", () => {
73
73
  test("高密度缩小平铺单元且长内容自动换行以避免相邻水印重叠", () => {
74
74
  const content =
75
75
  "zhangsan · 张三 · 13800138000 · zs@example.com · 内部资料请勿外传";
76
- const low = createWatermarkPattern(content, "low");
77
- const high = createWatermarkPattern(content, "high");
76
+ const low = createWatermarkPattern(content, "low", 14);
77
+ const high = createWatermarkPattern(content, "high", 14);
78
78
  const dimensions = (value: typeof low) =>
79
79
  value.size.match(/\d+/g)!.map(Number);
80
80
 
@@ -86,21 +86,34 @@ describe("水印设置", () => {
86
86
  ).toBeGreaterThan(1);
87
87
  });
88
88
 
89
- test("服务端策略内容会被规范化后渲染", () => {
89
+ test("字体大小会改变平铺尺寸并写入实际 SVG", () => {
90
+ const small = createWatermarkPattern("zhangsan · 张三", "medium", 12);
91
+ const large = createWatermarkPattern("zhangsan · 张三", "medium", 32);
92
+ const width = (value: typeof small) => Number(value.size.match(/\d+/)?.[0]);
93
+
94
+ expect(width(large)).toBeGreaterThan(width(small));
95
+ expect(decodeURIComponent(large.image)).toContain('font-size="32"');
96
+ });
97
+
98
+ test("服务端策略内容会被规范化后渲染且旧范围开关合并到总开关", () => {
90
99
  expect(
91
100
  normalizeWatermarkPolicy({
92
- enabled: false,
93
- businessPagesEnabled: true,
101
+ enabled: true,
102
+ businessPagesEnabled: false,
94
103
  density: "high",
95
104
  contentFields: ["phone", "phone"],
96
105
  customContent: ` ${"x".repeat(100)} `,
106
+ opacityPercent: 99,
107
+ fontSize: 2,
97
108
  }),
98
109
  ).toEqual({
99
110
  enabled: false,
100
- businessPagesEnabled: true,
111
+ businessPagesEnabled: false,
101
112
  density: "high",
102
113
  contentFields: ["phone"],
103
114
  customContent: "x".repeat(80),
115
+ opacityPercent: 30,
116
+ fontSize: 12,
104
117
  });
105
118
  });
106
119
  });
@@ -40,6 +40,7 @@ import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
40
40
  import { useLicenseRuntime } from "@/composables/useLicenseRuntime";
41
41
  // @windy-module system.license end
42
42
  import { usePlatformSettings } from "@/composables/usePlatformSettings";
43
+ import { webAdminUiComposition } from "@/router/web-composition-host";
43
44
  import { authorizedSidebarMenus } from "./navigation";
44
45
 
45
46
  interface NavigationGroup {
@@ -74,7 +75,13 @@ const license = useLicenseRuntime();
74
75
  const platform = usePlatformSettings();
75
76
  const openGroups = reactive<Record<string, boolean>>(loadOpenGroups());
76
77
  const authorizedMenus = computed(() =>
77
- access.snapshot.value ? authorizedSidebarMenus(access.snapshot.value) : [],
78
+ access.snapshot.value
79
+ ? authorizedSidebarMenus(
80
+ access.snapshot.value,
81
+ webAdminUiComposition.presentation,
82
+ webAdminUiComposition,
83
+ )
84
+ : [],
78
85
  );
79
86
  const settingsMenu = computed(() =>
80
87
  findMenu(authorizedMenus.value, "system.settings"),
@@ -17,6 +17,8 @@ const state = vi.hoisted(() => {
17
17
  density: "medium",
18
18
  contentFields: ["username", "displayName"],
19
19
  customContent: "",
20
+ opacityPercent: 8,
21
+ fontSize: 14,
20
22
  },
21
23
  };
22
24