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,311 @@
1
+ import {
2
+ indexProjectModuleAssembly,
3
+ satisfiesModuleHostRange,
4
+ validateProjectModuleAssembly,
5
+ type ModuleManifest,
6
+ type ProjectModuleRegistration,
7
+ } from "@southwind-ai/modules";
8
+ import type {
9
+ InstallableServerModule,
10
+ WindyServerModuleAdapter,
11
+ } from "@southwind-ai/server-sdk";
12
+
13
+ export const SERVER_MODULE_HOST_VERSION = "0.1.0";
14
+
15
+ export type {
16
+ ProjectModuleAssembly,
17
+ ProjectModuleRegistration,
18
+ } from "@southwind-ai/modules";
19
+
20
+ export type ProjectServerModule = WindyServerModuleAdapter<ModuleManifest>;
21
+
22
+ export interface ProjectServerComposition {
23
+ readonly modules: readonly InstallableServerModule<ModuleManifest>[];
24
+ readonly enabledModuleNames: ReadonlySet<string>;
25
+ }
26
+
27
+ interface ResolveProjectServerCompositionInput {
28
+ readonly assembly: unknown;
29
+ readonly adapters: readonly ProjectServerModule[];
30
+ readonly platformModules: readonly ModuleManifest[];
31
+ readonly importPackage?: (packageName: string) => Promise<unknown>;
32
+ readonly hostVersion?: string;
33
+ }
34
+
35
+ export async function resolveProjectServerComposition(
36
+ input: ResolveProjectServerCompositionInput,
37
+ ): Promise<ProjectServerComposition> {
38
+ const registrations = indexProjectModuleAssembly(
39
+ validateProjectModuleAssembly(input.assembly),
40
+ );
41
+ validatePlatformAssembly(registrations, input.platformModules);
42
+ const adapters = validateAdapters(input.adapters, registrations);
43
+ const hostVersion = input.hostVersion ?? SERVER_MODULE_HOST_VERSION;
44
+ const importPackage =
45
+ input.importPackage ?? ((packageName: string) => import(packageName));
46
+
47
+ const enabled = [...registrations.values()]
48
+ .filter(({ enabled }) => enabled)
49
+ .sort(
50
+ (left, right) =>
51
+ left.order - right.order || left.name.localeCompare(right.name),
52
+ );
53
+ const modules: InstallableServerModule<ModuleManifest>[] = [];
54
+
55
+ for (const registration of enabled) {
56
+ if (registration.package === null) continue;
57
+ const localAdapter = adapters.get(registration.name);
58
+ const canonicalManifest = await loadPackageManifest(
59
+ registration,
60
+ importPackage,
61
+ );
62
+ const adapter =
63
+ localAdapter ??
64
+ (await loadPackageAdapter(
65
+ registration,
66
+ canonicalManifest,
67
+ importPackage,
68
+ ));
69
+ assertHostCompatible(adapter, hostVersion);
70
+ const module = await adapter.create(registration.config);
71
+ assertInstallableModule(registration.name, module, canonicalManifest);
72
+ modules.push(module);
73
+ }
74
+ validateEnabledDependencies(
75
+ registrations,
76
+ input.platformModules,
77
+ modules.map(({ manifest }) => manifest),
78
+ );
79
+
80
+ return {
81
+ modules,
82
+ enabledModuleNames: new Set(enabled.map(({ name }) => name)),
83
+ };
84
+ }
85
+
86
+ function validateEnabledDependencies(
87
+ registrations: ReadonlyMap<string, ProjectModuleRegistration>,
88
+ platformModules: readonly ModuleManifest[],
89
+ businessModules: readonly ModuleManifest[],
90
+ ): void {
91
+ const enabledNames = new Set(
92
+ [...registrations.values()]
93
+ .filter(({ enabled }) => enabled)
94
+ .map(({ name }) => name),
95
+ );
96
+ const manifests = new Map(
97
+ [...platformModules, ...businessModules].map((manifest) => [
98
+ manifest.name,
99
+ manifest,
100
+ ]),
101
+ );
102
+ for (const name of enabledNames) {
103
+ const manifest = manifests.get(name);
104
+ if (!manifest) throw new Error(`启用的 Module 缺少 Manifest:${name}`);
105
+ for (const dependency of manifest.dependencies ?? []) {
106
+ if (!enabledNames.has(dependency)) {
107
+ throw new Error(`Module ${name} 依赖未启用 Module:${dependency}`);
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ function validatePlatformAssembly(
114
+ registrations: ReadonlyMap<string, ProjectModuleRegistration>,
115
+ platformModules: readonly ModuleManifest[],
116
+ ): void {
117
+ const platformNames = new Set(platformModules.map(({ name }) => name));
118
+ for (const name of platformNames) {
119
+ const registration = registrations.get(name);
120
+ if (!registration) {
121
+ throw new Error(`平台 Module 未在 windy.modules.json 注册:${name}`);
122
+ }
123
+ if (registration.package !== null) {
124
+ throw new Error(
125
+ `平台 Module 在 windy.modules.json 中必须使用 package=null:${name}`,
126
+ );
127
+ }
128
+ }
129
+ for (const registration of registrations.values()) {
130
+ if (
131
+ registration.package === null &&
132
+ !platformNames.has(registration.name)
133
+ ) {
134
+ throw new Error(
135
+ `windy.modules.json 引用了未知平台 Module:${registration.name}`,
136
+ );
137
+ }
138
+ }
139
+ for (const required of ["system", "system.identity"]) {
140
+ const registration = registrations.get(required);
141
+ if (!registration?.enabled || registration.package !== null) {
142
+ throw new Error(`windy.modules.json 必须启用平台 Module:${required}`);
143
+ }
144
+ }
145
+ for (const manifest of platformModules) {
146
+ if (!registrations.get(manifest.name)?.enabled) {
147
+ assertRuntimeDisableSafe(manifest);
148
+ }
149
+ }
150
+ }
151
+
152
+ function assertRuntimeDisableSafe(manifest: ModuleManifest): void {
153
+ if (manifest.metadata?.runtimeActivation === "physical-only") {
154
+ throw new Error(
155
+ `平台 Module 不能仅通过运行时停用:${manifest.name};请在生成或升级阶段物理移除`,
156
+ );
157
+ }
158
+ const ownedFeatures = new Set<string>(
159
+ manifest.features.map(({ key }) => key),
160
+ );
161
+ const guardedFeatureKeys = [
162
+ ...manifest.adminRoutes.map(({ featureKey }) => featureKey),
163
+ ...manifest.apiPermissions.map(({ featureKey }) => featureKey),
164
+ ...manifest.tasks.map(({ featureKey }) => featureKey),
165
+ ...manifest.tools.map(({ featureKey }) => featureKey),
166
+ ...(manifest.aiOperations ?? []).map(({ featureKey }) => featureKey),
167
+ ...manifest.providers.map(({ featureKey }) => featureKey),
168
+ ...(manifest.dataPolicies ?? []).map(({ featureKey }) => featureKey),
169
+ ].filter((key): key is string => Boolean(key));
170
+ if (guardedFeatureKeys.some((key) => !ownedFeatures.has(key))) {
171
+ throw new Error(
172
+ `平台 Module 不能仅通过运行时停用:${manifest.name};请在生成或升级阶段物理移除`,
173
+ );
174
+ }
175
+ }
176
+
177
+ function validateAdapters(
178
+ definitions: readonly ProjectServerModule[],
179
+ registrations: ReadonlyMap<string, ProjectModuleRegistration>,
180
+ ): ReadonlyMap<string, ProjectServerModule> {
181
+ const adapters = new Map<string, ProjectServerModule>();
182
+ for (const definition of definitions) {
183
+ if (!registrations.has(definition.name)) {
184
+ throw new Error(
185
+ `Server Adapter 未在 windy.modules.json 注册:${definition.name}`,
186
+ );
187
+ }
188
+ if (adapters.has(definition.name)) {
189
+ throw new Error(`Server Adapter 重复:${definition.name}`);
190
+ }
191
+ if (registrations.get(definition.name)?.package === null) {
192
+ throw new Error(
193
+ `平台 Module 不能注册 Server Adapter:${definition.name}`,
194
+ );
195
+ }
196
+ adapters.set(definition.name, definition);
197
+ }
198
+ return adapters;
199
+ }
200
+
201
+ async function loadPackageAdapter(
202
+ registration: ProjectModuleRegistration,
203
+ canonicalManifest: ModuleManifest,
204
+ importPackage: (packageName: string) => Promise<unknown>,
205
+ ): Promise<ProjectServerModule> {
206
+ const packageName = registration.package!;
207
+ const loaded = await importPackage(packageName);
208
+ if (!isRecord(loaded) || !isProjectServerModule(loaded.windyServerModule)) {
209
+ throw new Error(
210
+ `Module ${registration.name} 缺少 windyServerModule 标准导出`,
211
+ );
212
+ }
213
+ if (loaded.windyServerModule.name !== registration.name) {
214
+ throw new Error(
215
+ `Module package ${registration.package} 导出了错误 Adapter:${loaded.windyServerModule.name}`,
216
+ );
217
+ }
218
+ if (loaded.windyServerModule.runtimeMode !== "self-contained") {
219
+ throw new Error(
220
+ `动态 Module ${registration.name} 不能要求项目 runtime attachment`,
221
+ );
222
+ }
223
+ if (
224
+ isModuleManifest(loaded.windyModuleManifest) &&
225
+ loaded.windyModuleManifest !== canonicalManifest
226
+ ) {
227
+ throw new Error(`Module ${registration.name} 根导出与 ./manifest 不一致`);
228
+ }
229
+ return loaded.windyServerModule;
230
+ }
231
+
232
+ async function loadPackageManifest(
233
+ registration: ProjectModuleRegistration,
234
+ importPackage: (packageName: string) => Promise<unknown>,
235
+ ): Promise<ModuleManifest> {
236
+ const packageName = registration.package!;
237
+ const loaded = await importPackage(`${packageName}/manifest`);
238
+ if (!isRecord(loaded) || !isModuleManifest(loaded.windyModuleManifest)) {
239
+ throw new Error(
240
+ `Module ${registration.name} 缺少 ./manifest windyModuleManifest 标准导出`,
241
+ );
242
+ }
243
+ if (loaded.windyModuleManifest.name !== registration.name) {
244
+ throw new Error(
245
+ `Module package ${packageName} 导出了错误 Manifest:${loaded.windyModuleManifest.name}`,
246
+ );
247
+ }
248
+ return loaded.windyModuleManifest;
249
+ }
250
+
251
+ function isProjectServerModule(value: unknown): value is ProjectServerModule {
252
+ return (
253
+ isRecord(value) &&
254
+ typeof value.name === "string" &&
255
+ typeof value.hostRange === "string" &&
256
+ (value.runtimeMode === "self-contained" ||
257
+ value.runtimeMode === "project-attachment") &&
258
+ typeof value.create === "function"
259
+ );
260
+ }
261
+
262
+ function assertInstallableModule(
263
+ expectedName: string,
264
+ value: InstallableServerModule<ModuleManifest>,
265
+ packageManifest?: ModuleManifest,
266
+ ): void {
267
+ if (
268
+ !isRecord(value) ||
269
+ !isRecord(value.manifest) ||
270
+ typeof value.manifest.name !== "string" ||
271
+ typeof value.setup !== "function"
272
+ ) {
273
+ throw new Error(`Server Adapter ${expectedName} 返回了无效 Module`);
274
+ }
275
+ if (value.manifest.name !== expectedName) {
276
+ throw new Error(
277
+ `Server Adapter ${expectedName} 返回了错误 Manifest:${value.manifest.name}`,
278
+ );
279
+ }
280
+ if (packageManifest && value.manifest !== packageManifest) {
281
+ throw new Error(
282
+ `Server Adapter ${expectedName} 未使用 windyModuleManifest 同一声明`,
283
+ );
284
+ }
285
+ }
286
+
287
+ function assertHostCompatible(
288
+ adapter: ProjectServerModule,
289
+ hostVersion: string,
290
+ ): void {
291
+ if (!satisfiesModuleHostRange(hostVersion, adapter.hostRange)) {
292
+ throw new Error(
293
+ `Server Module ${adapter.name} 要求 Server Host ${adapter.hostRange},当前为 ${hostVersion}`,
294
+ );
295
+ }
296
+ }
297
+
298
+ function isRecord(value: unknown): value is Record<string, unknown> {
299
+ return typeof value === "object" && value !== null && !Array.isArray(value);
300
+ }
301
+
302
+ function isModuleManifest(value: unknown): value is ModuleManifest {
303
+ return (
304
+ isRecord(value) &&
305
+ typeof value.name === "string" &&
306
+ typeof value.version === "string" &&
307
+ Array.isArray(value.features) &&
308
+ Array.isArray(value.permissions) &&
309
+ Array.isArray(value.apiPermissions)
310
+ );
311
+ }
@@ -91,6 +91,7 @@ export interface ModuleRequestContextInput {
91
91
  userDirectory?: ModuleUserDirectoryPort;
92
92
  aiArtifacts?: ModuleAiArtifactPort;
93
93
  aiOperations?: ModuleAiOperationPort;
94
+ onAuditEmit?: (action: string) => void;
94
95
  }
95
96
 
96
97
  export function buildModuleRequestContext(
@@ -184,9 +185,11 @@ function createModuleAuditEmitter(
184
185
  const auditEvent = buildAuditEvent(input.guardContext, action, event);
185
186
  if (event.critical) {
186
187
  await input.runtime.auditSink.recordCriticalEvent(auditEvent);
188
+ input.onAuditEmit?.(action);
187
189
  return;
188
190
  }
189
191
  input.runtime.auditSink.recordEvent(auditEvent);
192
+ input.onAuditEmit?.(action);
190
193
  },
191
194
  };
192
195
  }
@@ -30,16 +30,25 @@ export function validateRouteDeclaration(
30
30
  `模块路由 Feature 未在 Manifest 注册:${binding.featureKey}`,
31
31
  );
32
32
  }
33
+ if (binding.anonymous !== true && !binding.auditAction) {
34
+ throw new Error(`模块路由缺少 Manifest 审计动作:${route}`);
35
+ }
36
+ if (
37
+ definition.auditAction !== undefined &&
38
+ definition.auditAction !== binding.auditAction
39
+ ) {
40
+ throw new Error(
41
+ `模块路由审计动作与 Manifest 声明不一致:${route}`,
42
+ );
43
+ }
33
44
  if (
34
- definition.auditAction &&
45
+ binding.auditAction &&
35
46
  !manifest.auditActions.some((action) =>
36
47
  typeof action === "string"
37
- ? action === definition.auditAction
38
- : action.key === definition.auditAction,
48
+ ? action === binding.auditAction
49
+ : action.key === binding.auditAction,
39
50
  )
40
51
  ) {
41
- throw new Error(
42
- `模块路由审计动作未在 Manifest 注册:${definition.auditAction}`,
43
- );
52
+ throw new Error(`模块路由审计动作未在 Manifest 注册:${binding.auditAction}`);
44
53
  }
45
54
  }
@@ -62,6 +62,18 @@ describe("模块 Guarded Route 安装", () => {
62
62
  );
63
63
  });
