create-windy 0.3.19 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) 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/server/Dockerfile +1 -0
  9. package/template/apps/server/package.json +1 -0
  10. package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
  11. package/template/apps/server/scripts/validate-module-composition.ts +28 -0
  12. package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
  13. package/template/apps/server/src/agent/routes.test.ts +20 -7
  14. package/template/apps/server/src/agent/routes.ts +14 -0
  15. package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
  16. package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
  17. package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
  18. package/template/apps/server/src/app/server-composition.ts +56 -0
  19. package/template/apps/server/src/application-services.ts +2 -0
  20. package/template/apps/server/src/bulk-data/service.ts +14 -0
  21. package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
  22. package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
  23. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
  24. package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
  25. package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
  26. package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
  27. package/template/apps/server/src/feature/routes.ts +33 -15
  28. package/template/apps/server/src/foundation-activation.test.ts +60 -0
  29. package/template/apps/server/src/foundation.ts +64 -5
  30. package/template/apps/server/src/guards.test.ts +23 -0
  31. package/template/apps/server/src/guards.ts +12 -0
  32. package/template/apps/server/src/index.ts +28 -34
  33. package/template/apps/server/src/installed-business-modules.ts +13 -24
  34. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
  35. package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
  36. package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
  37. package/template/apps/server/src/license/activation.test.ts +30 -0
  38. package/template/apps/server/src/license/activation.ts +26 -0
  39. package/template/apps/server/src/license/restriction.test.ts +16 -0
  40. package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
  41. package/template/apps/server/src/license/routes.ts +20 -4
  42. package/template/apps/server/src/license/runtime-service.test.ts +10 -2
  43. package/template/apps/server/src/module-bootstrap.ts +14 -23
  44. package/template/apps/server/src/module-composition.test.ts +43 -2
  45. package/template/apps/server/src/module-composition.ts +29 -0
  46. package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
  47. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
  48. package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
  49. package/template/apps/server/src/module-host/durable-context.ts +42 -2
  50. package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
  51. package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
  52. package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
  53. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  54. package/template/apps/server/src/module-host/initialize.test.ts +78 -2
  55. package/template/apps/server/src/module-host/initialize.ts +15 -4
  56. package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
  57. package/template/apps/server/src/module-host/project-attachments.ts +58 -0
  58. package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
  59. package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
  60. package/template/apps/server/src/module-host/project-composition.ts +311 -0
  61. package/template/apps/server/src/module-host/request-context.ts +3 -0
  62. package/template/apps/server/src/module-host/route-declaration.ts +15 -6
  63. package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
  64. package/template/apps/server/src/module-host/route-installer.ts +12 -2
  65. package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
  66. package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
  67. package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
  68. package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
  69. package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
  70. package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
  71. package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
  72. package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
  73. package/template/apps/server/src/module-host/task-definitions.ts +1 -0
  74. package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
  75. package/template/apps/server/src/module-host.test.ts +2 -0
  76. package/template/apps/server/src/module-preflight.test.ts +289 -0
  77. package/template/apps/server/src/module-preflight.ts +96 -0
  78. package/template/apps/server/src/module-runtime-validation.ts +1 -0
  79. package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
  80. package/template/apps/server/src/platform-http-runtime.ts +32 -7
  81. package/template/apps/server/src/route-guards.test.ts +22 -0
  82. package/template/apps/server/src/route-guards.ts +10 -3
  83. package/template/apps/server/src/runtime-audit-query.ts +50 -0
  84. package/template/apps/server/src/runtime-feature.test.ts +46 -0
  85. package/template/apps/server/src/runtime-feature.ts +5 -0
  86. package/template/apps/server/src/runtime.ts +10 -49
  87. package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
  88. package/template/apps/server/src/scheduler/audit.ts +1 -1
  89. package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
  90. package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
  91. package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
  92. package/template/apps/server/src/scheduler/service.test.ts +1 -0
  93. package/template/apps/server/src/scheduler/service.ts +18 -21
  94. package/template/apps/server/src/scheduler/types.ts +5 -1
  95. package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
  96. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
  97. package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
  98. package/template/apps/server/src/settings/watermark-routes.ts +29 -1
  99. package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
  100. package/template/apps/server/src/startup-composition-validation.ts +54 -0
  101. package/template/apps/server/src/startup-preflight.test.ts +36 -0
  102. package/template/apps/server/src/startup-preflight.ts +37 -0
  103. package/template/apps/server/src/system/audit-routes.test.ts +9 -3
  104. package/template/apps/server/src/system/department-routes.test.ts +5 -3
  105. package/template/apps/server/src/system/feature-routes.test.ts +4 -3
  106. package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
  107. package/template/apps/server/src/system/interface-catalog.ts +3 -0
  108. package/template/apps/server/src/system/menu-routes.test.ts +5 -3
  109. package/template/apps/server/src/system/route-test-runtime.ts +24 -0
  110. package/template/apps/server/src/system/routes-validation.test.ts +7 -3
  111. package/template/apps/server/src/system/routes.test.ts +21 -11
  112. package/template/apps/server/src/system/routes.ts +11 -2
  113. package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
  114. package/template/apps/server/src/work-order/routes.test.ts +8 -0
  115. package/template/apps/server/src/work-order/task.test.ts +6 -1
  116. package/template/apps/web/Dockerfile +5 -0
  117. package/template/apps/web/package.json +1 -0
  118. package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
  119. package/template/apps/web/src/app/web-composition.ts +24 -0
  120. package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
  121. package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
  122. package/template/apps/web/src/composables/watermark-settings.ts +40 -11
  123. package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
  124. package/template/apps/web/src/layout/AppSidebar.vue +8 -1
  125. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
  126. package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
  127. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
  128. package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
  129. package/template/apps/web/src/layout/navigation.ts +29 -4
  130. package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
  131. package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
  132. package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
  133. package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
  134. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
  135. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
  136. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
  137. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
  138. package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
  139. package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
  140. package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
  141. package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
  142. package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
  143. package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
  144. package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
  145. package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
  146. package/template/apps/web/src/router/business-routes.ts +32 -45
  147. package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
  148. package/template/apps/web/src/router/index.ts +64 -54
  149. package/template/apps/web/src/router/manifest-routes.ts +27 -7
  150. package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
  151. package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
  152. package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
  153. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
  154. package/template/apps/web/src/router/web-composition-host.ts +20 -0
  155. package/template/apps/web/src/services/work-order-api.ts +16 -0
  156. package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
  157. package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
  158. package/template/apps/web/vite.config.ts +22 -1
  159. package/template/docs/architecture/migration-bundle.md +10 -7
  160. package/template/docs/architecture/module-manifest-composition.md +85 -3
  161. package/template/docs/architecture/search-provider-protocol.md +6 -1
  162. package/template/docs/architecture/server-module-interface.md +70 -7
  163. package/template/docs/development/business-web-routes.md +53 -50
  164. package/template/docs/development/module-lifecycle.md +48 -54
  165. package/template/docs/platform/navigation-registry.md +5 -4
  166. package/template/docs/platform/platform-shell-settings.md +15 -10
  167. package/template/docs/platform/web-system-crud.md +1 -1
  168. package/template/package.json +1 -1
  169. package/template/packages/config/package.json +1 -1
  170. package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
  171. package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
  172. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  173. package/template/packages/database/package.json +1 -1
  174. package/template/packages/database/src/schema/platform-settings.ts +3 -0
  175. package/template/packages/example-work-order/package.json +6 -0
  176. package/template/packages/example-work-order/src/admin-page.ts +78 -0
  177. package/template/packages/example-work-order/src/admin.ts +35 -0
  178. package/template/packages/example-work-order/src/manifest.test.ts +62 -5
  179. package/template/packages/example-work-order/src/manifest.ts +54 -7
  180. package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
  181. package/template/packages/example-work-order/src/server-module.ts +2 -2
  182. package/template/packages/jobs/package.json +1 -1
  183. package/template/packages/modules/index.ts +4 -0
  184. package/template/packages/modules/package.json +3 -2
  185. package/template/packages/modules/src/admin-ui-entry.ts +8 -0
  186. package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
  187. package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
  188. package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
  189. package/template/packages/modules/src/capability-audit-validation.ts +38 -0
  190. package/template/packages/modules/src/capability-guard-validation.ts +148 -0
  191. package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
  192. package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
  193. package/template/packages/modules/src/composition.test.ts +74 -9
  194. package/template/packages/modules/src/composition.ts +8 -0
  195. package/template/packages/modules/src/manifest.ts +1 -0
  196. package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
  197. package/template/packages/modules/src/module-host-range.test.ts +57 -0
  198. package/template/packages/modules/src/module-host-range.ts +125 -0
  199. package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
  200. package/template/packages/modules/src/project-module-assembly.ts +126 -0
  201. package/template/packages/modules/src/system-admin-routes.ts +6 -5
  202. package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
  203. package/template/packages/modules/src/system-agent-tools.ts +2 -2
  204. package/template/packages/modules/src/system-api-binding.ts +43 -9
  205. package/template/packages/modules/src/system-api-control-plane.ts +202 -0
  206. package/template/packages/modules/src/system-api-data-governance.ts +4 -2
  207. package/template/packages/modules/src/system-api-operational.ts +57 -8
  208. package/template/packages/modules/src/system-api-permissions.ts +115 -21
  209. package/template/packages/modules/src/system-api-watermark.ts +3 -0
  210. package/template/packages/modules/src/system-audit-actions.ts +7 -0
  211. package/template/packages/modules/src/system-data-governance.ts +5 -2
  212. package/template/packages/modules/src/system-features.ts +31 -0
  213. package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
  214. package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
  215. package/template/packages/modules/src/system-module-catalog.ts +68 -7
  216. package/template/packages/modules/src/system-module-tasks.ts +8 -1
  217. package/template/packages/modules/src/system-modules-access.test.ts +4 -0
  218. package/template/packages/modules/src/system-modules.test.ts +10 -2
  219. package/template/packages/modules/src/system-modules.ts +6 -5
  220. package/template/packages/modules/src/system-permissions.ts +4 -0
  221. package/template/packages/modules/user/module.ts +19 -2
  222. package/template/packages/server-sdk/index.ts +8 -0
  223. package/template/packages/server-sdk/package.json +2 -1
  224. package/template/packages/server-sdk/src/durable-handler.ts +16 -1
  225. package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
  226. package/template/packages/server-sdk/src/module-adapter.ts +59 -0
  227. package/template/packages/server-sdk/src/route-definition.ts +3 -2
  228. package/template/packages/shared/package.json +1 -1
  229. package/template/packages/shared/src/audit.test.ts +7 -0
  230. package/template/packages/shared/src/audit.ts +16 -0
  231. package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
  232. package/template/packages/shared/src/platform-settings.ts +17 -0
  233. package/template/packages/shared/src/rbac.ts +2 -0
  234. package/template/apps/web/src/app/business-routes.ts +0 -22
