create-windy 0.3.18 → 0.4.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 (216) 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/agent-server/src/southwind_agent_server/adk_debug.py +38 -5
  9. package/template/apps/agent-server/tests/test_adk_debug_result_polling.py +139 -0
  10. package/template/apps/server/Dockerfile +1 -0
  11. package/template/apps/server/package.json +1 -0
  12. package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
  13. package/template/apps/server/scripts/validate-module-composition.ts +28 -0
  14. package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
  15. package/template/apps/server/src/agent/routes.test.ts +20 -7
  16. package/template/apps/server/src/agent/routes.ts +14 -0
  17. package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
  18. package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
  19. package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
  20. package/template/apps/server/src/app/server-composition.ts +56 -0
  21. package/template/apps/server/src/application-services.ts +2 -0
  22. package/template/apps/server/src/bulk-data/service.ts +14 -0
  23. package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
  24. package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
  25. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
  26. package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
  27. package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
  28. package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
  29. package/template/apps/server/src/feature/routes.ts +33 -15
  30. package/template/apps/server/src/foundation-activation.test.ts +60 -0
  31. package/template/apps/server/src/foundation.ts +64 -5
  32. package/template/apps/server/src/guards.test.ts +23 -0
  33. package/template/apps/server/src/guards.ts +12 -0
  34. package/template/apps/server/src/index.ts +28 -34
  35. package/template/apps/server/src/installed-business-modules.ts +13 -24
  36. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
  37. package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
  38. package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
  39. package/template/apps/server/src/license/activation.test.ts +30 -0
  40. package/template/apps/server/src/license/activation.ts +26 -0
  41. package/template/apps/server/src/license/restriction.test.ts +16 -0
  42. package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
  43. package/template/apps/server/src/license/routes.ts +20 -4
  44. package/template/apps/server/src/license/runtime-service.test.ts +10 -2
  45. package/template/apps/server/src/module-bootstrap.ts +14 -23
  46. package/template/apps/server/src/module-composition.test.ts +43 -2
  47. package/template/apps/server/src/module-composition.ts +29 -0
  48. package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
  49. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
  50. package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
  51. package/template/apps/server/src/module-host/durable-context.ts +42 -2
  52. package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
  53. package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
  54. package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
  55. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  56. package/template/apps/server/src/module-host/initialize.test.ts +78 -2
  57. package/template/apps/server/src/module-host/initialize.ts +15 -4
  58. package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
  59. package/template/apps/server/src/module-host/project-attachments.ts +58 -0
  60. package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
  61. package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
  62. package/template/apps/server/src/module-host/project-composition.ts +311 -0
  63. package/template/apps/server/src/module-host/request-context.ts +3 -0
  64. package/template/apps/server/src/module-host/route-declaration.ts +15 -6
  65. package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
  66. package/template/apps/server/src/module-host/route-installer.ts +12 -2
  67. package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
  68. package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
  69. package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
  70. package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
  71. package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
  72. package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
  73. package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
  74. package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
  75. package/template/apps/server/src/module-host/task-definitions.ts +1 -0
  76. package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
  77. package/template/apps/server/src/module-host.test.ts +2 -0
  78. package/template/apps/server/src/module-preflight.test.ts +289 -0
  79. package/template/apps/server/src/module-preflight.ts +96 -0
  80. package/template/apps/server/src/module-runtime-validation.ts +1 -0
  81. package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
  82. package/template/apps/server/src/platform-http-runtime.ts +32 -7
  83. package/template/apps/server/src/route-guards.test.ts +22 -0
  84. package/template/apps/server/src/route-guards.ts +10 -3
  85. package/template/apps/server/src/runtime-audit-query.ts +50 -0
  86. package/template/apps/server/src/runtime-feature.test.ts +46 -0
  87. package/template/apps/server/src/runtime-feature.ts +5 -0
  88. package/template/apps/server/src/runtime.ts +10 -49
  89. package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
  90. package/template/apps/server/src/scheduler/audit.ts +1 -1
  91. package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
  92. package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
  93. package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
  94. package/template/apps/server/src/scheduler/service.test.ts +1 -0
  95. package/template/apps/server/src/scheduler/service.ts +18 -21
  96. package/template/apps/server/src/scheduler/types.ts +5 -1
  97. package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
  98. package/template/apps/server/src/startup-composition-validation.ts +54 -0
  99. package/template/apps/server/src/startup-preflight.test.ts +36 -0
  100. package/template/apps/server/src/startup-preflight.ts +37 -0
  101. package/template/apps/server/src/system/audit-routes.test.ts +9 -3
  102. package/template/apps/server/src/system/department-routes.test.ts +5 -3
  103. package/template/apps/server/src/system/feature-routes.test.ts +4 -3
  104. package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
  105. package/template/apps/server/src/system/interface-catalog.ts +3 -0
  106. package/template/apps/server/src/system/menu-routes.test.ts +5 -3
  107. package/template/apps/server/src/system/route-test-runtime.ts +24 -0
  108. package/template/apps/server/src/system/routes-validation.test.ts +7 -3
  109. package/template/apps/server/src/system/routes.test.ts +21 -11
  110. package/template/apps/server/src/system/routes.ts +11 -2
  111. package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
  112. package/template/apps/server/src/work-order/routes.test.ts +8 -0
  113. package/template/apps/server/src/work-order/task.test.ts +6 -1
  114. package/template/apps/web/Dockerfile +5 -0
  115. package/template/apps/web/package.json +1 -0
  116. package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
  117. package/template/apps/web/src/app/web-composition.ts +24 -0
  118. package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
  119. package/template/apps/web/src/layout/AppSidebar.vue +8 -1
  120. package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
  121. package/template/apps/web/src/layout/navigation.ts +29 -4
  122. package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
  123. package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
  124. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
  125. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
  126. package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
  127. package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
  128. package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
  129. package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
  130. package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
  131. package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
  132. package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
  133. package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
  134. package/template/apps/web/src/router/business-routes.ts +32 -45
  135. package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
  136. package/template/apps/web/src/router/index.ts +64 -54
  137. package/template/apps/web/src/router/manifest-routes.ts +27 -7
  138. package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
  139. package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
  140. package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
  141. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
  142. package/template/apps/web/src/router/web-composition-host.ts +20 -0
  143. package/template/apps/web/src/services/work-order-api.ts +16 -0
  144. package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
  145. package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
  146. package/template/apps/web/vite.config.ts +22 -1
  147. package/template/docs/architecture/migration-bundle.md +10 -7
  148. package/template/docs/architecture/module-manifest-composition.md +85 -3
  149. package/template/docs/architecture/search-provider-protocol.md +6 -1
  150. package/template/docs/architecture/server-module-interface.md +70 -7
  151. package/template/docs/development/business-web-routes.md +53 -50
  152. package/template/docs/development/module-lifecycle.md +48 -54
  153. package/template/docs/platform/agent-runtime.md +2 -0
  154. package/template/docs/platform/navigation-registry.md +5 -4
  155. package/template/package.json +1 -1
  156. package/template/packages/config/package.json +1 -1
  157. package/template/packages/database/package.json +1 -1
  158. package/template/packages/example-work-order/package.json +6 -0
  159. package/template/packages/example-work-order/src/admin-page.ts +78 -0
  160. package/template/packages/example-work-order/src/admin.ts +35 -0
  161. package/template/packages/example-work-order/src/manifest.test.ts +62 -5
  162. package/template/packages/example-work-order/src/manifest.ts +54 -7
  163. package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
  164. package/template/packages/example-work-order/src/server-module.ts +2 -2
  165. package/template/packages/jobs/package.json +1 -1
  166. package/template/packages/modules/index.ts +4 -0
  167. package/template/packages/modules/package.json +3 -2
  168. package/template/packages/modules/src/admin-ui-entry.ts +8 -0
  169. package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
  170. package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
  171. package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
  172. package/template/packages/modules/src/capability-audit-validation.ts +38 -0
  173. package/template/packages/modules/src/capability-guard-validation.ts +148 -0
  174. package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
  175. package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
  176. package/template/packages/modules/src/composition.test.ts +74 -9
  177. package/template/packages/modules/src/composition.ts +8 -0
  178. package/template/packages/modules/src/manifest.ts +1 -0
  179. package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
  180. package/template/packages/modules/src/module-host-range.test.ts +57 -0
  181. package/template/packages/modules/src/module-host-range.ts +125 -0
  182. package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
  183. package/template/packages/modules/src/project-module-assembly.ts +126 -0
  184. package/template/packages/modules/src/system-admin-routes.ts +6 -5
  185. package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
  186. package/template/packages/modules/src/system-agent-tools.ts +2 -2
  187. package/template/packages/modules/src/system-api-binding.ts +43 -9
  188. package/template/packages/modules/src/system-api-control-plane.ts +202 -0
  189. package/template/packages/modules/src/system-api-data-governance.ts +4 -2
  190. package/template/packages/modules/src/system-api-operational.ts +57 -8
  191. package/template/packages/modules/src/system-api-permissions.ts +115 -21
  192. package/template/packages/modules/src/system-api-watermark.ts +3 -0
  193. package/template/packages/modules/src/system-audit-actions.ts +7 -0
  194. package/template/packages/modules/src/system-data-governance.ts +5 -2
  195. package/template/packages/modules/src/system-features.ts +31 -0
  196. package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
  197. package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
  198. package/template/packages/modules/src/system-module-catalog.ts +68 -7
  199. package/template/packages/modules/src/system-module-tasks.ts +8 -1
  200. package/template/packages/modules/src/system-modules-access.test.ts +4 -0
  201. package/template/packages/modules/src/system-modules.test.ts +10 -2
  202. package/template/packages/modules/src/system-modules.ts +6 -5
  203. package/template/packages/modules/src/system-permissions.ts +4 -0
  204. package/template/packages/modules/user/module.ts +19 -2
  205. package/template/packages/server-sdk/index.ts +8 -0
  206. package/template/packages/server-sdk/package.json +2 -1
  207. package/template/packages/server-sdk/src/durable-handler.ts +16 -1
  208. package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
  209. package/template/packages/server-sdk/src/module-adapter.ts +59 -0
  210. package/template/packages/server-sdk/src/route-definition.ts +3 -2
  211. package/template/packages/shared/package.json +1 -1
  212. package/template/packages/shared/src/audit.test.ts +7 -0
  213. package/template/packages/shared/src/audit.ts +16 -0
  214. package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
  215. package/template/packages/shared/src/rbac.ts +2 -0
  216. package/template/apps/web/src/app/business-routes.ts +0 -22
