create-windy 0.3.19 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -13
- package/dist/cli.js +788 -280
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +34 -10
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +18 -8
- package/template/README.md +32 -22
- package/template/apps/server/Dockerfile +1 -0
- package/template/apps/server/package.json +1 -0
- package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
- package/template/apps/server/scripts/validate-module-composition.ts +28 -0
- package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
- package/template/apps/server/src/agent/routes.test.ts +20 -7
- package/template/apps/server/src/agent/routes.ts +14 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
- package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
- package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
- package/template/apps/server/src/app/server-composition.ts +56 -0
- package/template/apps/server/src/application-services.ts +2 -0
- package/template/apps/server/src/bulk-data/service.ts +14 -0
- package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
- package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
- package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
- package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
- package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
- package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
- package/template/apps/server/src/feature/routes.ts +33 -15
- package/template/apps/server/src/foundation-activation.test.ts +60 -0
- package/template/apps/server/src/foundation.ts +64 -5
- package/template/apps/server/src/guards.test.ts +23 -0
- package/template/apps/server/src/guards.ts +12 -0
- package/template/apps/server/src/index.ts +28 -34
- package/template/apps/server/src/installed-business-modules.ts +13 -24
- package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
- package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
- package/template/apps/server/src/license/activation.test.ts +30 -0
- package/template/apps/server/src/license/activation.ts +26 -0
- package/template/apps/server/src/license/restriction.test.ts +16 -0
- package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
- package/template/apps/server/src/license/routes.ts +20 -4
- package/template/apps/server/src/license/runtime-service.test.ts +10 -2
- package/template/apps/server/src/module-bootstrap.ts +14 -23
- package/template/apps/server/src/module-composition.test.ts +43 -2
- package/template/apps/server/src/module-composition.ts +29 -0
- package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
- package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
- package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
- package/template/apps/server/src/module-host/durable-context.ts +42 -2
- package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
- package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
- package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
- package/template/apps/server/src/module-host/initialize.test.ts +78 -2
- package/template/apps/server/src/module-host/initialize.ts +15 -4
- package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
- package/template/apps/server/src/module-host/project-attachments.ts +58 -0
- package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
- package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
- package/template/apps/server/src/module-host/project-composition.ts +311 -0
- package/template/apps/server/src/module-host/request-context.ts +3 -0
- package/template/apps/server/src/module-host/route-declaration.ts +15 -6
- package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
- package/template/apps/server/src/module-host/route-installer.ts +12 -2
- package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
- package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
- package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
- package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
- package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
- package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
- package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
- package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
- package/template/apps/server/src/module-host/task-definitions.ts +1 -0
- package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
- package/template/apps/server/src/module-host.test.ts +2 -0
- package/template/apps/server/src/module-preflight.test.ts +289 -0
- package/template/apps/server/src/module-preflight.ts +96 -0
- package/template/apps/server/src/module-runtime-validation.ts +1 -0
- package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
- package/template/apps/server/src/platform-http-runtime.ts +32 -7
- package/template/apps/server/src/route-guards.test.ts +22 -0
- package/template/apps/server/src/route-guards.ts +10 -3
- package/template/apps/server/src/runtime-audit-query.ts +50 -0
- package/template/apps/server/src/runtime-feature.test.ts +46 -0
- package/template/apps/server/src/runtime-feature.ts +5 -0
- package/template/apps/server/src/runtime.ts +10 -49
- package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
- package/template/apps/server/src/scheduler/audit.ts +1 -1
- package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
- package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
- package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
- package/template/apps/server/src/scheduler/service.test.ts +1 -0
- package/template/apps/server/src/scheduler/service.ts +18 -21
- package/template/apps/server/src/scheduler/types.ts +5 -1
- package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
- package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
- package/template/apps/server/src/settings/watermark-routes.ts +29 -1
- package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
- package/template/apps/server/src/startup-composition-validation.ts +54 -0
- package/template/apps/server/src/startup-preflight.test.ts +36 -0
- package/template/apps/server/src/startup-preflight.ts +37 -0
- package/template/apps/server/src/system/audit-routes.test.ts +9 -3
- package/template/apps/server/src/system/department-routes.test.ts +5 -3
- package/template/apps/server/src/system/feature-routes.test.ts +4 -3
- package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
- package/template/apps/server/src/system/interface-catalog.ts +3 -0
- package/template/apps/server/src/system/menu-routes.test.ts +5 -3
- package/template/apps/server/src/system/route-test-runtime.ts +24 -0
- package/template/apps/server/src/system/routes-validation.test.ts +7 -3
- package/template/apps/server/src/system/routes.test.ts +21 -11
- package/template/apps/server/src/system/routes.ts +11 -2
- package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
- package/template/apps/server/src/work-order/routes.test.ts +8 -0
- package/template/apps/server/src/work-order/task.test.ts +6 -1
- package/template/apps/web/Dockerfile +5 -0
- package/template/apps/web/package.json +1 -0
- package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
- package/template/apps/web/src/app/web-composition.ts +24 -0
- package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
- package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
- package/template/apps/web/src/composables/watermark-settings.ts +40 -11
- package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
- package/template/apps/web/src/layout/AppSidebar.vue +8 -1
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
- package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
- package/template/apps/web/src/layout/navigation.ts +29 -4
- package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
- package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
- package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
- package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
- package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
- package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
- package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
- package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
- package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
- package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
- package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
- package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
- package/template/apps/web/src/router/business-routes.ts +32 -45
- package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
- package/template/apps/web/src/router/index.ts +64 -54
- package/template/apps/web/src/router/manifest-routes.ts +27 -7
- package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
- package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
- package/template/apps/web/src/router/web-composition-host.ts +20 -0
- package/template/apps/web/src/services/work-order-api.ts +16 -0
- package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
- package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
- package/template/apps/web/vite.config.ts +22 -1
- package/template/docs/architecture/migration-bundle.md +10 -7
- package/template/docs/architecture/module-manifest-composition.md +85 -3
- package/template/docs/architecture/search-provider-protocol.md +6 -1
- package/template/docs/architecture/server-module-interface.md +70 -7
- package/template/docs/development/business-web-routes.md +53 -50
- package/template/docs/development/module-lifecycle.md +48 -54
- package/template/docs/platform/navigation-registry.md +5 -4
- package/template/docs/platform/platform-shell-settings.md +15 -10
- package/template/docs/platform/web-system-crud.md +1 -1
- package/template/package.json +1 -1
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
- package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/schema/platform-settings.ts +3 -0
- package/template/packages/example-work-order/package.json +6 -0
- package/template/packages/example-work-order/src/admin-page.ts +78 -0
- package/template/packages/example-work-order/src/admin.ts +35 -0
- package/template/packages/example-work-order/src/manifest.test.ts +62 -5
- package/template/packages/example-work-order/src/manifest.ts +54 -7
- package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
- package/template/packages/example-work-order/src/server-module.ts +2 -2
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/index.ts +4 -0
- package/template/packages/modules/package.json +3 -2
- package/template/packages/modules/src/admin-ui-entry.ts +8 -0
- package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
- package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
- package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
- package/template/packages/modules/src/capability-audit-validation.ts +38 -0
- package/template/packages/modules/src/capability-guard-validation.ts +148 -0
- package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
- package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
- package/template/packages/modules/src/composition.test.ts +74 -9
- package/template/packages/modules/src/composition.ts +8 -0
- package/template/packages/modules/src/manifest.ts +1 -0
- package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
- package/template/packages/modules/src/module-host-range.test.ts +57 -0
- package/template/packages/modules/src/module-host-range.ts +125 -0
- package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
- package/template/packages/modules/src/project-module-assembly.ts +126 -0
- package/template/packages/modules/src/system-admin-routes.ts +6 -5
- package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
- package/template/packages/modules/src/system-agent-tools.ts +2 -2
- package/template/packages/modules/src/system-api-binding.ts +43 -9
- package/template/packages/modules/src/system-api-control-plane.ts +202 -0
- package/template/packages/modules/src/system-api-data-governance.ts +4 -2
- package/template/packages/modules/src/system-api-operational.ts +57 -8
- package/template/packages/modules/src/system-api-permissions.ts +115 -21
- package/template/packages/modules/src/system-api-watermark.ts +3 -0
- package/template/packages/modules/src/system-audit-actions.ts +7 -0
- package/template/packages/modules/src/system-data-governance.ts +5 -2
- package/template/packages/modules/src/system-features.ts +31 -0
- package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
- package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
- package/template/packages/modules/src/system-module-catalog.ts +68 -7
- package/template/packages/modules/src/system-module-tasks.ts +8 -1
- package/template/packages/modules/src/system-modules-access.test.ts +4 -0
- package/template/packages/modules/src/system-modules.test.ts +10 -2
- package/template/packages/modules/src/system-modules.ts +6 -5
- package/template/packages/modules/src/system-permissions.ts +4 -0
- package/template/packages/modules/user/module.ts +19 -2
- package/template/packages/server-sdk/index.ts +8 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/durable-handler.ts +16 -1
- package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
- package/template/packages/server-sdk/src/module-adapter.ts +59 -0
- package/template/packages/server-sdk/src/route-definition.ts +3 -2
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/audit.test.ts +7 -0
- package/template/packages/shared/src/audit.ts +16 -0
- package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
- package/template/packages/shared/src/platform-settings.ts +17 -0
- package/template/packages/shared/src/rbac.ts +2 -0
- 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
|
-
|
|
45
|
+
binding.auditAction &&
|
|
35
46
|
!manifest.auditActions.some((action) =>
|
|
36
47
|
typeof action === "string"
|
|
37
|
-
? action ===
|
|
38
|
-
: action.key ===
|
|
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 (
|
|
139
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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(
|
|
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
|
+
}
|