64
64
 
65
+ test("实现未重复声明动作时仍从 Manifest 自动审计读取 API", async () => {
66
+ const fixture = await createFixture();
67
+
68
+ await fixture.invoke("GET", "/shop/items");
69
+
70
+ expect(
71
+ fixture.runtime.auditSink
72
+ .list()
73
+ .filter(({ action }) => action === "data.read"),
74
+ ).toHaveLength(1);
75
+ });
76
+
65
77
  test("Handler 只能获得受限上下文:审计白名单、Jobs 命名空间与未挂载存储", async () => {
66
78
  const fixture = await createFixture({ withJobs: true });
67
79
  await fixture.invoke("GET", "/shop/items");
@@ -172,7 +184,6 @@ async function createFixture(options: FixtureOptions = {}) {
172
184
  method: "POST",
173
185
  path: "/api/shop/items",
174
186
  permissionKey: "shop.item.manage",
175
- auditAction: "crud.create",
176
187
  handler: () => {
177
188
  state.handled = true;
178
189
  state.context = undefined;
@@ -286,12 +297,14 @@ function shopManifest(): ModuleManifest {
286
297
  path: "/api/shop/items",
287
298
  permissionKey: "shop.item.read",
288
299
  featureKey: "shop.feature",
300
+ auditAction: "data.read",
289
301
  },
290
302
  {
291
303
  method: "POST",
292
304
  path: "/api/shop/items",
293
305
  permissionKey: "shop.item.manage",
294
306
  featureKey: "shop.feature",
307
+ auditAction: "crud.create",
295
308
  },
296
309
  ],
297
310
  features: [
@@ -310,7 +323,7 @@ function shopManifest(): ModuleManifest {
310
323
  tasks: [],
311
324
  tools: [],
312
325
  providers: [],
313
- auditActions: ["crud.create"],
326
+ auditActions: ["crud.create", "data.read"],
314
327
  };
315
328
  }
316
329
 
@@ -81,6 +81,7 @@ export function installModuleRoutes(
81
81
  { permission: binding, feature },
82
82
  );
83
83
  if (!decision.allowed) return forbidden(decision.reason);
84
+ const emittedAuditActions = new Set<string>();
84
85
  const requestContext = buildModuleRequestContext({
85
86
  moduleName,
86
87
  manifest,
@@ -133,10 +134,15 @@ export function installModuleRoutes(
133
134
  route.guardContext,
134
135
  )
135
136
  : undefined,
137
+ onAuditEmit: (action) => emittedAuditActions.add(action),
136
138
  });
137
139
  const result = await definition.handler(requestContext);
138
- if (definition.auditAction) {
139
- await requestContext.audit.emit(definition.auditAction, {
140
+ if (
141
+ binding.auditAction &&
142
+ isSuccessfulResult(result) &&
143
+ !emittedAuditActions.has(binding.auditAction)
144
+ ) {
145
+ await requestContext.audit.emit(binding.auditAction, {
140
146
  target: {
141
147
  type: "module-route",
142
148
  id: `${definition.method} ${definition.path}`,
@@ -148,6 +154,10 @@ export function installModuleRoutes(
148
154
  }
149
155
  }
150
156
 
157
+ function isSuccessfulResult(value: unknown): boolean {
158
+ return !(value instanceof Response) || value.status < 400;
159
+ }
160
+
151
161
  function stripApiPrefix(path: string): string {
152
162
  if (!path.startsWith("/api/")) {
153
163
  throw new Error(`模块路由路径必须以 /api 开头:${path}`);
@@ -17,6 +17,7 @@ describe("模块路由注册", () => {
17
17
  path: "/api/known/items",
18
18
  permissionKey: "known.item.read",
19
19
  featureKey: declaredFeature,
20
+ auditAction: "data.read",
20
21
  },
21
22
  ],
22
23
  features: [
@@ -29,6 +30,7 @@ describe("模块路由注册", () => {
29
30
  stage: "stable",
30
31
  },
31
32
  ],
33
+ auditActions: ["data.read"],
32
34
  });
33
35
 
34
36
  const initialized = await initializeModuleHosts({
@@ -50,6 +52,7 @@ describe("模块路由注册", () => {
50
52
  method: "GET",
51
53
  path: "/api/known/items",
52
54
  permissionKey: "known.item.read",
55
+ auditAction: "data.read",
53
56
  },
54
57
  ],
55
58
  });
@@ -78,9 +81,10 @@ describe("模块路由注册", () => {
78
81
  apiPermissions: [
79
82
  {
80
83
  method: "GET",
81
- path: "/api/known/items",
82
- permissionKey: "known.item.read",
83
- featureKey: featureKey("known.item"),
84
+ path: "/api/known/items",
85
+ permissionKey: "known.item.read",
86
+ featureKey: featureKey("known.item"),
87
+ auditAction: "data.read",
84
88
  },
85
89
  ],
86
90
  }),
@@ -105,6 +109,7 @@ describe("模块路由注册", () => {
105
109
  method: "GET",
106
110
  path: "/api/known/items",
107
111
  permissionKey: "known.item.manage",
112
+ auditAction: "data.read",
108
113
  },
109
114
  ],
110
115
  });
@@ -118,15 +123,19 @@ describe("模块路由注册", () => {
118
123
  method: "GET",
119
124
  path: "/api/known/items",
120
125
  permissionKey: "known.item.read",
126
+ auditAction: "data.read",
121
127
  },
122
128
  ],
129
+ auditActions: ["data.read"],
123
130
  });
124
131
  await expect(registerRoutes(readable, [route(), route()])).rejects.toThrow(
125
132
  "模块路由重复注册:GET /api/known/items",
126
133
  );
127
134
  await expect(
128
135
  registerRoutes(readable, [{ ...route(), auditAction: "crud.create" }]),
129
- ).rejects.toThrow("模块路由审计动作未在 Manifest 注册:crud.create");
136
+ ).rejects.toThrow(
137
+ "模块路由审计动作与 Manifest 声明不一致:GET /api/known/items",
138
+ );
130
139
  });
131
140
  });
132
141
 
@@ -0,0 +1,63 @@
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 { mergeLegacyAndProjectModules } from "./runtime-composition-compatibility.js";
5
+
6
+ function module(name: string, version = "1.0.0") {
7
+ const manifest = {
8
+ name,
9
+ version,
10
+ label: name,
11
+ description: name,
12
+ dependencies: [],
13
+ features: [],
14
+ permissions: [],
15
+ menus: [],
16
+ adminRoutes: [],
17
+ apiPermissions: [],
18
+ migrations: [],
19
+ schemaExports: [],
20
+ tasks: [],
21
+ tools: [],
22
+ providers: [],
23
+ auditActions: [],
24
+ } satisfies ModuleManifest;
25
+ return {
26
+ manifest,
27
+ setup() {},
28
+ } satisfies InstallableServerModule<ModuleManifest>;
29
+ }
30
+
31
+ describe("旧项目 Server 装配兼容", () => {
32
+ test("保留旧 project-owned 模块,并只把新增声明式模块交给新版 attachment", () => {
33
+ const legacyWorkOrder = module("work-order");
34
+ const declaredWorkOrder = module("work-order");
35
+ const declaredSearch = module("business.search");
36
+
37
+ const result = mergeLegacyAndProjectModules(
38
+ [legacyWorkOrder],
39
+ [declaredWorkOrder, declaredSearch],
40
+ );
41
+
42
+ expect(result.modules).toEqual([legacyWorkOrder, declaredSearch]);
43
+ expect([...result.projectOnlyModuleNames]).toEqual(["business.search"]);
44
+ });
45
+
46
+ test("同名旧模块与新版声明不兼容时 fail closed", () => {
47
+ expect(() =>
48
+ mergeLegacyAndProjectModules(
49
+ [module("work-order", "1.0.0")],
50
+ [module("work-order", "2.0.0")],
51
+ ),
52
+ ).toThrow("旧 Server Module 与声明式 Composition 不一致:work-order");
53
+ });
54
+
55
+ test("拒绝旧 project-owned 装配中的重复模块", () => {
56
+ expect(() =>
57
+ mergeLegacyAndProjectModules(
58
+ [module("work-order"), module("work-order")],
59
+ [],
60
+ ),
61
+ ).toThrow("旧 Server Module 重复:work-order");
62
+ });
63
+ });
@@ -0,0 +1,46 @@
1
+ import type { ModuleManifest } from "@southwind-ai/modules";
2
+ import type { InstallableServerModule } from "@southwind-ai/server-sdk";
3
+
4
+ export interface CompatibleRuntimeModules {
5
+ readonly modules: readonly InstallableServerModule<ModuleManifest>[];
6
+ readonly projectOnlyModuleNames: ReadonlySet<string>;
7
+ }
8
+
9
+ export function mergeLegacyAndProjectModules(
10
+ legacyModules: readonly InstallableServerModule<ModuleManifest>[],
11
+ projectModules: readonly InstallableServerModule<ModuleManifest>[],
12
+ ): CompatibleRuntimeModules {
13
+ const legacyByName = new Map<
14
+ string,
15
+ InstallableServerModule<ModuleManifest>
16
+ >();
17
+ for (const legacyModule of legacyModules) {
18
+ const name = legacyModule.manifest.name;
19
+ if (legacyByName.has(name)) {
20
+ throw new Error(`旧 Server Module 重复:${name}`);
21
+ }
22
+ legacyByName.set(name, legacyModule);
23
+ }
24
+
25
+ const modules = [...legacyModules];
26
+ const projectOnlyModuleNames = new Set<string>();
27
+ for (const projectModule of projectModules) {
28
+ const name = projectModule.manifest.name;
29
+ const legacyModule = legacyByName.get(name);
30
+ if (legacyModule) {
31
+ if (
32
+ JSON.stringify(legacyModule.manifest) !==
33
+ JSON.stringify(projectModule.manifest)
34
+ ) {
35
+ throw new Error(
36
+ `旧 Server Module 与声明式 Composition 不一致:${name}`,
37
+ );
38
+ }
39
+ continue;
40
+ }
41
+ modules.push(projectModule);
42
+ projectOnlyModuleNames.add(name);
43
+ }
44
+
45
+ return { modules, projectOnlyModuleNames };
46
+ }
@@ -0,0 +1,34 @@
1
+ import { systemModuleManifests } from "@southwind-ai/modules";
2
+ import { serverCompositionDeclaration } from "../app/server-composition.js";
3
+ import {
4
+ attachInstalledBusinessModules,
5
+ installedBusinessModules as legacyBusinessModules,
6
+ } from "../installed-business-modules.js";
7
+ import { attachProjectServerModules } from "./project-attachments.js";
8
+ import { resolveProjectServerComposition } from "./project-composition.js";
9
+ import { mergeLegacyAndProjectModules } from "./runtime-composition-compatibility.js";
10
+
11
+ export const serverComposition = await resolveProjectServerComposition({
12
+ assembly: serverCompositionDeclaration.assembly,
13
+ adapters: serverCompositionDeclaration.adapters,
14
+ platformModules: systemModuleManifests(),
15
+ });
16
+
17
+ const compatibleModules = mergeLegacyAndProjectModules(
18
+ legacyBusinessModules,
19
+ serverComposition.modules,
20
+ );
21
+
22
+ export const runtimeBusinessModules = compatibleModules.modules;
23
+
24
+ export async function attachRuntimeBusinessModules(
25
+ runtime: Parameters<typeof attachInstalledBusinessModules>[0],
26
+ ): Promise<void> {
27
+ await attachInstalledBusinessModules(runtime);
28
+ await attachProjectServerModules({
29
+ attachments: serverCompositionDeclaration.attachments ?? [],
30
+ adapters: serverCompositionDeclaration.adapters,
31
+ enabledModuleNames: compatibleModules.projectOnlyModuleNames,
32
+ runtime,
33
+ });
34
+ }