create-windy 0.2.18 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/cli.js +752 -186
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/application-services.ts +2 -2
- package/template/apps/server/src/audit/search-summary.ts +15 -8
- package/template/apps/server/src/example-modules.ts +58 -70
- package/template/apps/server/src/foundation.ts +8 -4
- package/template/apps/server/src/guards.test.ts +13 -0
- package/template/apps/server/src/guards.ts +12 -2
- package/template/apps/server/src/index.ts +48 -91
- package/template/apps/server/src/installed-business-modules.ts +23 -4
- package/template/apps/server/src/module-bootstrap.ts +83 -0
- package/template/apps/server/src/module-composition.test.ts +45 -1
- package/template/apps/server/src/module-composition.ts +45 -0
- package/template/apps/server/src/module-host/initialize.test.ts +123 -0
- package/template/apps/server/src/module-host/initialize.ts +331 -0
- package/template/apps/server/src/module-host/request-context.test.ts +66 -0
- package/template/apps/server/src/module-host/request-context.ts +156 -0
- package/template/apps/server/src/module-host/role-presets.test.ts +94 -0
- package/template/apps/server/src/module-host/role-presets.ts +76 -0
- package/template/apps/server/src/module-host/route-installer.test.ts +317 -0
- package/template/apps/server/src/module-host/route-installer.ts +97 -0
- package/template/apps/server/src/module-host/route-registration.test.ts +160 -0
- package/template/apps/server/src/module-host/search-providers.ts +31 -0
- package/template/apps/server/src/module-host/storage-port.test.ts +165 -0
- package/template/apps/server/src/module-host/storage-port.ts +59 -0
- package/template/apps/server/src/module-host/task-definitions.ts +41 -0
- package/template/apps/server/src/module-host/test-fixtures.ts +26 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +25 -0
- package/template/apps/server/src/module-host.test.ts +202 -58
- package/template/apps/server/src/module-host.ts +34 -113
- package/template/apps/server/src/route-guards.test.ts +112 -1
- package/template/apps/server/src/runtime-feature.ts +66 -31
- package/template/apps/server/src/runtime.test.ts +1 -0
- package/template/apps/server/src/runtime.ts +3 -1
- package/template/apps/server/src/system/built-in-roles.ts +1 -1
- package/template/apps/server/src/work-order/foundation-modules.test.ts +30 -10
- package/template/apps/server/src/work-order/routes.test.ts +30 -18
- package/template/apps/server/src/work-order/routes.ts +104 -135
- package/template/apps/server/src/work-order/search-api.test.ts +14 -3
- package/template/apps/server/src/work-order/search-provider.test.ts +23 -15
- package/template/apps/server/src/work-order/search-provider.ts +17 -12
- package/template/apps/server/src/work-order/task.test.ts +16 -11
- package/template/apps/web/Dockerfile +4 -1
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +4 -1
- package/template/apps/web/src/layout/GlobalWatermark.vue +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +4 -1
- package/template/docs/architecture/object-storage.md +12 -0
- package/template/packages/config/index.test.ts +57 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/config/src/platform.ts +28 -1
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/runner.ts +7 -9
- package/template/packages/example-work-order/index.ts +1 -0
- package/template/packages/example-work-order/package.json +2 -0
- package/template/packages/example-work-order/src/server-module.ts +61 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs/src/types.ts +7 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/catalog-validation.test.ts +212 -0
- package/template/packages/modules/src/catalog-validation.ts +19 -2
- package/template/packages/modules/src/migration-bundle-validation.test.ts +191 -0
- package/template/packages/server-sdk/index.ts +42 -0
- package/template/packages/server-sdk/package.json +16 -3
- package/template/packages/server-sdk/src/actor.ts +15 -0
- package/template/packages/server-sdk/src/audit-port.ts +22 -0
- package/template/packages/server-sdk/src/jobs-port.ts +24 -0
- package/template/packages/server-sdk/src/module-host.ts +36 -1
- package/template/packages/server-sdk/src/namespace.test.ts +26 -0
- package/template/packages/server-sdk/src/namespace.ts +13 -0
- package/template/packages/server-sdk/src/request-context.ts +21 -0
- package/template/packages/server-sdk/src/role-preset.ts +18 -0
- package/template/packages/server-sdk/src/route-definition.ts +26 -0
- package/template/packages/server-sdk/src/search-provider-port.ts +48 -0
- package/template/packages/server-sdk/src/storage-port.ts +25 -0
- package/template/packages/server-sdk/src/task-registration.ts +18 -0
- package/template/packages/server-sdk/src/tool-registration.ts +29 -0
- package/template/packages/shared/index.ts +1 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/license-catalog.test.ts +73 -1
- package/template/packages/shared/src/license-catalog.ts +95 -0
- package/template/packages/{database/src/bundle-validation.ts → shared/src/migration-bundle-validation.ts} +2 -1
- package/template/apps/server/src/work-order/task.ts +0 -30
|
@@ -26,9 +26,53 @@ describe("Server Module Composition", () => {
|
|
|
26
26
|
{ method: "POST", path: "/api/auth/users/:id/password" },
|
|
27
27
|
],
|
|
28
28
|
}),
|
|
29
|
+
).toThrow("API Route Handler Key 重复:POST /api/auth/users/:id/password");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("Host 注册实现必须在 Manifest 声明", () => {
|
|
33
|
+
const app = new Elysia({ prefix: "/api" }).post(
|
|
34
|
+
"/auth/users/:id/password",
|
|
35
|
+
() => null,
|
|
36
|
+
);
|
|
37
|
+
const base = {
|
|
38
|
+
modules: [fixture()],
|
|
39
|
+
routes: app.routes,
|
|
40
|
+
tasks: [],
|
|
41
|
+
tools: new Map(),
|
|
42
|
+
providers: [],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
expect(() =>
|
|
46
|
+
validateServerModuleComposition({
|
|
47
|
+
...base,
|
|
48
|
+
moduleRegistrations: {
|
|
49
|
+
routes: ["POST /api/auth/users/:id/password"],
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
).not.toThrow();
|
|
53
|
+
|
|
54
|
+
expect(() =>
|
|
55
|
+
validateServerModuleComposition({
|
|
56
|
+
...base,
|
|
57
|
+
moduleRegistrations: { routes: ["GET /api/auth/users"] },
|
|
58
|
+
}),
|
|
59
|
+
).toThrow("模块路由已注册但未在 Manifest 声明:GET /api/auth/users");
|
|
60
|
+
|
|
61
|
+
expect(() =>
|
|
62
|
+
validateServerModuleComposition({
|
|
63
|
+
...base,
|
|
64
|
+
moduleRegistrations: { tasks: ["system.unknown-task"] },
|
|
65
|
+
}),
|
|
29
66
|
).toThrow(
|
|
30
|
-
"
|
|
67
|
+
"定时任务 Handler已注册但未在 Manifest 声明:system.unknown-task",
|
|
31
68
|
);
|
|
69
|
+
|
|
70
|
+
expect(() =>
|
|
71
|
+
validateServerModuleComposition({
|
|
72
|
+
...base,
|
|
73
|
+
moduleRegistrations: { providers: ["system.unknown-search"] },
|
|
74
|
+
}),
|
|
75
|
+
).toThrow("搜索 Provider已注册但未在 Manifest 声明:system.unknown-search");
|
|
32
76
|
});
|
|
33
77
|
});
|
|
34
78
|
|
|
@@ -8,12 +8,24 @@ interface RegisteredHttpRoute {
|
|
|
8
8
|
path: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* 模块经 Host 注册的实现 Key,用于校验“实现了但没在 Manifest 声明”。
|
|
13
|
+
* 反向(声明了没实现)由 validateModuleComposition 的实现绑定校验覆盖。
|
|
14
|
+
*/
|
|
15
|
+
export interface ModuleImplementationRegistrations {
|
|
16
|
+
routes?: readonly string[];
|
|
17
|
+
tasks?: readonly string[];
|
|
18
|
+
tools?: readonly string[];
|
|
19
|
+
providers?: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
export interface ServerCompositionInput {
|
|
12
23
|
modules: readonly ModuleManifest[];
|
|
13
24
|
routes: readonly RegisteredHttpRoute[];
|
|
14
25
|
tasks?: readonly { key: string }[];
|
|
15
26
|
tools?: ReadonlyMap<string, unknown>;
|
|
16
27
|
providers?: readonly string[];
|
|
28
|
+
moduleRegistrations?: ModuleImplementationRegistrations;
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
export function validateServerModuleComposition(
|
|
@@ -25,4 +37,37 @@ export function validateServerModuleComposition(
|
|
|
25
37
|
tools: [...(input.tools?.keys() || [])],
|
|
26
38
|
providers: input.providers || [],
|
|
27
39
|
});
|
|
40
|
+
if (input.moduleRegistrations) {
|
|
41
|
+
validateRegistrationConsistency(input.modules, input.moduleRegistrations);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validateRegistrationConsistency(
|
|
46
|
+
modules: readonly ModuleManifest[],
|
|
47
|
+
registrations: ModuleImplementationRegistrations,
|
|
48
|
+
): void {
|
|
49
|
+
const declaredRoutes = new Set(
|
|
50
|
+
modules.flatMap((module) => module.apiPermissions.map(apiRouteKey)),
|
|
51
|
+
);
|
|
52
|
+
const declaredTasks = new Set(
|
|
53
|
+
modules.flatMap((module) => module.tasks.map(({ key }) => key)),
|
|
54
|
+
);
|
|
55
|
+
const declaredTools = new Set(
|
|
56
|
+
modules.flatMap((module) => module.tools.map(({ key }) => key)),
|
|
57
|
+
);
|
|
58
|
+
const declaredProviders = new Set(
|
|
59
|
+
modules.flatMap((module) => module.providers.map(({ key }) => key)),
|
|
60
|
+
);
|
|
61
|
+
for (const [label, registered, declared] of [
|
|
62
|
+
["模块路由", registrations.routes || [], declaredRoutes],
|
|
63
|
+
["定时任务 Handler", registrations.tasks || [], declaredTasks],
|
|
64
|
+
["Agent Tool Handler", registrations.tools || [], declaredTools],
|
|
65
|
+
["搜索 Provider", registrations.providers || [], declaredProviders],
|
|
66
|
+
] as const) {
|
|
67
|
+
for (const key of registered) {
|
|
68
|
+
if (!declared.has(key)) {
|
|
69
|
+
throw new Error(`${label}已注册但未在 Manifest 声明:${key}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
28
73
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { DurableHandlerRegistry } from "@southwind-ai/jobs";
|
|
3
|
+
import type { ModuleInitializer } from "@southwind-ai/server-sdk";
|
|
4
|
+
import { initializeModuleHosts } from "./initialize.js";
|
|
5
|
+
import { manifest } from "./test-fixtures.js";
|
|
6
|
+
|
|
7
|
+
describe("模块 Host 初始化", () => {
|
|
8
|
+
test("按安装顺序完成初始化后再创建运行时注册表", async () => {
|
|
9
|
+
const events: string[] = [];
|
|
10
|
+
const first = initializer("alpha", async (host) => {
|
|
11
|
+
events.push("alpha:start");
|
|
12
|
+
await Promise.resolve();
|
|
13
|
+
host.registerDurableHandler({
|
|
14
|
+
key: "alpha.refresh",
|
|
15
|
+
payload: { parse: () => ({}) },
|
|
16
|
+
execute: () => undefined,
|
|
17
|
+
});
|
|
18
|
+
events.push("alpha:end");
|
|
19
|
+
});
|
|
20
|
+
const second = initializer("beta", () => {
|
|
21
|
+
events.push("beta");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const initialized = await initializeModuleHosts({
|
|
25
|
+
modules: [manifest("alpha"), manifest("beta")],
|
|
26
|
+
initializers: [first, second],
|
|
27
|
+
});
|
|
28
|
+
expect(events).toEqual(["alpha:start", "alpha:end", "beta"]);
|
|
29
|
+
|
|
30
|
+
const registry = new DurableHandlerRegistry();
|
|
31
|
+
initialized.installDurableHandlers(registry);
|
|
32
|
+
expect(registry.keys()).toEqual(["alpha.refresh"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("未知模块、错误命名空间和重复注册立即失败", async () => {
|
|
36
|
+
await expect(
|
|
37
|
+
initializeModuleHosts({
|
|
38
|
+
modules: [manifest("known")],
|
|
39
|
+
initializers: [initializer("missing", () => undefined)],
|
|
40
|
+
}),
|
|
41
|
+
).rejects.toThrow("模块初始化器与已安装模块不匹配:missing");
|
|
42
|
+
|
|
43
|
+
await expect(
|
|
44
|
+
initializeModuleHosts({
|
|
45
|
+
modules: [manifest("known")],
|
|
46
|
+
initializers: [
|
|
47
|
+
initializer("known", (host) => {
|
|
48
|
+
host.registerUploadPolicy(policy("other.attachment"));
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
}),
|
|
52
|
+
).rejects.toThrow("上传用途不属于模块 known:other.attachment");
|
|
53
|
+
|
|
54
|
+
await expect(
|
|
55
|
+
initializeModuleHosts({
|
|
56
|
+
modules: [manifest("known")],
|
|
57
|
+
initializers: [
|
|
58
|
+
initializer("known", (host) => {
|
|
59
|
+
host.registerUploadPolicy(policy("known.attachment"));
|
|
60
|
+
host.registerUploadPolicy(policy("known.attachment"));
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
}),
|
|
64
|
+
).rejects.toThrow("上传用途重复注册:known.attachment");
|
|
65
|
+
|
|
66
|
+
await expect(
|
|
67
|
+
initializeModuleHosts({
|
|
68
|
+
modules: [],
|
|
69
|
+
initializers: [],
|
|
70
|
+
baseUploadPolicies: [policy("platform.file"), policy("platform.file")],
|
|
71
|
+
}),
|
|
72
|
+
).rejects.toThrow("上传用途重复注册:platform.file");
|
|
73
|
+
|
|
74
|
+
await expect(
|
|
75
|
+
initializeModuleHosts({
|
|
76
|
+
modules: [manifest("known")],
|
|
77
|
+
initializers: [
|
|
78
|
+
initializer("known", (host) => {
|
|
79
|
+
const definition = {
|
|
80
|
+
key: "known.refresh",
|
|
81
|
+
payload: { parse: () => ({}) },
|
|
82
|
+
execute: () => undefined,
|
|
83
|
+
};
|
|
84
|
+
host.registerDurableHandler(definition);
|
|
85
|
+
host.registerDurableHandler(definition);
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
).rejects.toThrow("任务 handler 重复注册:known.refresh");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("安装单元的 Manifest 必须来自同一平台能力目录", async () => {
|
|
93
|
+
await expect(
|
|
94
|
+
initializeModuleHosts({
|
|
95
|
+
modules: [manifest("known")],
|
|
96
|
+
installations: [
|
|
97
|
+
{
|
|
98
|
+
manifest: manifest("known"),
|
|
99
|
+
setup: () => undefined,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
}),
|
|
103
|
+
).rejects.toThrow("Server Module Manifest 与能力目录不是同一声明:known");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function initializer(
|
|
108
|
+
moduleName: string,
|
|
109
|
+
initialize: ModuleInitializer["initialize"],
|
|
110
|
+
): ModuleInitializer {
|
|
111
|
+
return { moduleName, initialize };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function policy(purpose: string) {
|
|
115
|
+
return {
|
|
116
|
+
purpose,
|
|
117
|
+
maxByteSize: 1024,
|
|
118
|
+
maxPartByteSize: 512,
|
|
119
|
+
sessionTtlMs: 60_000,
|
|
120
|
+
allowedTypes: { "text/plain": ["txt"] },
|
|
121
|
+
signature: "none" as const,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
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 InstallableServerModule,
|
|
9
|
+
type ModuleHost,
|
|
10
|
+
type ModuleInitializer,
|
|
11
|
+
type ModuleRolePreset,
|
|
12
|
+
type ModuleRouteDefinition,
|
|
13
|
+
type ModuleSearchProvider,
|
|
14
|
+
type ModuleTaskHandlerRegistration,
|
|
15
|
+
type ModuleToolHandlerRegistration,
|
|
16
|
+
type ModuleUploadPolicy,
|
|
17
|
+
} from "@southwind-ai/server-sdk";
|
|
18
|
+
|
|
19
|
+
export interface RegisteredModuleRoute {
|
|
20
|
+
readonly moduleName: string;
|
|
21
|
+
readonly definition: ModuleRouteDefinition;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RegisteredModuleTaskHandler {
|
|
25
|
+
readonly moduleName: string;
|
|
26
|
+
readonly registration: ModuleTaskHandlerRegistration;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RegisteredModuleToolHandler {
|
|
30
|
+
readonly moduleName: string;
|
|
31
|
+
readonly registration: ModuleToolHandlerRegistration;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RegisteredModuleRolePreset {
|
|
35
|
+
readonly moduleName: string;
|
|
36
|
+
readonly preset: ModuleRolePreset;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RegisteredModuleSearchProvider {
|
|
40
|
+
readonly moduleName: string;
|
|
41
|
+
readonly provider: ModuleSearchProvider;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface InitializeModuleHostsInput {
|
|
45
|
+
modules: readonly ModuleManifest[];
|
|
46
|
+
/** Manifest 与 setup 由同一业务模块安装单元贡献。 */
|
|
47
|
+
installations?: readonly InstallableServerModule<ModuleManifest>[];
|
|
48
|
+
/** @deprecated 仅供旧模块迁移;新模块必须使用 installations。 */
|
|
49
|
+
initializers?: readonly ModuleInitializer[];
|
|
50
|
+
baseUploadPolicies?: readonly ModuleUploadPolicy[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface InitializedModuleHosts {
|
|
54
|
+
readonly manifests: readonly ModuleManifest[];
|
|
55
|
+
readonly uploadPolicies: readonly ModuleUploadPolicy[];
|
|
56
|
+
readonly routes: readonly RegisteredModuleRoute[];
|
|
57
|
+
readonly taskHandlers: readonly RegisteredModuleTaskHandler[];
|
|
58
|
+
readonly toolHandlers: readonly RegisteredModuleToolHandler[];
|
|
59
|
+
readonly rolePresets: readonly RegisteredModuleRolePreset[];
|
|
60
|
+
readonly searchProviders: readonly RegisteredModuleSearchProvider[];
|
|
61
|
+
manifestOf(moduleName: string): ModuleManifest;
|
|
62
|
+
/** 该模块经 Host 注册的上传用途集合(不含平台基础用途)。 */
|
|
63
|
+
uploadPurposesOf(moduleName: string): ReadonlySet<string>;
|
|
64
|
+
installDurableHandlers(registry: DurableHandlerRegistry): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type HandlerInstaller = (registry: DurableHandlerRegistry) => void;
|
|
68
|
+
|
|
69
|
+
export async function initializeModuleHosts(
|
|
70
|
+
input: InitializeModuleHostsInput,
|
|
71
|
+
): Promise<InitializedModuleHosts> {
|
|
72
|
+
const manifests = new Map(
|
|
73
|
+
input.modules.map((module) => [module.name, module]),
|
|
74
|
+
);
|
|
75
|
+
const initializedNames = new Set<string>();
|
|
76
|
+
const uploadPolicies: ModuleUploadPolicy[] = [];
|
|
77
|
+
const uploadPurposes = new Set<string>();
|
|
78
|
+
for (const policy of input.baseUploadPolicies ?? []) {
|
|
79
|
+
if (uploadPurposes.has(policy.purpose)) {
|
|
80
|
+
throw new Error(`上传用途重复注册:${policy.purpose}`);
|
|
81
|
+
}
|
|
82
|
+
uploadPurposes.add(policy.purpose);
|
|
83
|
+
uploadPolicies.push(policy);
|
|
84
|
+
}
|
|
85
|
+
const state: HostCollections = {
|
|
86
|
+
uploadPolicies,
|
|
87
|
+
uploadPurposes,
|
|
88
|
+
modulePurposes: new Map<string, Set<string>>(),
|
|
89
|
+
handlerKeys: new Set<string>(),
|
|
90
|
+
handlerInstallers: [],
|
|
91
|
+
routes: [],
|
|
92
|
+
routeKeys: new Set<string>(),
|
|
93
|
+
taskHandlers: [],
|
|
94
|
+
taskKeys: new Set<string>(),
|
|
95
|
+
toolHandlers: [],
|
|
96
|
+
toolKeys: new Set<string>(),
|
|
97
|
+
rolePresets: [],
|
|
98
|
+
rolePresetKeys: new Set<string>(),
|
|
99
|
+
searchProviders: [],
|
|
100
|
+
searchProviderKeys: new Set<string>(),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const installations = [
|
|
104
|
+
...(input.installations ?? []).map((installation) => ({
|
|
105
|
+
moduleName: installation.manifest.name.trim(),
|
|
106
|
+
manifest: installation.manifest,
|
|
107
|
+
setup: installation.setup,
|
|
108
|
+
legacy: false,
|
|
109
|
+
})),
|
|
110
|
+
...(input.initializers ?? []).map((initializer) => ({
|
|
111
|
+
moduleName: initializer.moduleName.trim(),
|
|
112
|
+
manifest: undefined,
|
|
113
|
+
setup: initializer.initialize,
|
|
114
|
+
legacy: true,
|
|
115
|
+
})),
|
|
116
|
+
];
|
|
117
|
+
for (const installation of installations) {
|
|
118
|
+
const moduleName = installation.moduleName;
|
|
119
|
+
const manifest = manifests.get(moduleName);
|
|
120
|
+
if (!manifest) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
installation.legacy
|
|
123
|
+
? `模块初始化器与已安装模块不匹配:${moduleName}`
|
|
124
|
+
: `Server Module 与已安装模块不匹配:${moduleName}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (initializedNames.has(moduleName)) {
|
|
128
|
+
throw new Error(`模块初始化器重复注册:${moduleName}`);
|
|
129
|
+
}
|
|
130
|
+
initializedNames.add(moduleName);
|
|
131
|
+
if (installation.manifest && installation.manifest !== manifest) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Server Module Manifest 与能力目录不是同一声明:${moduleName}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
await installation.setup(createScopedHost(manifest, state));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
manifests: input.modules,
|
|
141
|
+
uploadPolicies: [...state.uploadPolicies],
|
|
142
|
+
routes: [...state.routes],
|
|
143
|
+
taskHandlers: [...state.taskHandlers],
|
|
144
|
+
toolHandlers: [...state.toolHandlers],
|
|
145
|
+
rolePresets: [...state.rolePresets],
|
|
146
|
+
searchProviders: [...state.searchProviders],
|
|
147
|
+
manifestOf(moduleName) {
|
|
148
|
+
const manifest = manifests.get(moduleName);
|
|
149
|
+
if (!manifest) throw new Error(`模块未安装:${moduleName}`);
|
|
150
|
+
return manifest;
|
|
151
|
+
},
|
|
152
|
+
uploadPurposesOf(moduleName) {
|
|
153
|
+
return state.modulePurposes.get(moduleName) ?? new Set<string>();
|
|
154
|
+
},
|
|
155
|
+
installDurableHandlers(registry) {
|
|
156
|
+
for (const install of state.handlerInstallers) install(registry);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface HostCollections {
|
|
162
|
+
uploadPolicies: ModuleUploadPolicy[];
|
|
163
|
+
uploadPurposes: Set<string>;
|
|
164
|
+
modulePurposes: Map<string, Set<string>>;
|
|
165
|
+
handlerKeys: Set<string>;
|
|
166
|
+
handlerInstallers: HandlerInstaller[];
|
|
167
|
+
routes: RegisteredModuleRoute[];
|
|
168
|
+
routeKeys: Set<string>;
|
|
169
|
+
taskHandlers: RegisteredModuleTaskHandler[];
|
|
170
|
+
taskKeys: Set<string>;
|
|
171
|
+
toolHandlers: RegisteredModuleToolHandler[];
|
|
172
|
+
toolKeys: Set<string>;
|
|
173
|
+
rolePresets: RegisteredModuleRolePreset[];
|
|
174
|
+
rolePresetKeys: Set<string>;
|
|
175
|
+
searchProviders: RegisteredModuleSearchProvider[];
|
|
176
|
+
searchProviderKeys: Set<string>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function createScopedHost(
|
|
180
|
+
manifest: ModuleManifest,
|
|
181
|
+
state: HostCollections,
|
|
182
|
+
): ModuleHost {
|
|
183
|
+
const moduleName = manifest.name;
|
|
184
|
+
return {
|
|
185
|
+
moduleName,
|
|
186
|
+
registerUploadPolicy(policy) {
|
|
187
|
+
requireModuleNamespace("上传用途", policy.purpose, moduleName);
|
|
188
|
+
if (state.uploadPurposes.has(policy.purpose)) {
|
|
189
|
+
throw new Error(`上传用途重复注册:${policy.purpose}`);
|
|
190
|
+
}
|
|
191
|
+
state.uploadPurposes.add(policy.purpose);
|
|
192
|
+
state.uploadPolicies.push(policy);
|
|
193
|
+
const purposes =
|
|
194
|
+
state.modulePurposes.get(moduleName) ?? new Set<string>();
|
|
195
|
+
purposes.add(policy.purpose);
|
|
196
|
+
state.modulePurposes.set(moduleName, purposes);
|
|
197
|
+
},
|
|
198
|
+
registerDurableHandler<T>(definition: DurableHandlerDefinition<T>) {
|
|
199
|
+
requireModuleNamespace("任务 handler", definition.key, moduleName);
|
|
200
|
+
if (state.handlerKeys.has(definition.key)) {
|
|
201
|
+
throw new Error(`任务 handler 重复注册:${definition.key}`);
|
|
202
|
+
}
|
|
203
|
+
state.handlerKeys.add(definition.key);
|
|
204
|
+
state.handlerInstallers.push((registry) => {
|
|
205
|
+
registry.register(definition);
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
registerRoutes(routes) {
|
|
209
|
+
for (const definition of routes) {
|
|
210
|
+
requireModuleNamespace(
|
|
211
|
+
"路由权限",
|
|
212
|
+
definition.permissionKey,
|
|
213
|
+
moduleName,
|
|
214
|
+
);
|
|
215
|
+
validateRouteDeclaration(manifest, definition);
|
|
216
|
+
const key = `${definition.method} ${definition.path}`;
|
|
217
|
+
if (state.routeKeys.has(key)) {
|
|
218
|
+
throw new Error(`模块路由重复注册:${key}`);
|
|
219
|
+
}
|
|
220
|
+
state.routeKeys.add(key);
|
|
221
|
+
state.routes.push({ moduleName, definition });
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
registerTaskHandlers(registrations) {
|
|
225
|
+
for (const registration of registrations) {
|
|
226
|
+
requireModuleNamespace("定时任务", registration.taskKey, moduleName);
|
|
227
|
+
if (!manifest.tasks.some(({ key }) => key === registration.taskKey)) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`定时任务 Handler 未在 Manifest 声明:${registration.taskKey}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (state.taskKeys.has(registration.taskKey)) {
|
|
233
|
+
throw new Error(`定时任务 Handler 重复注册:${registration.taskKey}`);
|
|
234
|
+
}
|
|
235
|
+
state.taskKeys.add(registration.taskKey);
|
|
236
|
+
state.taskHandlers.push({ moduleName, registration });
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
registerToolHandlers(registrations) {
|
|
240
|
+
for (const registration of registrations) {
|
|
241
|
+
requireModuleNamespace("Agent Tool", registration.toolKey, moduleName);
|
|
242
|
+
if (!manifest.tools.some(({ key }) => key === registration.toolKey)) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Agent Tool Handler 未在 Manifest 声明:${registration.toolKey}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (state.toolKeys.has(registration.toolKey)) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Agent Tool Handler 重复注册:${registration.toolKey}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
state.toolKeys.add(registration.toolKey);
|
|
253
|
+
state.toolHandlers.push({ moduleName, registration });
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
registerRolePresets(presets) {
|
|
257
|
+
for (const preset of presets) {
|
|
258
|
+
requireModuleNamespace("角色预设", preset.key, moduleName);
|
|
259
|
+
if (state.rolePresetKeys.has(preset.key)) {
|
|
260
|
+
throw new Error(`角色预设重复注册:${preset.key}`);
|
|
261
|
+
}
|
|
262
|
+
for (const permissionKey of preset.permissionKeys) {
|
|
263
|
+
requireModuleNamespace("角色预设权限", permissionKey, moduleName);
|
|
264
|
+
if (!manifest.permissions.some(({ key }) => key === permissionKey)) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`角色预设引用了模块 ${moduleName} 未声明权限:${permissionKey}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
state.rolePresetKeys.add(preset.key);
|
|
271
|
+
state.rolePresets.push({ moduleName, preset });
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
registerSearchProviders(providers) {
|
|
275
|
+
for (const provider of providers) {
|
|
276
|
+
requireModuleNamespace("搜索 Provider", provider.key, moduleName);
|
|
277
|
+
if (!manifest.providers.some(({ key }) => key === provider.key)) {
|
|
278
|
+
throw new Error(`搜索 Provider 未在 Manifest 声明:${provider.key}`);
|
|
279
|
+
}
|
|
280
|
+
if (state.searchProviderKeys.has(provider.key)) {
|
|
281
|
+
throw new Error(`搜索 Provider 重复注册:${provider.key}`);
|
|
282
|
+
}
|
|
283
|
+
state.searchProviderKeys.add(provider.key);
|
|
284
|
+
state.searchProviders.push({ moduleName, provider });
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function validateRouteDeclaration(
|
|
291
|
+
manifest: ModuleManifest,
|
|
292
|
+
definition: ModuleRouteDefinition,
|
|
293
|
+
): void {
|
|
294
|
+
const binding = manifest.apiPermissions.find(
|
|
295
|
+
(item) =>
|
|
296
|
+
item.method === definition.method && item.path === definition.path,
|
|
297
|
+
);
|
|
298
|
+
const route = `${definition.method} ${definition.path}`;
|
|
299
|
+
if (!binding) {
|
|
300
|
+
throw new Error(`模块路由未在 Manifest 声明:${route}`);
|
|
301
|
+
}
|
|
302
|
+
if (binding.permissionKey !== definition.permissionKey) {
|
|
303
|
+
throw new Error(`模块路由权限与 Manifest 声明不一致:${route}`);
|
|
304
|
+
}
|
|
305
|
+
if (
|
|
306
|
+
definition.featureKey !== undefined &&
|
|
307
|
+
binding.featureKey !== definition.featureKey
|
|
308
|
+
) {
|
|
309
|
+
throw new Error(`模块路由 Feature 与 Manifest 声明不一致:${route}`);
|
|
310
|
+
}
|
|
311
|
+
if (
|
|
312
|
+
binding.featureKey &&
|
|
313
|
+
!manifest.features.some(({ key }) => key === binding.featureKey)
|
|
314
|
+
) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`模块路由 Feature 未在 Manifest 注册:${binding.featureKey}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (
|
|
320
|
+
definition.auditAction &&
|
|
321
|
+
!manifest.auditActions.some((action) =>
|
|
322
|
+
typeof action === "string"
|
|
323
|
+
? action === definition.auditAction
|
|
324
|
+
: action.key === definition.auditAction,
|
|
325
|
+
)
|
|
326
|
+
) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`模块路由审计动作未在 Manifest 注册:${definition.auditAction}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -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
|
+
});
|