create-windy 0.2.19 → 0.2.21

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 (127) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +392 -160
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/README.md +1 -0
  6. package/template/apps/agent-server/Dockerfile +20 -0
  7. package/template/apps/agent-server/README.md +46 -0
  8. package/template/apps/agent-server/pyproject.toml +44 -0
  9. package/template/apps/agent-server/src/southwind_agent_server/__init__.py +5 -0
  10. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +39 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/app.py +263 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/config.py +141 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +99 -0
  14. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +52 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/engine.py +85 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +197 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +246 -0
  18. package/template/apps/agent-server/src/southwind_agent_server/service.py +180 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +317 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/sse.py +41 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/store.py +144 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +80 -0
  23. package/template/apps/agent-server/tests/test_api.py +207 -0
  24. package/template/apps/agent-server/tests/test_config.py +56 -0
  25. package/template/apps/agent-server/tests/test_openai_provider.py +224 -0
  26. package/template/apps/agent-server/tests/test_sqlite_store.py +93 -0
  27. package/template/apps/agent-server/uv.lock +925 -0
  28. package/template/apps/server/src/agent/execution-grants.ts +89 -0
  29. package/template/apps/server/src/agent/remote-runtime.ts +128 -0
  30. package/template/apps/server/src/agent/run-contracts.ts +43 -0
  31. package/template/apps/server/src/agent/run-facade.test.ts +257 -0
  32. package/template/apps/server/src/agent/run-facade.ts +190 -0
  33. package/template/apps/server/src/agent/run-routes.ts +222 -0
  34. package/template/apps/server/src/agent/runtime-bootstrap.ts +53 -0
  35. package/template/apps/server/src/agent/tool-host.test.ts +242 -0
  36. package/template/apps/server/src/agent/tool-host.ts +153 -0
  37. package/template/apps/server/src/application-services.ts +2 -2
  38. package/template/apps/server/src/audit/search-summary.ts +15 -8
  39. package/template/apps/server/src/example-modules.ts +58 -70
  40. package/template/apps/server/src/foundation.ts +8 -4
  41. package/template/apps/server/src/guards.test.ts +13 -0
  42. package/template/apps/server/src/guards.ts +12 -2
  43. package/template/apps/server/src/index.ts +48 -95
  44. package/template/apps/server/src/installed-business-modules.ts +23 -4
  45. package/template/apps/server/src/module-bootstrap.ts +83 -0
  46. package/template/apps/server/src/module-composition.test.ts +54 -1
  47. package/template/apps/server/src/module-composition.ts +58 -0
  48. package/template/apps/server/src/module-host/initialize-contracts.ts +67 -0
  49. package/template/apps/server/src/module-host/initialize.test.ts +192 -0
  50. package/template/apps/server/src/module-host/initialize.ts +328 -0
  51. package/template/apps/server/src/module-host/request-context.test.ts +66 -0
  52. package/template/apps/server/src/module-host/request-context.ts +156 -0
  53. package/template/apps/server/src/module-host/role-presets.test.ts +94 -0
  54. package/template/apps/server/src/module-host/role-presets.ts +76 -0
  55. package/template/apps/server/src/module-host/route-installer.test.ts +317 -0
  56. package/template/apps/server/src/module-host/route-installer.ts +97 -0
  57. package/template/apps/server/src/module-host/route-registration.test.ts +160 -0
  58. package/template/apps/server/src/module-host/search-providers.ts +31 -0
  59. package/template/apps/server/src/module-host/storage-port.test.ts +165 -0
  60. package/template/apps/server/src/module-host/storage-port.ts +59 -0
  61. package/template/apps/server/src/module-host/task-definitions.ts +41 -0
  62. package/template/apps/server/src/module-host/test-fixtures.ts +26 -0
  63. package/template/apps/server/src/module-host/tool-handlers.ts +25 -0
  64. package/template/apps/server/src/module-host.test.ts +202 -58
  65. package/template/apps/server/src/module-host.ts +35 -113
  66. package/template/apps/server/src/module-runtime-validation.ts +40 -0
  67. package/template/apps/server/src/route-guards.test.ts +112 -1
  68. package/template/apps/server/src/runtime-feature.ts +66 -31
  69. package/template/apps/server/src/runtime.test.ts +1 -0
  70. package/template/apps/server/src/runtime.ts +3 -1
  71. package/template/apps/server/src/system/built-in-roles.ts +1 -1
  72. package/template/apps/server/src/work-order/foundation-modules.test.ts +30 -10
  73. package/template/apps/server/src/work-order/routes.test.ts +30 -18
  74. package/template/apps/server/src/work-order/routes.ts +104 -135
  75. package/template/apps/server/src/work-order/search-api.test.ts +14 -3
  76. package/template/apps/server/src/work-order/search-provider.test.ts +23 -15
  77. package/template/apps/server/src/work-order/search-provider.ts +17 -12
  78. package/template/apps/server/src/work-order/task.test.ts +16 -11
  79. package/template/apps/web/Dockerfile +4 -1
  80. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +4 -1
  81. package/template/apps/web/src/layout/GlobalWatermark.vue +2 -0
  82. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +4 -1
  83. package/template/docker-compose.yml +25 -0
  84. package/template/docs/architecture/ai-runtime.md +744 -0
  85. package/template/docs/architecture/object-storage.md +12 -0
  86. package/template/docs/platform/agent-runtime.md +128 -0
  87. package/template/docs/platform/agent-tools.md +8 -1
  88. package/template/package.json +1 -0
  89. package/template/packages/config/index.test.ts +100 -0
  90. package/template/packages/config/index.ts +1 -0
  91. package/template/packages/config/package.json +2 -1
  92. package/template/packages/config/src/ai-openai-compatible.ts +131 -0
  93. package/template/packages/config/src/platform.ts +28 -1
  94. package/template/packages/database/package.json +1 -1
  95. package/template/packages/database/src/runner.ts +7 -9
  96. package/template/packages/example-work-order/index.ts +1 -0
  97. package/template/packages/example-work-order/package.json +2 -0
  98. package/template/packages/example-work-order/src/server-module.ts +61 -0
  99. package/template/packages/jobs/package.json +1 -1
  100. package/template/packages/jobs/src/types.ts +7 -1
  101. package/template/packages/modules/package.json +1 -1
  102. package/template/packages/modules/src/catalog-validation.test.ts +212 -0
  103. package/template/packages/modules/src/catalog-validation.ts +19 -2
  104. package/template/packages/modules/src/migration-bundle-validation.test.ts +191 -0
  105. package/template/packages/server-sdk/index.ts +53 -0
  106. package/template/packages/server-sdk/package.json +18 -3
  107. package/template/packages/server-sdk/src/actor.ts +15 -0
  108. package/template/packages/server-sdk/src/ai-operation-registration.ts +31 -0
  109. package/template/packages/server-sdk/src/audit-port.ts +22 -0
  110. package/template/packages/server-sdk/src/jobs-port.ts +24 -0
  111. package/template/packages/server-sdk/src/module-host.ts +41 -1
  112. package/template/packages/server-sdk/src/namespace.test.ts +26 -0
  113. package/template/packages/server-sdk/src/namespace.ts +13 -0
  114. package/template/packages/server-sdk/src/request-context.ts +21 -0
  115. package/template/packages/server-sdk/src/role-preset.ts +18 -0
  116. package/template/packages/server-sdk/src/route-definition.ts +26 -0
  117. package/template/packages/server-sdk/src/search-provider-port.ts +48 -0
  118. package/template/packages/server-sdk/src/storage-port.ts +25 -0
  119. package/template/packages/server-sdk/src/task-registration.ts +18 -0
  120. package/template/packages/server-sdk/src/tool-host-port.ts +22 -0
  121. package/template/packages/server-sdk/src/tool-registration.ts +29 -0
  122. package/template/packages/shared/index.ts +1 -0
  123. package/template/packages/shared/package.json +1 -1
  124. package/template/packages/shared/src/license-catalog.test.ts +73 -1
  125. package/template/packages/shared/src/license-catalog.ts +95 -0
  126. package/template/packages/{database/src/bundle-validation.ts → shared/src/migration-bundle-validation.ts} +2 -1
  127. package/template/apps/server/src/work-order/task.ts +0 -30
