create-windy 0.2.18 → 0.2.20

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 (84) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +752 -186
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/apps/server/src/application-services.ts +2 -2
  6. package/template/apps/server/src/audit/search-summary.ts +15 -8
  7. package/template/apps/server/src/example-modules.ts +58 -70
  8. package/template/apps/server/src/foundation.ts +8 -4
  9. package/template/apps/server/src/guards.test.ts +13 -0
  10. package/template/apps/server/src/guards.ts +12 -2
  11. package/template/apps/server/src/index.ts +48 -91
  12. package/template/apps/server/src/installed-business-modules.ts +23 -4
  13. package/template/apps/server/src/module-bootstrap.ts +83 -0
  14. package/template/apps/server/src/module-composition.test.ts +45 -1
  15. package/template/apps/server/src/module-composition.ts +45 -0
  16. package/template/apps/server/src/module-host/initialize.test.ts +123 -0
  17. package/template/apps/server/src/module-host/initialize.ts +331 -0
  18. package/template/apps/server/src/module-host/request-context.test.ts +66 -0
  19. package/template/apps/server/src/module-host/request-context.ts +156 -0
  20. package/template/apps/server/src/module-host/role-presets.test.ts +94 -0
  21. package/template/apps/server/src/module-host/role-presets.ts +76 -0
  22. package/template/apps/server/src/module-host/route-installer.test.ts +317 -0
  23. package/template/apps/server/src/module-host/route-installer.ts +97 -0
  24. package/template/apps/server/src/module-host/route-registration.test.ts +160 -0
  25. package/template/apps/server/src/module-host/search-providers.ts +31 -0
  26. package/template/apps/server/src/module-host/storage-port.test.ts +165 -0
  27. package/template/apps/server/src/module-host/storage-port.ts +59 -0
  28. package/template/apps/server/src/module-host/task-definitions.ts +41 -0
  29. package/template/apps/server/src/module-host/test-fixtures.ts +26 -0
  30. package/template/apps/server/src/module-host/tool-handlers.ts +25 -0
  31. package/template/apps/server/src/module-host.test.ts +202 -58
  32. package/template/apps/server/src/module-host.ts +34 -113
  33. package/template/apps/server/src/route-guards.test.ts +112 -1
  34. package/template/apps/server/src/runtime-feature.ts +66 -31
  35. package/template/apps/server/src/runtime.test.ts +1 -0
  36. package/template/apps/server/src/runtime.ts +3 -1
  37. package/template/apps/server/src/system/built-in-roles.ts +1 -1
  38. package/template/apps/server/src/work-order/foundation-modules.test.ts +30 -10
  39. package/template/apps/server/src/work-order/routes.test.ts +30 -18
  40. package/template/apps/server/src/work-order/routes.ts +104 -135
  41. package/template/apps/server/src/work-order/search-api.test.ts +14 -3
  42. package/template/apps/server/src/work-order/search-provider.test.ts +23 -15
  43. package/template/apps/server/src/work-order/search-provider.ts +17 -12
  44. package/template/apps/server/src/work-order/task.test.ts +16 -11
  45. package/template/apps/web/Dockerfile +4 -1
  46. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +4 -1
  47. package/template/apps/web/src/layout/GlobalWatermark.vue +2 -0
  48. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +4 -1
  49. package/template/docs/architecture/object-storage.md +12 -0
  50. package/template/packages/config/index.test.ts +57 -0
  51. package/template/packages/config/package.json +1 -1
  52. package/template/packages/config/src/platform.ts +28 -1
  53. package/template/packages/database/package.json +1 -1
  54. package/template/packages/database/src/runner.ts +7 -9
  55. package/template/packages/example-work-order/index.ts +1 -0
  56. package/template/packages/example-work-order/package.json +2 -0
  57. package/template/packages/example-work-order/src/server-module.ts +61 -0
  58. package/template/packages/jobs/package.json +1 -1
  59. package/template/packages/jobs/src/types.ts +7 -1
  60. package/template/packages/modules/package.json +1 -1
  61. package/template/packages/modules/src/catalog-validation.test.ts +212 -0
  62. package/template/packages/modules/src/catalog-validation.ts +19 -2
  63. package/template/packages/modules/src/migration-bundle-validation.test.ts +191 -0
  64. package/template/packages/server-sdk/index.ts +42 -0
  65. package/template/packages/server-sdk/package.json +16 -3
  66. package/template/packages/server-sdk/src/actor.ts +15 -0
  67. package/template/packages/server-sdk/src/audit-port.ts +22 -0
  68. package/template/packages/server-sdk/src/jobs-port.ts +24 -0
  69. package/template/packages/server-sdk/src/module-host.ts +36 -1
  70. package/template/packages/server-sdk/src/namespace.test.ts +26 -0
  71. package/template/packages/server-sdk/src/namespace.ts +13 -0
  72. package/template/packages/server-sdk/src/request-context.ts +21 -0
  73. package/template/packages/server-sdk/src/role-preset.ts +18 -0
  74. package/template/packages/server-sdk/src/route-definition.ts +26 -0
  75. package/template/packages/server-sdk/src/search-provider-port.ts +48 -0
  76. package/template/packages/server-sdk/src/storage-port.ts +25 -0
  77. package/template/packages/server-sdk/src/task-registration.ts +18 -0
  78. package/template/packages/server-sdk/src/tool-registration.ts +29 -0
  79. package/template/packages/shared/index.ts +1 -0
  80. package/template/packages/shared/package.json +1 -1
  81. package/template/packages/shared/src/license-catalog.test.ts +73 -1
  82. package/template/packages/shared/src/license-catalog.ts +95 -0
  83. package/template/packages/{database/src/bundle-validation.ts → shared/src/migration-bundle-validation.ts} +2 -1
  84. package/template/apps/server/src/work-order/task.ts +0 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "1d4ba94edf509d57d0e3bd9ca36902b4e3778526",
