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
@@ -10,6 +10,7 @@ import type {
10
10
  InitializedModuleHosts,
11
11
  InitializeModuleHostsInput,
12
12
  RegisteredModuleAiOperationHandler,
13
+ RegisteredModuleDurableHandler,
13
14
  RegisteredModuleGovernedResource,
14
15
  RegisteredModuleRolePreset,
15
16
  RegisteredModuleRoute,
@@ -26,11 +27,13 @@ import {
26
27
  } from "./resource-file-policies.js";
27
28
  import { validateRouteDeclaration } from "./route-declaration.js";
28
29
  import { createScopedSandboxHost } from "./sandbox-tool-registration.js";
30
+ import { validateSearchProviderRegistration } from "./search-provider-registration.js";
29
31
 
30
32
  export type {
31
33
  InitializedModuleHosts,
32
34
  InitializeModuleHostsInput,
33
35
  RegisteredModuleAiOperationHandler,
36
+ RegisteredModuleDurableHandler,
34
37
  RegisteredModuleGovernedResource,
35
38
  RegisteredModuleRolePreset,
36
39
  RegisteredModuleRoute,
@@ -73,6 +76,7 @@ export async function initializeModuleHosts(
73
76
  resourceFileAccessPolicyKeys: new Set<string>(),
74
77
  handlerKeys: new Set<string>(),
75
78
  handlerInstallers: [],
79
+ durableHandlers: [],
76
80
  routes: [],
77
81
  routeKeys: new Set<string>(),
78
82
  taskHandlers: [],
@@ -141,6 +145,7 @@ export async function initializeModuleHosts(
141
145
  toolHandlers: [...state.toolHandlers],
142
146
  sandboxToolHandlers: [...state.sandboxToolHandlers],
143
147
  aiOperationHandlers: [...state.aiOperationHandlers],
148
+ durableHandlers: [...state.durableHandlers],
144
149
  governedResources: [...state.governedResources],
145
150
  rolePresets: [...state.rolePresets],
146
151
  searchProviders: [...state.searchProviders],
@@ -155,6 +160,13 @@ export async function initializeModuleHosts(
155
160
  resourceFileAccessPoliciesOf(moduleName) {
156
161
  return state.moduleResourceFileAccessPolicies.get(moduleName) ?? [];
157
162
  },
163
+ durableHandlerOf(moduleName, handlerKey) {
164
+ return state.durableHandlers.find(
165
+ (entry) =>
166
+ entry.moduleName === moduleName &&
167
+ entry.definition.key === handlerKey,
168
+ )?.definition;
169
+ },
158
170
  installDurableHandlers(registry) {
159
171
  for (const install of state.handlerInstallers) install(registry);
160
172
  },
@@ -174,6 +186,7 @@ interface HostCollections {
174
186
  resourceFileAccessPolicyKeys: Set<string>;
175
187
  handlerKeys: Set<string>;
176
188
  handlerInstallers: HandlerInstaller[];
189
+ durableHandlers: RegisteredModuleDurableHandler[];
177
190
  routes: RegisteredModuleRoute[];
178
191
  routeKeys: Set<string>;
179
192
  taskHandlers: RegisteredModuleTaskHandler[];
@@ -215,7 +228,7 @@ function createScopedHost(
215
228
  },
216
229
  registerDurableHandler(definition) {
217
230
  requireModuleNamespace("任务 handler", definition.key, moduleName);
218
- registerModuleDurableHandler(moduleName, state, definition);
231
+ registerModuleDurableHandler(manifest, state, definition);
219
232
  },
220
233
  registerGovernedResources(registrations) {
221
234
  registerGovernedResources(manifest, state, registrations);
@@ -323,9 +336,7 @@ function createScopedHost(
323
336
  registerSearchProviders(providers) {
324
337
  for (const provider of providers) {
325
338
  requireModuleNamespace("搜索 Provider", provider.key, moduleName);
326
- if (!manifest.providers.some(({ key }) => key === provider.key)) {
327
- throw new Error(`搜索 Provider 未在 Manifest 声明:${provider.key}`);
328
- }
339
+ validateSearchProviderRegistration(manifest, provider);
329
340
  if (state.searchProviderKeys.has(provider.key)) {
330
341
  throw new Error(`搜索 Provider 重复注册:${provider.key}`);
331
342
  }
@@ -0,0 +1,92 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { attachProjectServerModules } from "./project-attachments.js";
3
+
4
+ describe("project-owned Server Module attachment", () => {
5
+ test("只挂载已启用且有本地 Adapter 的显式 attachment", async () => {
6
+ const attached: string[] = [];
7
+ await attachProjectServerModules({
8
+ attachments: [
9
+ {
10
+ name: "enabled",
11
+ attach: () => {
12
+ attached.push("enabled");
13
+ },
14
+ },
15
+ {
16
+ name: "disabled",
17
+ attach: () => {
18
+ attached.push("disabled");
19
+ },
20
+ },
21
+ ],
22
+ adapters: [
23
+ adapter("enabled", "project-attachment"),
24
+ adapter("disabled", "project-attachment"),
25
+ ],
26
+ enabledModuleNames: new Set(["enabled"]),
27
+ runtime: {},
28
+ });
29
+
30
+ expect(attached).toEqual(["enabled"]);
31
+ });
32
+
33
+ test("重复或没有本地 Adapter 的 attachment fail closed", async () => {
34
+ await expect(
35
+ attachProjectServerModules({
36
+ attachments: [
37
+ { name: "case", attach() {} },
38
+ { name: "case", attach() {} },
39
+ ],
40
+ adapters: [adapter("case", "project-attachment")],
41
+ enabledModuleNames: new Set(["case"]),
42
+ runtime: {},
43
+ }),
44
+ ).rejects.toThrow("attachment 重复:case");
45
+
46
+ await expect(
47
+ attachProjectServerModules({
48
+ attachments: [{ name: "package-only", attach() {} }],
49
+ adapters: [],
50
+ enabledModuleNames: new Set(["package-only"]),
51
+ runtime: {},
52
+ }),
53
+ ).rejects.toThrow("attachment 缺少本地 Adapter:package-only");
54
+ });
55
+
56
+ test("先完整预检再执行,且启用的 project-attachment 不能缺失", async () => {
57
+ const effects: string[] = [];
58
+ await expect(
59
+ attachProjectServerModules({
60
+ attachments: [
61
+ { name: "case", attach: () => void effects.push("first") },
62
+ { name: "case", attach: () => void effects.push("duplicate") },
63
+ ],
64
+ adapters: [adapter("case", "project-attachment")],
65
+ enabledModuleNames: new Set(["case"]),
66
+ runtime: {},
67
+ }),
68
+ ).rejects.toThrow("attachment 重复:case");
69
+ expect(effects).toEqual([]);
70
+
71
+ await expect(
72
+ attachProjectServerModules({
73
+ attachments: [],
74
+ adapters: [adapter("case", "project-attachment")],
75
+ enabledModuleNames: new Set(["case"]),
76
+ runtime: {},
77
+ }),
78
+ ).rejects.toThrow("缺少 runtime attachment:case");
79
+ });
80
+ });
81
+
82
+ function adapter(
83
+ name: string,
84
+ runtimeMode: "self-contained" | "project-attachment",
85
+ ) {
86
+ return {
87
+ name,
88
+ hostRange: "^0.1.0",
89
+ runtimeMode,
90
+ create: () => ({ manifest: {}, setup() {} }),
91
+ };
92
+ }
@@ -0,0 +1,58 @@
1
+ import type {
2
+ WindyServerModuleAdapter,
3
+ WindyServerModuleAttachment,
4
+ } from "@southwind-ai/server-sdk";
5
+
6
+ export async function attachProjectServerModules<TRuntime>(input: {
7
+ readonly attachments: readonly WindyServerModuleAttachment<TRuntime>[];
8
+ readonly adapters: readonly WindyServerModuleAdapter<unknown>[];
9
+ readonly enabledModuleNames: ReadonlySet<string>;
10
+ readonly runtime: TRuntime;
11
+ }): Promise<void> {
12
+ const active = validateProjectServerAttachments(input);
13
+ for (const attachment of active) {
14
+ await attachment.attach(input.runtime);
15
+ }
16
+ }
17
+
18
+ export function validateProjectServerAttachments<TRuntime>(input: {
19
+ readonly attachments: readonly WindyServerModuleAttachment<TRuntime>[];
20
+ readonly adapters: readonly WindyServerModuleAdapter<unknown>[];
21
+ readonly enabledModuleNames: ReadonlySet<string>;
22
+ }): readonly WindyServerModuleAttachment<TRuntime>[] {
23
+ const adapters = new Map(
24
+ input.adapters.map((adapter) => [adapter.name, adapter]),
25
+ );
26
+ const seen = new Set<string>();
27
+ for (const attachment of input.attachments) {
28
+ if (seen.has(attachment.name)) {
29
+ throw new Error(`项目 Server Module attachment 重复:${attachment.name}`);
30
+ }
31
+ seen.add(attachment.name);
32
+ const adapter = adapters.get(attachment.name);
33
+ if (!adapter) {
34
+ throw new Error(
35
+ `项目 Server Module attachment 缺少本地 Adapter:${attachment.name}`,
36
+ );
37
+ }
38
+ if (adapter.runtimeMode !== "project-attachment") {
39
+ throw new Error(
40
+ `项目 Server Module attachment 不能挂载 self-contained Adapter:${attachment.name}`,
41
+ );
42
+ }
43
+ }
44
+ for (const adapter of input.adapters) {
45
+ if (
46
+ adapter.runtimeMode === "project-attachment" &&
47
+ input.enabledModuleNames.has(adapter.name) &&
48
+ !seen.has(adapter.name)
49
+ ) {
50
+ throw new Error(
51
+ `项目 Server Module 缺少 runtime attachment:${adapter.name}`,
52
+ );
53
+ }
54
+ }
55
+ return input.attachments.filter(({ name }) =>
56
+ input.enabledModuleNames.has(name),
57
+ );
58
+ }
@@ -0,0 +1,186 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import type { InstallableServerModule } from "@southwind-ai/server-sdk";
4
+ import {
5
+ SERVER_MODULE_HOST_VERSION,
6
+ resolveProjectServerComposition,
7
+ } from "./project-composition.js";
8
+
9
+ describe("package Server Module 标准导出", () => {
10
+ test("从 package 根加载 Adapter,从 ./manifest 加载唯一 Manifest", async () => {
11
+ const exports = packageExports("case", `^${SERVER_MODULE_HOST_VERSION}`);
12
+ const composition = await resolveProjectServerComposition({
13
+ assembly: assembly(),
14
+ adapters: [],
15
+ platformModules: platformModules(),
16
+ importPackage: packageImporter(exports),
17
+ });
18
+
19
+ expect(composition.modules[0]?.manifest).toBe(exports.windyModuleManifest);
20
+ });
21
+
22
+ test("Host range、标准导出与 Manifest 身份漂移时 fail closed", async () => {
23
+ const incompatible = packageExports("case", "^9.0.0");
24
+ await expect(resolve(incompatible)).rejects.toThrow(
25
+ "要求 Server Host ^9.0.0",
26
+ );
27
+
28
+ const valid = packageExports("case", "^0.1.0");
29
+ await expect(
30
+ resolveProjectServerComposition({
31
+ assembly: assembly(),
32
+ adapters: [],
33
+ platformModules: platformModules(),
34
+ importPackage: async (specifier) =>
35
+ specifier.endsWith("/manifest")
36
+ ? { windyModuleManifest: valid.windyModuleManifest }
37
+ : {},
38
+ }),
39
+ ).rejects.toThrow("缺少 windyServerModule 标准导出");
40
+
41
+ await expect(
42
+ resolve({
43
+ ...valid,
44
+ windyServerModule: {
45
+ ...valid.windyServerModule,
46
+ create: () => installable("other"),
47
+ },
48
+ }),
49
+ ).rejects.toThrow("返回了错误 Manifest:other");
50
+
51
+ await expect(
52
+ resolveProjectServerComposition({
53
+ assembly: assembly(),
54
+ adapters: [],
55
+ platformModules: platformModules(),
56
+ importPackage: async (specifier) =>
57
+ specifier.endsWith("/manifest") ? {} : valid,
58
+ }),
59
+ ).rejects.toThrow("缺少 ./manifest windyModuleManifest 标准导出");
60
+
61
+ await expect(
62
+ resolve({
63
+ ...valid,
64
+ windyServerModule: {
65
+ ...valid.windyServerModule,
66
+ create: () => installable("case"),
67
+ },
68
+ }),
69
+ ).rejects.toThrow("未使用 windyModuleManifest 同一声明");
70
+
71
+ await expect(
72
+ resolveProjectServerComposition({
73
+ assembly: assembly(),
74
+ adapters: [],
75
+ platformModules: platformModules(),
76
+ importPackage: async (specifier) =>
77
+ specifier.endsWith("/manifest")
78
+ ? { windyModuleManifest: installable("case").manifest }
79
+ : valid,
80
+ }),
81
+ ).rejects.toThrow("根导出与 ./manifest 不一致");
82
+ });
83
+
84
+ test("本地 Adapter 仍必须复用 package ./manifest", async () => {
85
+ const canonical = installable("case");
86
+ await expect(
87
+ resolveProjectServerComposition({
88
+ assembly: assembly(),
89
+ adapters: [
90
+ {
91
+ name: "case",
92
+ hostRange: "^0.1.0",
93
+ runtimeMode: "project-attachment",
94
+ create: () => installable("case"),
95
+ },
96
+ ],
97
+ platformModules: platformModules(),
98
+ importPackage: async (specifier) => {
99
+ expect(specifier).toBe("@customer/case/manifest");
100
+ return { windyModuleManifest: canonical.manifest };
101
+ },
102
+ }),
103
+ ).rejects.toThrow("未使用 windyModuleManifest 同一声明");
104
+ });
105
+ });
106
+
107
+ function resolve(exports: ReturnType<typeof packageExports>) {
108
+ return resolveProjectServerComposition({
109
+ assembly: assembly(),
110
+ adapters: [],
111
+ platformModules: platformModules(),
112
+ importPackage: packageImporter(exports),
113
+ });
114
+ }
115
+
116
+ function packageImporter(exports: ReturnType<typeof packageExports>) {
117
+ return async (specifier: string) =>
118
+ specifier.endsWith("/manifest")
119
+ ? { windyModuleManifest: exports.windyModuleManifest }
120
+ : exports;
121
+ }
122
+
123
+ function packageExports(name: string, hostRange: string) {
124
+ const module = installable(name);
125
+ return {
126
+ windyModuleManifest: module.manifest,
127
+ windyServerModule: {
128
+ name,
129
+ hostRange,
130
+ runtimeMode: "self-contained" as const,
131
+ create: () => module,
132
+ },
133
+ };
134
+ }
135
+
136
+ function assembly() {
137
+ return {
138
+ schemaVersion: 1 as const,
139
+ modules: [
140
+ registration("system", null, 10),
141
+ registration("system.identity", null, 20),
142
+ registration("case", "@customer/case", 100),
143
+ ],
144
+ };
145
+ }
146
+
147
+ function registration(name: string, packageName: string | null, order: number) {
148
+ return {
149
+ name,
150
+ package: packageName,
151
+ enabled: true,
152
+ order,
153
+ admin: { visible: true },
154
+ config: {},
155
+ };
156
+ }
157
+
158
+ function platformModules(): readonly ModuleManifest[] {
159
+ return [
160
+ installable("system").manifest,
161
+ installable("system.identity").manifest,
162
+ ];
163
+ }
164
+
165
+ function installable(name: string): InstallableServerModule<ModuleManifest> {
166
+ return {
167
+ manifest: {
168
+ name,
169
+ version: "1.0.0",
170
+ label: name,
171
+ description: `${name} module`,
172
+ menus: [],
173
+ adminRoutes: [],
174
+ permissions: [],
175
+ apiPermissions: [],
176
+ features: [],
177
+ migrations: [],
178
+ schemaExports: [],
179
+ tasks: [],
180
+ tools: [],
181
+ providers: [],
182
+ auditActions: [],
183
+ },
184
+ setup() {},
185
+ };
186
+ }
@@ -0,0 +1,260 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ // @windy-module system.data-governance begin
4
+ systemModuleManifests,
5
+ // @windy-module system.data-governance end
6
+ type ModuleManifest,
7
+ } from "@southwind-ai/modules";
8
+ import type { InstallableServerModule } from "@southwind-ai/server-sdk";
9
+ import { resolveProjectServerComposition } from "./project-composition.js";
10
+
11
+ describe("project-owned Server Composition", () => {
12
+ test("按装配清单启用状态与顺序加载项目 Adapter", async () => {
13
+ const adapters = [adapter("disabled"), adapter("second"), adapter("first")];
14
+ const composition = await resolveProjectServerComposition({
15
+ assembly: assembly([
16
+ registration("disabled", false, 100),
17
+ registration("second", true, 300),
18
+ registration("first", true, 200),
19
+ ]),
20
+ adapters,
21
+ platformModules: platformModules(),
22
+ importPackage: localManifestImporter(adapters),
23
+ });
24
+
25
+ expect(composition.modules.map(({ manifest }) => manifest.name)).toEqual([
26
+ "first",
27
+ "second",
28
+ ]);
29
+ });
30
+
31
+ test("平台 Module 与 package 注册必须双向一致", async () => {
32
+ await expect(
33
+ resolveProjectServerComposition({
34
+ assembly: assembly([]),
35
+ adapters: [],
36
+ platformModules: platformModules(
37
+ "system",
38
+ "system.identity",
39
+ "system.audit",
40
+ ),
41
+ importPackage: unexpectedImport,
42
+ }),
43
+ ).rejects.toThrow("平台 Module 未在 windy.modules.json 注册:system.audit");
44
+
45
+ await expect(
46
+ resolveProjectServerComposition({
47
+ assembly: {
48
+ ...assembly([]),
49
+ modules: [
50
+ ...assembly([]).modules,
51
+ {
52
+ name: "ghost",
53
+ package: null,
54
+ enabled: true,
55
+ order: 500,
56
+ admin: { visible: true },
57
+ config: {},
58
+ },
59
+ ],
60
+ },
61
+ adapters: [],
62
+ platformModules: platformModules(),
63
+ importPackage: unexpectedImport,
64
+ }),
65
+ ).rejects.toThrow("引用了未知平台 Module:ghost");
66
+
67
+ await expect(
68
+ resolveProjectServerComposition({
69
+ assembly: {
70
+ ...assembly([]),
71
+ modules: assembly([]).modules.map((module) =>
72
+ module.name === "system.identity"
73
+ ? { ...module, enabled: false }
74
+ : module,
75
+ ),
76
+ },
77
+ adapters: [],
78
+ platformModules: platformModules(),
79
+ importPackage: unexpectedImport,
80
+ }),
81
+ ).rejects.toThrow(
82
+ "windy.modules.json 必须启用平台 Module:system.identity",
83
+ );
84
+ });
85
+
86
+ // @windy-module system.data-governance begin
87
+ test("跨模块 Feature 贡献不能声明为运行时停用", async () => {
88
+ const platform = systemModuleManifests().filter(({ name }) =>
89
+ ["system", "system.identity", "system.data-governance"].includes(name),
90
+ );
91
+ await expect(
92
+ resolveProjectServerComposition({
93
+ assembly: {
94
+ ...assembly([]),
95
+ modules: [
96
+ ...assembly([]).modules,
97
+ {
98
+ name: "system.data-governance",
99
+ package: null,
100
+ enabled: false,
101
+ order: 500,
102
+ admin: { visible: true },
103
+ config: {},
104
+ },
105
+ ],
106
+ },
107
+ adapters: [],
108
+ platformModules: platform,
109
+ importPackage: unexpectedImport,
110
+ }),
111
+ ).rejects.toThrow(
112
+ "平台 Module 不能仅通过运行时停用:system.data-governance",
113
+ );
114
+ });
115
+ // @windy-module system.data-governance end
116
+
117
+ test("启用模块的 Manifest 依赖必须同时启用", async () => {
118
+ const caseModule = installable("case");
119
+ caseModule.manifest.dependencies = ["system.scheduler"];
120
+ const caseAdapter = {
121
+ ...adapter("case"),
122
+ create: () => caseModule,
123
+ };
124
+ await expect(
125
+ resolveProjectServerComposition({
126
+ assembly: {
127
+ ...assembly([registration("case", true, 600)]),
128
+ modules: [
129
+ ...assembly([]).modules,
130
+ {
131
+ name: "system.scheduler",
132
+ package: null,
133
+ enabled: false,
134
+ order: 500,
135
+ admin: { visible: true },
136
+ config: {},
137
+ },
138
+ registration("case", true, 600),
139
+ ],
140
+ },
141
+ adapters: [caseAdapter],
142
+ platformModules: platformModules(
143
+ "system",
144
+ "system.identity",
145
+ "system.scheduler",
146
+ ),
147
+ importPackage: localManifestImporter([caseAdapter]),
148
+ }),
149
+ ).rejects.toThrow("Module case 依赖未启用 Module:system.scheduler");
150
+ });
151
+
152
+ test("装配清单拒绝重复 order、负数 order 和非法 package", async () => {
153
+ for (const modules of [
154
+ [registration("first", true, 100), registration("second", true, 100)],
155
+ [registration("negative", true, -1)],
156
+ [{ ...registration("bad", true, 100), package: "../bad" }],
157
+ [{ ...registration("uppercase", true, 100), package: "@Customer/case" }],
158
+ ]) {
159
+ await expect(
160
+ resolveProjectServerComposition({
161
+ assembly: assembly(modules),
162
+ adapters: [],
163
+ platformModules: platformModules(),
164
+ importPackage: unexpectedImport,
165
+ }),
166
+ ).rejects.toThrow("windy.modules.json");
167
+ }
168
+ });
169
+ });
170
+
171
+ function assembly(modules: Array<ReturnType<typeof registration>>) {
172
+ return {
173
+ schemaVersion: 1 as const,
174
+ modules: [
175
+ {
176
+ name: "system",
177
+ package: null,
178
+ enabled: true,
179
+ order: 10,
180
+ admin: { visible: true },
181
+ config: {},
182
+ },
183
+ {
184
+ name: "system.identity",
185
+ package: null,
186
+ enabled: true,
187
+ order: 20,
188
+ admin: { visible: true },
189
+ config: {},
190
+ },
191
+ ...modules,
192
+ ],
193
+ };
194
+ }
195
+
196
+ function registration(name: string, enabled: boolean, order: number) {
197
+ return {
198
+ name,
199
+ package: `@customer/${name}`,
200
+ enabled,
201
+ order,
202
+ admin: { visible: true },
203
+ config: {},
204
+ };
205
+ }
206
+
207
+ function adapter(name: string) {
208
+ const module = installable(name);
209
+ return {
210
+ name,
211
+ hostRange: "^0.1.0",
212
+ runtimeMode: "project-attachment" as const,
213
+ create: () => module,
214
+ };
215
+ }
216
+
217
+ function localManifestImporter(
218
+ adapters: readonly ReturnType<typeof adapter>[],
219
+ ) {
220
+ return async (specifier: string) => {
221
+ const adapter = adapters.find(({ name }) =>
222
+ specifier.endsWith(`/${name}/manifest`),
223
+ );
224
+ if (!adapter) throw new Error(`未找到测试 Adapter:${specifier}`);
225
+ return { windyModuleManifest: (await adapter.create()).manifest };
226
+ };
227
+ }
228
+
229
+ function installable(name: string): InstallableServerModule<ModuleManifest> {
230
+ return {
231
+ manifest: {
232
+ name,
233
+ version: "1.0.0",
234
+ label: name,
235
+ description: `${name} module`,
236
+ menus: [],
237
+ adminRoutes: [],
238
+ permissions: [],
239
+ apiPermissions: [],
240
+ features: [],
241
+ migrations: [],
242
+ schemaExports: [],
243
+ tasks: [],
244
+ tools: [],
245
+ providers: [],
246
+ auditActions: [],
247
+ },
248
+ setup() {},
249
+ };
250
+ }
251
+
252
+ async function unexpectedImport(): Promise<never> {
253
+ throw new Error("不应加载 package");
254
+ }
255
+
256
+ function platformModules(...names: string[]): readonly ModuleManifest[] {
257
+ return (names.length ? names : ["system", "system.identity"])
258
+ .map(installable)
259
+ .map(({ manifest }) => manifest);
260
+ }