@@ -1,113 +1,35 @@
1
- import {
2
- DurableHandlerRegistry,
3
- type DurableHandlerDefinition,
4
- } from "@southwind-ai/jobs";
5
- import type {
6
- ModuleHost,
7
- ModuleInitializer,
8
- ModuleUploadPolicy,
9
- } from "@southwind-ai/server-sdk";
10
-
11
- interface InstalledModule {
12
- name: string;
13
- }
14
-
15
- export interface InitializeModuleHostsInput {
16
- modules: readonly InstalledModule[];
17
- initializers: readonly ModuleInitializer[];
18
- baseUploadPolicies?: readonly ModuleUploadPolicy[];
19
- }
20
-
21
- export interface InitializedModuleHosts {
22
- readonly uploadPolicies: readonly ModuleUploadPolicy[];
23
- installDurableHandlers(registry: DurableHandlerRegistry): void;
24
- }
25
-
26
- type HandlerInstaller = (registry: DurableHandlerRegistry) => void;
27
-
28
- export async function initializeModuleHosts(
29
- input: InitializeModuleHostsInput,
30
- ): Promise<InitializedModuleHosts> {
31
- const installedNames = new Set(input.modules.map(({ name }) => name));
32
- const initializedNames = new Set<string>();
33
- const uploadPolicies: ModuleUploadPolicy[] = [];
34
- const uploadPurposes = new Set<string>();
35
- for (const policy of input.baseUploadPolicies ?? []) {
36
- if (uploadPurposes.has(policy.purpose)) {
37
- throw new Error(`上传用途重复注册:${policy.purpose}`);
38
- }
39
- uploadPurposes.add(policy.purpose);
40
- uploadPolicies.push(policy);
41
- }
42
- const handlerKeys = new Set<string>();
43
- const handlerInstallers: HandlerInstaller[] = [];
44
-
45
- for (const initializer of input.initializers) {
46
- const moduleName = initializer.moduleName.trim();
47
- if (!installedNames.has(moduleName)) {
48
- throw new Error(`模块初始化器与已安装模块不匹配:${moduleName}`);
49
- }
50
- if (initializedNames.has(moduleName)) {
51
- throw new Error(`模块初始化器重复注册:${moduleName}`);
52
- }
53
- initializedNames.add(moduleName);
54
- await initializer.initialize(
55
- createScopedHost({
56
- moduleName,
57
- uploadPolicies,
58
- uploadPurposes,
59
- handlerKeys,
60
- handlerInstallers,
61
- }),
62
- );
63
- }
64
-
65
- return {
66
- uploadPolicies: [...uploadPolicies],
67
- installDurableHandlers(registry) {
68
- for (const install of handlerInstallers) install(registry);
69
- },
70
- };
71
- }
72
-
73
- interface ScopedHostState {
74
- moduleName: string;
75
- uploadPolicies: ModuleUploadPolicy[];
76
- uploadPurposes: Set<string>;
77
- handlerKeys: Set<string>;
78
- handlerInstallers: HandlerInstaller[];
79
- }
80
-
81
- function createScopedHost(state: ScopedHostState): ModuleHost {
82
- return {
83
- moduleName: state.moduleName,
84
- registerUploadPolicy(policy) {
85
- requireModuleNamespace("上传用途", policy.purpose, state.moduleName);
86
- if (state.uploadPurposes.has(policy.purpose)) {
87
- throw new Error(`上传用途重复注册:${policy.purpose}`);
88
- }
89
- state.uploadPurposes.add(policy.purpose);
90
- state.uploadPolicies.push(policy);
91
- },
92
- registerDurableHandler<T>(definition: DurableHandlerDefinition<T>) {
93
- requireModuleNamespace("任务 handler", definition.key, state.moduleName);
94
- if (state.handlerKeys.has(definition.key)) {
95
- throw new Error(`任务 handler 重复注册:${definition.key}`);
96
- }
97
- state.handlerKeys.add(definition.key);
98
- state.handlerInstallers.push((registry) => {
99
- registry.register(definition);
100
- });
101
- },
102
- };
103
- }
104
-
105
- function requireModuleNamespace(
106
- kind: string,
107
- value: string,
108
- moduleName: string,
109
- ): void {
110
- if (value !== moduleName && !value.startsWith(`${moduleName}.`)) {
111
- throw new Error(`${kind}不属于模块 ${moduleName}:${value}`);
112
- }
113
- }
1
+ export {
2
+ initializeModuleHosts,
3
+ type InitializedModuleHosts,
4
+ type InitializeModuleHostsInput,
5
+ type RegisteredModuleRolePreset,
6
+ type RegisteredModuleRoute,
7
+ type RegisteredModuleSearchProvider,
8
+ type RegisteredModuleTaskHandler,
9
+ type RegisteredModuleToolHandler,
10
+ type RegisteredModuleAiOperationHandler,
11
+ } from "./module-host/initialize.js";
12
+ export {
13
+ buildModuleRequestContext,
14
+ requestActorFromModule,
15
+ toModuleActor,
16
+ } from "./module-host/request-context.js";
17
+ export {
18
+ installModuleRoutes,
19
+ type ModuleRouteInstallContext,
20
+ } from "./module-host/route-installer.js";
21
+ export {
22
+ moduleRolePresetId,
23
+ synchronizeModuleRolePresets,
24
+ } from "./module-host/role-presets.js";
25
+ export {
26
+ adaptModuleSearchProvider,
27
+ adaptModuleSearchProviders,
28
+ } from "./module-host/search-providers.js";
29
+ export {
30
+ createModuleStoragePort,
31
+ type ModuleStorageScope,
32
+ type ModuleStorageServices,
33
+ } from "./module-host/storage-port.js";
34
+ export { createModuleTaskDefinitions } from "./module-host/task-definitions.js";
35
+ export { createModuleToolHandlerMap } from "./module-host/tool-handlers.js";
@@ -0,0 +1,40 @@
1
+ import type { ModuleManifest } from "@southwind-ai/modules";
2
+ import type { ModuleToolHandlers } from "./agent/routes.js";
3
+ import { validateServerModuleComposition } from "./module-composition.js";
4
+ import type { InitializedModuleHosts } from "./module-host.js";
5
+
6
+ export function validateInstalledRuntime(input: {
7
+ modules: ModuleManifest[];
8
+ routes: readonly { method: string; path: string }[];
9
+ tasks: readonly { key: string }[];
10
+ tools: ModuleToolHandlers;
11
+ providers: readonly string[];
12
+ hosts: InitializedModuleHosts;
13
+ }): void {
14
+ const aiOperations = input.hosts.aiOperationHandlers.map(
15
+ ({ registration }) => registration.operationKey,
16
+ );
17
+ validateServerModuleComposition({
18
+ modules: input.modules,
19
+ routes: input.routes,
20
+ tasks: input.tasks,
21
+ tools: input.tools,
22
+ aiOperations,
23
+ providers: input.providers,
24
+ moduleRegistrations: {
25
+ routes: input.hosts.routes.map(
26
+ ({ definition }) => `${definition.method} ${definition.path}`,
27
+ ),
28
+ tasks: input.hosts.taskHandlers.map(
29
+ ({ registration }) => registration.taskKey,
30
+ ),
31
+ tools: input.hosts.toolHandlers.map(
32
+ ({ registration }) => registration.toolKey,
33
+ ),
34
+ aiOperations,
35
+ providers: input.hosts.searchProviders.map(
36
+ ({ provider }) => provider.key,
37
+ ),
38
+ },
39
+ });
40
+ }
@@ -1,5 +1,9 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { entitlementKey, featureKey } from "@southwind-ai/shared";
2
+ import {
3
+ entitlementKey,
4
+ featureKey,
5
+ licenseVersionKey,
6
+ } from "@southwind-ai/shared";
3
7
  import { evaluateAccessGuards } from "./access-guards.js";
