create-windy 0.3.18 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +28 -13
  2. package/dist/cli.js +788 -280
  3. package/package.json +1 -1
  4. package/template/.agents/skills/windy-business-module/SKILL.md +34 -10
  5. package/template/.windy-template.json +2 -2
  6. package/template/AGENTS.md +18 -8
  7. package/template/README.md +32 -22
  8. package/template/apps/agent-server/src/southwind_agent_server/adk_debug.py +38 -5
  9. package/template/apps/agent-server/tests/test_adk_debug_result_polling.py +139 -0
  10. package/template/apps/server/Dockerfile +1 -0
  11. package/template/apps/server/package.json +1 -0
  12. package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
  13. package/template/apps/server/scripts/validate-module-composition.ts +28 -0
  14. package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
  15. package/template/apps/server/src/agent/routes.test.ts +20 -7
  16. package/template/apps/server/src/agent/routes.ts +14 -0
  17. package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
  18. package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
  19. package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
  20. package/template/apps/server/src/app/server-composition.ts +56 -0
  21. package/template/apps/server/src/application-services.ts +2 -0
  22. package/template/apps/server/src/bulk-data/service.ts +14 -0
  23. package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
  24. package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
  25. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
  26. package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
  27. package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
  28. package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
  29. package/template/apps/server/src/feature/routes.ts +33 -15
  30. package/template/apps/server/src/foundation-activation.test.ts +60 -0
  31. package/template/apps/server/src/foundation.ts +64 -5
  32. package/template/apps/server/src/guards.test.ts +23 -0
  33. package/template/apps/server/src/guards.ts +12 -0
  34. package/template/apps/server/src/index.ts +28 -34
  35. package/template/apps/server/src/installed-business-modules.ts +13 -24
  36. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
  37. package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
  38. package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
  39. package/template/apps/server/src/license/activation.test.ts +30 -0
  40. package/template/apps/server/src/license/activation.ts +26 -0
  41. package/template/apps/server/src/license/restriction.test.ts +16 -0
  42. package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
  43. package/template/apps/server/src/license/routes.ts +20 -4
  44. package/template/apps/server/src/license/runtime-service.test.ts +10 -2
  45. package/template/apps/server/src/module-bootstrap.ts +14 -23
  46. package/template/apps/server/src/module-composition.test.ts +43 -2
  47. package/template/apps/server/src/module-composition.ts +29 -0
  48. package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
  49. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
  50. package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
  51. package/template/apps/server/src/module-host/durable-context.ts +42 -2
  52. package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
  53. package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
  54. package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
  55. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  56. package/template/apps/server/src/module-host/initialize.test.ts +78 -2
  57. package/template/apps/server/src/module-host/initialize.ts +15 -4
  58. package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
  59. package/template/apps/server/src/module-host/project-attachments.ts +58 -0
  60. package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
  61. package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
  62. package/template/apps/server/src/module-host/project-composition.ts +311 -0
  63. package/template/apps/server/src/module-host/request-context.ts +3 -0
  64. package/template/apps/server/src/module-host/route-declaration.ts +15 -6
  65. package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
  66. package/template/apps/server/src/module-host/route-installer.ts +12 -2
  67. package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
  68. package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
  69. package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
  70. package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
  71. package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
  72. package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
  73. package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
  74. package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
  75. package/template/apps/server/src/module-host/task-definitions.ts +1 -0
  76. package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
  77. package/template/apps/server/src/module-host.test.ts +2 -0
  78. package/template/apps/server/src/module-preflight.test.ts +289 -0
  79. package/template/apps/server/src/module-preflight.ts +96 -0
  80. package/template/apps/server/src/module-runtime-validation.ts +1 -0
  81. package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
  82. package/template/apps/server/src/platform-http-runtime.ts +32 -7
  83. package/template/apps/server/src/route-guards.test.ts +22 -0
  84. package/template/apps/server/src/route-guards.ts +10 -3
  85. package/template/apps/server/src/runtime-audit-query.ts +50 -0
  86. package/template/apps/server/src/runtime-feature.test.ts +46 -0
  87. package/template/apps/server/src/runtime-feature.ts +5 -0
  88. package/template/apps/server/src/runtime.ts +10 -49
  89. package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
  90. package/template/apps/server/src/scheduler/audit.ts +1 -1
  91. package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
  92. package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
  93. package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
  94. package/template/apps/server/src/scheduler/service.test.ts +1 -0
  95. package/template/apps/server/src/scheduler/service.ts +18 -21
  96. package/template/apps/server/src/scheduler/types.ts +5 -1
  97. package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
  98. package/template/apps/server/src/startup-composition-validation.ts +54 -0
  99. package/template/apps/server/src/startup-preflight.test.ts +36 -0
  100. package/template/apps/server/src/startup-preflight.ts +37 -0
  101. package/template/apps/server/src/system/audit-routes.test.ts +9 -3
  102. package/template/apps/server/src/system/department-routes.test.ts +5 -3
  103. package/template/apps/server/src/system/feature-routes.test.ts +4 -3
  104. package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
  105. package/template/apps/server/src/system/interface-catalog.ts +3 -0
  106. package/template/apps/server/src/system/menu-routes.test.ts +5 -3
  107. package/template/apps/server/src/system/route-test-runtime.ts +24 -0
  108. package/template/apps/server/src/system/routes-validation.test.ts +7 -3
  109. package/template/apps/server/src/system/routes.test.ts +21 -11
  110. package/template/apps/server/src/system/routes.ts +11 -2
  111. package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
  112. package/template/apps/server/src/work-order/routes.test.ts +8 -0
  113. package/template/apps/server/src/work-order/task.test.ts +6 -1
  114. package/template/apps/web/Dockerfile +5 -0
  115. package/template/apps/web/package.json +1 -0
  116. package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
  117. package/template/apps/web/src/app/web-composition.ts +24 -0
  118. package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
  119. package/template/apps/web/src/layout/AppSidebar.vue +8 -1
  120. package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
  121. package/template/apps/web/src/layout/navigation.ts +29 -4
  122. package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
  123. package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
  124. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
  125. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
  126. package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
  127. package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
  128. package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
  129. package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
  130. package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
  131. package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
  132. package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
  133. package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
  134. package/template/apps/web/src/router/business-routes.ts +32 -45
  135. package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
  136. package/template/apps/web/src/router/index.ts +64 -54
  137. package/template/apps/web/src/router/manifest-routes.ts +27 -7
  138. package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
  139. package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
  140. package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
  141. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
  142. package/template/apps/web/src/router/web-composition-host.ts +20 -0
  143. package/template/apps/web/src/services/work-order-api.ts +16 -0
  144. package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
  145. package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
  146. package/template/apps/web/vite.config.ts +22 -1
  147. package/template/docs/architecture/migration-bundle.md +10 -7
  148. package/template/docs/architecture/module-manifest-composition.md +85 -3
  149. package/template/docs/architecture/search-provider-protocol.md +6 -1
  150. package/template/docs/architecture/server-module-interface.md +70 -7
  151. package/template/docs/development/business-web-routes.md +53 -50
  152. package/template/docs/development/module-lifecycle.md +48 -54
  153. package/template/docs/platform/agent-runtime.md +2 -0
  154. package/template/docs/platform/navigation-registry.md +5 -4
  155. package/template/package.json +1 -1
  156. package/template/packages/config/package.json +1 -1
  157. package/template/packages/database/package.json +1 -1
  158. package/template/packages/example-work-order/package.json +6 -0
  159. package/template/packages/example-work-order/src/admin-page.ts +78 -0
  160. package/template/packages/example-work-order/src/admin.ts +35 -0
  161. package/template/packages/example-work-order/src/manifest.test.ts +62 -5
  162. package/template/packages/example-work-order/src/manifest.ts +54 -7
  163. package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
  164. package/template/packages/example-work-order/src/server-module.ts +2 -2
  165. package/template/packages/jobs/package.json +1 -1
  166. package/template/packages/modules/index.ts +4 -0
  167. package/template/packages/modules/package.json +3 -2
  168. package/template/packages/modules/src/admin-ui-entry.ts +8 -0
  169. package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
  170. package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
  171. package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
  172. package/template/packages/modules/src/capability-audit-validation.ts +38 -0
  173. package/template/packages/modules/src/capability-guard-validation.ts +148 -0
  174. package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
  175. package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
  176. package/template/packages/modules/src/composition.test.ts +74 -9
  177. package/template/packages/modules/src/composition.ts +8 -0
  178. package/template/packages/modules/src/manifest.ts +1 -0
  179. package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
  180. package/template/packages/modules/src/module-host-range.test.ts +57 -0
  181. package/template/packages/modules/src/module-host-range.ts +125 -0
  182. package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
  183. package/template/packages/modules/src/project-module-assembly.ts +126 -0
  184. package/template/packages/modules/src/system-admin-routes.ts +6 -5
  185. package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
  186. package/template/packages/modules/src/system-agent-tools.ts +2 -2
  187. package/template/packages/modules/src/system-api-binding.ts +43 -9
  188. package/template/packages/modules/src/system-api-control-plane.ts +202 -0
  189. package/template/packages/modules/src/system-api-data-governance.ts +4 -2
  190. package/template/packages/modules/src/system-api-operational.ts +57 -8
  191. package/template/packages/modules/src/system-api-permissions.ts +115 -21
  192. package/template/packages/modules/src/system-api-watermark.ts +3 -0
  193. package/template/packages/modules/src/system-audit-actions.ts +7 -0
  194. package/template/packages/modules/src/system-data-governance.ts +5 -2
  195. package/template/packages/modules/src/system-features.ts +31 -0
  196. package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
  197. package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
  198. package/template/packages/modules/src/system-module-catalog.ts +68 -7
  199. package/template/packages/modules/src/system-module-tasks.ts +8 -1
  200. package/template/packages/modules/src/system-modules-access.test.ts +4 -0
  201. package/template/packages/modules/src/system-modules.test.ts +10 -2
  202. package/template/packages/modules/src/system-modules.ts +6 -5
  203. package/template/packages/modules/src/system-permissions.ts +4 -0
  204. package/template/packages/modules/user/module.ts +19 -2
  205. package/template/packages/server-sdk/index.ts +8 -0
  206. package/template/packages/server-sdk/package.json +2 -1
  207. package/template/packages/server-sdk/src/durable-handler.ts +16 -1
  208. package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
  209. package/template/packages/server-sdk/src/module-adapter.ts +59 -0
  210. package/template/packages/server-sdk/src/route-definition.ts +3 -2
  211. package/template/packages/shared/package.json +1 -1
  212. package/template/packages/shared/src/audit.test.ts +7 -0
  213. package/template/packages/shared/src/audit.ts +16 -0
  214. package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
  215. package/template/packages/shared/src/rbac.ts +2 -0
  216. package/template/apps/web/src/app/business-routes.ts +0 -22
