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.
- package/README.md +5 -1
- package/dist/cli.js +392 -160
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/README.md +1 -0
- package/template/apps/agent-server/Dockerfile +20 -0
- package/template/apps/agent-server/README.md +46 -0
- package/template/apps/agent-server/pyproject.toml +44 -0
- package/template/apps/agent-server/src/southwind_agent_server/__init__.py +5 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +39 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +263 -0
- package/template/apps/agent-server/src/southwind_agent_server/config.py +141 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +99 -0
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +52 -0
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +85 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +197 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +246 -0
- package/template/apps/agent-server/src/southwind_agent_server/service.py +180 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +317 -0
- package/template/apps/agent-server/src/southwind_agent_server/sse.py +41 -0
- package/template/apps/agent-server/src/southwind_agent_server/store.py +144 -0
- package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +80 -0
- package/template/apps/agent-server/tests/test_api.py +207 -0
- package/template/apps/agent-server/tests/test_config.py +56 -0
- package/template/apps/agent-server/tests/test_openai_provider.py +224 -0
- package/template/apps/agent-server/tests/test_sqlite_store.py +93 -0
- package/template/apps/agent-server/uv.lock +925 -0
- package/template/apps/server/src/agent/execution-grants.ts +89 -0
- package/template/apps/server/src/agent/remote-runtime.ts +128 -0
- package/template/apps/server/src/agent/run-contracts.ts +43 -0
- package/template/apps/server/src/agent/run-facade.test.ts +257 -0
- package/template/apps/server/src/agent/run-facade.ts +190 -0
- package/template/apps/server/src/agent/run-routes.ts +222 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +53 -0
- package/template/apps/server/src/agent/tool-host.test.ts +242 -0
- package/template/apps/server/src/agent/tool-host.ts +153 -0
- 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 -95
- 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 +54 -1
- package/template/apps/server/src/module-composition.ts +58 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +67 -0
- package/template/apps/server/src/module-host/initialize.test.ts +192 -0
- package/template/apps/server/src/module-host/initialize.ts +328 -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 +35 -113
- package/template/apps/server/src/module-runtime-validation.ts +40 -0
- 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/docker-compose.yml +25 -0
- package/template/docs/architecture/ai-runtime.md +744 -0
- package/template/docs/architecture/object-storage.md +12 -0
- package/template/docs/platform/agent-runtime.md +128 -0
- package/template/docs/platform/agent-tools.md +8 -1
- package/template/package.json +1 -0
- package/template/packages/config/index.test.ts +100 -0
- package/template/packages/config/index.ts +1 -0
- package/template/packages/config/package.json +2 -1
- package/template/packages/config/src/ai-openai-compatible.ts +131 -0
- 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 +53 -0
- package/template/packages/server-sdk/package.json +18 -3
- package/template/packages/server-sdk/src/actor.ts +15 -0
- package/template/packages/server-sdk/src/ai-operation-registration.ts +31 -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 +41 -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-host-port.ts +22 -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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ModuleInitializer } from "@southwind-ai/server-sdk";
|
|
3
|
+
import type { FileMetadata, UploadPolicy } from "@southwind-ai/storage";
|
|
4
|
+
import { createStorageRuntime } from "../storage/runtime.js";
|
|
5
|
+
import { initializeModuleHosts } from "./initialize.js";
|
|
6
|
+
import { createModuleStoragePort } from "./storage-port.js";
|
|
7
|
+
import { manifest } from "./test-fixtures.js";
|
|
8
|
+
|
|
9
|
+
const SHOP_POLICY = policy("shop.attachment");
|
|
10
|
+
const OTHER_POLICY = policy("other.attachment");
|
|
11
|
+
|
|
12
|
+
describe("模块受限 Upload/BlobRead Port", () => {
|
|
13
|
+
test("本模块用途可以创建上传会话,Owner 由宿主补齐", async () => {
|
|
14
|
+
const { port } = setup();
|
|
15
|
+
const session = await port.createUploadSession({
|
|
16
|
+
purpose: "shop.attachment",
|
|
17
|
+
filename: "现场照片.txt",
|
|
18
|
+
mimeType: "text/plain",
|
|
19
|
+
expectedByteSize: 3,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(session.ownerId).toBe("user_shop");
|
|
23
|
+
expect(session.purpose).toBe("shop.attachment");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("跨模块与平台用途创建会话被拒绝", async () => {
|
|
27
|
+
const { port } = setup();
|
|
28
|
+
|
|
29
|
+
await expect(
|
|
30
|
+
port.createUploadSession({
|
|
31
|
+
purpose: "other.attachment",
|
|
32
|
+
filename: "a.txt",
|
|
33
|
+
mimeType: "text/plain",
|
|
34
|
+
expectedByteSize: 3,
|
|
35
|
+
}),
|
|
36
|
+
).rejects.toThrow("上传用途不属于模块 shop:other.attachment");
|
|
37
|
+
|
|
38
|
+
await expect(
|
|
39
|
+
port.createUploadSession({
|
|
40
|
+
purpose: "bulk-data",
|
|
41
|
+
filename: "a.txt",
|
|
42
|
+
mimeType: "text/plain",
|
|
43
|
+
expectedByteSize: 3,
|
|
44
|
+
}),
|
|
45
|
+
).rejects.toThrow("上传用途不属于模块 shop:bulk-data");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("文件读取保持 Owner 约束", async () => {
|
|
49
|
+
const { storage, port } = setup();
|
|
50
|
+
const file = await seedFile(storage, "user_other", "shop.attachment");
|
|
51
|
+
|
|
52
|
+
await expect(port.openFile(file.id)).rejects.toThrow("无权访问该文件");
|
|
53
|
+
|
|
54
|
+
const ownerPort = createModuleStoragePort(
|
|
55
|
+
{ uploads: storage.uploads, files: storage.files },
|
|
56
|
+
{
|
|
57
|
+
moduleName: "shop",
|
|
58
|
+
ownerId: "user_other",
|
|
59
|
+
purposes: new Set(["shop.attachment"]),
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
const opened = await ownerPort.openFile(file.id);
|
|
63
|
+
expect(opened.id).toBe(file.id);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("Own 文件但用途越界时读取被拒绝", async () => {
|
|
67
|
+
const { storage, port } = setup();
|
|
68
|
+
const file = await seedFile(storage, "user_shop", "other.attachment");
|
|
69
|
+
|
|
70
|
+
await expect(port.openFile(file.id)).rejects.toThrow(
|
|
71
|
+
"上传用途不属于模块 shop:other.attachment",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("公开读取强制用途属于本模块", async () => {
|
|
76
|
+
const { storage, port } = setup();
|
|
77
|
+
const file = await seedFile(storage, "user_shop", "shop.attachment");
|
|
78
|
+
|
|
79
|
+
await expect(port.openPublicFile(file.id, "bulk-data")).rejects.toThrow(
|
|
80
|
+
"上传用途不属于模块 shop:bulk-data",
|
|
81
|
+
);
|
|
82
|
+
const opened = await port.openPublicFile(file.id, "shop.attachment");
|
|
83
|
+
expect(opened.id).toBe(file.id);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("命名空间重叠模块注册同一用途在启动时失败", async () => {
|
|
87
|
+
const initializer = (
|
|
88
|
+
moduleName: string,
|
|
89
|
+
register: ModuleInitializer["initialize"],
|
|
90
|
+
): ModuleInitializer => ({ moduleName, initialize: register });
|
|
91
|
+
|
|
92
|
+
await expect(
|
|
93
|
+
initializeModuleHosts({
|
|
94
|
+
modules: [manifest("shop"), manifest("shop.sub")],
|
|
95
|
+
initializers: [
|
|
96
|
+
initializer("shop", (host) => {
|
|
97
|
+
host.registerUploadPolicy(policy("shop.sub"));
|
|
98
|
+
}),
|
|
99
|
+
initializer("shop.sub", (host) => {
|
|
100
|
+
host.registerUploadPolicy(policy("shop.sub"));
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
).rejects.toThrow("上传用途重复注册:shop.sub");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
function setup() {
|
|
109
|
+
const storage = createStorageRuntime({}, undefined, [
|
|
110
|
+
SHOP_POLICY,
|
|
111
|
+
OTHER_POLICY,
|
|
112
|
+
]);
|
|
113
|
+
const port = createModuleStoragePort(
|
|
114
|
+
{ uploads: storage.uploads, files: storage.files },
|
|
115
|
+
{
|
|
116
|
+
moduleName: "shop",
|
|
117
|
+
ownerId: "user_shop",
|
|
118
|
+
purposes: new Set(["shop.attachment"]),
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
return { storage, port };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function policy(purpose: string): UploadPolicy {
|
|
125
|
+
return {
|
|
126
|
+
purpose,
|
|
127
|
+
maxByteSize: 1024,
|
|
128
|
+
maxPartByteSize: 512,
|
|
129
|
+
sessionTtlMs: 60_000,
|
|
130
|
+
allowedTypes: { "text/plain": ["txt"] },
|
|
131
|
+
signature: "none",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function seedFile(
|
|
136
|
+
storage: ReturnType<typeof createStorageRuntime>,
|
|
137
|
+
ownerId: string,
|
|
138
|
+
purpose: string,
|
|
139
|
+
): Promise<FileMetadata> {
|
|
140
|
+
const data = new TextEncoder().encode("abc");
|
|
141
|
+
const sha256 = new Bun.CryptoHasher("sha256").update(data).digest("hex");
|
|
142
|
+
const session = await storage.uploads.create({
|
|
143
|
+
ownerId,
|
|
144
|
+
purpose,
|
|
145
|
+
filename: "a.txt",
|
|
146
|
+
mimeType: "text/plain",
|
|
147
|
+
expectedByteSize: data.length,
|
|
148
|
+
expectedSha256: sha256,
|
|
149
|
+
});
|
|
150
|
+
await storage.uploads.appendPart({
|
|
151
|
+
sessionId: session.id,
|
|
152
|
+
ownerId,
|
|
153
|
+
partNumber: 0,
|
|
154
|
+
byteOffset: 0,
|
|
155
|
+
byteSize: data.length,
|
|
156
|
+
sha256,
|
|
157
|
+
body: new ReadableStream<Uint8Array>({
|
|
158
|
+
start(controller) {
|
|
159
|
+
controller.enqueue(data);
|
|
160
|
+
controller.close();
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
return storage.uploads.complete(session.id, ownerId);
|
|
165
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ModuleStoragePort } from "@southwind-ai/server-sdk";
|
|
2
|
+
import {
|
|
3
|
+
UploadError,
|
|
4
|
+
type FileService,
|
|
5
|
+
type UploadService,
|
|
6
|
+
} from "@southwind-ai/storage";
|
|
7
|
+
|
|
8
|
+
export interface ModuleStorageServices {
|
|
9
|
+
uploads: UploadService;
|
|
10
|
+
files: FileService;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ModuleStorageScope {
|
|
14
|
+
moduleName: string;
|
|
15
|
+
ownerId: string;
|
|
16
|
+
/** 本模块经 Host 注册的上传用途集合;跨模块与平台用途一律拒绝。 */
|
|
17
|
+
purposes: ReadonlySet<string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 模块受限 Upload / BlobRead Port。
|
|
22
|
+
*
|
|
23
|
+
* - 上传会话强制 purpose ∈ 本模块已注册 Policy,Owner 由宿主以当前 Actor 补齐;
|
|
24
|
+
* - 文件读取走 FileService 的 Owner 约束,且文件用途必须属于本模块;
|
|
25
|
+
* - 公开读取走 FileService 的 Purpose 约束,用途同样必须属于本模块;
|
|
26
|
+
* - 模块不直接接触 BlobStore 或 Upload Repository。
|
|
27
|
+
*/
|
|
28
|
+
export function createModuleStoragePort(
|
|
29
|
+
services: ModuleStorageServices,
|
|
30
|
+
scope: ModuleStorageScope,
|
|
31
|
+
): ModuleStoragePort {
|
|
32
|
+
return {
|
|
33
|
+
async createUploadSession(input) {
|
|
34
|
+
requireModulePurpose(scope, input.purpose);
|
|
35
|
+
return services.uploads.create({ ...input, ownerId: scope.ownerId });
|
|
36
|
+
},
|
|
37
|
+
async openFile(fileId) {
|
|
38
|
+
const file = await services.files.open(fileId, scope.ownerId);
|
|
39
|
+
requireModulePurpose(scope, file.purpose);
|
|
40
|
+
return file;
|
|
41
|
+
},
|
|
42
|
+
async openPublicFile(fileId, purpose) {
|
|
43
|
+
requireModulePurpose(scope, purpose);
|
|
44
|
+
return services.files.openPublic(fileId, purpose);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function requireModulePurpose(
|
|
50
|
+
scope: ModuleStorageScope,
|
|
51
|
+
purpose: string,
|
|
52
|
+
): void {
|
|
53
|
+
if (!scope.purposes.has(purpose)) {
|
|
54
|
+
throw new UploadError(
|
|
55
|
+
"UPLOAD_FORBIDDEN",
|
|
56
|
+
`上传用途不属于模块 ${scope.moduleName}:${purpose}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FeatureFlagDefinition } from "@southwind-ai/shared";
|
|
2
|
+
import type { ScheduledTaskDefinition } from "../scheduler/types.js";
|
|
3
|
+
import type { InitializedModuleHosts } from "./initialize.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 把模块经 Host 注册的定时任务 Handler 与 Manifest 声明合并为调度定义。
|
|
7
|
+
* 调度元数据一律以 Manifest 为准;声明了任务但没注册 Handler 的模块
|
|
8
|
+
* 会在最终 Composition 校验时报错。
|
|
9
|
+
*/
|
|
10
|
+
export function createModuleTaskDefinitions(
|
|
11
|
+
hosts: InitializedModuleHosts,
|
|
12
|
+
features: readonly FeatureFlagDefinition[],
|
|
13
|
+
): ScheduledTaskDefinition[] {
|
|
14
|
+
const handlerByKey = new Map(
|
|
15
|
+
hosts.taskHandlers.map(
|
|
16
|
+
({ registration }) => [registration.taskKey, registration] as const,
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
return hosts.manifests.flatMap((manifest) =>
|
|
20
|
+
manifest.tasks.flatMap((task) => {
|
|
21
|
+
const registration = handlerByKey.get(task.key);
|
|
22
|
+
if (!registration) return [];
|
|
23
|
+
const feature = features.find(({ key }) => key === task.featureKey);
|
|
24
|
+
if (!feature) throw new Error(`缺少 ${task.featureKey} Feature 定义`);
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
key: task.key,
|
|
28
|
+
label: task.label,
|
|
29
|
+
description: task.description,
|
|
30
|
+
intervalSeconds: task.intervalSeconds,
|
|
31
|
+
maxAttempts: task.maxAttempts,
|
|
32
|
+
retryDelaySeconds: task.retryDelaySeconds,
|
|
33
|
+
permissionKey: task.permissionKey,
|
|
34
|
+
feature,
|
|
35
|
+
handler: registration.handler,
|
|
36
|
+
isIdleResult: registration.isIdleResult,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
2
|
+
|
|
3
|
+
/** 模块 Host 测试共用的最小 Manifest 夹具。 */
|
|
4
|
+
export function manifest(
|
|
5
|
+
name: string,
|
|
6
|
+
overrides: Partial<ModuleManifest> = {},
|
|
7
|
+
): ModuleManifest {
|
|
8
|
+
return {
|
|
9
|
+
name,
|
|
10
|
+
version: "0.1.0",
|
|
11
|
+
label: name,
|
|
12
|
+
description: `${name} 模块`,
|
|
13
|
+
menus: [],
|
|
14
|
+
adminRoutes: [],
|
|
15
|
+
permissions: [],
|
|
16
|
+
apiPermissions: [],
|
|
17
|
+
features: [],
|
|
18
|
+
migrations: [],
|
|
19
|
+
schemaExports: [],
|
|
20
|
+
tasks: [],
|
|
21
|
+
tools: [],
|
|
22
|
+
providers: [],
|
|
23
|
+
auditActions: [],
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ModuleToolHandlers } from "../agent/routes.js";
|
|
2
|
+
import type { InitializedModuleHosts } from "./initialize.js";
|
|
3
|
+
import { toModuleActor } from "./request-context.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 把模块经 Host 注册的 Agent Tool Handler 适配为 Agent 路由层的
|
|
7
|
+
* Handler Map;注册与 Manifest 声明的一致性由 validateToolHandlerContract 校验。
|
|
8
|
+
*/
|
|
9
|
+
export function createModuleToolHandlerMap(
|
|
10
|
+
hosts: InitializedModuleHosts,
|
|
11
|
+
): ModuleToolHandlers {
|
|
12
|
+
return new Map(
|
|
13
|
+
hosts.toolHandlers.map(({ registration }) => [
|
|
14
|
+
registration.toolKey,
|
|
15
|
+
{
|
|
16
|
+
scopeMode: registration.scopeMode,
|
|
17
|
+
execute: (query, context) =>
|
|
18
|
+
registration.execute(query, {
|
|
19
|
+
actor: toModuleActor(context.actor),
|
|
20
|
+
requestId: context.requestId,
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
]),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
3
3
|
import type { ModuleInitializer } from "@southwind-ai/server-sdk";
|
|
4
4
|
// @windy-module work-order begin
|
|
5
|
-
import {
|
|
5
|
+
import { createWorkOrderModule } from "./example-modules.js";
|
|
6
6
|
// @windy-module work-order end
|
|
7
7
|
import { initializeModuleHosts } from "./module-host.js";
|
|
8
8
|
// @windy-module work-order begin
|
|
@@ -13,88 +13,177 @@ import {
|
|
|
13
13
|
// @windy-module work-order end
|
|
14
14
|
|
|
15
15
|
describe("模块 Host 初始化", () => {
|
|
16
|
-
test("
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
events.push("beta");
|
|
30
|
-
});
|
|
16
|
+
test("任务、工具、角色预设与 Provider 注册强制命名空间与 Manifest 声明", async () => {
|
|
17
|
+
await expect(
|
|
18
|
+
initializeModuleHosts({
|
|
19
|
+
modules: [manifest("known")],
|
|
20
|
+
initializers: [
|
|
21
|
+
initializer("known", (host) => {
|
|
22
|
+
host.registerTaskHandlers([
|
|
23
|
+
{ taskKey: "other.snapshot", handler: () => undefined },
|
|
24
|
+
]);
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
}),
|
|
28
|
+
).rejects.toThrow("定时任务不属于模块 known:other.snapshot");
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
await expect(
|
|
31
|
+
initializeModuleHosts({
|
|
32
|
+
modules: [manifest("known")],
|
|
33
|
+
initializers: [
|
|
34
|
+
initializer("known", (host) => {
|
|
35
|
+
host.registerTaskHandlers([
|
|
36
|
+
{ taskKey: "known.snapshot", handler: () => undefined },
|
|
37
|
+
]);
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
}),
|
|
41
|
+
).rejects.toThrow("定时任务 Handler 未在 Manifest 声明:known.snapshot");
|
|
42
|
+
|
|
43
|
+
const withTask = manifest("known", {
|
|
44
|
+
tasks: [
|
|
45
|
+
{
|
|
46
|
+
key: "known.snapshot",
|
|
47
|
+
label: "快照",
|
|
48
|
+
description: "",
|
|
49
|
+
permissionKey: "known.item.read",
|
|
50
|
+
featureKey: "known.feature",
|
|
51
|
+
intervalSeconds: 60,
|
|
52
|
+
maxAttempts: 3,
|
|
53
|
+
retryDelaySeconds: 10,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
35
56
|
});
|
|
36
|
-
expect(
|
|
57
|
+
await expect(
|
|
58
|
+
initializeModuleHosts({
|
|
59
|
+
modules: [withTask],
|
|
60
|
+
initializers: [
|
|
61
|
+
initializer("known", (host) => {
|
|
62
|
+
const registration = {
|
|
63
|
+
taskKey: "known.snapshot",
|
|
64
|
+
handler: () => undefined,
|
|
65
|
+
};
|
|
66
|
+
host.registerTaskHandlers([registration, registration]);
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
}),
|
|
70
|
+
).rejects.toThrow("定时任务 Handler 重复注册:known.snapshot");
|
|
37
71
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
72
|
+
await expect(
|
|
73
|
+
initializeModuleHosts({
|
|
74
|
+
modules: [manifest("known")],
|
|
75
|
+
initializers: [
|
|
76
|
+
initializer("known", (host) => {
|
|
77
|
+
host.registerToolHandlers([
|
|
78
|
+
{
|
|
79
|
+
toolKey: "other.list",
|
|
80
|
+
scopeMode: "platform",
|
|
81
|
+
execute: () => ({ items: [], total: 0, page: 1, pageSize: 10 }),
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
}),
|
|
85
|
+
],
|
|
86
|
+
}),
|
|
87
|
+
).rejects.toThrow("Agent Tool不属于模块 known:other.list");
|
|
42
88
|
|
|
43
|
-
test("未知模块、错误命名空间和重复注册立即失败", async () => {
|
|
44
89
|
await expect(
|
|
45
90
|
initializeModuleHosts({
|
|
46
|
-
modules: [
|
|
47
|
-
initializers: [
|
|
91
|
+
modules: [manifest("known")],
|
|
92
|
+
initializers: [
|
|
93
|
+
initializer("known", (host) => {
|
|
94
|
+
host.registerToolHandlers([
|
|
95
|
+
{
|
|
96
|
+
toolKey: "known.list",
|
|
97
|
+
scopeMode: "platform",
|
|
98
|
+
execute: () => ({ items: [], total: 0, page: 1, pageSize: 10 }),
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
48
103
|
}),
|
|
49
|
-
).rejects.toThrow("
|
|
104
|
+
).rejects.toThrow("Agent Tool Handler 未在 Manifest 声明:known.list");
|
|
50
105
|
|
|
51
106
|
await expect(
|
|
52
107
|
initializeModuleHosts({
|
|
53
|
-
modules: [
|
|
108
|
+
modules: [
|
|
109
|
+
manifest("known", {
|
|
110
|
+
permissions: [permission("known.item.read", "known")],
|
|
111
|
+
}),
|
|
112
|
+
],
|
|
54
113
|
initializers: [
|
|
55
114
|
initializer("known", (host) => {
|
|
56
|
-
host.
|
|
115
|
+
host.registerRolePresets([
|
|
116
|
+
{
|
|
117
|
+
key: "known.manager",
|
|
118
|
+
label: "管理员",
|
|
119
|
+
permissionKeys: ["other.item.read"],
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
57
122
|
}),
|
|
58
123
|
],
|
|
59
124
|
}),
|
|
60
|
-
).rejects.toThrow("
|
|
125
|
+
).rejects.toThrow("角色预设权限不属于模块 known:other.item.read");
|
|
61
126
|
|
|
62
127
|
await expect(
|
|
63
128
|
initializeModuleHosts({
|
|
64
|
-
modules: [
|
|
129
|
+
modules: [manifest("known")],
|
|
65
130
|
initializers: [
|
|
66
131
|
initializer("known", (host) => {
|
|
67
|
-
host.
|
|
68
|
-
|
|
132
|
+
host.registerRolePresets([
|
|
133
|
+
{
|
|
134
|
+
key: "known.manager",
|
|
135
|
+
label: "管理员",
|
|
136
|
+
permissionKeys: ["known.item.manage"],
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
69
139
|
}),
|
|
70
140
|
],
|
|
71
141
|
}),
|
|
72
|
-
).rejects.toThrow("
|
|
142
|
+
).rejects.toThrow("角色预设引用了模块 known 未声明权限:known.item.manage");
|
|
73
143
|
|
|
74
144
|
await expect(
|
|
75
145
|
initializeModuleHosts({
|
|
76
|
-
modules: [],
|
|
77
|
-
initializers: [
|
|
78
|
-
|
|
146
|
+
modules: [manifest("known")],
|
|
147
|
+
initializers: [
|
|
148
|
+
initializer("known", (host) => {
|
|
149
|
+
host.registerSearchProviders([
|
|
150
|
+
{
|
|
151
|
+
key: "other.search",
|
|
152
|
+
providerVersion: "0.1.0",
|
|
153
|
+
search: () => Promise.resolve({ hits: [] }),
|
|
154
|
+
health: () =>
|
|
155
|
+
Promise.resolve({
|
|
156
|
+
status: "healthy",
|
|
157
|
+
checkedAt: new Date().toISOString(),
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
}),
|
|
162
|
+
],
|
|
79
163
|
}),
|
|
80
|
-
).rejects.toThrow("
|
|
164
|
+
).rejects.toThrow("搜索 Provider不属于模块 known:other.search");
|
|
81
165
|
|
|
82
166
|
await expect(
|
|
83
167
|
initializeModuleHosts({
|
|
84
|
-
modules: [
|
|
168
|
+
modules: [manifest("known")],
|
|
85
169
|
initializers: [
|
|
86
170
|
initializer("known", (host) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
171
|
+
host.registerSearchProviders([
|
|
172
|
+
{
|
|
173
|
+
key: "known.search",
|
|
174
|
+
providerVersion: "0.1.0",
|
|
175
|
+
search: () => Promise.resolve({ hits: [] }),
|
|
176
|
+
health: () =>
|
|
177
|
+
Promise.resolve({
|
|
178
|
+
status: "healthy",
|
|
179
|
+
checkedAt: new Date().toISOString(),
|
|
180
|
+
}),
|
|
181
|
+
},
|
|
182
|
+
]);
|
|
94
183
|
}),
|
|
95
184
|
],
|
|
96
185
|
}),
|
|
97
|
-
).rejects.toThrow("
|
|
186
|
+
).rejects.toThrow("搜索 Provider 未在 Manifest 声明:known.search");
|
|
98
187
|
});
|
|
99
188
|
|
|
100
189
|
// @windy-module work-order begin
|
|
@@ -110,9 +199,10 @@ describe("模块 Host 初始化", () => {
|
|
|
110
199
|
),
|
|
111
200
|
).toBe(false);
|
|
112
201
|
|
|
202
|
+
const workOrder = createWorkOrderModule();
|
|
113
203
|
const withWorkOrder = await initializeModuleHosts({
|
|
114
|
-
modules: [
|
|
115
|
-
|
|
204
|
+
modules: [workOrder.manifest],
|
|
205
|
+
installations: [workOrder],
|
|
116
206
|
baseUploadPolicies: platformUploadPolicies,
|
|
117
207
|
});
|
|
118
208
|
const storage = createStorageRuntime(
|
|
@@ -129,6 +219,35 @@ describe("模块 Host 初始化", () => {
|
|
|
129
219
|
});
|
|
130
220
|
expect(session.purpose).toBe("work-order.ticket.attachment");
|
|
131
221
|
});
|
|
222
|
+
|
|
223
|
+
test("work-order 初始化器声明式注册路由、任务、Provider 与角色预设", async () => {
|
|
224
|
+
const workOrder = createWorkOrderModule();
|
|
225
|
+
const initialized = await initializeModuleHosts({
|
|
226
|
+
modules: [workOrder.manifest],
|
|
227
|
+
installations: [workOrder],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(
|
|
231
|
+
initialized.routes.map(
|
|
232
|
+
({ definition }) => `${definition.method} ${definition.path}`,
|
|
233
|
+
),
|
|
234
|
+
).toEqual([
|
|
235
|
+
"GET /api/work-orders",
|
|
236
|
+
"GET /api/work-orders/:id",
|
|
237
|
+
"POST /api/work-orders",
|
|
238
|
+
"PUT /api/work-orders/:id",
|
|
239
|
+
"DELETE /api/work-orders/:id",
|
|
240
|
+
]);
|
|
241
|
+
expect(
|
|
242
|
+
initialized.taskHandlers.map(({ registration }) => registration.taskKey),
|
|
243
|
+
).toEqual(["work-order.ticket.snapshot"]);
|
|
244
|
+
expect(
|
|
245
|
+
initialized.searchProviders.map(({ provider }) => provider.key),
|
|
246
|
+
).toEqual(["work-order.ticket.search"]);
|
|
247
|
+
expect(initialized.rolePresets.map(({ preset }) => preset.key)).toEqual([
|
|
248
|
+
"work-order.ticket-manager",
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
132
251
|
// @windy-module work-order end
|
|
133
252
|
});
|
|
134
253
|
|
|
@@ -139,13 +258,38 @@ function initializer(
|
|
|
139
258
|
return { moduleName, initialize };
|
|
140
259
|
}
|
|
141
260
|
|
|
142
|
-
function
|
|
261
|
+
function manifest(
|
|
262
|
+
name: string,
|
|
263
|
+
overrides: Partial<ModuleManifest> = {},
|
|
264
|
+
): ModuleManifest {
|
|
265
|
+
return {
|
|
266
|
+
name,
|
|
267
|
+
version: "0.1.0",
|
|
268
|
+
label: name,
|
|
269
|
+
description: `${name} 模块`,
|
|
270
|
+
menus: [],
|
|
271
|
+
adminRoutes: [],
|
|
272
|
+
permissions: [],
|
|
273
|
+
apiPermissions: [],
|
|
274
|
+
features: [],
|
|
275
|
+
migrations: [],
|
|
276
|
+
schemaExports: [],
|
|
277
|
+
tasks: [],
|
|
278
|
+
tools: [],
|
|
279
|
+
providers: [],
|
|
280
|
+
auditActions: [],
|
|
281
|
+
...overrides,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function permission(key: string, module: string) {
|
|
143
286
|
return {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
287
|
+
key,
|
|
288
|
+
label: key,
|
|
289
|
+
module,
|
|
290
|
+
moduleLabel: module,
|
|
291
|
+
resource: "item",
|
|
292
|
+
resourceLabel: "事项",
|
|
293
|
+
action: "read" as const,
|
|
150
294
|
};
|
|
151
295
|
}
|