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
@@ -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
+ });
@@ -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"),
@@ -23,6 +23,7 @@ import {
23
23
  import { Input } from "@/components/ui/input";
24
24
  import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
25
25
  import { useGlobalSearch } from "@/composables/useGlobalSearch";
26
+ import { webAdminUiComposition } from "@/router/web-composition-host";
26
27
  import GlobalSearchResults from "./GlobalSearchResults.vue";
27
28
  import { authorizedSidebarMenus } from "./navigation";
28
29
 
@@ -42,7 +43,13 @@ const businessSearch = useGlobalSearch(query);
42
43
 
43
44
  const items = computed(() =>
44
45
  access.snapshot.value
45
- ? flattenMenus(authorizedSidebarMenus(access.snapshot.value))
46
+ ? flattenMenus(
47
+ authorizedSidebarMenus(
48
+ access.snapshot.value,
49
+ webAdminUiComposition.presentation,
50
+ webAdminUiComposition,
51
+ ),
52
+ )
46
53
  : [],
47
54
  );
48
55
  const results = computed(() => {
@@ -5,6 +5,7 @@ import type {
5
5
  MenuNode,
6
6
  PermissionDefinition,
7
7
  } from "@southwind-ai/shared";
8
+ import type { AdminRoutePresentation } from "../router/admin-ui-composition";
8
9
 
9
10
  export interface WebAccessActor {
10
11
  id: string;
@@ -47,6 +48,11 @@ export interface WebAccessDecision {
47
48
  reason: "allowed" | "missing-permission" | "feature-disabled";
48
49
  }
49
50
 
51
+ export interface AdminRouteActivation {
52
+ readonly knownRouteKeys: ReadonlySet<string>;
53
+ readonly visibleRouteKeys: ReadonlySet<string>;
54
+ }
55
+
50
56
  export const dashboardMenu: MenuNode = {
51
57
  key: "dashboard",
52
58
  title: "工作台",
@@ -80,9 +86,11 @@ export function evaluateWebAccess(
80
86
 
81
87
  export function authorizedSidebarMenus(
82
88
  snapshot: WebAccessSnapshot,
89
+ presentation: ReadonlyMap<string, AdminRoutePresentation> = new Map(),
90
+ activation?: AdminRouteActivation,
83
91
  ): MenuNode[] {
84
92
  return [dashboardMenu, ...snapshot.menus]
85
- .map((menu) => authorizeMenu(menu, snapshot))
93
+ .map((menu) => authorizeMenu(menu, snapshot, presentation, activation))
86
94
  .filter((menu): menu is MenuNode => Boolean(menu))
87
95
  .sort((left, right) => left.order - right.order);
88
96
  }
@@ -94,14 +102,31 @@ export function canToggleFeatureFlags(snapshot: WebAccessSnapshot): boolean {
94
102
  function authorizeMenu(
95
103
  menu: MenuNode,
96
104
  snapshot: WebAccessSnapshot,
105
+ presentation: ReadonlyMap<string, AdminRoutePresentation>,
106
+ activation?: AdminRouteActivation,
97
107
  ): MenuNode | undefined {
98
108
  if (!menu.visible) return undefined;
109
+ if (
110
+ activation?.knownRouteKeys.has(menu.key) &&
111
+ !activation.visibleRouteKeys.has(menu.key)
112
+ ) {
113
+ return undefined;
114
+ }
115
+ const override = presentation.get(menu.key);
116
+ if (override?.hidden) return undefined;
99
117
  const children = menu.children
100
- ?.map((child) => authorizeMenu(child, snapshot))
101
- .filter((child): child is MenuNode => Boolean(child));
118
+ ?.map((child) => authorizeMenu(child, snapshot, presentation, activation))
119
+ .filter((child): child is MenuNode => Boolean(child))
120
+ .sort((left, right) => left.order - right.order);
102
121
  const access = evaluateWebAccess(snapshot, menu);
103
122
  if (!access.allowed || (menu.type === "catalog" && children?.length === 0)) {
104
123
  return undefined;
105
124
  }
106
- return { ...menu, children };
125
+ return {
126
+ ...menu,
127
+ ...(override?.title === undefined ? {} : { title: override.title }),
128
+ ...(override?.icon === undefined ? {} : { icon: override.icon }),
129
+ ...(override?.order === undefined ? {} : { order: override.order }),
130
+ children,
131
+ };
107
132
  }
@@ -6,6 +6,7 @@ import {
6
6
  evaluateWebAccess,
7
7
  type WebAccessSnapshot,
8
8
  } from "./navigation";
9
+ import type { AdminRoutePresentation } from "../router/admin-ui-composition";
9
10
 
10
11
  describe("Web 访问快照", () => {
11
12
  test("菜单同时受服务端权限和 Feature 有效态约束", () => {
@@ -45,6 +46,56 @@ describe("Web 访问快照", () => {
45
46
  "system.audit",
46
47
  );
47
48
  });
49
+
50
+ test("客户可按稳定 Route Key 隐藏、排序或替换菜单呈现", () => {
51
+ const snapshot = accessSnapshot();
52
+ snapshot.menus[0]!.children!.push({
53
+ key: "system.role",
54
+ title: "角色管理",
55
+ type: "menu",
56
+ order: 20,
57
+ visible: true,
58
+ path: "/admin/system/roles",
59
+ });
60
+ const presentation = new Map<string, AdminRoutePresentation>([
61
+ [
62
+ "system.audit",
63
+ {
64
+ routeKey: "system.audit",
65
+ title: "操作留痕",
66
+ icon: "scroll-text",
67
+ order: 30,
68
+ },
69
+ ],
70
+ [
71
+ "system.role",
72
+ {
73
+ routeKey: "system.role",
74
+ hidden: true,
75
+ },
76
+ ],
77
+ ]);
78
+
79
+ const menus = authorizedSidebarMenus(snapshot, presentation);
80
+ const audit = menus[1]?.children?.[0];
81
+
82
+ expect(audit).toMatchObject({
83
+ key: "system.audit",
84
+ title: "操作留痕",
85
+ icon: "scroll-text",
86
+ order: 30,
87
+ });
88
+ expect(flattenKeys(menus)).not.toContain("system.role");
89
+ });
90
+
91
+ test("根装配清单关闭 Admin 呈现后不保留失效菜单", () => {
92
+ const menus = authorizedSidebarMenus(accessSnapshot(), new Map(), {
93
+ knownRouteKeys: new Set(["system.audit"]),
94
+ visibleRouteKeys: new Set(),
95
+ });
96
+
97
+ expect(flattenKeys(menus)).not.toContain("system.audit");
98
+ });
48
99
  });
49
100
 
50
101
  function accessSnapshot(): WebAccessSnapshot {
@@ -5,6 +5,7 @@ import {
5
5
  authorizedSidebarMenus,
6
6
  type WebAccessSnapshot,
7
7
  } from "@/layout/navigation";
8
+ import { webAdminUiComposition } from "@/router/web-composition-host";
8
9
 
9
10
  export interface DashboardMetric {
10
11
  label: string;
@@ -32,7 +33,11 @@ export function buildDashboardModel(
32
33
  ): DashboardModel {
33
34
  const features = snapshot?.features || [];
34
35
  const availableMenuCount = snapshot
35
- ? authorizedSidebarMenus(snapshot)
36
+ ? authorizedSidebarMenus(
37
+ snapshot,
38
+ webAdminUiComposition.presentation,
39
+ webAdminUiComposition,
40
+ )
36
41
  .flatMap((menu) => (menu.children?.length ? menu.children : [menu]))
37
42
  .filter((menu) => menu.key !== "dashboard" && Boolean(menu.path)).length
38
43
  : 0;
@@ -0,0 +1,202 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ WorkOrder,
4
+ WorkOrderPriority,
5
+ WorkOrderWorkflowStatus,
6
+ } from "@southwind-ai/example-work-order/contract";
7
+ import { DEFAULT_PAGE_SIZE } from "@southwind-ai/shared";
8
+ import { RefreshCwIcon, SearchIcon } from "@lucide/vue";
9
+ import { onMounted, shallowRef } from "vue";
10
+ import DataPagination from "@/components/common/DataPagination.vue";
11
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { Button } from "@/components/ui/button";
14
+ import {
15
+ Card,
16
+ CardContent,
17
+ CardDescription,
18
+ CardHeader,
19
+ CardTitle,
20
+ } from "@/components/ui/card";
21
+ import { Input } from "@/components/ui/input";
22
+ import {
23
+ Table,
24
+ TableBody,
25
+ TableCell,
26
+ TableEmpty,
27
+ TableHead,
28
+ TableHeader,
29
+ TableRow,
30
+ } from "@/components/ui/table";
31
+ import { formatDateTime } from "@/lib/date-time";
32
+ import { listWorkOrders } from "@/services/work-order-api";
33
+
34
+ const items = shallowRef<WorkOrder[]>([]);
35
+ const page = shallowRef(1);
36
+ const pageSize = shallowRef(DEFAULT_PAGE_SIZE);
37
+ const total = shallowRef(0);
38
+ const query = shallowRef("");
39
+ const loading = shallowRef(false);
40
+ const error = shallowRef("");
41
+
42
+ onMounted(() => void load());
43
+
44
+ async function load() {
45
+ if (loading.value) return;
46
+ loading.value = true;
47
+ error.value = "";
48
+ try {
49
+ const result = await listWorkOrders({
50
+ page: page.value,
51
+ pageSize: pageSize.value,
52
+ q: query.value,
53
+ });
54
+ items.value = result.items;
55
+ page.value = result.page;
56
+ pageSize.value = result.pageSize;
57
+ total.value = result.total;
58
+ } catch (currentError) {
59
+ error.value =
60
+ currentError instanceof Error ? currentError.message : "工单加载失败";
61
+ } finally {
62
+ loading.value = false;
63
+ }
64
+ }
65
+
66
+ function search() {
67
+ page.value = 1;
68
+ void load();
69
+ }
70
+
71
+ function setPage(value: number) {
72
+ page.value = value;
73
+ void load();
74
+ }
75
+
76
+ function setPageSize(value: number) {
77
+ pageSize.value = value;
78
+ page.value = 1;
79
+ void load();
80
+ }
81
+
82
+ const priorityLabels: Record<WorkOrderPriority, string> = {
83
+ low: "低",
84
+ medium: "中",
85
+ high: "高",
86
+ };
87
+ const workflowLabels: Record<WorkOrderWorkflowStatus, string> = {
88
+ open: "待处理",
89
+ "in-progress": "处理中",
90
+ resolved: "已解决",
91
+ };
92
+ </script>
93
+
94
+ <template>
95
+ <main class="space-y-5">
96
+ <header class="space-y-2">
97
+ <h1 class="text-2xl font-semibold text-foreground">工单管理</h1>
98
+ <p class="text-sm text-muted-foreground">
99
+ 示例业务模块提供的工单列表,入口、权限和功能开关均来自 Module Manifest。
100
+ </p>
101
+ </header>
102
+
103
+ <Alert v-if="error" variant="destructive">
104
+ <AlertTitle>无法加载工单</AlertTitle>
105
+ <AlertDescription>{{ error }}</AlertDescription>
106
+ </Alert>
107
+
108
+ <Card>
109
+ <CardHeader>
110
+ <CardTitle>工单列表</CardTitle>
111
+ <CardDescription>
112
+ 按当前账号的数据范围查看工单,可使用标题或描述搜索。
113
+ </CardDescription>
114
+ </CardHeader>
115
+ <CardContent class="space-y-4">
116
+ <form class="flex flex-col gap-2 sm:flex-row" @submit.prevent="search">
117
+ <Input
118
+ v-model="query"
119
+ aria-label="搜索工单"
120
+ placeholder="搜索标题或描述"
121
+ class="sm:max-w-sm"
122
+ />
123
+ <Button type="submit" :disabled="loading">
124
+ <SearchIcon data-icon="inline-start" />
125
+ 搜索
126
+ </Button>
127
+ <Button
128
+ type="button"
129
+ variant="outline"
130
+ :disabled="loading"
131
+ @click="load"
132
+ >
133
+ <RefreshCwIcon
134
+ data-icon="inline-start"
135
+ :class="{ 'animate-spin': loading }"
136
+ />
137
+ 刷新
138
+ </Button>
139
+ </form>
140
+
141
+ <div class="overflow-hidden rounded-md border">
142
+ <Table>
143
+ <TableHeader>
144
+ <TableRow>
145
+ <TableHead>工单</TableHead>
146
+ <TableHead>优先级</TableHead>
147
+ <TableHead>进度</TableHead>
148
+ <TableHead>负责人</TableHead>
149
+ <TableHead>最近更新</TableHead>
150
+ </TableRow>
151
+ </TableHeader>
152
+ <TableBody>
153
+ <TableEmpty v-if="!loading && items.length === 0" :colspan="5">
154
+ 暂无符合条件的工单
155
+ </TableEmpty>
156
+ <TableRow v-for="item in items" :key="item.id">
157
+ <TableCell>
158
+ <div class="space-y-1">
159
+ <p class="font-medium text-foreground">{{ item.title }}</p>
160
+ <p class="line-clamp-2 text-xs text-muted-foreground">
161
+ {{ item.description }}
162
+ </p>
163
+ </div>
164
+ </TableCell>
165
+ <TableCell>
166
+ <Badge
167
+ :variant="
168
+ item.priority === 'high' ? 'destructive' : 'outline'
169
+ "
170
+ >
171
+ {{ priorityLabels[item.priority] }}
172
+ </Badge>
173
+ </TableCell>
174
+ <TableCell>
175
+ <Badge
176
+ :variant="
177
+ item.workflowStatus === 'resolved'
178
+ ? 'secondary'
179
+ : 'default'
180
+ "
181
+ >
182
+ {{ workflowLabels[item.workflowStatus] }}
183
+ </Badge>
184
+ </TableCell>
185
+ <TableCell>{{ item.ownerId }}</TableCell>
186
+ <TableCell>{{ formatDateTime(item.updatedAt) }}</TableCell>
187
+ </TableRow>
188
+ </TableBody>
189
+ </Table>
190
+ </div>
191
+
192
+ <DataPagination
193
+ :page="page"
194
+ :page-size="pageSize"
195
+ :total="total"
196
+ @update:page="setPage"
197
+ @update:page-size="setPageSize"
198
+ />
199
+ </CardContent>
200
+ </Card>
201
+ </main>
202
+ </template>
@@ -0,0 +1,51 @@
1
+ import { flushPromises, mount } from "@vue/test-utils";
2
+ import { beforeEach, describe, expect, test, vi } from "vitest";
3
+ import WorkOrderListPage from "./WorkOrderListPage.vue";
4
+
5
+ const mocks = vi.hoisted(() => ({
6
+ list: vi.fn(),
7
+ }));
8
+
9
+ vi.mock("@/services/work-order-api", () => ({
10
+ listWorkOrders: mocks.list,
11
+ }));
12
+
13
+ describe("WorkOrderListPage", () => {
14
+ beforeEach(() => {
15
+ mocks.list.mockReset();
16
+ mocks.list.mockResolvedValue({
17
+ items: [
18
+ {
19
+ id: "ticket_1",
20
+ title: "登录异常",
21
+ description: "用户无法进入工作台",
22
+ priority: "high",
23
+ workflowStatus: "in-progress",
24
+ ownerId: "user_1",
25
+ status: "active",
26
+ createdAt: "2026-07-29T01:00:00.000Z",
27
+ createdBy: "user_1",
28
+ updatedAt: "2026-07-29T02:00:00.000Z",
29
+ updatedBy: "user_1",
30
+ },
31
+ ],
32
+ page: 1,
33
+ pageSize: 10,
34
+ total: 1,
35
+ });
36
+ });
37
+
38
+ test("加载并呈现当前数据范围内的工单", async () => {
39
+ const wrapper = mount(WorkOrderListPage);
40
+ await flushPromises();
41
+
42
+ expect(mocks.list).toHaveBeenCalledWith({
43
+ page: 1,
44
+ pageSize: 10,
45
+ q: "",
46
+ });
47
+ expect(wrapper.text()).toContain("登录异常");
48
+ expect(wrapper.text()).toContain("高");
49
+ expect(wrapper.text()).toContain("处理中");
50
+ });
51
+ });