@@ -1,7 +1,7 @@
1
1
  import { randomUUIDv7 } from "bun";
2
2
  import type { SchedulerOperationsSnapshot } from "@southwind-ai/shared";
3
3
  import { runGuardedBackgroundTask } from "../background/task-guard.js";
4
- import type { RequestGuardContext } from "../guards.js";
4
+ import type { RequestActor, RequestGuardContext } from "../guards.js";
5
5
  import type { LegacyDurableAdapter } from "../jobs/legacy-adapter.js";
6
6
  import type { createServerRuntime } from "../runtime.js";
7
7
  import type { SchedulerRepository } from "./repository.js";
@@ -41,6 +41,9 @@ export class SchedulerService {
41
41
  private readonly runtime: ServerRuntime,
42
42
  private readonly clock: SchedulerClock = systemClock,
43
43
  private readonly durable?: LegacyDurableAdapter,
44
+ private readonly resolveActor?: (
45
+ actorId: string,
46
+ ) => Promise<RequestActor | undefined>,
44
47
  ) {
45
48
  this.recorder = new SchedulerExecutionRecorder(repository, runtime, clock);
46
49
  validateTaskDefinitions(definitions);
@@ -196,13 +199,14 @@ export class SchedulerService {
196
199
  await this.executeScheduled(run, false);
197
200
  return;
198
201
  }
199
- await this.execute(
200
- run,
201
- definition,
202
- manualResumeContext(run, definition),
203
- false,
204
- false,
202
+ const actor = await this.resolveActor?.(run.requestedBy);
203
+ const context = await this.runtime.createGuardContextForActor(
204
+ new Request(
205
+ `http://scheduler.local/tasks/${encodeURIComponent(run.taskKey)}/resume`,
206
+ ),
207
+ actor ?? deniedResumeActor(run.requestedBy),
205
208
  );
209
+ await this.execute(run, definition, context, true, false);
206
210
  }
207
211
 
208
212
  private async executeScheduled(run: ScheduledTaskRun, useDurable = true) {
@@ -262,7 +266,7 @@ export class SchedulerService {
262
266
  finishedAt: this.clock.now(),
263
267
  error: guarded.reason,
264
268
  });
265
- this.recorder.denied(context, blocked, guarded.reason);
269
+ this.recorder.denied(context, blocked, definition, guarded.reason);
266
270
  return blocked;
267
271
  }