@@ -0,0 +1,44 @@
1
+ import { defineComponent, h, type Component } from "vue";
2
+ import type { AdminPageBinding } from "./admin-ui-composition";
3
+
4
+ export function isolateAdminPageLoader(
5
+ moduleName: string,
6
+ routeKey: string,
7
+ load: AdminPageBinding["load"],
8
+ ): AdminPageBinding["load"] {
9
+ return async () => {
10
+ try {
11
+ const loaded = await load();
12
+ if (!loaded?.default) {
13
+ throw new Error("模块页面缺少 default export");
14
+ }
15
+ return loaded;
16
+ } catch (error) {
17
+ console.error("Admin Extension 页面加载失败", {
18
+ moduleName,
19
+ routeKey,
20
+ error,
21
+ });
22
+ return {
23
+ default: failureComponent(moduleName, routeKey),
24
+ };
25
+ }
26
+ };
27
+ }
28
+
29
+ function failureComponent(moduleName: string, routeKey: string): Component {
30
+ return defineComponent({
31
+ name: "AdminExtensionLoadFailure",
32
+ setup() {
33
+ return () =>
34
+ h("main", { class: "space-y-3 p-6" }, [
35
+ h("h1", { class: "text-xl font-semibold" }, "模块页面暂不可用"),
36
+ h(
37
+ "p",
38
+ { class: "text-sm text-muted-foreground" },
39
+ `模块 ${moduleName} 的页面 ${routeKey} 加载失败。其他管理功能仍可继续使用。`,
40
+ ),
41
+ ]);
42
+ },
43
+ });
44
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { isolateAdminPageLoader } from "./admin-extension-loader";
3
+
4
+ describe("Admin Extension 页面隔离", () => {
5
+ test("正常 loader 原样返回页面组件", async () => {
6
+ const page = { name: "CasePage" };
7
+ const load = isolateAdminPageLoader("case", "case.record", async () => ({
8
+ default: page,
9
+ }));
10
+
11
+ expect(await load()).toEqual({ default: page });
12
+ });
13
+
14
+ test("loader 失败时返回 Admin 壳内故障页而不是抛出", async () => {
15
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
16
+ const load = isolateAdminPageLoader("case", "case.record", async () => {
17
+ throw new Error("secret stack");
18
+ });
19
+
20
+ const result = await load();
21
+
22
+ expect(result.default).toMatchObject({
23
+ name: "AdminExtensionLoadFailure",
24
+ });
25
+ expect(error).toHaveBeenCalledOnce();
26
+ });
27
+
28
+ test("loader 缺少 default export 时同样隔离为故障页", async () => {
29
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
30
+ const load = isolateAdminPageLoader(
31
+ "case",
32
+ "case.record",
33
+ async () => ({}) as never,
34
+ );
35
+
36
+ const result = await load();
37
+
38
+ expect(result.default).toMatchObject({
39
+ name: "AdminExtensionLoadFailure",
40
+ });
41
+ expect(error).toHaveBeenCalledWith(
42
+ "Admin Extension 页面加载失败",
43
+ expect.objectContaining({
44
+ moduleName: "case",
45
+ routeKey: "case.record",
46
+ }),
47
+ );
48
+ });
49
+ });
@@ -0,0 +1,33 @@
1
+ export interface AdminUiCompositionValidationPluginOptions {
2
+ readonly validate?: () => void;
3
+ readonly validatorEntry?: string;
4
+ readonly runValidator?: (entry: string) => number;
5
+ }
6
+
7
+ export interface AdminUiCompositionValidationPlugin {
8
+ readonly name: string;
9
+ readonly apply: "build";
10
+ buildStart(): void;
11
+ }
12
+
13
+ export function adminUiCompositionValidationPlugin(
14
+ options: AdminUiCompositionValidationPluginOptions,
15
+ ): AdminUiCompositionValidationPlugin {
16
+ return {
17
+ name: "windy-admin-ui-composition-validation",
18
+ apply: "build",
19
+ buildStart() {
20
+ if (options.validate) {
21
+ options.validate();
22
+ return;
23
+ }
24
+ if (!options.validatorEntry || !options.runValidator) {
25
+ throw new Error("Admin UI 组合构建门禁缺少 validatorEntry 或执行器");
26
+ }
27
+ const exitCode = options.runValidator(options.validatorEntry);
28
+ if (exitCode !== 0) {
29
+ throw new Error(`Admin UI 组合构建校验失败(exit ${exitCode})`);
30
+ }
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { validateProjectModuleAssembly } from "@southwind-ai/modules/admin-ui";
3
+ import { adminUiCompositionValidationPlugin } from "./admin-ui-composition-plugin";
4
+
5
+ describe("Admin UI 组合构建门禁", () => {
6
+ test("Vite buildStart 使用运行时同一解析器并 fail closed", () => {
7
+ const plugin = adminUiCompositionValidationPlugin({
8
+ validate: () =>
9
+ validateProjectModuleAssembly({ schemaVersion: 2, modules: [] }),
10
+ });
11
+
12
+ expect(typeof plugin.buildStart).toBe("function");
13
+ expect(() => {
14
+ if (typeof plugin.buildStart !== "function") {
15
+ throw new Error("buildStart 未注册");
16
+ }
17
+ plugin.buildStart.call(undefined as never);
18
+ }).toThrow("schemaVersion 不受支持");
19
+ });
20
+ });
@@ -0,0 +1,313 @@
1
+ import {
2
+ indexProjectModuleAssembly,
3
+ satisfiesModuleHostRange,
4
+ type AdminPageBinding as ContractAdminPageBinding,
5
+ type AdminUiManifest,
6
+ type ModuleAdminRouteDefinition,
7
+ type ProjectModuleAssembly,
8
+ type WindyAdminUiModule,
9
+ } from "@southwind-ai/modules/admin-ui";
10
+ import type { Component } from "vue";
11
+
12
+ export const ADMIN_UI_HOST_VERSION = "0.1.0";
13
+
14
+ export type AdminPageBinding = ContractAdminPageBinding<Component>;
15
+ export type AdminUiModule = WindyAdminUiModule<Component>;
16
+
17
+ export interface AdminUiModuleOverride {
18
+ readonly moduleName: string;
19
+ readonly pages: readonly AdminPageBinding[];
20
+ }
21
+
22
+ export interface AdminRoutePresentation {
23
+ readonly routeKey: string;
24
+ readonly hidden?: boolean;
25
+ readonly title?: string;
26
+ readonly icon?: string;
27
+ readonly order?: number;
28
+ }
29
+
30
+ export interface WebCompositionDeclaration {
31
+ readonly overrides: readonly AdminUiModuleOverride[];
32
+ readonly presentation?: readonly AdminRoutePresentation[];
33
+ }
34
+
35
+ export interface ResolvedAdminUiComposition {
36
+ readonly modules: readonly AdminUiModule[];
37
+ readonly adminRoutes: readonly ModuleAdminRouteDefinition[];
38
+ readonly activePlatformRouteKeys: ReadonlySet<string>;
39
+ readonly knownRouteKeys: ReadonlySet<string>;
40
+ readonly activeRouteKeys: ReadonlySet<string>;
41
+ readonly visibleRouteKeys: ReadonlySet<string>;
42
+ readonly presentation: ReadonlyMap<string, AdminRoutePresentation>;
43
+ }
44
+
45
+ export function defineWebComposition(
46
+ declaration: WebCompositionDeclaration,
47
+ ): WebCompositionDeclaration {
48
+ return declaration;
49
+ }
50
+
51
+ export function resolveAdminUiComposition(input: {
52
+ readonly declaration: WebCompositionDeclaration;
53
+ readonly assembly: ProjectModuleAssembly;
54
+ readonly platformModules: readonly AdminUiManifest[];
55
+ readonly installedModules: readonly AdminUiModule[];
56
+ readonly hostVersion?: string;
57
+ }): ResolvedAdminUiComposition {
58
+ const hostVersion = input.hostVersion ?? ADMIN_UI_HOST_VERSION;
59
+ const assemblyByName = indexProjectModuleAssembly(input.assembly);
60
+ const platformByName = uniqueManifests("平台", input.platformModules);
61
+ const installedByName = uniqueInstalledModules(input.installedModules);
62
+ validatePlatformAssembly(platformByName, assemblyByName);
63
+ validateInstalledAssembly(installedByName, assemblyByName, hostVersion);
64
+
65
+ const overrides = uniqueOverrides(input.declaration.overrides);
66
+ const modules = [...installedByName.values()]
67
+ .map((module) => applyOverride(module, overrides.get(module.manifest.name)))
68
+ .map(validatePageBindings)
69
+ .sort(
70
+ (left, right) =>
71
+ assemblyByName.get(left.manifest.name)!.order -
72
+ assemblyByName.get(right.manifest.name)!.order,
73
+ );
74
+
75
+ validateAdminRouteIdentities([
76
+ ...platformByName.values(),
77
+ ...modules.map(({ manifest }) => manifest),
78
+ ]);
79
+
80
+ const activePlatformRoutes = [...platformByName.values()].flatMap(
81
+ (manifest) =>
82
+ assemblyByName.get(manifest.name)!.enabled ? manifest.adminRoutes : [],
83
+ );
84
+ const businessRoutes = modules.flatMap(
85
+ ({ manifest }) => manifest.adminRoutes,
86
+ );
87
+ const adminRoutes = [...activePlatformRoutes, ...businessRoutes];
88
+ const knownRouteKeys = new Set(
89
+ [
90
+ ...platformByName.values(),
91
+ ...modules.map(({ manifest }) => manifest),
92
+ ].flatMap(({ adminRoutes }) => adminRoutes.map(({ key }) => key)),
93
+ );
94
+ const presentation = validatePresentation(
95
+ input.declaration.presentation ?? [],
96
+ knownRouteKeys,
97
+ );
98
+ const activePlatformRouteKeys = new Set(
99
+ activePlatformRoutes.map(({ key }) => key),
100
+ );
101
+ const activeRouteKeys = new Set(adminRoutes.map(({ key }) => key));
102
+ const visibleRouteKeys = new Set([
103
+ ...activePlatformRoutes.flatMap(({ key }) => {
104
+ const owner = [...platformByName.values()].find((manifest) =>
105
+ manifest.adminRoutes.some((route) => route.key === key),
106
+ )!;
107
+ return assemblyByName.get(owner.name)!.admin.visible ? [key] : [];
108
+ }),
109
+ ...businessRoutes.flatMap(({ key }) => {
110
+ const owner = modules.find(({ manifest }) =>
111
+ manifest.adminRoutes.some((route) => route.key === key),
112
+ )!;
113
+ return assemblyByName.get(owner.manifest.name)!.admin.visible
114
+ ? [key]
115
+ : [];
116
+ }),
117
+ ]);
118
+
119
+ return {
120
+ modules,
121
+ adminRoutes,
122
+ activePlatformRouteKeys,
123
+ knownRouteKeys,
124
+ activeRouteKeys,
125
+ visibleRouteKeys,
126
+ presentation,
127
+ };
128
+ }
129
+
130
+ export function validateAdminRoutePathCollisions(
131
+ composition: ResolvedAdminUiComposition,
132
+ reservedPaths: readonly string[],
133
+ ): void {
134
+ const paths = new Set<string>();
135
+ for (const path of [
136
+ ...reservedPaths,
137
+ ...composition.adminRoutes.map(({ path }) => path),
138
+ ]) {
139
+ if (paths.has(path)) throw new Error(`Web Route 路径重复:${path}`);
140
+ paths.add(path);
141
+ }
142
+ }
143
+
144
+ function uniqueManifests(
145
+ label: string,
146
+ manifests: readonly AdminUiManifest[],
147
+ ): ReadonlyMap<string, AdminUiManifest> {
148
+ const result = new Map<string, AdminUiManifest>();
149
+ for (const manifest of manifests) {
150
+ if (!manifest.name.trim() || !manifest.version.trim()) {
151
+ throw new Error(`${label} Admin UI Manifest 身份无效`);
152
+ }
153
+ if (result.has(manifest.name)) {
154
+ throw new Error(`${label} Admin UI Module 重复:${manifest.name}`);
155
+ }
156
+ result.set(manifest.name, manifest);
157
+ }
158
+ return result;
159
+ }
160
+
161
+ function uniqueInstalledModules(
162
+ modules: readonly AdminUiModule[],
163
+ ): ReadonlyMap<string, AdminUiModule> {
164
+ const result = new Map<string, AdminUiModule>();
165
+ for (const module of modules) {
166
+ const name = module.manifest.name;
167
+ if (result.has(name)) throw new Error(`Admin UI Module 重复:${name}`);
168
+ result.set(name, module);
169
+ }
170
+ return result;
171
+ }
172
+
173
+ function validatePlatformAssembly(
174
+ platformByName: ReadonlyMap<string, AdminUiManifest>,
175
+ assemblyByName: ReturnType<typeof indexProjectModuleAssembly>,
176
+ ): void {
177
+ for (const name of platformByName.keys()) {
178
+ const entry = assemblyByName.get(name);
179
+ if (!entry) {
180
+ throw new Error(`平台 Module 未在 windy.modules.json 注册:${name}`);
181
+ }
182
+ if (entry.package !== null) {
183
+ throw new Error(
184
+ `平台 Module 在 windy.modules.json 中必须使用 package=null:${name}`,
185
+ );
186
+ }
187
+ }
188
+ for (const entry of assemblyByName.values()) {
189
+ if (entry.package === null && !platformByName.has(entry.name)) {
190
+ throw new Error(
191
+ `windy.modules.json 引用了未知平台 Module:${entry.name}`,
192
+ );
193
+ }
194
+ }
195
+ }
196
+
197
+ function validateInstalledAssembly(
198
+ installedByName: ReadonlyMap<string, AdminUiModule>,
199
+ assemblyByName: ReturnType<typeof indexProjectModuleAssembly>,
200
+ hostVersion: string,
201
+ ): void {
202
+ for (const module of installedByName.values()) {
203
+ const entry = assemblyByName.get(module.manifest.name);
204
+ if (!entry) {
205
+ throw new Error(
206
+ `Admin UI Module 未在 windy.modules.json 注册:${module.manifest.name}`,
207
+ );
208
+ }
209
+ if (!entry.enabled) {
210
+ throw new Error(`已禁用 Module 不应加载 Admin UI:${entry.name}`);
211
+ }
212
+ if (entry.package === null) {
213
+ throw new Error(`平台 Module 不能使用 package Admin UI:${entry.name}`);
214
+ }
215
+ if (!satisfiesModuleHostRange(hostVersion, module.hostRange)) {
216
+ throw new Error(
217
+ `Admin UI Module ${module.manifest.name} 要求 Host ${module.hostRange},当前为 ${hostVersion}`,
218
+ );
219
+ }
220
+ }
221
+ for (const entry of assemblyByName.values()) {
222
+ if (
223
+ entry.package !== null &&
224
+ entry.enabled &&
225
+ !installedByName.has(entry.name)
226
+ ) {
227
+ throw new Error(`已启用 Module 缺少标准 Admin UI 导出:${entry.name}`);
228
+ }
229
+ }
230
+ }
231
+
232
+ function uniqueOverrides(
233
+ overrides: readonly AdminUiModuleOverride[],
234
+ ): ReadonlyMap<string, AdminUiModuleOverride> {
235
+ const result = new Map<string, AdminUiModuleOverride>();
236
+ for (const override of overrides) {
237
+ if (result.has(override.moduleName)) {
238
+ throw new Error(`Admin UI Override 重复:${override.moduleName}`);
239
+ }
240
+ result.set(override.moduleName, override);
241
+ }
242
+ return result;
243
+ }
244
+
245
+ function applyOverride(
246
+ module: AdminUiModule,
247
+ override: AdminUiModuleOverride | undefined,
248
+ ): AdminUiModule {
249
+ if (!override) return module;
250
+ const pages = new Map(
251
+ module.pages.map((page) => [page.routeKey, page] as const),
252
+ );
253
+ for (const page of override.pages) pages.set(page.routeKey, page);
254
+ return { ...module, pages: [...pages.values()] };
255
+ }
256
+
257
+ function validatePageBindings(module: AdminUiModule): AdminUiModule {
258
+ const declared = new Set(module.manifest.adminRoutes.map(({ key }) => key));
259
+ const bound = new Set<string>();
260
+ for (const page of module.pages) {
261
+ if (!declared.has(page.routeKey)) {
262
+ throw new Error(
263
+ `${module.manifest.name} 绑定了未知 Admin Route:${page.routeKey}`,
264
+ );
265
+ }
266
+ if (bound.has(page.routeKey)) {
267
+ throw new Error(
268
+ `${module.manifest.name} Admin Route 重复绑定:${page.routeKey}`,
269
+ );
270
+ }
271
+ bound.add(page.routeKey);
272
+ }
273
+ for (const routeKey of declared) {
274
+ if (!bound.has(routeKey)) {
275
+ throw new Error(
276
+ `${module.manifest.name} 缺少 Admin Route 页面绑定:${routeKey}`,
277
+ );
278
+ }
279
+ }
280
+ return module;
281
+ }
282
+
283
+ function validateAdminRouteIdentities(
284
+ manifests: readonly AdminUiManifest[],
285
+ ): void {
286
+ const keys = new Set<string>();
287
+ const paths = new Set<string>();
288
+ for (const route of manifests.flatMap(({ adminRoutes }) => adminRoutes)) {
289
+ if (keys.has(route.key)) throw new Error(`Admin Route 重复:${route.key}`);
290
+ if (paths.has(route.path)) {
291
+ throw new Error(`Admin Route 路径重复:${route.path}`);
292
+ }
293
+ keys.add(route.key);
294
+ paths.add(route.path);
295
+ }
296
+ }
297
+
298
+ function validatePresentation(
299
+ entries: readonly AdminRoutePresentation[],
300
+ knownRouteKeys: ReadonlySet<string>,
301
+ ): ReadonlyMap<string, AdminRoutePresentation> {
302
+ const byRoute = new Map<string, AdminRoutePresentation>();
303
+ for (const entry of entries) {
304
+ if (!knownRouteKeys.has(entry.routeKey)) {
305
+ throw new Error(`Admin UI 呈现引用未知 Route:${entry.routeKey}`);
306
+ }
307
+ if (byRoute.has(entry.routeKey)) {
308
+ throw new Error(`Admin UI 呈现重复:${entry.routeKey}`);
309
+ }
310
+ byRoute.set(entry.routeKey, entry);
311
+ }
312
+ return byRoute;
313
+ }
@@ -0,0 +1,225 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { AdminUiManifest } from "@southwind-ai/modules/admin-ui";
3
+ import {
4
+ defineWebComposition,
5
+ resolveAdminUiComposition,
6
+ validateAdminRoutePathCollisions,
7
+ type AdminUiModule,
8
+ } from "./admin-ui-composition";
9
+
10
+ describe("Admin UI 模块组合", () => {
11
+ test("浏览器 Host 只消费身份、版本、Admin Route 与页面 loader", () => {
12
+ const load = async () => ({ default: {} });
13
+ const composition = resolveAdminUiComposition({
14
+ declaration: defineWebComposition({ overrides: [] }),
15
+ assembly: assembly([assemblyEntry("case", 100)]),
16
+ platformModules: [],
17
+ installedModules: [installed(caseManifest(), load)],
18
+ });
19
+
20
+ expect(composition.modules).toEqual([
21
+ {
22
+ hostRange: "^0.1.0",
23
+ manifest: caseManifest(),
24
+ pages: [{ routeKey: "case.record", load }],
25
+ },
26
+ ]);
27
+ expect(composition.modules[0]!.manifest).not.toHaveProperty("permissions");
28
+ expect(composition.modules[0]!.manifest).not.toHaveProperty("migrations");
29
+ });
30
+
31
+ test("启用控制路由,admin.visible 只控制入口呈现", () => {
32
+ const composition = resolveAdminUiComposition({
33
+ declaration: { overrides: [] },
34
+ assembly: assembly([
35
+ assemblyEntry("case", 200, true, false),
36
+ assemblyEntry("lead", 100),
37
+ assemblyEntry("disabled", 300, false),
38
+ ]),
39
+ platformModules: [],
40
+ installedModules: [
41
+ installed(caseManifest("case", "/cases", "案件")),
42
+ installed(caseManifest("lead", "/leads", "线索")),
43
+ ],
44
+ });
45
+
46
+ expect(composition.modules.map(({ manifest }) => manifest.name)).toEqual([
47
+ "lead",
48
+ "case",
49
+ ]);
50
+ expect(composition.activeRouteKeys.has("case.record")).toBe(true);
51
+ expect(composition.visibleRouteKeys.has("case.record")).toBe(false);
52
+ expect(composition.activeRouteKeys.has("disabled.record")).toBe(false);
53
+ });
54
+
55
+ test("项目 override 可替换 package 默认页面 loader", () => {
56
+ const packageLoad = async () => ({ default: { name: "Package" } });
57
+ const projectLoad = async () => ({ default: { name: "Project" } });
58
+ const composition = resolveAdminUiComposition({
59
+ declaration: {
60
+ overrides: [
61
+ {
62
+ moduleName: "case",
63
+ pages: [{ routeKey: "case.record", load: projectLoad }],
64
+ },
65
+ ],
66
+ },
67
+ assembly: assembly([assemblyEntry("case", 100)]),
68
+ platformModules: [],
69
+ installedModules: [installed(caseManifest(), packageLoad)],
70
+ });
71
+
72
+ expect(composition.modules[0]?.pages[0]?.load).toBe(projectLoad);
73
+ });
74
+
75
+ test("纯 API 或 Task Module 用空 Admin 导出完成自动装配", () => {
76
+ const composition = resolveAdminUiComposition({
77
+ declaration: { overrides: [] },
78
+ assembly: assembly([assemblyEntry("jobs", 100)]),
79
+ platformModules: [],
80
+ installedModules: [
81
+ {
82
+ manifest: { name: "jobs", version: "1.0.0", adminRoutes: [] },
83
+ hostRange: "^0.1.0",
84
+ pages: [],
85
+ },
86
+ ],
87
+ });
88
+
89
+ expect(composition.modules[0]?.manifest.name).toBe("jobs");
90
+ expect(composition.adminRoutes).toEqual([]);
91
+ });
92
+
93
+ test("未知、重复、Host 不兼容和页面绑定缺失均 fail closed", () => {
94
+ const valid = installed(caseManifest());
95
+ expect(() =>
96
+ resolveAdminUiComposition({
97
+ declaration: { overrides: [] },
98
+ assembly: assembly([assemblyEntry("other", 100)]),
99
+ platformModules: [],
100
+ installedModules: [valid],
101
+ }),
102
+ ).toThrow("未在 windy.modules.json 注册:case");
103
+ expect(() =>
104
+ resolveAdminUiComposition({
105
+ declaration: { overrides: [] },
106
+ assembly: assembly([assemblyEntry("case", 100)]),
107
+ platformModules: [],
108
+ installedModules: [valid, valid],
109
+ }),
110
+ ).toThrow("Admin UI Module 重复:case");
111
+ expect(() =>
112
+ resolveAdminUiComposition({
113
+ declaration: { overrides: [] },
114
+ assembly: assembly([assemblyEntry("case", 100)]),
115
+ platformModules: [],
116
+ installedModules: [{ ...valid, hostRange: "^1.0.0" }],
117
+ }),
118
+ ).toThrow("要求 Host ^1.0.0");
119
+ expect(() =>
120
+ resolveAdminUiComposition({
121
+ declaration: { overrides: [] },
122
+ assembly: assembly([assemblyEntry("case", 100)]),
123
+ platformModules: [],
124
+ installedModules: [{ ...valid, pages: [] }],
125
+ }),
126
+ ).toThrow("缺少 Admin Route 页面绑定:case.record");
127
+ });
128
+
129
+ test("平台清单与 package 标准导出都必须和装配清单双向对应", () => {
130
+ expect(() =>
131
+ resolveAdminUiComposition({
132
+ declaration: { overrides: [] },
133
+ assembly: assembly([]),
134
+ platformModules: [caseManifest()],
135
+ installedModules: [],
136
+ }),
137
+ ).toThrow("平台 Module 未在 windy.modules.json 注册:case");
138
+ expect(() =>
139
+ resolveAdminUiComposition({
140
+ declaration: { overrides: [] },
141
+ assembly: assembly([assemblyEntry("ghost", 100, true, true, null)]),
142
+ platformModules: [],
143
+ installedModules: [],
144
+ }),
145
+ ).toThrow("windy.modules.json 引用了未知平台 Module:ghost");
146
+ expect(() =>
147
+ resolveAdminUiComposition({
148
+ declaration: { overrides: [] },
149
+ assembly: assembly([assemblyEntry("case", 100)]),
150
+ platformModules: [],
151
+ installedModules: [],
152
+ }),
153
+ ).toThrow("已启用 Module 缺少标准 Admin UI 导出:case");
154
+ });
155
+
156
+ test("构建门禁拒绝与登录、首页或 Admin 壳路由碰撞", () => {
157
+ const composition = resolveAdminUiComposition({
158
+ declaration: { overrides: [] },
159
+ assembly: assembly([assemblyEntry("case", 100)]),
160
+ platformModules: [],
161
+ installedModules: [installed(caseManifest("case", "/admin"))],
162
+ });
163
+
164
+ expect(() =>
165
+ validateAdminRoutePathCollisions(composition, ["/login", "/admin"]),
166
+ ).toThrow("Web Route 路径重复:/admin");
167
+ });
168
+ });
169
+
170
+ function assembly(modules: ReturnType<typeof assemblyEntry>[]) {
171
+ return { schemaVersion: 1 as const, modules };
172
+ }
173
+
174
+ function assemblyEntry(
175
+ name: string,
176
+ order: number,
177
+ enabled = true,
178
+ visible = true,
179
+ packageName: string | null = `@customer/${name}`,
180
+ ) {
181
+ return {
182
+ name,
183
+ package: packageName,
184
+ enabled,
185
+ order,
186
+ admin: { visible },
187
+ config: {},
188
+ };
189
+ }
190
+
191
+ function installed(
192
+ manifest: AdminUiManifest,
193
+ load: AdminUiModule["pages"][number]["load"] = async () => ({
194
+ default: {},
195
+ }),
196
+ ): AdminUiModule {
197
+ return {
198
+ manifest,
199
+ hostRange: "^0.1.0",
200
+ pages: manifest.adminRoutes.map(({ key }) => ({ routeKey: key, load })),
201
+ };
202
+ }
203
+
204
+ function caseManifest(
205
+ name = "case",
206
+ path = "/cases",
207
+ title = "案件",
208
+ ): AdminUiManifest {
209
+ const key = `${name}.record`;
210
+ return {
211
+ name,
212
+ version: "1.2.3",
213
+ adminRoutes: [
214
+ {
215
+ key,
216
+ path,
217
+ title,
218
+ surface: "sidebar",
219
+ directAccess: "guarded",
220
+ featureKey: key,
221
+ permissionKey: `${key}.read`,
222
+ },
223
+ ],
224
+ };
225
+ }