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
@@ -0,0 +1,328 @@
1
+ import {
2
+ DurableHandlerRegistry,
3
+ type DurableHandlerDefinition,
4
+ } from "@southwind-ai/jobs";
5
+ import type { ModuleManifest } from "@southwind-ai/modules";
6
+ import {
7
+ requireModuleNamespace,
8
+ type ModuleHost,
9
+ type ModuleRouteDefinition,
10
+ type ModuleUploadPolicy,
11
+ } from "@southwind-ai/server-sdk";
12
+ import type {
13
+ InitializedModuleHosts,
14
+ InitializeModuleHostsInput,
15
+ RegisteredModuleAiOperationHandler,
16
+ RegisteredModuleRolePreset,
17
+ RegisteredModuleRoute,
18
+ RegisteredModuleSearchProvider,
19
+ RegisteredModuleTaskHandler,
20
+ RegisteredModuleToolHandler,
21
+ } from "./initialize-contracts.js";
22
+
23
+ export type {
24
+ InitializedModuleHosts,
25
+ InitializeModuleHostsInput,
26
+ RegisteredModuleAiOperationHandler,
27
+ RegisteredModuleRolePreset,
28
+ RegisteredModuleRoute,
29
+ RegisteredModuleSearchProvider,
30
+ RegisteredModuleTaskHandler,
31
+ RegisteredModuleToolHandler,
32
+ } from "./initialize-contracts.js";
33
+
34
+ type HandlerInstaller = (registry: DurableHandlerRegistry) => void;
35
+
36
+ export async function initializeModuleHosts(
37
+ input: InitializeModuleHostsInput,
38
+ ): Promise<InitializedModuleHosts> {
39
+ const manifests = new Map(
40
+ input.modules.map((module) => [module.name, module]),
41
+ );
42
+ const initializedNames = new Set<string>();
43
+ const uploadPolicies: ModuleUploadPolicy[] = [];
44
+ const uploadPurposes = new Set<string>();
45
+ for (const policy of input.baseUploadPolicies ?? []) {
46
+ if (uploadPurposes.has(policy.purpose)) {
47
+ throw new Error(`上传用途重复注册:${policy.purpose}`);
48
+ }
49
+ uploadPurposes.add(policy.purpose);
50
+ uploadPolicies.push(policy);
51
+ }
52
+ const state: HostCollections = {
53
+ uploadPolicies,
54
+ uploadPurposes,
55
+ modulePurposes: new Map<string, Set<string>>(),
56
+ handlerKeys: new Set<string>(),
57
+ handlerInstallers: [],
58
+ routes: [],
59
+ routeKeys: new Set<string>(),
60
+ taskHandlers: [],
61
+ taskKeys: new Set<string>(),
62
+ toolHandlers: [],
63
+ toolKeys: new Set<string>(),
64
+ aiOperationHandlers: [],
65
+ aiOperationKeys: new Set<string>(),
66
+ rolePresets: [],
67
+ rolePresetKeys: new Set<string>(),
68
+ searchProviders: [],
69
+ searchProviderKeys: new Set<string>(),
70
+ };
71
+
72
+ const installations = [
73
+ ...(input.installations ?? []).map((installation) => ({
74
+ moduleName: installation.manifest.name.trim(),
75
+ manifest: installation.manifest,
76
+ setup: installation.setup,
77
+ legacy: false,
78
+ })),
79
+ ...(input.initializers ?? []).map((initializer) => ({
80
+ moduleName: initializer.moduleName.trim(),
81
+ manifest: undefined,
82
+ setup: initializer.initialize,
83
+ legacy: true,
84
+ })),
85
+ ];
86
+ for (const installation of installations) {
87
+ const moduleName = installation.moduleName;
88
+ const manifest = manifests.get(moduleName);
89
+ if (!manifest) {
90
+ throw new Error(
91
+ installation.legacy
92
+ ? `模块初始化器与已安装模块不匹配:${moduleName}`
93
+ : `Server Module 与已安装模块不匹配:${moduleName}`,
94
+ );
95
+ }
96
+ if (initializedNames.has(moduleName)) {
97
+ throw new Error(`模块初始化器重复注册:${moduleName}`);
98
+ }
99
+ initializedNames.add(moduleName);
100
+ if (installation.manifest && installation.manifest !== manifest) {
101
+ throw new Error(
102
+ `Server Module Manifest 与能力目录不是同一声明:${moduleName}`,
103
+ );
104
+ }
105
+ await installation.setup(createScopedHost(manifest, state));
106
+ }
107
+
108
+ return {
109
+ manifests: input.modules,
110
+ uploadPolicies: [...state.uploadPolicies],
111
+ routes: [...state.routes],
112
+ taskHandlers: [...state.taskHandlers],
113
+ toolHandlers: [...state.toolHandlers],
114
+ aiOperationHandlers: [...state.aiOperationHandlers],
115
+ rolePresets: [...state.rolePresets],
116
+ searchProviders: [...state.searchProviders],
117
+ manifestOf(moduleName) {
118
+ const manifest = manifests.get(moduleName);
119
+ if (!manifest) throw new Error(`模块未安装:${moduleName}`);
120
+ return manifest;
121
+ },
122
+ uploadPurposesOf(moduleName) {
123
+ return state.modulePurposes.get(moduleName) ?? new Set<string>();
124
+ },
125
+ installDurableHandlers(registry) {
126
+ for (const install of state.handlerInstallers) install(registry);
127
+ },
128
+ };
129
+ }
130
+
131
+ interface HostCollections {
132
+ uploadPolicies: ModuleUploadPolicy[];
133
+ uploadPurposes: Set<string>;
134
+ modulePurposes: Map<string, Set<string>>;
135
+ handlerKeys: Set<string>;
136
+ handlerInstallers: HandlerInstaller[];
137
+ routes: RegisteredModuleRoute[];
138
+ routeKeys: Set<string>;
139
+ taskHandlers: RegisteredModuleTaskHandler[];
140
+ taskKeys: Set<string>;
141
+ toolHandlers: RegisteredModuleToolHandler[];
142
+ toolKeys: Set<string>;
143
+ aiOperationHandlers: RegisteredModuleAiOperationHandler[];
144
+ aiOperationKeys: Set<string>;
145
+ rolePresets: RegisteredModuleRolePreset[];
146
+ rolePresetKeys: Set<string>;
147
+ searchProviders: RegisteredModuleSearchProvider[];
148
+ searchProviderKeys: Set<string>;
149
+ }
150
+
151
+ function createScopedHost(
152
+ manifest: ModuleManifest,
153
+ state: HostCollections,
154
+ ): ModuleHost {
155
+ const moduleName = manifest.name;
156
+ return {
157
+ moduleName,
158
+ registerUploadPolicy(policy) {
159
+ requireModuleNamespace("上传用途", policy.purpose, moduleName);
160
+ if (state.uploadPurposes.has(policy.purpose)) {
161
+ throw new Error(`上传用途重复注册:${policy.purpose}`);
162
+ }
163
+ state.uploadPurposes.add(policy.purpose);
164
+ state.uploadPolicies.push(policy);
165
+ const purposes =
166
+ state.modulePurposes.get(moduleName) ?? new Set<string>();
167
+ purposes.add(policy.purpose);
168
+ state.modulePurposes.set(moduleName, purposes);
169
+ },
170
+ registerDurableHandler<T>(definition: DurableHandlerDefinition<T>) {
171
+ requireModuleNamespace("任务 handler", definition.key, moduleName);
172
+ if (state.handlerKeys.has(definition.key)) {
173
+ throw new Error(`任务 handler 重复注册:${definition.key}`);
174
+ }
175
+ state.handlerKeys.add(definition.key);
176
+ state.handlerInstallers.push((registry) => {
177
+ registry.register(definition);
178
+ });
179
+ },
180
+ registerRoutes(routes) {
181
+ for (const definition of routes) {
182
+ requireModuleNamespace(
183
+ "路由权限",
184
+ definition.permissionKey,
185
+ moduleName,
186
+ );
187
+ validateRouteDeclaration(manifest, definition);
188
+ const key = `${definition.method} ${definition.path}`;
189
+ if (state.routeKeys.has(key)) {
190
+ throw new Error(`模块路由重复注册:${key}`);
191
+ }
192
+ state.routeKeys.add(key);
193
+ state.routes.push({ moduleName, definition });
194
+ }
195
+ },
196
+ registerTaskHandlers(registrations) {
197
+ for (const registration of registrations) {
198
+ requireModuleNamespace("定时任务", registration.taskKey, moduleName);
199
+ if (!manifest.tasks.some(({ key }) => key === registration.taskKey)) {
200
+ throw new Error(
201
+ `定时任务 Handler 未在 Manifest 声明:${registration.taskKey}`,
202
+ );
203
+ }
204
+ if (state.taskKeys.has(registration.taskKey)) {
205
+ throw new Error(`定时任务 Handler 重复注册:${registration.taskKey}`);
206
+ }
207
+ state.taskKeys.add(registration.taskKey);
208
+ state.taskHandlers.push({ moduleName, registration });
209
+ }
210
+ },
211
+ registerToolHandlers(registrations) {
212
+ for (const registration of registrations) {
213
+ requireModuleNamespace("Agent Tool", registration.toolKey, moduleName);
214
+ if (!manifest.tools.some(({ key }) => key === registration.toolKey)) {
215
+ throw new Error(
216
+ `Agent Tool Handler 未在 Manifest 声明:${registration.toolKey}`,
217
+ );
218
+ }
219
+ if (state.toolKeys.has(registration.toolKey)) {
220
+ throw new Error(
221
+ `Agent Tool Handler 重复注册:${registration.toolKey}`,
222
+ );
223
+ }
224
+ state.toolKeys.add(registration.toolKey);
225
+ state.toolHandlers.push({ moduleName, registration });
226
+ }
227
+ },
228
+ registerAiOperationHandlers(registrations) {
229
+ for (const registration of registrations) {
230
+ requireModuleNamespace(
231
+ "AI Operation",
232
+ registration.operationKey,
233
+ moduleName,
234
+ );
235
+ if (
236
+ !manifest.aiOperations?.some(
237
+ ({ key }) => key === registration.operationKey,
238
+ )
239
+ ) {
240
+ throw new Error(
241
+ `AI Operation Handler 未在 Manifest 声明:${registration.operationKey}`,
242
+ );
243
+ }
244
+ if (state.aiOperationKeys.has(registration.operationKey)) {
245
+ throw new Error(
246
+ `AI Operation Handler 重复注册:${registration.operationKey}`,
247
+ );
248
+ }
249
+ state.aiOperationKeys.add(registration.operationKey);
250
+ state.aiOperationHandlers.push({ moduleName, registration });
251
+ }
252
+ },
253
+ registerRolePresets(presets) {
254
+ for (const preset of presets) {
255
+ requireModuleNamespace("角色预设", preset.key, moduleName);
256
+ if (state.rolePresetKeys.has(preset.key)) {
257
+ throw new Error(`角色预设重复注册:${preset.key}`);
258
+ }
259
+ for (const permissionKey of preset.permissionKeys) {
260
+ requireModuleNamespace("角色预设权限", permissionKey, moduleName);
261
+ if (!manifest.permissions.some(({ key }) => key === permissionKey)) {
262
+ throw new Error(
263
+ `角色预设引用了模块 ${moduleName} 未声明权限:${permissionKey}`,
264
+ );
265
+ }
266
+ }
267
+ state.rolePresetKeys.add(preset.key);
268
+ state.rolePresets.push({ moduleName, preset });
269
+ }
270
+ },
271
+ registerSearchProviders(providers) {
272
+ for (const provider of providers) {
273
+ requireModuleNamespace("搜索 Provider", provider.key, moduleName);
274
+ if (!manifest.providers.some(({ key }) => key === provider.key)) {
275
+ throw new Error(`搜索 Provider 未在 Manifest 声明:${provider.key}`);
276
+ }
277
+ if (state.searchProviderKeys.has(provider.key)) {
278
+ throw new Error(`搜索 Provider 重复注册:${provider.key}`);
279
+ }
280
+ state.searchProviderKeys.add(provider.key);
281
+ state.searchProviders.push({ moduleName, provider });
282
+ }
283
+ },
284
+ };
285
+ }
286
+
287
+ function validateRouteDeclaration(
288
+ manifest: ModuleManifest,
289
+ definition: ModuleRouteDefinition,
290
+ ): void {
291
+ const binding = manifest.apiPermissions.find(
292
+ (item) =>
293
+ item.method === definition.method && item.path === definition.path,
294
+ );
295
+ const route = `${definition.method} ${definition.path}`;
296
+ if (!binding) {
297
+ throw new Error(`模块路由未在 Manifest 声明:${route}`);
298
+ }
299
+ if (binding.permissionKey !== definition.permissionKey) {
300
+ throw new Error(`模块路由权限与 Manifest 声明不一致:${route}`);
301
+ }
302
+ if (
303
+ definition.featureKey !== undefined &&
304
+ binding.featureKey !== definition.featureKey
305
+ ) {
306
+ throw new Error(`模块路由 Feature 与 Manifest 声明不一致:${route}`);
307
+ }
308
+ if (
309
+ binding.featureKey &&
310
+ !manifest.features.some(({ key }) => key === binding.featureKey)
311
+ ) {
312
+ throw new Error(
313
+ `模块路由 Feature 未在 Manifest 注册:${binding.featureKey}`,
314
+ );
315
+ }
316
+ if (
317
+ definition.auditAction &&
318
+ !manifest.auditActions.some((action) =>
319
+ typeof action === "string"
320
+ ? action === definition.auditAction
321
+ : action.key === definition.auditAction,
322
+ )
323
+ ) {
324
+ throw new Error(
325
+ `模块路由审计动作未在 Manifest 注册:${definition.auditAction}`,
326
+ );
327
+ }
328
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildScopeContext } from "../data-access/context.js";
3
+ import type { RequestActor } from "../guards.js";
4
+ import { requestActorFromModule, toModuleActor } from "./request-context.js";
5
+
6
+ describe("模块 Actor 运行时边界", () => {
7
+ test("只下发 SDK 白名单字段并保留宿主内部身份映射", () => {
8
+ const requestActor: RequestActor = {
9
+ id: "user-1",
10
+ type: "user",
11
+ name: "测试用户",
12
+ username: "tester",
13
+ displayName: "测试用户",
14
+ email: "private@example.test",
15
+ phone: "13800000000",
16
+ credentialState: "active",
17
+ passwordChangedAt: "2026-07-25T00:00:00.000Z",
18
+ roleCodes: ["operator"],
19
+ permissionKeys: ["work-order.ticket.read"],
20
+ scopeContext: buildScopeContext("user-1", undefined, [
21
+ {
22
+ id: "role-1",
23
+ code: "operator",
24
+ dataScope: "self",
25
+ departmentIds: [],
26
+ },
27
+ ]),
28
+ };
29
+
30
+ const actor = toModuleActor(requestActor);
31
+
32
+ expect(actor).toEqual({
33
+ id: "user-1",
34
+ type: "user",
35
+ name: "测试用户",
36
+ username: "tester",
37
+ displayName: "测试用户",
38
+ roleCodes: ["operator"],
39
+ permissionKeys: ["work-order.ticket.read"],
40
+ });
41
+ expect(Object.keys(actor).sort()).toEqual([
42
+ "displayName",
43
+ "id",
44
+ "name",
45
+ "permissionKeys",
46
+ "roleCodes",
47
+ "type",
48
+ "username",
49
+ ]);
50
+ expect(Object.isFrozen(actor)).toBe(true);
51
+ expect(Object.isFrozen(actor.roleCodes)).toBe(true);
52
+ expect(requestActorFromModule(actor)).toBe(requestActor);
53
+ });
54
+
55
+ test("拒绝业务模块自行构造的 Actor", () => {
56
+ expect(() =>
57
+ requestActorFromModule({
58
+ id: "forged",
59
+ type: "user",
60
+ name: "伪造用户",
61
+ roleCodes: [],
62
+ permissionKeys: [],
63
+ }),
64
+ ).toThrow("不是由当前宿主颁发");
65
+ });
66
+ });
@@ -0,0 +1,156 @@
1
+ import { randomUUIDv7 } from "bun";
2
+ import type { DurableJobs } from "@southwind-ai/jobs";
3
+ import type {
4
+ ModuleActor,
5
+ ModuleAuditEmitInput,
6
+ ModuleAuditEmitter,
7
+ ModuleJobSubmitter,
8
+ ModuleRequestContext,
9
+ ModuleStoragePort,
10
+ } from "@southwind-ai/server-sdk";
11
+ import { requireModuleNamespace } from "@southwind-ai/server-sdk";
12
+ import type { AuditEvent } from "@southwind-ai/shared";
13
+ import type { ModuleManifest } from "@southwind-ai/modules";
14
+ import type { RequestActor, RequestGuardContext } from "../guards.js";
15
+ import type { createServerRuntime } from "../runtime.js";
16
+
17
+ type ServerRuntime = ReturnType<typeof createServerRuntime>;
18
+
19
+ const requestActorByModuleView = new WeakMap<object, RequestActor>();
20
+
21
+ /**
22
+ * 收窄为模块 Actor 只读视图。只复制 SDK 明确公开的字段,避免凭据状态、
23
+ * 联系方式与数据范围令牌在运行时通过属性枚举泄漏给业务模块。
24
+ */
25
+ export function toModuleActor(actor: RequestActor): ModuleActor {
26
+ const view: ModuleActor = Object.freeze({
27
+ id: actor.id,
28
+ type: actor.type,
29
+ name: actor.name,
30
+ ...(actor.username ? { username: actor.username } : {}),
31
+ ...(actor.displayName ? { displayName: actor.displayName } : {}),
32
+ roleCodes: Object.freeze([...actor.roleCodes]),
33
+ permissionKeys: Object.freeze([...actor.permissionKeys]),
34
+ });
35
+ requestActorByModuleView.set(view, actor);
36
+ return view;
37
+ }
38
+
39
+ /**
40
+ * Host 内部适配器通过对象身份恢复原始 Actor,使平台内置模块仍能复用宿主的数据
41
+ * 范围执行链;原始 Actor 不作为 SDK 字段下发,外部业务包也无法访问该映射。
42
+ */
43
+ export function requestActorFromModule(actor: ModuleActor): RequestActor {
44
+ const requestActor = requestActorByModuleView.get(actor);
45
+ if (!requestActor) {
46
+ throw new Error("模块 Actor 不是由当前宿主颁发");
47
+ }
48
+ return requestActor;
49
+ }
50
+
51
+ export interface ModuleRequestContextInput {
52
+ moduleName: string;
53
+ manifest: ModuleManifest;
54
+ guardContext: RequestGuardContext;
55
+ params: Record<string, string | undefined>;
56
+ query: Record<string, string | undefined>;
57
+ body?: unknown;
58
+ runtime: ServerRuntime;
59
+ jobs?: DurableJobs;
60
+ storage?: ModuleStoragePort;
61
+ }
62
+
63
+ export function buildModuleRequestContext(
64
+ input: ModuleRequestContextInput,
65
+ ): ModuleRequestContext {
66
+ return {
67
+ requestId: input.guardContext.requestId,
68
+ actor: toModuleActor(input.guardContext.actor),
69
+ params: input.params,
70
+ query: input.query,
71
+ body: input.body,
72
+ audit: createModuleAuditEmitter(input),
73
+ jobs: createModuleJobSubmitter(input),
74
+ storage: input.storage ?? unavailableModuleStorage(input.moduleName),
75
+ };
76
+ }
77
+
78
+ function createModuleAuditEmitter(
79
+ input: ModuleRequestContextInput,
80
+ ): ModuleAuditEmitter {
81
+ const allowed = new Set(
82
+ input.manifest.auditActions.map((action) =>
83
+ typeof action === "string" ? action : action.key,
84
+ ),
85
+ );
86
+ return {
87
+ async emit(action: string, event: ModuleAuditEmitInput = {}) {
88
+ if (!allowed.has(action)) {
89
+ throw new Error(
90
+ `审计动作未在模块 ${input.moduleName} Manifest 注册:${action}`,
91
+ );
92
+ }
93
+ const auditEvent = buildAuditEvent(input.guardContext, action, event);
94
+ if (event.critical) {
95
+ await input.runtime.auditSink.recordCriticalEvent(auditEvent);
96
+ return;
97
+ }
98
+ input.runtime.auditSink.recordEvent(auditEvent);
99
+ },
100
+ };
101
+ }
102
+
103
+ function buildAuditEvent(
104
+ context: RequestGuardContext,
105
+ action: string,
106
+ input: ModuleAuditEmitInput,
107
+ ): AuditEvent {
108
+ return {
109
+ id: randomUUIDv7(),
110
+ action,
111
+ outcome: "success",
112
+ actor: {
113
+ id: context.actor.id,
114
+ type: context.actor.type === "anonymous" ? "user" : context.actor.type,
115
+ name: context.actor.name,
116
+ },
117
+ target: input.target,
118
+ requestId: context.requestId,
119
+ metadata: input.metadata,
120
+ occurredAt: new Date().toISOString(),
121
+ };
122
+ }
123
+
124
+ function createModuleJobSubmitter(
125
+ input: ModuleRequestContextInput,
126
+ ): ModuleJobSubmitter {
127
+ return {
128
+ async submit(job) {
129
+ requireModuleNamespace("任务 handler", job.handlerKey, input.moduleName);
130
+ if (!input.jobs) {
131
+ throw new Error("Durable Jobs 运行时未挂载");
132
+ }
133
+ const result = await input.jobs.submit({
134
+ ...job,
135
+ queue: input.moduleName,
136
+ requestedBy: input.guardContext.actor.id,
137
+ });
138
+ return { jobId: result.job.id, created: result.created };
139
+ },
140
+ };
141
+ }
142
+
143
+ /**
144
+ * 存储 Port 未挂载时的防御性实现:任何访问都以明确错误失败,
145
+ * 而不是让模块悄悄绕过 purpose / owner 约束。
146
+ */
147
+ function unavailableModuleStorage(moduleName: string): ModuleStoragePort {
148
+ const fail = (): never => {
149
+ throw new Error(`模块 ${moduleName} 的存储 Port 未挂载`);
150
+ };
151
+ return {
152
+ createUploadSession: () => fail(),
153
+ openFile: () => fail(),
154
+ openPublicFile: () => fail(),
155
+ };
156
+ }
@@ -0,0 +1,94 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createFoundationSnapshot } from "../foundation.js";
3
+ import type { ManagedRole } from "../system/entities.js";
4
+ import { InMemoryEntityRepository } from "../system/repository.js";
5
+ import type { RegisteredModuleRolePreset } from "./initialize.js";
6
+ import {
7
+ moduleRolePresetId,
8
+ synchronizeModuleRolePresets,
9
+ } from "./role-presets.js";
10
+
11
+ describe("模块角色预设同步", () => {
12
+ test("预设落库为独立角色且重复同步幂等", async () => {
13
+ const roles = new InMemoryEntityRepository<ManagedRole>([]);
14
+ const snapshot = createFoundationSnapshot();
15
+
16
+ await synchronizeModuleRolePresets({ roles }, snapshot, [preset()]);
17
+ const created = await roles.get(moduleRolePresetId("shop.manager"));
18
+ expect(created).toMatchObject({
19
+ code: "shop.manager",
20
+ name: "店长",
21
+ dataScope: "department-and-children",
22
+ permissionKeys: ["shop.item.manage", "shop.item.read"],
23
+ status: "active",
24
+ });
25
+
26
+ await synchronizeModuleRolePresets({ roles }, snapshot, [preset()]);
27
+ const again = await roles.get(moduleRolePresetId("shop.manager"));
28
+ expect(again?.permissionKeys).toEqual(created?.permissionKeys);
29
+ });
30
+
31
+ test("已存在角色保留管理员手工追加的授权", async () => {
32
+ const roles = new InMemoryEntityRepository<ManagedRole>([]);
33
+ const snapshot = createFoundationSnapshot();
34
+ await synchronizeModuleRolePresets({ roles }, snapshot, [preset()]);
35
+
36
+ const id = moduleRolePresetId("shop.manager");
37
+ const existing = await roles.get(id);
38
+ if (!existing) throw new Error("角色预设未创建");
39
+ await roles.update(
40
+ id,
41
+ { permissionKeys: [...existing.permissionKeys, "system.menu.read"] },
42
+ actor,
43
+ );
44
+
45
+ await synchronizeModuleRolePresets({ roles }, snapshot, [preset()]);
46
+ const merged = await roles.get(id);
47
+ expect(merged?.permissionKeys).toContain("system.menu.read");
48
+ expect(merged?.permissionKeys).toContain("shop.item.manage");
49
+ });
50
+
51
+ test("预设 Code 与既有角色冲突时启动失败", async () => {
52
+ const roles = new InMemoryEntityRepository<ManagedRole>([
53
+ {
54
+ id: "role_custom",
55
+ code: "shop.manager",
56
+ name: "手工角色",
57
+ dataScope: "self",
58
+ permissionKeys: [],
59
+ menuKeys: [],
60
+ status: "active",
61
+ createdAt: "2026-07-16T00:00:00.000Z",
62
+ createdBy: "system",
63
+ updatedAt: "2026-07-16T00:00:00.000Z",
64
+ updatedBy: "system",
65
+ },
66
+ ]);
67
+
68
+ await expect(
69
+ synchronizeModuleRolePresets({ roles }, createFoundationSnapshot(), [
70
+ preset(),
71
+ ]),
72
+ ).rejects.toThrow("角色预设与既有角色冲突:shop.manager");
73
+ });
74
+ });
75
+
76
+ function preset(): RegisteredModuleRolePreset {
77
+ return {
78
+ moduleName: "shop",
79
+ preset: {
80
+ key: "shop.manager",
81
+ label: "店长",
82
+ dataScope: "department-and-children",
83
+ permissionKeys: ["shop.item.read", "shop.item.manage"],
84
+ },
85
+ };
86
+ }
87
+
88
+ const actor = {
89
+ id: "system_bootstrap",
90
+ type: "service" as const,
91
+ name: "平台初始化",
92
+ roleCodes: ["system"],
93
+ permissionKeys: ["system.role.manage"],
94
+ };