4
8
  import { createServerRuntime } from "./runtime.js";
5
9
 
@@ -81,6 +85,113 @@ describe("access guard pipeline", () => {
81
85
  expect("edition" in context.license!).toBe(false);
82
86
  });
83
87
 
88
+ test("Feature 依赖未启用时拒绝并记录 security.denied,依赖启用后放行", async () => {
89
+ const dependency = {
90
+ key: featureKey("business.base"),
91
+ label: "业务基础",
92
+ module: "business",
93
+ visible: "visible" as const,
94
+ enabled: true,
95
+ stage: "stable" as const,
96
+ };
97
+ const capability = {
98
+ ...dependency,
99
+ key: featureKey("business.report"),
100
+ dependencies: [dependency.key],
101
+ };
102
+ let dependencyEnabled = false;
103
+ const runtime = createServerRuntime(
104
+ {},
105
+ {
106
+ actorResolver: { resolve: () => actor(["business.report.read"]) },
107
+ licenseResolver: { resolve: () => ({ entitlements: [] }) },
108
+ featureCatalog: [dependency, capability],
109
+ featureResolver: {
110
+ resolve: (key) => ({
111
+ key,
112
+ enabled: key === dependency.key ? dependencyEnabled : true,
113
+ visible: "visible" as const,
114
+ }),
115
+ },
116
+ },
117
+ );
118
+ const policy = {
119
+ permission: { permissionKey: "business.report.read" },
120
+ feature: capability,
121
+ };
122
+
123
+ const deniedContext = await runtime.createGuardContext(
124
+ new Request("http://local"),
125
+ );
126
+ const denied = await evaluateAccessGuards(runtime, deniedContext, policy);
127
+
128
+ expect(denied.decision.reason).toBe("dependency-missing");
129
+ expect(denied.feature?.auditEvent?.action).toBe("security.denied");
130
+ const auditEvents = await runtime.listAuditEvents({
131
+ page: 1,
132
+ pageSize: 10,
133
+ });
134
+ expect(
135
+ auditEvents.items.some(
136
+ (event) =>
137
+ event.action === "security.denied" &&
138
+ event.target?.id === "business.report",
139
+ ),
140
+ ).toBe(true);
141
+
142
+ dependencyEnabled = true;
143
+ const allowedContext = await runtime.createGuardContext(
144
+ new Request("http://local"),
145
+ );
146
+ const allowed = await evaluateAccessGuards(runtime, allowedContext, policy);
147
+
148
+ expect(allowed.decision.reason).toBe("allowed");
149
+ });
150
+
151
+ test("Feature 依赖不在当前 License 版本内时 fail closed", async () => {
152
+ const dependency = {
153
+ key: featureKey("business.advanced-base"),
154
+ label: "高级业务基础",
155
+ module: "business",
156
+ visible: "visible" as const,
157
+ enabled: true,
158
+ stage: "stable" as const,
159
+ allowedLicenseVersions: [licenseVersionKey("advanced")],
160
+ };
161
+ const capability = {
162
+ ...dependency,
163
+ key: featureKey("business.report"),
164
+ allowedLicenseVersions: [licenseVersionKey("standard")],
165
+ dependencies: [dependency.key],
166
+ };
167
+ const runtime = createServerRuntime(
168
+ {},
169
+ {
170
+ actorResolver: { resolve: () => actor(["business.report.read"]) },
171
+ licenseResolver: {
172
+ resolve: () => ({
173
+ versionKey: licenseVersionKey("standard"),
174
+ entitlements: [],
175
+ }),
176
+ },
177
+ featureCatalog: [dependency, capability],
178
+ featureResolver: {
179
+ resolve: (key) => ({ key, enabled: true, visible: "visible" }),
180
+ },
181
+ },
182
+ );
183
+ const context = await runtime.createGuardContext(
184
+ new Request("http://local"),
185
+ );
186
+
187
+ const result = await evaluateAccessGuards(runtime, context, {
188
+ permission: { permissionKey: "business.report.read" },
189
+ feature: capability,
190
+ });
191
+
192
+ expect(result.decision.reason).toBe("dependency-missing");
193
+ });
194
+
84
195
  test("缺少 Entitlement 时不读取 Feature 状态,开关不能绕过商业授权", async () => {
85
196
  let featureResolveCount = 0;
86
197
  const runtime = createServerRuntime(
@@ -1,4 +1,10 @@
1
- import type { FeatureFlagDefinition } from "@southwind-ai/shared";
1
+ import {
2
+ isLicenseVersionAllowed,
3
+ resolveFeatureState,
4
+ type FeatureFlagDefinition,
5
+ type FeatureFlagState,
6
+ } from "@southwind-ai/shared";
7
+ import type { RequestGuardContext } from "./guards.js";
2
8
 
3
9
  export interface FeatureFlagRuntimeResolver {
4
10
  resolve(
@@ -16,11 +22,16 @@ export interface FeatureFlagRuntimeState {
16
22
  allowedLicenseVersions?: FeatureFlagDefinition["allowedLicenseVersions"];
17
23
  }
18
24
 
25
+ /** 运行时解析后的 Feature;disabledReason 复用 shared resolveFeatureState 语义。 */
26
+ export interface ResolvedFeatureFlag extends FeatureFlagDefinition {
27
+ disabledReason?: FeatureFlagState["reason"];
28
+ }
29
+
19
30
  export class RuntimeFeatureCatalog {
20
31
  private readonly definitions = new Map<string, FeatureFlagDefinition>();
21
32
  private readonly requestCache = new WeakMap<
22
33
  object,
23
- Map<string, FeatureFlagDefinition>
34
+ Map<string, ResolvedFeatureFlag>
24
35
  >();
25
36
 
26
37
  constructor(
@@ -34,18 +45,18 @@ export class RuntimeFeatureCatalog {
34
45
  }
35
46
 
36
47
  resolve(
37
- context: object,
48
+ context: RequestGuardContext,
38
49
  manifestFeature: FeatureFlagDefinition,
39
- ): Promise<FeatureFlagDefinition> {
50
+ ): Promise<ResolvedFeatureFlag> {
40
51
  this.definitions.set(manifestFeature.key, manifestFeature);
41
52
  return this.resolveInternal(context, manifestFeature, new Set());
42
53
  }
43
54
 
44
55
  private async resolveInternal(
45
- context: object,
56
+ context: RequestGuardContext,
46
57
  manifestFeature: FeatureFlagDefinition,
47
58
  visiting: ReadonlySet<string>,
48
- ): Promise<FeatureFlagDefinition> {
59
+ ): Promise<ResolvedFeatureFlag> {
49
60
  const cache = this.cacheFor(context);
50
61
  const cached = cache.get(manifestFeature.key);
51
62
  if (cached) return cached;
@@ -53,33 +64,42 @@ export class RuntimeFeatureCatalog {
53
64
  return disabledFeature(manifestFeature);
54
65
  }
55
66
 
56
- const resolved = await this.resolveOwnState(manifestFeature);
57
- if (!resolved.enabled || !manifestFeature.dependencies?.length) {
58
- cache.set(manifestFeature.key, resolved);
59
- return resolved;
60
- }
61
-
62
- const nextVisiting = new Set(visiting);
63
- nextVisiting.add(manifestFeature.key);
64
- for (const dependencyKey of manifestFeature.dependencies) {
65
- const dependency = this.definitions.get(dependencyKey);
66
- if (!dependency) {
67
- const disabled = disabledFeature(resolved);
68
- cache.set(manifestFeature.key, disabled);
69
- return disabled;
70
- }
71
- const dependencyState = await this.resolveInternal(
72
- context,
73
- dependency,
74
- nextVisiting,
75
- );
76
- if (!dependencyState.enabled || dependencyState.visible !== "visible") {
77
- const disabled = { ...resolved, enabled: false };
78
- cache.set(manifestFeature.key, disabled);
79
- return disabled;
67
+ const own = await this.resolveOwnState(manifestFeature);
68
+ // dependencies 来自 manifest;启用集合取运行时解析后的依赖状态,
69
+ // 最终判定统一交给 shared resolveFeatureState,避免第二套漂移逻辑。
70
+ const enabledDependencies = new Set<string>();
71
+ if (
72
+ own.enabled &&
73
+ own.visible === "visible" &&
74
+ manifestFeature.dependencies?.length
75
+ ) {
76
+ const nextVisiting = new Set(visiting);
77
+ nextVisiting.add(manifestFeature.key);
78
+ for (const dependencyKey of manifestFeature.dependencies) {
79
+ const dependency = this.definitions.get(dependencyKey);
80
+ if (!dependency) continue;
81
+ const dependencyState = await this.resolveInternal(
82
+ context,
83
+ dependency,
84
+ nextVisiting,
85
+ );
86
+ if (
87
+ dependencyState.enabled &&
88
+ dependencyState.visible === "visible" &&
89
+ isFeatureLicensedForContext(dependencyState, context)
90
+ ) {
91
+ enabledDependencies.add(dependencyKey);
92
+ }
80
93
  }
81
94
  }
82
95
 
96
+ const state = resolveFeatureState(own, enabledDependencies, true);
97
+ const resolved: ResolvedFeatureFlag = {
98
+ ...own,
99
+ enabled: state.enabled,
100
+ visible: state.visible,
101
+ ...(state.reason ? { disabledReason: state.reason } : {}),
102
+ };
83
103
  cache.set(manifestFeature.key, resolved);
84
104
  return resolved;
85
105
  }
@@ -108,7 +128,9 @@ export class RuntimeFeatureCatalog {
108
128
  }
109
129
  }
110
130
 
111
- private cacheFor(context: object): Map<string, FeatureFlagDefinition> {
131
+ private cacheFor(
132
+ context: RequestGuardContext,
133
+ ): Map<string, ResolvedFeatureFlag> {
112
134
  let cache = this.requestCache.get(context);
113
135
  if (!cache) {
114
136
  cache = new Map();
@@ -118,6 +140,19 @@ export class RuntimeFeatureCatalog {
118
140
  }
119
141
  }
120
142
 
143
+ function isFeatureLicensedForContext(
144
+ feature: FeatureFlagDefinition,
145
+ context: RequestGuardContext,
146
+ ): boolean {
147
+ return (
148
+ isLicenseVersionAllowed(feature, context.license?.versionKey) &&
149
+ (!feature.requiredEntitlement ||
150
+ Boolean(
151
+ context.license?.entitlements?.includes(feature.requiredEntitlement),
152
+ ))
153
+ );
154
+ }
155
+
121
156
  function disabledFeature(
122
157
  feature: FeatureFlagDefinition,
123
158
  ): FeatureFlagDefinition {
@@ -273,6 +273,7 @@ describe("server runtime", () => {
273
273
  expect(await runtime.resolveFeature(context, capability)).toMatchObject({
274
274
  enabled: false,
275
275
  visible: "visible",
276
+ disabledReason: "dependency-missing",
276
277
  });
277
278
  });
278
279
 
@@ -25,11 +25,13 @@ import { developmentAdminActor } from "./runtime-development.js";
25
25
  import {
26
26
  RuntimeFeatureCatalog,
27
27
  type FeatureFlagRuntimeResolver,
28
+ type ResolvedFeatureFlag,
28
29
  } from "./runtime-feature.js";
29
30
 
30
31
  export type {
31
32
  FeatureFlagRuntimeResolver,
32
33
  FeatureFlagRuntimeState,
34
+ ResolvedFeatureFlag,
33
35
  } from "./runtime-feature.js";
34
36
 
35
37
  export interface SessionRecord {
@@ -230,7 +232,7 @@ export function createServerRuntime(
230
232
  async resolveFeature(
231
233
  context: ReturnType<typeof createRequestGuardContext>,
232
234
  manifestFeature: FeatureFlagDefinition,
233
- ): Promise<FeatureFlagDefinition> {
235
+ ): Promise<ResolvedFeatureFlag> {
234
236
  return featureCatalog.resolve(context, manifestFeature);
235
237
  },
236
238
  recordDecisions(decisions: Array<GuardDecision | undefined>) {
@@ -90,7 +90,7 @@ function role(
90
90
  };
91
91
  }
92
92
 
93
- function menuKeysForPermissions(
93
+ export function menuKeysForPermissions(
94
94
  snapshot: FoundationSnapshot,
95
95
  permissions: string[],
96
96
  ): string[] {
@@ -1,12 +1,15 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { featureKey } from "@southwind-ai/shared";
3
+ import { validateCapabilityCatalogs } from "@southwind-ai/modules";
3
4
  import {
4
5
  createFoundationSnapshot,
5
6
  loadFoundationModules,
6
7
  } from "../foundation.js";
7
- import { createExampleModuleRuntime } from "../example-modules.js";
8
- import type { ManagedDepartment } from "../system/entities.js";
9
- import { InMemoryEntityRepository } from "../system/repository.js";
8
+ import { createWorkOrderModule } from "../example-modules.js";
9
+ import {
10
+ adaptModuleSearchProviders,
11
+ initializeModuleHosts,
12
+ } from "../module-host.js";
10
13
 
11
14
  describe("平台模块 composition root", () => {
12
15
  test("默认安装平台能力模块与可删除的工单模块", () => {
@@ -47,6 +50,21 @@ describe("平台模块 composition root", () => {
47
50
  );
48
51
  });
49
52
 
53
+ test("示例工单的合法 Migration Bundle 通过启动校验", () => {
54
+ const modules = loadFoundationModules();
55
+ const workOrder = modules.find(({ name }) => name === "work-order");
56
+
57
+ expect(() => validateCapabilityCatalogs(modules)).not.toThrow();
58
+ expect(workOrder?.migrationBundle?.migrations.map(({ id }) => id)).toEqual([
59
+ "work-order.0001.create-work-orders",
60
+ ]);
61
+ expect(
62
+ workOrder?.migrationBundle?.migrations.every(({ checksum }) =>
63
+ checksum.startsWith("sha256:"),
64
+ ),
65
+ ).toBe(true);
66
+ });
67
+
50
68
  test("业务 API 的 package 依赖图不经过 Windy Web 或 Vue", async () => {
51
69
  const serverPackage = await packageJson(
52
70
  new URL("../../package.json", import.meta.url),
@@ -66,21 +84,23 @@ describe("平台模块 composition root", () => {
66
84
  });
67
85
  expect(serverPackage.dependencies).not.toHaveProperty("@southwind-ai/web");
68
86
  expect(serverPackage.dependencies).not.toHaveProperty("vue");
69
- expect(businessPackage.dependencies).not.toHaveProperty("@southwind-ai/web");
87
+ expect(businessPackage.dependencies).not.toHaveProperty(
88
+ "@southwind-ai/web",
89
+ );
70
90
  expect(businessPackage.dependencies).not.toHaveProperty("vue");
71
91
  expect(webPackage.dependencies).not.toHaveProperty(
72
92
  "@southwind-ai/example-work-order",
73
93
  );
74
94
  });
75
95
 
76
- test("示例模块运行时从同一 composition seam 提供搜索 Handler", () => {
77
- const snapshot = createFoundationSnapshot();
78
- const runtime = createExampleModuleRuntime({
79
- features: snapshot.features,
80
- departments: new InMemoryEntityRepository<ManagedDepartment>(),
96
+ test("示例模块经 Host 声明式装配提供搜索 Provider", async () => {
97
+ const module = createWorkOrderModule();
98
+ const hosts = await initializeModuleHosts({
99
+ modules: [module.manifest],
100
+ installations: [module],
81
101
  });
82
102
 
83
- expect(runtime?.providers.map(({ key }) => key)).toEqual([
103
+ expect(adaptModuleSearchProviders(hosts).map(({ key }) => key)).toEqual([
84
104
  "work-order.ticket.search",
85
105
  ]);
86
106
  });
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import type { WorkOrder } from "@southwind-ai/example-work-order";
2
+ import {
3
+ workOrderModule,
4
+ type WorkOrder,
5
+ } from "@southwind-ai/example-work-order";
3
6
  import { licenseVersionKey } from "@southwind-ai/shared";
7
+ import { initializeModuleHosts, installModuleRoutes } from "../module-host.js";
8
+ import { createWorkOrderModule } from "../example-modules.js";
4
9
  import { InMemoryEntityRepository } from "../system/repository.js";
5
10
  import { InMemoryScopedEntityRepository } from "../data-access/scoped-repository.js";
6
11
  import { buildScopeContext } from "../data-access/context.js";
@@ -8,8 +13,6 @@ import type { ManagedDepartment } from "../system/entities.js";
8
13
  import { FakeRouteApp } from "../system/route-test-app.js";
9
14
  import { createServerRuntime } from "../runtime.js";
10
15
  import type { RequestActor } from "../guards.js";
11
- import { registerWorkOrderRoutes } from "./routes.js";
12
- import { WorkOrderService } from "./service.js";
13
16
  import { workOrderDataScopeBinding } from "./repository.js";
14
17
 
15
18
  describe("示例工单 routes", () => {
@@ -39,6 +42,9 @@ describe("示例工单 routes", () => {
39
42
  expect(await reason(rbacDenied.invoke("GET", "/work-orders"))).toBe(
40
43
  "missing-permission",
41
44
  );
45
+ expect(
46
+ rbacDenied.runtime.auditSink.list().map(({ action }) => action),
47
+ ).toContain("security.denied");
42
48
 
43
49
  const licenseDenied = await createFixture({ licenseVersion: "basic" });
44
50
  expect(await reason(licenseDenied.invoke("GET", "/work-orders"))).toBe(
@@ -100,23 +106,29 @@ async function createFixture(options: FixtureOptions = {}) {
100
106
  },
101
107
  },
102
108
  );
109
+ const module = createWorkOrderModule();
110
+ const hosts = await initializeModuleHosts({
111
+ modules: [module.manifest],
112
+ installations: [module],
113
+ });
114
+ module.attach({
115
+ departments: new InMemoryEntityRepository<ManagedDepartment>([
116
+ department("dept_a"),
117
+ department("dept_b"),
118
+ ]),
119
+ repository: new InMemoryScopedEntityRepository<WorkOrder>(
120
+ [
121
+ ticket("ticket_self", "user_self", "dept_a"),
122
+ ticket("ticket_other", "user_other", "dept_b"),
123
+ ],
124
+ workOrderDataScopeBinding,
125
+ ),
126
+ });
103
127
  const app = new FakeRouteApp();
104
- const workOrders = new InMemoryScopedEntityRepository<WorkOrder>(
105
- [
106
- ticket("ticket_self", "user_self", "dept_a"),
107
- ticket("ticket_other", "user_other", "dept_b"),
108
- ],
109
- workOrderDataScopeBinding,
110
- );
111
- const departments = new InMemoryEntityRepository<ManagedDepartment>([
112
- department("dept_a"),
113
- department("dept_b"),
114
- ]);
115
- registerWorkOrderRoutes(
116
- app,
117
- new WorkOrderService(workOrders, departments),
128
+ installModuleRoutes(hosts, app, {
118
129
  runtime,
119
- );
130
+ features: workOrderModule().features,
131
+ });
120
132
  const guardContext = await runtime.createGuardContext(
121
133
  new Request("http://localhost/api/work-orders"),
122
134
  );