4
- "generatorVersion": "0.2.18",
3
+ "templateCommit": "0219ad152473b48f39df25b3ea261dcef52171c7",
4
+ "generatorVersion": "0.2.20",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",
@@ -40,7 +40,7 @@ interface ApplicationServicesInput {
40
40
  foundation: ReturnType<typeof createFoundationSnapshot>;
41
41
  runtime: ReturnType<typeof createServerRuntime>;
42
42
  storage: ReturnType<typeof createStorageRuntime>;
43
- exampleTasks: ScheduledTaskDefinition[];
43
+ moduleTasks: ScheduledTaskDefinition[];
44
44
  bulkRetentionDays: number;
45
45
  installModuleDurableHandlers?: (registry: DurableHandlerRegistry) => void;
46
46
  }
@@ -90,7 +90,7 @@ export async function createApplicationServices(
90
90
  return { claimed: Boolean(result && result.status !== "idle") };
91
91
  },
92
92
  }),
93
- ...input.exampleTasks,
93
+ ...input.moduleTasks,
94
94
  ];
95
95
  const services = createLegacyServiceRuntime({
96
96
  db: persistence?.db,
@@ -2,13 +2,26 @@ import type { AuditEvent } from "@southwind-ai/shared";
2
2
  import { randomUUIDv7 } from "bun";
3
3
  import type { RequestGuardContext } from "../guards.js";
4
4
 
5
+ export function searchAuditMetadata(
6
+ query: string,
7
+ matched: number,
8
+ ): Record<string, unknown> {
9
+ const normalized = query.trim();
10
+ return {
11
+ queryDigest: new Bun.CryptoHasher("sha256")
12
+ .update(normalized)
13
+ .digest("hex"),
14
+ queryLength: normalized.length,
15
+ matched,
16
+ };
17
+ }
18
+
5
19
  export function searchAuditEvent(
6
20
  context: RequestGuardContext,
7
21
  resourceName: string,
8
22
  query: string,
9
23
  matched: number,
10
24
  ): AuditEvent {
11
- const normalized = query.trim();
12
25
  return {
13
26
  id: randomUUIDv7(),
14
27
  action: "data.search",
@@ -20,13 +33,7 @@ export function searchAuditEvent(
20
33
  },
21
34
  target: { type: resourceName },
22
35
  requestId: context.requestId,
23
- metadata: {
24
- queryDigest: new Bun.CryptoHasher("sha256")
25
- .update(normalized)
26
- .digest("hex"),
27
- queryLength: normalized.length,
28
- matched,
29
- },
36
+ metadata: searchAuditMetadata(query, matched),
30
37
  occurredAt: new Date().toISOString(),
31
38
  };
32
39
  }
@@ -1,86 +1,74 @@
1
- import type { FeatureFlagDefinition } from "@southwind-ai/shared";
1
+ // @windy-module work-order begin
2
+ import {
3
+ createWorkOrderServerModule,
4
+ type WorkOrder,
5
+ } from "@southwind-ai/example-work-order";
6
+ import type { ModuleManifest } from "@southwind-ai/modules";
7
+ import type {
8
+ InstallableServerModule,
9
+ ModuleInitializer,
10
+ } from "@southwind-ai/server-sdk";
2
11
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
3
- import type { createServerRuntime } from "./runtime.js";
4
- import type { ScheduledTaskDefinition } from "./scheduler/types.js";
5
- import type { SearchProvider } from "./search/contracts.js";
12
+ import { platformDataActor } from "./data-access/platform-actor.js";
13
+ import type { ScopedEntityRepository } from "./data-access/scoped-repository.js";
6
14
  import type { ManagedDepartment } from "./system/entities.js";
7
15
  import type { EntityRepository } from "./system/repository.js";
8
- import type { RouteApp } from "./system/route-types.js";
9
- // @windy-module work-order begin
10
- import type { ModuleInitializer } from "@southwind-ai/server-sdk";
11
16
  import { createWorkOrderRepository } from "./work-order/repository.js";
12
17
  import { createDrizzleWorkOrderRepository } from "./work-order/drizzle-repository.js";
13
- import { registerWorkOrderRoutes } from "./work-order/routes.js";
14
- import { WorkOrderService } from "./work-order/service.js";
18
+ import { createWorkOrderRouteDefinitions } from "./work-order/routes.js";
15
19
  import { WorkOrderSearchProvider } from "./work-order/search-provider.js";
16
- import { createWorkOrderTaskDefinitions } from "./work-order/task.js";
17
- // @windy-module work-order end
18
-
19
- // @windy-module work-order begin
20
- export const workOrderModuleInitializer: ModuleInitializer = {
21
- moduleName: "work-order",
22
- initialize(host) {
23
- host.registerUploadPolicy({
24
- purpose: "work-order.ticket.attachment",
25
- maxByteSize: 20 * 1024 * 1024,
26
- maxPartByteSize: 4 * 1024 * 1024,
27
- sessionTtlMs: 2 * 60 * 60_000,
28
- allowedTypes: {
29
- "application/pdf": ["pdf"],
30
- "image/png": ["png"],
31
- "image/jpeg": ["jpg", "jpeg"],
32
- "text/plain": ["txt"],
33
- },
34
- signature: "none",
35
- });
36
- },
37
- };
38
- // @windy-module work-order end
20
+ import { WorkOrderService } from "./work-order/service.js";
39
21
 
40
- export interface ExampleModuleRuntimeDependencies {
41
- features: FeatureFlagDefinition[];
22
+ export interface WorkOrderModuleDependencies {
42
23
  departments: EntityRepository<ManagedDepartment>;
43
24
  db?: NodePgDatabase;
25
+ /** 测试可注入预置数据的仓储;默认按 db 选择 Drizzle 或内存实现。 */
26
+ repository?: ScopedEntityRepository<WorkOrder>;
44
27
  }
45
28
 
46
- export interface ExampleModuleRuntime {
47
- service: WorkOrderService;
48
- tasks: ScheduledTaskDefinition[];
49
- providers: SearchProvider[];
29
+ export interface WorkOrderModule {
30
+ readonly manifest: ModuleManifest;
31
+ setup: InstallableServerModule<ModuleManifest>["setup"];
32
+ /** @deprecated 测试兼容入口;生产装配使用 manifest + setup。 */
33
+ readonly initializer: ModuleInitializer;
34
+ /** 宿主运行时就绪后挂载模块服务;路由与任务 Handler 延迟解析它。 */
35
+ attach(dependencies: WorkOrderModuleDependencies): void;
50
36
  }
51
37
 
52
- export function createExampleModuleRuntime(
53
- dependencies: ExampleModuleRuntimeDependencies,
54
- ): ExampleModuleRuntime | undefined {
55
- if (!dependencies.features.some(({ key }) => key === "work-order.ticket")) {
56
- return undefined;
57
- }
58
- const service = new WorkOrderService(
59
- dependencies.db
60
- ? createDrizzleWorkOrderRepository(dependencies.db)
61
- : createWorkOrderRepository(),
62
- dependencies.departments,
63
- );
38
+ /**
39
+ * 示例工单模块:以 initializer + Host 声明式装配的样板。
40
+ * Manifest 声明 Route/Task/Provider,初始化器注册实现与受限 Port 定义,
41
+ * 不再由宿主手工逐项接线。
42
+ */
43
+ export function createWorkOrderModule(): WorkOrderModule {
44
+ let service: WorkOrderService | undefined;
45
+ const requireService = () => {
46
+ if (!service) throw new Error("示例工单模块尚未挂载");
47
+ return service;
48
+ };
49
+ const serverModule = createWorkOrderServerModule({
50
+ routes: createWorkOrderRouteDefinitions(requireService),
51
+ snapshot: () =>
52
+ requireService().operationalSnapshot(
53
+ platformDataActor("task_work_order_snapshot", "工单运营快照任务"),
54
+ ),
55
+ searchProvider: new WorkOrderSearchProvider(requireService),
56
+ });
64
57
  return {
65
- service,
66
- tasks: createWorkOrderTaskDefinitions(dependencies.features, service),
67
- providers: [new WorkOrderSearchProvider(service)],
58
+ ...serverModule,
59
+ initializer: {
60
+ moduleName: serverModule.manifest.name,
61
+ initialize: serverModule.setup,
62
+ },
63
+ attach(dependencies) {
64
+ service = new WorkOrderService(
65
+ dependencies.repository ??
66
+ (dependencies.db
67
+ ? createDrizzleWorkOrderRepository(dependencies.db)
68
+ : createWorkOrderRepository()),
69
+ dependencies.departments,
70
+ );
71
+ },
68
72
  };
69
73
  }
70
-
71
- export interface ExampleModuleRouteDependencies {
72
- app: RouteApp;
73
- runtime: ReturnType<typeof createServerRuntime>;
74
- moduleRuntime?: ExampleModuleRuntime;
75
- }
76
-
77
- export function registerExampleModuleRoutes(
78
- dependencies: ExampleModuleRouteDependencies,
79
- ): void {
80
- if (!dependencies.moduleRuntime) return;
81
- registerWorkOrderRoutes(
82
- dependencies.app,
83
- dependencies.moduleRuntime.service,
84
- dependencies.runtime,
85
- );
86
- }
74
+ // @windy-module work-order end
@@ -13,7 +13,8 @@ import type {
13
13
  MenuNode,
14
14
  PermissionDefinition,
15
15
  } from "@southwind-ai/shared";
16
- import { installedBusinessModuleFactories } from "./installed-business-modules.js";
16
+ import { resolveLicenseCatalog } from "@southwind-ai/shared";
17
+ import { installedBusinessModules } from "./installed-business-modules.js";
17
18
 
18
19
  export interface FoundationSnapshot {
19
20
  platform: {
@@ -38,8 +39,8 @@ export interface FoundationSnapshot {
38
39
  export function loadFoundationModules(): ModuleManifest[] {
39
40
  const registry = createModuleRegistry();
40
41
  for (const factory of systemModuleFactories) registry.register(factory);
41
- for (const factory of installedBusinessModuleFactories) {
42
- registry.register(factory);
42
+ for (const module of installedBusinessModules) {
43
+ registry.register(() => module.manifest);
43
44
  }
44
45
  return registry.list();
45
46
  }
@@ -72,7 +73,10 @@ export function createFoundationSnapshot(
72
73
  config: PlatformConfig = loadPlatformConfig(),
73
74
  ): FoundationSnapshot {
74
75
  const modules = loadFoundationModules();
75
- const catalogs = validateCapabilityCatalogs(modules);
76
+ const catalogs = validateCapabilityCatalogs(
77
+ modules,
78
+ resolveLicenseCatalog(config.license.versions),
79
+ );
76
80
 
77
81
  return {
78
82
  platform: {
@@ -71,4 +71,17 @@ describe("guards", () => {
71
71
  }).reason,
72
72
  ).toBe("entitlement-not-granted");
73
73
  });
74
+
75
+ test("运行时解析出依赖缺失时拒绝并记录 security.denied", () => {
76
+ const denied = evaluateFeatureGuard(context, {
77
+ ...feature,
78
+ enabled: false,
79
+ disabledReason: "dependency-missing",
80
+ });
81
+
82
+ expect(denied.allowed).toBe(false);
83
+ expect(denied.reason).toBe("dependency-missing");
84
+ expect(denied.auditEvent?.action).toBe("security.denied");
85
+ expect(denied.auditEvent?.target?.id).toBe(feature.key);
86
+ });
74
87
  });
@@ -3,6 +3,7 @@ import type {
3
3
  AuditEvent,
4
4
  EntitlementKey,
5
5
  FeatureFlagDefinition,
6
+ FeatureFlagState,
6
7
  LicenseVersionKey,
7
8
  } from "@southwind-ai/shared";
8
9
  import { correlationId, isLicenseVersionAllowed } from "@southwind-ai/shared";
@@ -47,6 +48,7 @@ export type GuardDecisionReason =
47
48
  | "missing-permission"
48
49
  | "role-not-allowed"
49
50
  | "feature-disabled"
51
+ | "dependency-missing"
50
52
  | "license-version-not-allowed"
51
53
  | "entitlement-not-granted"
52
54
  | "license-restricted";
@@ -120,13 +122,21 @@ export function evaluateRoleGuard(
120
122
 
121
123
  export function evaluateFeatureGuard(
122
124
  context: RequestGuardContext,
123
- feature: FeatureFlagDefinition,
125
+ feature: FeatureFlagDefinition & {
126
+ disabledReason?: FeatureFlagState["reason"];
127
+ },
124
128
  ): GuardDecision {
125
129
  if (feature.enabled && feature.visible === "visible") {
126
130
  return { allowed: true, reason: "allowed" };
127
131
  }
128
132
 
129
- return deniedDecision(context, "feature-disabled", feature.key);
133
+ return deniedDecision(
134
+ context,
135
+ feature.disabledReason === "dependency-missing"
136
+ ? "dependency-missing"
137
+ : "feature-disabled",
138
+ feature.key,
139
+ );
130
140
  }
131
141
 
132
142
  export function evaluateEntitlementGuard(
@@ -2,10 +2,6 @@ import { loadPlatformConfig } from "@southwind-ai/config";
2
2
  import pino from "pino";
3
3
  import { registerAdminBootstrapRoute } from "./admin/routes.js";
4
4
  import { registerSessionContextRoute } from "./session/routes.js";
5
- import {
6
- createFoundationSnapshot,
7
- loadFoundationModules,
8
- } from "./foundation.js";
9
5
  import { registerFoundationRoute } from "./foundation-route.js";
10
6
  // @windy-module system.scheduler begin
11
7
  import { createApplicationServices } from "./application-services.js";
@@ -44,10 +40,6 @@ import { registerSystemOperationRoutes } from "./system/operations.js";
44
40
  import { createPostgresMonitorProbe } from "./system/postgres-monitoring.js";
45
41
  // @windy-module system.operations end
46
42
  import { registerSystemRoutes } from "./system/routes.js";
47
- import {
48
- createSystemRepositories,
49
- synchronizeBuiltInRoles,
50
- } from "./system/seed.js";
51
43
  // @windy-module system.scheduler begin
52
44
  import { registerSchedulerRoutes } from "./scheduler/routes.js";
53
45
  import { SchedulerWorker } from "./scheduler/worker.js";
@@ -59,25 +51,17 @@ import { registerProfileRoutes } from "./profile/routes.js";
59
51
  // @windy-module system.notification begin
60
52
  import { registerNotificationRoutes } from "./notification/routes.js";
61
53
  // @windy-module system.notification end
62
- // @windy-module work-order begin
63
- import {
64
- createExampleModuleRuntime,
65
- registerExampleModuleRoutes,
66
- workOrderModuleInitializer,
67
- } from "./example-modules.js";
68
- // @windy-module work-order end
69
- // @windy-module system.configuration begin
70
- import { bootstrapModuleConfiguration } from "./configuration/bootstrap.js";
71
- // @windy-module system.configuration end
72
54
  // @windy-module system.storage begin
73
- import {
74
- createStorageRuntime,
75
- platformUploadPolicies,
76
- } from "./storage/runtime.js";
55
+ import { createStorageRuntime } from "./storage/runtime.js";
77
56
  import { registerStorageRoutes } from "./storage/routes.js";
78
57
  // @windy-module system.storage end
79
58
  import { validateServerModuleComposition } from "./module-composition.js";
80
- import { initializeModuleHosts } from "./module-host.js";
59
+ import {
60
+ adaptModuleSearchProviders,
61
+ createModuleTaskDefinitions,
62
+ createModuleToolHandlerMap,
63
+ installModuleRoutes,
64
+ } from "./module-host.js";
81
65
  // @windy-module system.search begin
82
66
  import { createSearchHost } from "./search/host.js";
83
67
  // @windy-module system.search end
@@ -88,6 +72,7 @@ import {
88
72
  createPlatformObservability,
89
73
  logPlatformStartup,
90
74
  } from "./observability/bootstrap.js";
75
+ import { bootstrapServerModules } from "./module-bootstrap.js";
91
76
 
92
77
  const logger = pino({ name: "windy-server" });
93
78
  const platformConfig = loadPlatformConfig();
@@ -101,45 +86,16 @@ const persistence = createServerPersistence(
101
86
  // @windy-module system.audit begin
102
87
  await persistence?.auditWriter.recover();
103
88
  // @windy-module system.audit end
104
- // @windy-module system.configuration begin
105
- const configurationRuntime = await bootstrapModuleConfiguration(
106
- platformConfig,
107
- persistence?.configVersionRepository,
108
- );
109
- // @windy-module system.configuration end
110
- const foundationSnapshot = createFoundationSnapshot(platformConfig);
111
- // @windy-module system.configuration begin
112
- configurationRuntime.bindFoundation(foundationSnapshot);
113
- // @windy-module system.configuration end
114
- const foundationModules = loadFoundationModules();
115
- const moduleHosts = await initializeModuleHosts({
116
- modules: foundationModules,
117
- initializers: [
118
- // @windy-module work-order begin
119
- workOrderModuleInitializer,
120
- // @windy-module work-order end
121
- ],
122
- baseUploadPolicies: [
123
- // @windy-module system.storage begin
124
- ...platformUploadPolicies,
125
- // @windy-module system.storage end
126
- ],
127
- });
128
- void moduleHosts;
129
- const systemRepositories = createSystemRepositories(foundationSnapshot, {
130
- db: persistence?.db,
131
- });
132
- const featureSync = await systemRepositories.features.syncManifest(
133
- foundationSnapshot.features,
134
- );
135
- await systemRepositories.menuRegistry.synchronize({
136
- id: "system_bootstrap",
137
- type: "service",
138
- name: "平台初始化",
139
- roleCodes: ["system"],
140
- permissionKeys: ["system.menu.manage"],
141
- });
142
- await synchronizeBuiltInRoles(systemRepositories, foundationSnapshot);
89
+ const {
90
+ // @windy-module system.configuration begin
91
+ configurationRuntime,
92
+ // @windy-module system.configuration end
93
+ featureSync,
94
+ foundationModules,
95
+ foundationSnapshot,
96
+ moduleHosts,
97
+ systemRepositories,
98
+ } = await bootstrapServerModules(platformConfig, persistence);
143
99
  // @windy-module system.license begin
144
100
  const platformLicenseOptions = createPlatformLicenseRuntimeOptions(process.env);
145
101
  const platformLicenseRuntime = new PlatformLicenseRuntime(
@@ -195,33 +151,18 @@ const platformSettings = createPlatformSettingsRuntime(persistence?.db, {
195
151
  logoUrl: platformConfig.logoUrl,
196
152
  });
197
153
  // @windy-module system.settings end
198
- // @windy-module work-order begin
199
- const exampleModuleRuntime = createExampleModuleRuntime({
200
- features: foundationSnapshot.features,
201
- departments: systemRepositories.departments,
202
- db: persistence?.db,
203
- });
204
- // @windy-module work-order end
205
- // @windy-module system.search begin
206
- const businessSearchProviders = [
207
- // @windy-module work-order begin
208
- ...(exampleModuleRuntime?.providers || []),
209
- // @windy-module work-order end
210
- ];
211
- // @windy-module system.search end
212
154
  // @windy-module system.search begin
213
155
  const searchHost = createSearchHost(
214
156
  foundationModules,
215
- { providers: businessSearchProviders },
157
+ { providers: adaptModuleSearchProviders(moduleHosts) },
216
158
  runtime,
217
159
  );
218
160
  // @windy-module system.search end
219
161
  // @windy-module system.scheduler begin
220
- const businessTaskDefinitions = [
221
- // @windy-module work-order begin
222
- ...(exampleModuleRuntime?.tasks || []),
223
- // @windy-module work-order end
224
- ];
162
+ const moduleTaskDefinitions = createModuleTaskDefinitions(
163
+ moduleHosts,
164
+ foundationSnapshot.features,
165
+ );
225
166
  const {
226
167
  bulkDataService,
227
168
  scheduler,
@@ -229,13 +170,14 @@ const {
229
170
  notifications: notificationRepository,
230
171
  workflow: workflowService,
231
172
  governedExportService,
173
+ durable: durableJobRuntime,
232
174
  } = await createApplicationServices({
233
175
  persistence,
234
176
  systemRepositories,
235
177
  foundation: foundationSnapshot,
236
178
  runtime,
237
179
  storage: storageRuntime,
238
- exampleTasks: businessTaskDefinitions,
180
+ moduleTasks: moduleTaskDefinitions,
239
181
  bulkRetentionDays: readPositiveInt(process.env.BULK_DATA_RETENTION_DAYS, 7),
240
182
  installModuleDurableHandlers: moduleHosts.installDurableHandlers,
241
183
  });
@@ -246,19 +188,15 @@ const schedulerWorker = new SchedulerWorker(
246
188
  (result) => observability.metrics.observeSchedulerTick(result),
247
189
  );
248
190
  // @windy-module system.scheduler end
249
-
250
191
  await bootstrapAdminPassword({
251
192
  authService,
252
193
  authRepository: persistence?.authRepository,
253
194
  repositories: systemRepositories,
254
195
  password: process.env.WINDY_BOOTSTRAP_ADMIN_PASSWORD,
255
196
  });
256
-
257
197
  const app = createPlatformHttpApp(runtime, observability);
258
198
  observability.register(app);
259
-
260
199
  registerAuthSessionRoute(app);
261
-
262
200
  registerAuthRoutes(app, authService, {
263
201
  sessionCookieName: platformConfig.security.sessionCookieName,
264
202
  secureCookie: process.env.NODE_ENV === "production",
@@ -345,7 +283,10 @@ registerSystemOperationRoutes(app, runtime, foundationSnapshot.features, {
345
283
  });
346
284
  // @windy-module system.operations end
347
285
  // @windy-module system.agent begin
348
- const toolHandlers = createSystemToolHandlers(systemRepositories, runtime);
286
+ const toolHandlers = new Map([
287
+ ...createSystemToolHandlers(systemRepositories, runtime),
288
+ ...createModuleToolHandlerMap(moduleHosts),
289
+ ]);
349
290
  registerAgentRoutes(app, foundationModules, toolHandlers, runtime);
350
291
  // @windy-module system.agent end
351
292
  // @windy-module system.scheduler begin
@@ -360,10 +301,14 @@ registerBulkDataRoutes(
360
301
  );
361
302
  // @windy-module system.bulk-data end
362
303
  // @windy-module work-order begin
363
- registerExampleModuleRoutes({
364
- app,
304
+ installModuleRoutes(moduleHosts, app, {
365
305
  runtime,
366
- moduleRuntime: exampleModuleRuntime,
306
+ features: foundationSnapshot.features,
307
+ jobs: durableJobRuntime.service,
308
+ storage: {
309
+ uploads: storageRuntime.uploads,
310
+ files: storageRuntime.files,
311
+ },
367
312
  });
368
313
  // @windy-module work-order end
369
314
 
@@ -379,6 +324,18 @@ validateServerModuleComposition({
379
324
  // @windy-module system.search begin
380
325
  providers: searchHost.providerKeys,
381
326
  // @windy-module system.search end
327
+ moduleRegistrations: {
328
+ routes: moduleHosts.routes.map(
329
+ ({ definition }) => `${definition.method} ${definition.path}`,
330
+ ),
331
+ tasks: moduleHosts.taskHandlers.map(
332
+ ({ registration }) => registration.taskKey,
333
+ ),
334
+ tools: moduleHosts.toolHandlers.map(
335
+ ({ registration }) => registration.toolKey,
336
+ ),
337
+ providers: moduleHosts.searchProviders.map(({ provider }) => provider.key),
338
+ },
382
339
  });
383
340
 
384
341
  const port = process.env.PORT ? Number(process.env.PORT) : 9_747;
@@ -1,10 +1,29 @@
1
1
  // @windy-module work-order begin
2
- import { workOrderModule } from "@southwind-ai/example-work-order";
2
+ import { createWorkOrderModule } from "./example-modules.js";
3
3
  // @windy-module work-order end
4
- import type { ModuleFactory } from "@southwind-ai/modules";
4
+ import type { ModuleManifest } from "@southwind-ai/modules";
5
+ import type { InstallableServerModule } from "@southwind-ai/server-sdk";
6
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
7
+ import type { ManagedDepartment } from "./system/entities.js";
8
+ import type { EntityRepository } from "./system/repository.js";
5
9
 
6
- export const installedBusinessModuleFactories: ModuleFactory[] = [
10
+ export interface BusinessModuleRuntime {
11
+ readonly db?: NodePgDatabase;
12
+ readonly departments: EntityRepository<ManagedDepartment>;
13
+ }
14
+
15
+ interface InstalledBusinessModule extends InstallableServerModule<ModuleManifest> {
16
+ attach(runtime: BusinessModuleRuntime): void;
17
+ }
18
+
19
+ export const installedBusinessModules: readonly InstalledBusinessModule[] = [
7
20
  // @windy-module work-order begin
8
- workOrderModule,
21
+ createWorkOrderModule(),
9
22
  // @windy-module work-order end
10
23
  ];
24
+
25
+ export function attachInstalledBusinessModules(
26
+ runtime: BusinessModuleRuntime,
27
+ ): void {
28
+ for (const module of installedBusinessModules) module.attach(runtime);
29
+ }
@@ -0,0 +1,83 @@
1
+ import type { PlatformConfig } from "@southwind-ai/config";
2
+ // @windy-module system.configuration begin
3
+ import { bootstrapModuleConfiguration } from "./configuration/bootstrap.js";
4
+ // @windy-module system.configuration end
5
+ import {
6
+ createFoundationSnapshot,
7
+ loadFoundationModules,
8
+ } from "./foundation.js";
9
+ import {
10
+ attachInstalledBusinessModules,
11
+ installedBusinessModules,
12
+ } from "./installed-business-modules.js";
13
+ import {
14
+ initializeModuleHosts,
15
+ synchronizeModuleRolePresets,
16
+ } from "./module-host.js";
17
+ import type { ServerPersistence } from "./persistence.js";
18
+ // @windy-module system.storage begin
19
+ import { platformUploadPolicies } from "./storage/runtime.js";
20
+ // @windy-module system.storage end
21
+ import {
22
+ createSystemRepositories,
23
+ synchronizeBuiltInRoles,
24
+ } from "./system/seed.js";
25
+
26
+ export async function bootstrapServerModules(
27
+ platformConfig: PlatformConfig,
28
+ persistence: ServerPersistence | undefined,
29
+ ) {
30
+ // @windy-module system.configuration begin
31
+ const configurationRuntime = await bootstrapModuleConfiguration(
32
+ platformConfig,
33
+ persistence?.configVersionRepository,
34
+ );
35
+ // @windy-module system.configuration end
36
+ const foundationSnapshot = createFoundationSnapshot(platformConfig);
37
+ // @windy-module system.configuration begin
38
+ configurationRuntime.bindFoundation(foundationSnapshot);
39
+ // @windy-module system.configuration end
40
+ const foundationModules = loadFoundationModules();
41
+ const moduleHosts = await initializeModuleHosts({
42
+ modules: foundationModules,
43
+ installations: installedBusinessModules,
44
+ baseUploadPolicies: [
45
+ // @windy-module system.storage begin
46
+ ...platformUploadPolicies,
47
+ // @windy-module system.storage end
48
+ ],
49
+ });
50
+ const systemRepositories = createSystemRepositories(foundationSnapshot, {
51
+ db: persistence?.db,
52
+ });
53
+ const featureSync = await systemRepositories.features.syncManifest(
54
+ foundationSnapshot.features,
55
+ );
56
+ await systemRepositories.menuRegistry.synchronize({
57
+ id: "system_bootstrap",
58
+ type: "service",
59
+ name: "平台初始化",
60
+ roleCodes: ["system"],
61
+ permissionKeys: ["system.menu.manage"],
62
+ });
63
+ await synchronizeBuiltInRoles(systemRepositories, foundationSnapshot);
64
+ await synchronizeModuleRolePresets(
65
+ systemRepositories,
66
+ foundationSnapshot,
67
+ moduleHosts.rolePresets,
68
+ );
69
+ attachInstalledBusinessModules({
70
+ db: persistence?.db,
71
+ departments: systemRepositories.departments,
72
+ });
73
+ return {
74
+ // @windy-module system.configuration begin
75
+ configurationRuntime,
76
+ // @windy-module system.configuration end
77
+ featureSync,
78
+ foundationModules,
79
+ foundationSnapshot,
80
+ moduleHosts,
81
+ systemRepositories,
82
+ };
83
+ }