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,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 { DurableHandlerRegistry } from "@southwind-ai/jobs";
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 { workOrderModuleInitializer } from "./example-modules.js";
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("按安装顺序完成初始化后再创建运行时注册表", async () => {
17
- const events: string[] = [];
18
- const first = initializer("alpha", async (host) => {
19
- events.push("alpha:start");
20
- await Promise.resolve();
21
- host.registerDurableHandler({
22
- key: "alpha.refresh",
23
- payload: { parse: () => ({}) },
24
- execute: () => undefined,
25
- });
26
- events.push("alpha:end");
27
- });
28
- const second = initializer("beta", () => {
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
- const initialized = await initializeModuleHosts({
33
- modules: [{ name: "alpha" }, { name: "beta" }],
34
- initializers: [first, second],
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(events).toEqual(["alpha:start", "alpha:end", "beta"]);
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
- const registry = new DurableHandlerRegistry();
39
- initialized.installDurableHandlers(registry);
40
- expect(registry.keys()).toEqual(["alpha.refresh"]);
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: [{ name: "known" }],
47
- initializers: [initializer("missing", () => undefined)],
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("模块初始化器与已安装模块不匹配:missing");
104
+ ).rejects.toThrow("Agent Tool Handler 未在 Manifest 声明:known.list");
50
105
 
51
106
  await expect(
52
107
  initializeModuleHosts({
53
- modules: [{ name: "known" }],
108
+ modules: [
109
+ manifest("known", {
110
+ permissions: [permission("known.item.read", "known")],
111
+ }),
112
+ ],
54
113
  initializers: [
55
114
  initializer("known", (host) => {
56
- host.registerUploadPolicy(policy("other.attachment"));
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("上传用途不属于模块 known:other.attachment");
125
+ ).rejects.toThrow("角色预设权限不属于模块 known:other.item.read");
61
126
 
62
127
  await expect(
63
128
  initializeModuleHosts({
64
- modules: [{ name: "known" }],
129
+ modules: [manifest("known")],
65
130
  initializers: [
66
131
  initializer("known", (host) => {
67
- host.registerUploadPolicy(policy("known.attachment"));
68
- host.registerUploadPolicy(policy("known.attachment"));
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("上传用途重复注册:known.attachment");
142
+ ).rejects.toThrow("角色预设引用了模块 known 未声明权限:known.item.manage");
73
143
 
74
144
  await expect(
75
145
  initializeModuleHosts({
76
- modules: [],
77
- initializers: [],
78
- baseUploadPolicies: [policy("platform.file"), policy("platform.file")],
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("上传用途重复注册:platform.file");
164
+ ).rejects.toThrow("搜索 Provider不属于模块 known:other.search");
81
165
 
82
166
  await expect(
83
167
  initializeModuleHosts({
84
- modules: [{ name: "known" }],
168
+ modules: [manifest("known")],
85
169
  initializers: [
86
170
  initializer("known", (host) => {
87
- const definition = {
88
- key: "known.refresh",
89
- payload: { parse: () => ({}) },
90
- execute: () => undefined,
91
- };
92
- host.registerDurableHandler(definition);
93
- host.registerDurableHandler(definition);
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("任务 handler 重复注册:known.refresh");
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: [{ name: "work-order" }],
115
- initializers: [workOrderModuleInitializer],
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 policy(purpose: string) {
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
- purpose,
145
- maxByteSize: 1024,
146
- maxPartByteSize: 512,
147
- sessionTtlMs: 60_000,
148
- allowedTypes: { "text/plain": ["txt"] },
149
- signature: "none" as const,
287
+ key,
288
+ label: key,
289
+ module,
290
+ moduleLabel: module,
291
+ resource: "item",
292
+ resourceLabel: "事项",
293
+ action: "read" as const,
150
294
  };
151
295
  }