268
272
  return this.recorder.success(run, definition, context, guarded.value);
@@ -321,19 +325,12 @@ export class SchedulerService {
321
325
  }
322
326
  }
323
327
 
324
- function manualResumeContext(
325
- run: ScheduledTaskRun,
326
- definition: ScheduledTaskDefinition,
327
- ): RequestGuardContext {
328
+ function deniedResumeActor(actorId: string): RequestActor {
328
329
  return {
329
- requestId: randomUUIDv7(),
330
- actor: {
331
- id: run.requestedBy,
332
- type: "user",
333
- name: run.requestedBy,
334
- roleCodes: [],
335
- permissionKeys: [definition.permissionKey],
336
- },
337
- startedAt: new Date().toISOString(),
330
+ id: actorId,
331
+ type: "user",
332
+ name: actorId,
333
+ roleCodes: [],
334
+ permissionKeys: [],
338
335
  };
339
336
  }
@@ -1,4 +1,7 @@
1
- import type { FeatureFlagDefinition } from "@southwind-ai/shared";
1
+ import type {
2
+ AuditActionType,
3
+ FeatureFlagDefinition,
4
+ } from "@southwind-ai/shared";
2
5
 
3
6
  export interface SchedulerClock {
4
7
  now(): Date;
@@ -20,6 +23,7 @@ export interface ScheduledTaskDefinition {
20
23
  maxAttempts: number;
21
24
  retryDelaySeconds: number;
22
25
  permissionKey: string;
26
+ auditAction: AuditActionType;
23
27
  feature: FeatureFlagDefinition;
24
28
  handler: ScheduledTaskHandler;
25
29
  isIdleResult?: (value: Record<string, unknown> | void) => boolean;
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validateCompositionBeforeStartup } from "./startup-composition-validation.js";
3
+
4
+ describe("完整 Server Composition 启动前门禁", () => {
5
+ test("正常启动先执行无数据库验证子进程", () => {
6
+ let databaseUrlSeen = "not-called";
7
+ validateCompositionBeforeStartup(
8
+ ["bun", "src/index.ts"],
9
+ (_command, _args, options) => {
10
+ databaseUrlSeen = options.env.DATABASE_URL ?? "";
11
+ return { status: 0 };
12
+ },
13
+ );
14
+
15
+ expect(databaseUrlSeen).toBe("");
16
+ });
17
+
18
+ test("验证子进程自身不递归,并把失败传回启动进程", () => {
19
+ let calls = 0;
20
+ validateCompositionBeforeStartup(
21
+ ["bun", "src/index.ts", "--validate-module-composition"],
22
+ () => {
23
+ calls += 1;
24
+ return { status: 0 };
25
+ },
26
+ );
27
+ expect(calls).toBe(0);
28
+
29
+ expect(() =>
30
+ validateCompositionBeforeStartup(["bun", "src/index.ts"], () => ({
31
+ status: 1,
32
+ })),
33
+ ).toThrow("启动前校验失败");
34
+ });
35
+ });
@@ -0,0 +1,54 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const VALIDATION_ARGUMENT = "--validate-module-composition";
5
+ const validatorPath = fileURLToPath(
6
+ new URL("../scripts/validate-module-composition.ts", import.meta.url),
7
+ );
8
+ const serverRoot = fileURLToPath(new URL("..", import.meta.url));
9
+
10
+ interface ValidationProcessResult {
11
+ readonly status: number | null;
12
+ readonly error?: Error;
13
+ }
14
+
15
+ type SpawnValidationProcess = (
16
+ command: string,
17
+ args: readonly string[],
18
+ options: {
19
+ readonly cwd: string;
20
+ readonly env: NodeJS.ProcessEnv;
21
+ readonly stdio: "inherit";
22
+ },
23
+ ) => ValidationProcessResult;
24
+
25
+ /**
26
+ * 正常启动先用无数据库子进程走一遍与 build 相同的完整路由/任务/Provider 门禁。
27
+ */
28
+ export function validateCompositionBeforeStartup(
29
+ argv: readonly string[] = process.argv,
30
+ spawn: SpawnValidationProcess = spawnValidationProcess,
31
+ ): void {
32
+ if (argv.includes(VALIDATION_ARGUMENT)) return;
33
+ const env = { ...process.env };
34
+ delete env.DATABASE_URL;
35
+ const result = spawn(process.execPath, [validatorPath], {
36
+ cwd: serverRoot,
37
+ env,
38
+ stdio: "inherit",
39
+ });
40
+ if (result.error) throw result.error;
41
+ if (result.status !== 0) {
42
+ throw new Error(
43
+ `Server 完整 Module Composition 启动前校验失败(exit ${result.status ?? "unknown"})`,
44
+ );
45
+ }
46
+ }
47
+
48
+ function spawnValidationProcess(
49
+ command: string,
50
+ args: readonly string[],
51
+ options: Parameters<SpawnValidationProcess>[2],
52
+ ): ValidationProcessResult {
53
+ return spawnSync(command, [...args], options);
54
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { runPreflightBeforeRecovery } from "./startup-preflight.js";
3
+
4
+ describe("Server 启动前门禁顺序", () => {
5
+ test("先完成无持久化 preflight,再恢复审计队列", async () => {
6
+ const calls: string[] = [];
7
+ const result = await runPreflightBeforeRecovery(
8
+ () => {
9
+ calls.push("prepare");
10
+ return Promise.resolve("ready");
11
+ },
12
+ () => {
13
+ calls.push("recover");
14
+ return Promise.resolve();
15
+ },
16
+ );
17
+
18
+ expect(result).toBe("ready");
19
+ expect(calls).toEqual(["prepare", "recover"]);
20
+ });
21
+
22
+ test("preflight 失败时不触发恢复写入", async () => {
23
+ let recovered = false;
24
+ await expect(
25
+ runPreflightBeforeRecovery(
26
+ () => Promise.reject(new Error("invalid composition")),
27
+ () => {
28
+ recovered = true;
29
+ return Promise.resolve();
30
+ },
31
+ ),
32
+ ).rejects.toThrow("invalid composition");
33
+
34
+ expect(recovered).toBe(false);
35
+ });
36
+ });
@@ -0,0 +1,37 @@
1
+ import type { ModuleUploadPolicy } from "@southwind-ai/server-sdk";
2
+ import { prepareInstalledServerModules } from "./module-preflight.js";
3
+ import { createPlatformBootstrap } from "./platform-bootstrap.js";
4
+ // @windy-module system.storage begin
5
+ import { createPlatformUploadPolicies } from "./storage/runtime.js";
6
+ // @windy-module system.storage end
7
+
8
+ /**
9
+ * 在恢复审计队列、执行迁移或写入目录前,完成项目装配、Host 与 attachment 门禁。
10
+ */
11
+ export async function createPreparedPlatformBootstrap() {
12
+ const bootstrap = createPlatformBootstrap();
13
+ const baseUploadPolicies: ModuleUploadPolicy[] = [
14
+ // @windy-module system.storage begin
15
+ ...createPlatformUploadPolicies(bootstrap.platformConfig),
16
+ // @windy-module system.storage end
17
+ ];
18
+ const recoverAuditQueue = () =>
19
+ // @windy-module system.audit begin
20
+ bootstrap.persistence?.auditWriter.recover() ??
21
+ // @windy-module system.audit end
22
+ Promise.resolve();
23
+ const preparedModules = await runPreflightBeforeRecovery(
24
+ () => prepareInstalledServerModules(baseUploadPolicies),
25
+ recoverAuditQueue,
26
+ );
27
+ return { ...bootstrap, preparedModules };
28
+ }
29
+
30
+ export async function runPreflightBeforeRecovery<T>(
31
+ prepare: () => Promise<T>,
32
+ recover: () => Promise<void>,
33
+ ): Promise<T> {
34
+ const prepared = await prepare();
35
+ await recover();
36
+ return prepared;
37
+ }
@@ -6,12 +6,14 @@ import { registerSystemRoutes } from "./routes.js";
6
6
  import { createSystemRepositories } from "./seed.js";
7
7
 
8
8
  async function auditFixture() {
9
+ const snapshot = createFoundationSnapshot();
9
10
  const runtime = createServerRuntime({ WINDY_DEV_ADMIN_TOKEN: "token_1" });
10
11
  const app = new FakeRouteApp();
11
12
  registerSystemRoutes(
12
13
  app,
13
- createSystemRepositories(createFoundationSnapshot()),
14
+ createSystemRepositories(snapshot),
14
15
  runtime,
16
+ snapshot.features,
15
17
  );
16
18
  const guardContext = await runtime.createGuardContext(
17
19
  new Request("http://localhost/api/system/audit-logs", {
@@ -54,10 +56,12 @@ describe("audit routes", () => {
54
56
  },
55
57
  );
56
58
  const app = new FakeRouteApp();
59
+ const snapshot = createFoundationSnapshot();
57
60
  registerSystemRoutes(
58
61
  app,
59
- createSystemRepositories(createFoundationSnapshot()),
62
+ createSystemRepositories(snapshot),
60
63
  runtime,
64
+ snapshot.features,
61
65
  );
62
66
  const guardContext = await runtime.createGuardContext(
63
67
  new Request("http://localhost/api/system/audit-logs", {
@@ -108,10 +112,12 @@ describe("audit routes", () => {
108
112
  },
109
113
  );
110
114
  const app = new FakeRouteApp();
115
+ const snapshot = createFoundationSnapshot();
111
116
  registerSystemRoutes(
112
117
  app,
113
- createSystemRepositories(createFoundationSnapshot()),
118
+ createSystemRepositories(snapshot),
114
119
  runtime,
120
+ snapshot.features,
115
121
  );
116
122
  const guardContext = await runtime.createGuardContext(
117
123
  new Request("http://localhost/api/system/audit-logs", {
@@ -1,14 +1,16 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { createFoundationSnapshot } from "../foundation.js";
3
- import { createServerRuntime } from "../runtime.js";
4
3
  import { FakeRouteApp } from "./route-test-app.js";
4
+ import { createRouteTestRuntime } from "./route-test-runtime.js";
5
5
  import { registerSystemRoutes } from "./routes.js";
6
6
  import { createSystemRepositories } from "./seed.js";
7
7
 
8
8
  async function fixture() {
9
9
  const snapshot = createFoundationSnapshot();
10
10
  const repositories = createSystemRepositories(snapshot);
11
- const runtime = createServerRuntime({ WINDY_DEV_ADMIN_TOKEN: "token_1" });
11
+ const runtime = createRouteTestRuntime(snapshot, {
12
+ WINDY_DEV_ADMIN_TOKEN: "token_1",
13
+ });
12
14
  const app = new FakeRouteApp();
13
15
  registerSystemRoutes(app, repositories, runtime, snapshot.features);
14
16
  const guardContext = await runtime.createGuardContext(
@@ -27,7 +29,7 @@ async function fixture() {
27
29
  describe("部门组织路由", () => {
28
30
  test("部门处置缺少权限时经过统一 Guard 拒绝", async () => {
29
31
  const snapshot = createFoundationSnapshot();
30
- const runtime = createServerRuntime();
32
+ const runtime = createRouteTestRuntime(snapshot);
31
33
  const app = new FakeRouteApp();
32
34
  registerSystemRoutes(
33
35
  app,
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { createFoundationSnapshot } from "../foundation.js";
3
- import { createServerRuntime } from "../runtime.js";
4
3
  import { FakeRouteApp } from "./route-test-app.js";
4
+ import { createRouteTestRuntime } from "./route-test-runtime.js";
5
5
  import { registerSystemRoutes } from "./routes.js";
6
6
  import { createSystemRepositories } from "./seed.js";
7
7
 
@@ -10,7 +10,8 @@ describe("feature routes", () => {
10
10
  const snapshot = createFoundationSnapshot();
11
11
  const repositories = createSystemRepositories(snapshot);
12
12
  await repositories.features.syncManifest(snapshot.features);
13
- const runtime = createServerRuntime(
13
+ const runtime = createRouteTestRuntime(
14
+ snapshot,
14
15
  { WINDY_DEV_ADMIN_TOKEN: "token_1" },
15
16
  { featureResolver: repositories.features },
16
17
  );
@@ -46,7 +47,7 @@ describe("feature routes", () => {
46
47
  });
47
48
  expect(denied.status).toBe(403);
48
49
  await expect(denied.json()).resolves.toMatchObject({
49
- reason: "feature-disabled",
50
+ reason: "dependency-missing",
50
51
  });
51
52
  expect(app.handlers.has("POST /system/features")).toBe(false);
52
53
  expect(app.handlers.has("POST /system/features/import")).toBe(false);
@@ -15,7 +15,8 @@ describe("系统接口权限目录", () => {
15
15
  ),
16
16
  ).toMatchObject({
17
17
  permissionKey: "system.user.manage",
18
- featureKey: "system.rbac",
18
+ featureKey: "system.user",
19
+ auditAction: "crud.create",
19
20
  guardOrder: ["license", "feature", "permission"],
20
21
  source: "module-manifest",
21
22
  });
@@ -52,11 +53,12 @@ describe("系统接口权限目录", () => {
52
53
  ),
53
54
  ).toMatchObject({
54
55
  permissionKey: "system.feature.manage",
55
- guardOrder: ["license", "permission"],
56
+ featureKey: "system.feature",
57
+ guardOrder: ["license", "feature", "permission"],
56
58
  });
57
59
  expect(
58
60
  interfaces.find((item) => item.path === "/api/foundation"),
59
- ).toMatchObject({ anonymous: true });
61
+ ).toMatchObject({ anonymous: true, auditAction: undefined });
60
62
  // @windy-module system.bulk-data begin
61
63
  expect(
62
64
  interfaces.find(
@@ -1,5 +1,6 @@
1
1
  import type { ModuleManifest } from "@southwind-ai/modules";
2
2
  import type {
3
+ AuditActionType,
3
4
  EntitlementKey,
4
5
  HttpMethod,
5
6
  LicenseVersionKey,
@@ -19,6 +20,7 @@ export interface SystemInterfacePermissionDetail {
19
20
  permissionKey: string;
20
21
  anonymous: boolean;
21
22
  featureKey?: string;
23
+ auditAction?: AuditActionType;
22
24
  allowedLicenseVersions?: readonly LicenseVersionKey[];
23
25
  entitlementKey?: EntitlementKey;
24
26
  guardOrder: readonly GuardStep[];
@@ -56,6 +58,7 @@ export function collectSystemInterfacePermissions(
56
58
  permissionKey: binding.permissionKey,
57
59
  anonymous: binding.anonymous === true,
58
60
  featureKey: feature?.key,
61
+ auditAction: binding.auditAction,
59
62
  allowedLicenseVersions: feature?.allowedLicenseVersions,
60
63
  entitlementKey: feature?.requiredEntitlement,
61
64
  guardOrder: resolveGuardOrder(feature),
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { createFoundationSnapshot } from "../foundation.js";
3
- import { createServerRuntime } from "../runtime.js";
4
3
  import { FakeRouteApp } from "./route-test-app.js";
4
+ import { createRouteTestRuntime } from "./route-test-runtime.js";
5
5
  import { registerSystemRoutes } from "./routes.js";
6
6
  import { createSystemRepositories } from "./seed.js";
7
7
 
@@ -9,7 +9,9 @@ describe("菜单结构批量路由", () => {
9
9
  test("完整草稿通过 system.menu.manage Guard 后一次保存并写审计", async () => {
10
10
  const snapshot = createFoundationSnapshot();
11
11
  const repositories = createSystemRepositories(snapshot);
12
- const runtime = createServerRuntime({ WINDY_DEV_ADMIN_TOKEN: "token_1" });
12
+ const runtime = createRouteTestRuntime(snapshot, {
13
+ WINDY_DEV_ADMIN_TOKEN: "token_1",
14
+ });
13
15
  const app = new FakeRouteApp();
14
16
  registerSystemRoutes(app, repositories, runtime, snapshot.features);
15
17
  const guardContext = await runtime.createGuardContext(
@@ -49,7 +51,7 @@ describe("菜单结构批量路由", () => {
49
51
  test("缺少管理权限时拒绝保存菜单草稿", async () => {
50
52
  const snapshot = createFoundationSnapshot();
51
53
  const repositories = createSystemRepositories(snapshot);
52
- const runtime = createServerRuntime();
54
+ const runtime = createRouteTestRuntime(snapshot);
53
55
  const app = new FakeRouteApp();
54
56
  registerSystemRoutes(app, repositories, runtime, snapshot.features);
55
57
  const guardContext = await runtime.createGuardContext(
@@ -0,0 +1,24 @@
1
+ import type { FeatureFlagDefinition } from "@southwind-ai/shared";
2
+ import { createServerRuntime, type ServerRuntimeOptions } from "../runtime.js";
3
+
4
+ interface TestFeatureCatalog {
5
+ readonly features: readonly FeatureFlagDefinition[];
6
+ readonly disabledModuleNames?: readonly string[];
7
+ }
8
+
9
+ type TestRuntimeOptions = Omit<
10
+ ServerRuntimeOptions,
11
+ "featureCatalog" | "disabledModuleNames"
12
+ >;
13
+
14
+ export function createRouteTestRuntime(
15
+ catalog: TestFeatureCatalog,
16
+ env: Record<string, string | undefined> = {},
17
+ options: TestRuntimeOptions = {},
18
+ ) {
19
+ return createServerRuntime(env, {
20
+ ...options,
21
+ featureCatalog: catalog.features,
22
+ disabledModuleNames: new Set(catalog.disabledModuleNames),
23
+ });
24
+ }
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { createFoundationSnapshot } from "../foundation.js";
3
- import { createServerRuntime } from "../runtime.js";
4
3
  import { FakeRouteApp } from "./route-test-app.js";
4
+ import { createRouteTestRuntime } from "./route-test-runtime.js";
5
5
  import { registerSystemRoutes } from "./routes.js";
6
6
  import { createSystemRepositories } from "./seed.js";
7
7
 
@@ -71,12 +71,16 @@ describe("system route query validation", () => {
71
71
  });
72
72
 
73
73
  async function createFixture(path: string) {
74
- const runtime = createServerRuntime({ WINDY_DEV_ADMIN_TOKEN: "token_1" });
74
+ const snapshot = createFoundationSnapshot();
75
+ const runtime = createRouteTestRuntime(snapshot, {
76
+ WINDY_DEV_ADMIN_TOKEN: "token_1",
77
+ });
75
78
  const app = new FakeRouteApp();
76
79
  registerSystemRoutes(
77
80
  app,
78
- createSystemRepositories(createFoundationSnapshot()),
81
+ createSystemRepositories(snapshot),
79
82
  runtime,
83
+ snapshot.features,
80
84
  );
81
85
  const guardContext = await runtime.createGuardContext(
82
86
  new Request(`http://localhost${path}`, {
@@ -12,19 +12,21 @@ import { registerSystemRoutes } from "./routes.js";
12
12
  import { createSystemRepositories } from "./seed.js";
13
13
 
14
14
  async function routeFixture(
15
- features: FeatureFlagDefinition[] = [],
15
+ features?: FeatureFlagDefinition[],
16
16
  auditWriter?: AuditEventWriter,
17
17
  ) {
18
+ const snapshot = createFoundationSnapshot();
19
+ const activeFeatures = features ?? snapshot.features;
18
20
  const runtime = createServerRuntime(
19
21
  { WINDY_DEV_ADMIN_TOKEN: "token_1" },
20
- { auditWriter },
22
+ { auditWriter, featureCatalog: activeFeatures },
21
23
  );
22
24
  const app = new FakeRouteApp();
23
25
  registerSystemRoutes(
24
26
  app,
25
- createSystemRepositories(createFoundationSnapshot()),
27
+ createSystemRepositories(snapshot),
26
28
  runtime,
27
- features,
29
+ activeFeatures,
28
30
  );
29
31
  const guardContext = await runtime.createGuardContext(
30
32
  new Request("http://localhost/api/system/users", {
@@ -78,12 +80,17 @@ describe("system routes", () => {
78
80
  });
79
81
 
80
82
  test("缺少权限时返回 403 Response 并记录 security.denied", async () => {
81
- const runtime = createServerRuntime();
83
+ const snapshot = createFoundationSnapshot();
84
+ const runtime = createServerRuntime(
85
+ {},
86
+ { featureCatalog: snapshot.features },
87
+ );
82
88
  const app = new FakeRouteApp();
83
89
  registerSystemRoutes(
84
90
  app,
85
- createSystemRepositories(createFoundationSnapshot()),
91
+ createSystemRepositories(snapshot),
86
92
  runtime,
93
+ snapshot.features,
87
94
  );
88
95
  const guardContext = await runtime.createGuardContext(
89
96
  new Request("http://localhost/api/system/users"),
@@ -127,7 +134,7 @@ describe("system routes", () => {
127
134
 
128
135
  expect(hiddenResponse.status).toBe(403);
129
136
  await expect(hiddenResponse.json()).resolves.toMatchObject({
130
- reason: "feature-disabled",
137
+ reason: "dependency-missing",
131
138
  });
132
139
 
133
140
  // @windy-module system.license begin
@@ -209,7 +216,7 @@ describe("system routes", () => {
209
216
 
210
217
  test("用户与角色变更使用关键策略,普通资源变更保持降级可用", async () => {
211
218
  const policies: string[] = [];
212
- const { app, guardContext } = await routeFixture([], {
219
+ const { app, guardContext } = await routeFixture(undefined, {
213
220
  write: async (_event, policy) => void policies.push(policy || "ordinary"),
214
221
  });
215
222
 
@@ -234,7 +241,7 @@ describe("system routes", () => {
234
241
  });
235
242
 
236
243
  test("关键资源审计失败会拒绝请求,普通资源仍完成并报告降级", async () => {
237
- const { app, runtime, guardContext } = await routeFixture([], {
244
+ const { app, runtime, guardContext } = await routeFixture(undefined, {
238
245
  write: async () => {
239
246
  throw new Error("audit persistence unavailable");
240
247
  },
@@ -281,7 +288,10 @@ describe("system routes", () => {
281
288
  await repositories.features.syncManifest(snapshot.features);
282
289
  const runtime = createServerRuntime(
283
290
  { WINDY_DEV_ADMIN_TOKEN: "token_1" },
284
- { featureResolver: repositories.features },
291
+ {
292
+ featureResolver: repositories.features,
293
+ featureCatalog: snapshot.features,
294
+ },
285
295
  );
286
296
  const app = new FakeRouteApp();
287
297
  registerSystemRoutes(app, repositories, runtime, snapshot.features);
@@ -315,7 +325,7 @@ describe("system routes", () => {
315
325
  });
316
326
  expect(denied.status).toBe(403);
317
327
  await expect(denied.json()).resolves.toMatchObject({
318
- reason: "feature-disabled",
328
+ reason: "dependency-missing",
319
329
  });
320
330
  expect(app.handlers.has("POST /system/features")).toBe(false);
321
331
  expect(app.handlers.has("POST /system/features/import")).toBe(false);
@@ -27,7 +27,7 @@ export function registerSystemRoutes(
27
27
  app: RouteApp,
28
28
  repositories: SystemRepositories,
29
29
  runtime: ReturnType<typeof createServerRuntime>,
30
- features: FeatureFlagDefinition[] = [],
30
+ features: FeatureFlagDefinition[],
31
31
  options: {
32
32
  authRepository?: Pick<AuthRepository, "revokeUserSessions">;
33
33
  } = {},
@@ -98,7 +98,16 @@ export function registerSystemRoutes(
98
98
  ),
99
99
  runtime,
100
100
  );
101
- registerFeatureFlagRoutes(app, repositories.features, runtime);
101
+ const featureManagement = findSystemFeature(features, "feature");
102
+ if (!featureManagement) {
103
+ throw new Error("功能开关管理缺少 system.feature Feature");
104
+ }
105
+ registerFeatureFlagRoutes(
106
+ app,
107
+ repositories.features,
108
+ runtime,
109
+ featureManagement,
110
+ );
102
111
  // @windy-module system.license begin
103
112
  registerResourceRoutes(
104
113
  app,
@@ -43,7 +43,9 @@ describe("平台模块 composition root", () => {
43
43
  "advanced",
44
44
  ]);
45
45
  const workOrder = modules.find(({ name }) => name === "work-order");
46
- expect(workOrder?.adminRoutes).toEqual([]);
46
+ expect(workOrder?.adminRoutes.map(({ key }) => key)).toEqual([
47
+ "work-order.ticket.list",
48
+ ]);
47
49
  expect(workOrder?.apiPermissions).toHaveLength(5);
48
50
  expect(workOrder?.tasks.map(({ key }) => key)).toContain(
49
51
  "work-order.ticket.snapshot",
@@ -88,9 +90,9 @@ describe("平台模块 composition root", () => {
88
90
  "@southwind-ai/web",
89
91
  );
90
92
  expect(businessPackage.dependencies).not.toHaveProperty("vue");
91
- expect(webPackage.dependencies).not.toHaveProperty(
92
- "@southwind-ai/example-work-order",
93
- );
93
+ expect(webPackage.dependencies).toMatchObject({
94
+ "@southwind-ai/example-work-order": "workspace:*",
95
+ });
94
96
  });
95
97
 
96
98
  test("示例模块经 Host 声明式装配提供搜索 Provider", async () => {
@@ -22,6 +22,9 @@ describe("示例工单 routes", () => {
22
22
  items: WorkOrder[];
23
23
  };
24
24
  expect(listed.items.map(({ id }) => id)).toEqual(["ticket_self"]);
25
+ expect(fixture.runtime.auditSink.list()).toContainEqual(
26
+ expect.objectContaining({ action: "data.read" }),
27
+ );
25
28
 
26
29
  const denied = (await fixture.invoke("GET", "/work-orders/:id", {
27
30
  id: "ticket_other",
@@ -76,6 +79,11 @@ describe("示例工单 routes", () => {
76
79
  target: expect.objectContaining({ id: created.id, type: "work-order" }),
77
80
  }),
78
81
  );
82
+ expect(
83
+ fixture.runtime.auditSink
84
+ .list()
85
+ .filter(({ action }) => action === "crud.create"),
86
+ ).toHaveLength(1);
79
87
  });
80
88
  });
81
89