@@ -0,0 +1,185 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ModuleHost } from "@southwind-ai/server-sdk";
3
+ import { featureKey } from "@southwind-ai/shared";
4
+ import { initializeModuleHosts } from "./initialize.js";
5
+ import {
6
+ governedResourceRouteKeys,
7
+ validateHostedModuleImplementations,
8
+ } from "./implementation-completeness.js";
9
+ import { manifest } from "./test-fixtures.js";
10
+
11
+ describe("Server Module 实现完整性", () => {
12
+ test("Manifest 声明的 API、任务和 Provider 必须由同一安装单元实现", async () => {
13
+ const complete = completeRouteModule();
14
+ const completeHosts = await initializeModuleHosts({
15
+ modules: [complete.manifest],
16
+ installations: [complete],
17
+ });
18
+
19
+ expect(() =>
20
+ validateHostedModuleImplementations([complete], completeHosts),
21
+ ).not.toThrow();
22
+
23
+ const incomplete = {
24
+ manifest: complete.manifest,
25
+ setup() {},
26
+ };
27
+ const incompleteHosts = await initializeModuleHosts({
28
+ modules: [incomplete.manifest],
29
+ installations: [incomplete],
30
+ });
31
+
32
+ expect(() =>
33
+ validateHostedModuleImplementations([incomplete], incompleteHosts),
34
+ ).toThrow("API Route GET /api/catalog/items 缺少 Handler");
35
+ });
36
+
37
+ test("治理资源注册派生读取、明文和四个导出 Handler Key", () => {
38
+ expect(
39
+ governedResourceRouteKeys({
40
+ readPath: "/api/people/persons/:id",
41
+ revealPath: "/api/people/persons/:id/plaintext",
42
+ export: {
43
+ basePath: "/api/people/persons/exports",
44
+ validateQuery: () => ({}),
45
+ readPage: () => ({ records: [] }),
46
+ },
47
+ }),
48
+ ).toEqual([
49
+ "GET /api/people/persons/:id",
50
+ "POST /api/people/persons/:id/plaintext",
51
+ "POST /api/people/persons/exports",
52
+ "GET /api/people/persons/exports/:id",
53
+ "POST /api/people/persons/exports/:id/cancel",
54
+ "GET /api/people/persons/exports/:id/download",
55
+ ]);
56
+ });
57
+
58
+ test("Durable Handler 的 Host 声明与 Registry 安装必须双向一致", async () => {
59
+ const capabilityKey = "reports.refresh";
60
+ const permissionKey = "reports.refresh.execute";
61
+ const moduleManifest = manifest("reports", {
62
+ features: [
63
+ {
64
+ key: featureKey(capabilityKey),
65
+ label: "报表刷新",
66
+ module: "reports",
67
+ visible: "visible",
68
+ enabled: true,
69
+ stage: "stable",
70
+ },
71
+ ],
72
+ permissions: [
73
+ {
74
+ key: permissionKey,
75
+ label: "执行报表刷新",
76
+ module: "reports",
77
+ moduleLabel: "报表",
78
+ resource: "refresh",
79
+ resourceLabel: "刷新",
80
+ action: "execute",
81
+ },
82
+ ],
83
+ tasks: [
84
+ {
85
+ key: capabilityKey,
86
+ label: "报表刷新",
87
+ description: "刷新报表。",
88
+ featureKey: capabilityKey,
89
+ permissionKey,
90
+ auditAction: permissionKey,
91
+ intervalSeconds: 60,
92
+ maxAttempts: 3,
93
+ retryDelaySeconds: 5,
94
+ },
95
+ ],
96
+ auditActions: [permissionKey],
97
+ });
98
+ const installation = {
99
+ manifest: moduleManifest,
100
+ setup(host: ModuleHost) {
101
+ moduleSetup(host);
102
+ },
103
+ };
104
+ const hosts = await initializeModuleHosts({
105
+ modules: [moduleManifest],
106
+ installations: [installation],
107
+ });
108
+
109
+ expect(() =>
110
+ validateHostedModuleImplementations([installation], hosts),
111
+ ).not.toThrow();
112
+ expect(() =>
113
+ validateHostedModuleImplementations([installation], {
114
+ ...hosts,
115
+ installDurableHandlers() {},
116
+ }),
117
+ ).toThrow("Durable Handler reports:reports.refresh 缺少 Handler");
118
+
119
+ function moduleSetup(host: ModuleHost) {
120
+ host.registerTaskHandlers([
121
+ { taskKey: capabilityKey, handler: () => undefined },
122
+ ]);
123
+ host.registerDurableHandler({
124
+ key: capabilityKey,
125
+ featureKey: capabilityKey,
126
+ permissionKey,
127
+ auditAction: permissionKey,
128
+ payload: { parse: () => ({}) },
129
+ execute: () => undefined,
130
+ });
131
+ }
132
+ });
133
+ });
134
+
135
+ function completeRouteModule() {
136
+ const capabilityKey = "catalog.item";
137
+ const permissionKey = "catalog.item.read";
138
+ const moduleManifest = manifest("catalog", {
139
+ features: [
140
+ {
141
+ key: featureKey(capabilityKey),
142
+ label: "目录项",
143
+ module: "catalog",
144
+ visible: "visible",
145
+ enabled: true,
146
+ stage: "stable",
147
+ },
148
+ ],
149
+ permissions: [
150
+ {
151
+ key: permissionKey,
152
+ label: "查看目录",
153
+ module: "catalog",
154
+ moduleLabel: "目录",
155
+ resource: "item",
156
+ resourceLabel: "目录项",
157
+ action: "read",
158
+ },
159
+ ],
160
+ apiPermissions: [
161
+ {
162
+ method: "GET",
163
+ path: "/api/catalog/items",
164
+ permissionKey,
165
+ featureKey: capabilityKey,
166
+ auditAction: "data.read",
167
+ },
168
+ ],
169
+ auditActions: ["data.read"],
170
+ });
171
+ return {
172
+ manifest: moduleManifest,
173
+ setup(host: ModuleHost) {
174
+ host.registerRoutes([
175
+ {
176
+ method: "GET",
177
+ path: "/api/catalog/items",
178
+ permissionKey,
179
+ featureKey: capabilityKey,
180
+ handler: () => ({ items: [] }),
181
+ },
182
+ ]);
183
+ },
184
+ };
185
+ }
@@ -0,0 +1,132 @@
1
+ import { DurableHandlerRegistry } from "@southwind-ai/jobs";
2
+ import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import type {
4
+ InstallableServerModule,
5
+ ModuleGovernedResourceRegistration,
6
+ } from "@southwind-ai/server-sdk";
7
+ import type { InitializedModuleHosts } from "./initialize-contracts.js";
8
+
9
+ /**
10
+ * 校验经 Module Host 安装的业务模块是否实现了 Manifest 中所有 Server 能力。
11
+ *
12
+ * Admin Route 页面由 Web 构建门禁校验;这里覆盖 API、任务、Tool、AI Operation
13
+ * 与 Provider,且只检查 Host 安装单元,不把平台静态实现伪装成业务 Handler。
14
+ */
15
+ export function validateHostedModuleImplementations(
16
+ installations: readonly InstallableServerModule<ModuleManifest>[],
17
+ hosts: InitializedModuleHosts,
18
+ ): void {
19
+ for (const installation of installations) {
20
+ const moduleName = installation.manifest.name;
21
+ assertComplete(
22
+ "API Route",
23
+ installation.manifest.apiPermissions.map(
24
+ ({ method, path }) => `${method.toUpperCase()} ${path}`,
25
+ ),
26
+ hosts.routes
27
+ .filter((entry) => entry.moduleName === moduleName)
28
+ .map(
29
+ ({ definition }) =>
30
+ `${definition.method.toUpperCase()} ${definition.path}`,
31
+ )
32
+ .concat(
33
+ hosts.governedResources
34
+ .filter((entry) => entry.moduleName === moduleName)
35
+ .flatMap(({ registration }) =>
36
+ governedResourceRouteKeys(registration),
37
+ ),
38
+ ),
39
+ );
40
+ assertComplete(
41
+ "定时任务",
42
+ installation.manifest.tasks.map(({ key }) => key),
43
+ hosts.taskHandlers
44
+ .filter((entry) => entry.moduleName === moduleName)
45
+ .map(({ registration }) => registration.taskKey),
46
+ );
47
+ assertComplete(
48
+ "Agent Tool",
49
+ installation.manifest.tools.map(({ key }) => key),
50
+ [...hosts.toolHandlers, ...hosts.sandboxToolHandlers]
51
+ .filter((entry) => entry.moduleName === moduleName)
52
+ .map(({ registration }) => registration.toolKey),
53
+ );
54
+ assertComplete(
55
+ "AI Operation",
56
+ (installation.manifest.aiOperations ?? []).map(({ key }) => key),
57
+ hosts.aiOperationHandlers
58
+ .filter((entry) => entry.moduleName === moduleName)
59
+ .map(({ registration }) => registration.operationKey),
60
+ );
61
+ assertComplete(
62
+ "Provider",
63
+ installation.manifest.providers.map(({ key }) => key),
64
+ hosts.searchProviders
65
+ .filter((entry) => entry.moduleName === moduleName)
66
+ .map(({ provider }) => provider.key),
67
+ );
68
+ }
69
+ validateDurableHandlerInstallations(hosts);
70
+ }
71
+
72
+ export function governedResourceRouteKeys(
73
+ registration: Pick<
74
+ ModuleGovernedResourceRegistration,
75
+ "readPath" | "revealPath" | "export"
76
+ >,
77
+ ): string[] {
78
+ return [
79
+ `GET ${registration.readPath}`,
80
+ `POST ${registration.revealPath}`,
81
+ ...(registration.export
82
+ ? [
83
+ `POST ${registration.export.basePath}`,
84
+ `GET ${registration.export.basePath}/:id`,
85
+ `POST ${registration.export.basePath}/:id/cancel`,
86
+ `GET ${registration.export.basePath}/:id/download`,
87
+ ]
88
+ : []),
89
+ ];
90
+ }
91
+
92
+ function assertComplete(
93
+ label: string,
94
+ declared: readonly string[],
95
+ registered: readonly string[],
96
+ ): void {
97
+ const implementations = new Set(registered);
98
+ for (const key of declared) {
99
+ if (!implementations.has(key)) {
100
+ throw new Error(`${label} ${key} 缺少 Handler`);
101
+ }
102
+ }
103
+ }
104
+
105
+ function validateDurableHandlerInstallations(
106
+ hosts: InitializedModuleHosts,
107
+ ): void {
108
+ const registry = new DurableHandlerRegistry();
109
+ hosts.installDurableHandlers(registry);
110
+ const hosted = hosts.durableHandlers.map(
111
+ ({ moduleName, definition }) => `${moduleName}:${definition.key}`,
112
+ );
113
+ const installed = registry.keys().map((key) => {
114
+ const handler = registry.get(key);
115
+ return `${handler?.moduleName ?? ""}:${key}`;
116
+ });
117
+ assertSameRegistrations("Durable Handler", hosted, installed);
118
+ }
119
+
120
+ function assertSameRegistrations(
121
+ label: string,
122
+ hosted: readonly string[],
123
+ installed: readonly string[],
124
+ ): void {
125
+ assertComplete(label, hosted, installed);
126
+ const declarations = new Set(hosted);
127
+ for (const key of installed) {
128
+ if (!declarations.has(key)) {
129
+ throw new Error(`${label} ${key} 未在 Host 注册`);
130
+ }
131
+ }
132
+ }
@@ -3,6 +3,7 @@ import type { ModuleManifest } from "@southwind-ai/modules";
3
3
  import type {
4
4
  InstallableServerModule,
5
5
  ModuleAiOperationHandlerRegistration,
6
+ ModuleDurableHandlerDefinition,
6
7
  ModuleInitializer,
7
8
  ModuleGovernedResourceRegistration,
8
9
  ModuleRolePreset,
@@ -40,6 +41,14 @@ export interface RegisteredModuleAiOperationHandler {
40
41
  readonly registration: ModuleAiOperationHandlerRegistration;
41
42
  }
42
43
 
44
+ export interface RegisteredModuleDurableHandler {
45
+ readonly moduleName: string;
46
+ readonly definition: Pick<
47
+ ModuleDurableHandlerDefinition<unknown>,
48
+ "key" | "featureKey" | "permissionKey" | "auditAction"
49
+ >;
50
+ }
51
+
43
52
  export interface RegisteredModuleGovernedResource {
44
53
  readonly moduleName: string;
45
54
  readonly registration: ModuleGovernedResourceRegistration;
@@ -73,6 +82,7 @@ export interface InitializedModuleHosts {
73
82
  readonly toolHandlers: readonly RegisteredModuleToolHandler[];
74
83
  readonly sandboxToolHandlers: readonly RegisteredModuleSandboxToolHandler[];
75
84
  readonly aiOperationHandlers: readonly RegisteredModuleAiOperationHandler[];
85
+ readonly durableHandlers: readonly RegisteredModuleDurableHandler[];
76
86
  readonly governedResources: readonly RegisteredModuleGovernedResource[];
77
87
  readonly rolePresets: readonly RegisteredModuleRolePreset[];
78
88
  readonly searchProviders: readonly RegisteredModuleSearchProvider[];
@@ -83,5 +93,9 @@ export interface InitializedModuleHosts {
83
93
  resourceFileAccessPoliciesOf(
84
94
  moduleName: string,
85
95
  ): readonly ModuleResourceFileAccessPolicy[];
96
+ durableHandlerOf(
97
+ moduleName: string,
98
+ handlerKey: string,
99
+ ): RegisteredModuleDurableHandler["definition"] | undefined;
86
100
  installDurableHandlers(registry: DurableHandlerRegistry): void;
87
101
  }
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { DurableHandlerRegistry } from "@southwind-ai/jobs";
3
+ import { featureKey } from "@southwind-ai/shared";
3
4
  import type {
4
5
  ModuleInitializer,
5
6
  ModuleSandboxToolHost,
@@ -15,6 +16,9 @@ describe("模块 Host 初始化", () => {
15
16
  await Promise.resolve();
16
17
  host.registerDurableHandler({
17
18
  key: "alpha.refresh",
19
+ featureKey: "alpha.refresh",
20
+ permissionKey: "alpha.refresh.execute",
21
+ auditAction: "alpha.refresh.execute",
18
22
  payload: { parse: () => ({}) },
19
23
  execute: () => undefined,
20
24
  });
@@ -25,7 +29,7 @@ describe("模块 Host 初始化", () => {
25
29
  });
26
30
 
27
31
  const initialized = await initializeModuleHosts({
28
- modules: [manifest("alpha"), manifest("beta")],
32
+ modules: [durableManifest("alpha"), manifest("beta")],
29
33
  initializers: [first, second],
30
34
  });
31
35
  expect(events).toEqual(["alpha:start", "alpha:end", "beta"]);
@@ -33,6 +37,13 @@ describe("模块 Host 初始化", () => {
33
37
  const registry = new DurableHandlerRegistry();
34
38
  initialized.installDurableHandlers(registry);
35
39
  expect(registry.keys()).toEqual(["alpha.refresh"]);
40
+ expect(
41
+ initialized.durableHandlerOf("alpha", "alpha.refresh"),
42
+ ).toMatchObject({
43
+ featureKey: "alpha.refresh",
44
+ permissionKey: "alpha.refresh.execute",
45
+ auditAction: "alpha.refresh.execute",
46
+ });
36
47
  });
37
48
 
38
49
  test("未知模块、错误命名空间和重复注册立即失败", async () => {
@@ -76,11 +87,14 @@ describe("模块 Host 初始化", () => {
76
87
 
77
88
  await expect(
78
89
  initializeModuleHosts({
79
- modules: [manifest("known")],
90
+ modules: [durableManifest("known")],
80
91
  initializers: [
81
92
  initializer("known", (host) => {
82
93
  const definition = {
83
94
  key: "known.refresh",
95
+ featureKey: "known.refresh",
96
+ permissionKey: "known.refresh.execute",
97
+ auditAction: "known.refresh.execute",
84
98
  payload: { parse: () => ({}) },
85
99
  execute: () => undefined,
86
100
  };
@@ -92,6 +106,26 @@ describe("模块 Host 初始化", () => {
92
106
  ).rejects.toThrow("任务 handler 重复注册:known.refresh");
93
107
  });
94
108
 
109
+ test("Durable Handler 必须绑定 Manifest 已声明能力", async () => {
110
+ await expect(
111
+ initializeModuleHosts({
112
+ modules: [durableManifest("known")],
113
+ initializers: [
114
+ initializer("known", (host) => {
115
+ host.registerDurableHandler({
116
+ key: "known.refresh",
117
+ featureKey: "known.missing",
118
+ permissionKey: "known.refresh.execute",
119
+ auditAction: "known.refresh.execute",
120
+ payload: { parse: () => ({}) },
121
+ execute: () => undefined,
122
+ });
123
+ }),
124
+ ],
125
+ }),
126
+ ).rejects.toThrow("任务 handler Feature 未在 Manifest 注册:known.missing");
127
+ });
128
+
95
129
  test("安装单元的 Manifest 必须来自同一平台能力目录", async () => {
96
130
  await expect(
97
131
  initializeModuleHosts({
@@ -238,3 +272,45 @@ function policy(purpose: string) {
238
272
  signature: "none" as const,
239
273
  };
240
274
  }
275
+
276
+ function durableManifest(name: string) {
277
+ const capabilityKey = `${name}.refresh`;
278
+ const permissionKey = `${capabilityKey}.execute`;
279
+ return manifest(name, {
280
+ features: [
281
+ {
282
+ key: featureKey(capabilityKey),
283
+ label: "刷新",
284
+ module: name,
285
+ visible: "visible",
286
+ enabled: true,
287
+ stage: "stable",
288
+ },
289
+ ],
290
+ permissions: [
291
+ {
292
+ key: permissionKey,
293
+ label: "执行刷新",
294
+ module: name,
295
+ moduleLabel: name,
296
+ resource: "refresh",
297
+ resourceLabel: "刷新",
298
+ action: "execute",
299
+ },
300
+ ],
301
+ tasks: [
302
+ {
303
+ key: capabilityKey,
304
+ label: "刷新",
305
+ description: "刷新模块数据。",
306
+ featureKey: capabilityKey,
307
+ permissionKey,
308
+ auditAction: permissionKey,
309
+ intervalSeconds: 60,
310
+ maxAttempts: 3,
311
+ retryDelaySeconds: 5,
312
+ },
313
+ ],
314
+ auditActions: [permissionKey],
315
+ });
316
+ }
@@ -10,6 +10,7 @@ import type {
10
10
  InitializedModuleHosts,
11
11
  InitializeModuleHostsInput,
12
12
  RegisteredModuleAiOperationHandler,
13
+ RegisteredModuleDurableHandler,
13
14
  RegisteredModuleGovernedResource,
14
15
  RegisteredModuleRolePreset,
15
16
  RegisteredModuleRoute,
@@ -26,11 +27,13 @@ import {
26
27
  } from "./resource-file-policies.js";
27
28
  import { validateRouteDeclaration } from "./route-declaration.js";
28
29
  import { createScopedSandboxHost } from "./sandbox-tool-registration.js";
30
+ import { validateSearchProviderRegistration } from "./search-provider-registration.js";
29
31
 
30
32
  export type {
31
33
  InitializedModuleHosts,
32
34
  InitializeModuleHostsInput,
33
35
  RegisteredModuleAiOperationHandler,
36
+ RegisteredModuleDurableHandler,
34
37
  RegisteredModuleGovernedResource,
35
38
  RegisteredModuleRolePreset,
36
39
  RegisteredModuleRoute,
@@ -73,6 +76,7 @@ export async function initializeModuleHosts(
73
76
  resourceFileAccessPolicyKeys: new Set<string>(),
74
77
  handlerKeys: new Set<string>(),
75
78
  handlerInstallers: [],
79
+ durableHandlers: [],
76
80
  routes: [],
77
81
  routeKeys: new Set<string>(),
78
82
  taskHandlers: [],
@@ -141,6 +145,7 @@ export async function initializeModuleHosts(
141
145
  toolHandlers: [...state.toolHandlers],
142
146
  sandboxToolHandlers: [...state.sandboxToolHandlers],
143
147
  aiOperationHandlers: [...state.aiOperationHandlers],
148
+ durableHandlers: [...state.durableHandlers],
144
149
  governedResources: [...state.governedResources],
145
150
  rolePresets: [...state.rolePresets],
146
151
  searchProviders: [...state.searchProviders],
@@ -155,6 +160,13 @@ export async function initializeModuleHosts(
155
160
  resourceFileAccessPoliciesOf(moduleName) {
156
161
  return state.moduleResourceFileAccessPolicies.get(moduleName) ?? [];
157
162
  },
163
+ durableHandlerOf(moduleName, handlerKey) {
164
+ return state.durableHandlers.find(
165
+ (entry) =>
166
+ entry.moduleName === moduleName &&
167
+ entry.definition.key === handlerKey,
168
+ )?.definition;
169
+ },
158
170
  installDurableHandlers(registry) {
159
171
  for (const install of state.handlerInstallers) install(registry);
160
172
  },
@@ -174,6 +186,7 @@ interface HostCollections {
174
186
  resourceFileAccessPolicyKeys: Set<string>;
175
187
  handlerKeys: Set<string>;
176
188
  handlerInstallers: HandlerInstaller[];
189
+ durableHandlers: RegisteredModuleDurableHandler[];
177
190
  routes: RegisteredModuleRoute[];
178
191
  routeKeys: Set<string>;
179
192
  taskHandlers: RegisteredModuleTaskHandler[];
@@ -215,7 +228,7 @@ function createScopedHost(
215
228
  },
216
229
  registerDurableHandler(definition) {
217
230
  requireModuleNamespace("任务 handler", definition.key, moduleName);
218
- registerModuleDurableHandler(moduleName, state, definition);
231
+ registerModuleDurableHandler(manifest, state, definition);
219
232
  },
220
233
  registerGovernedResources(registrations) {
221
234
  registerGovernedResources(manifest, state, registrations);
@@ -323,9 +336,7 @@ function createScopedHost(
323
336
  registerSearchProviders(providers) {
324
337
  for (const provider of providers) {
325
338
  requireModuleNamespace("搜索 Provider", provider.key, moduleName);
326
- if (!manifest.providers.some(({ key }) => key === provider.key)) {
327
- throw new Error(`搜索 Provider 未在 Manifest 声明:${provider.key}`);
328
- }
339
+ validateSearchProviderRegistration(manifest, provider);
329
340
  if (state.searchProviderKeys.has(provider.key)) {
330
341
  throw new Error(`搜索 Provider 重复注册:${provider.key}`);
331
342
  }
@@ -0,0 +1,92 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { attachProjectServerModules } from "./project-attachments.js";
3
+
4
+ describe("project-owned Server Module attachment", () => {
5
+ test("只挂载已启用且有本地 Adapter 的显式 attachment", async () => {
6
+ const attached: string[] = [];
7
+ await attachProjectServerModules({
8
+ attachments: [
9
+ {
10
+ name: "enabled",
11
+ attach: () => {
12
+ attached.push("enabled");
13
+ },
14
+ },
15
+ {
16
+ name: "disabled",
17
+ attach: () => {
18
+ attached.push("disabled");
19
+ },
20
+ },
21
+ ],
22
+ adapters: [
23
+ adapter("enabled", "project-attachment"),
24
+ adapter("disabled", "project-attachment"),
25
+ ],
26
+ enabledModuleNames: new Set(["enabled"]),
27
+ runtime: {},
28
+ });
29
+
30
+ expect(attached).toEqual(["enabled"]);
31
+ });
32
+
33
+ test("重复或没有本地 Adapter 的 attachment fail closed", async () => {
34
+ await expect(
35
+ attachProjectServerModules({
36
+ attachments: [
37
+ { name: "case", attach() {} },
38
+ { name: "case", attach() {} },
39
+ ],
40
+ adapters: [adapter("case", "project-attachment")],
41
+ enabledModuleNames: new Set(["case"]),
42
+ runtime: {},
43
+ }),
44
+ ).rejects.toThrow("attachment 重复:case");
45
+
46
+ await expect(
47
+ attachProjectServerModules({
48
+ attachments: [{ name: "package-only", attach() {} }],
49
+ adapters: [],
50
+ enabledModuleNames: new Set(["package-only"]),
51
+ runtime: {},
52
+ }),
53
+ ).rejects.toThrow("attachment 缺少本地 Adapter:package-only");
54
+ });
55
+
56
+ test("先完整预检再执行,且启用的 project-attachment 不能缺失", async () => {
57
+ const effects: string[] = [];
58
+ await expect(
59
+ attachProjectServerModules({
60
+ attachments: [
61
+ { name: "case", attach: () => void effects.push("first") },
62
+ { name: "case", attach: () => void effects.push("duplicate") },
63
+ ],
64
+ adapters: [adapter("case", "project-attachment")],
65
+ enabledModuleNames: new Set(["case"]),
66
+ runtime: {},
67
+ }),
68
+ ).rejects.toThrow("attachment 重复:case");
69
+ expect(effects).toEqual([]);
70
+
71
+ await expect(
72
+ attachProjectServerModules({
73
+ attachments: [],
74
+ adapters: [adapter("case", "project-attachment")],
75
+ enabledModuleNames: new Set(["case"]),
76
+ runtime: {},
77
+ }),
78
+ ).rejects.toThrow("缺少 runtime attachment:case");
79
+ });
80
+ });
81
+
82
+ function adapter(
83
+ name: string,
84
+ runtimeMode: "self-contained" | "project-attachment",
85
+ ) {
86
+ return {
87
+ name,
88
+ hostRange: "^0.1.0",
89
+ runtimeMode,
90
+ create: () => ({ manifest: {}, setup() {} }),
91
+ };
92
+ }