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
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { DurableHandlerDefinition } from "@southwind-ai/jobs";
|
|
2
|
+
import type { ModuleAiOperationHandlerRegistration } from "./ai-operation-registration.js";
|
|
3
|
+
import type { ModuleRolePreset } from "./role-preset.js";
|
|
4
|
+
import type { ModuleRouteDefinition } from "./route-definition.js";
|
|
5
|
+
import type { ModuleSearchProvider } from "./search-provider-port.js";
|
|
6
|
+
import type { ModuleTaskHandlerRegistration } from "./task-registration.js";
|
|
7
|
+
import type { ModuleToolHandlerRegistration } from "./tool-registration.js";
|
|
2
8
|
|
|
3
9
|
export interface ModuleUploadPolicy {
|
|
4
10
|
purpose: string;
|
|
@@ -10,20 +16,54 @@ export interface ModuleUploadPolicy {
|
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
|
-
* 模块作用域 Host
|
|
19
|
+
* 模块作用域 Host。注册的所有 Key(上传用途、任务、工具、角色预设、Provider)
|
|
20
|
+
* 必须使用初始化器模块名作为命名空间,越界或重复注册在 Server 监听前抛错。
|
|
21
|
+
*
|
|
22
|
+
* 模块只获得受限 Port(Guarded Route、Actor 视图、Audit、Durable Submit、
|
|
23
|
+
* 受限 Upload/BlobRead);Worker 与 Repository 始终由宿主持有。
|
|
14
24
|
*/
|
|
15
25
|
export interface ModuleHost {
|
|
16
26
|
readonly moduleName: string;
|
|
17
27
|
registerUploadPolicy(policy: ModuleUploadPolicy): void;
|
|
18
28
|
registerDurableHandler<T>(definition: DurableHandlerDefinition<T>): void;
|
|
29
|
+
/** 注册 Guarded API 路由;须与 Manifest `apiPermissions` 声明一致。 */
|
|
30
|
+
registerRoutes(routes: readonly ModuleRouteDefinition[]): void;
|
|
31
|
+
/** 注册 Manifest `tasks` 已声明定时任务的执行体。 */
|
|
32
|
+
registerTaskHandlers(
|
|
33
|
+
registrations: readonly ModuleTaskHandlerRegistration[],
|
|
34
|
+
): void;
|
|
35
|
+
/** 注册 Manifest `tools` 已声明 Agent Tool 的执行体。 */
|
|
36
|
+
registerToolHandlers(
|
|
37
|
+
registrations: readonly ModuleToolHandlerRegistration[],
|
|
38
|
+
): void;
|
|
39
|
+
/** 注册 Manifest `aiOperations` 已声明的模型无关 Operation 执行计划。 */
|
|
40
|
+
registerAiOperationHandlers(
|
|
41
|
+
registrations: readonly ModuleAiOperationHandlerRegistration[],
|
|
42
|
+
): void;
|
|
43
|
+
/** 注册角色预设;permissionKeys 须属于本模块 Manifest 权限声明。 */
|
|
44
|
+
registerRolePresets(presets: readonly ModuleRolePreset[]): void;
|
|
45
|
+
/** 注册 Manifest `providers` 已声明的搜索 Provider。 */
|
|
46
|
+
registerSearchProviders(providers: readonly ModuleSearchProvider[]): void;
|
|
19
47
|
}
|
|
20
48
|
|
|
21
49
|
/**
|
|
22
50
|
* 已安装模块的 Server 初始化入口。
|
|
23
51
|
*
|
|
24
52
|
* 初始化器在 Storage 与 Durable Job 运行时创建前顺序执行;这里只注册定义,不启动 Worker。
|
|
53
|
+
* 路由、任务、工具与 Provider 的实现体可延迟绑定宿主运行时创建的服务。
|
|
25
54
|
*/
|
|
26
55
|
export interface ModuleInitializer {
|
|
27
56
|
readonly moduleName: string;
|
|
28
57
|
initialize(host: ModuleHost): Promise<void> | void;
|
|
29
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 可安装业务模块的完整 Server 契约。
|
|
62
|
+
*
|
|
63
|
+
* Manifest 与 setup 必须由同一安装单元贡献,避免宿主分别维护声明工厂和初始化器。
|
|
64
|
+
* TManifest 由能力目录包提供,SDK 因而不依赖具体 Manifest 实现。
|
|
65
|
+
*/
|
|
66
|
+
export interface InstallableServerModule<TManifest> {
|
|
67
|
+
readonly manifest: TManifest;
|
|
68
|
+
setup(host: ModuleHost): Promise<void> | void;
|
|
69
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { requireModuleNamespace } from "./namespace.js";
|
|
3
|
+
|
|
4
|
+
describe("模块命名空间约束", () => {
|
|
5
|
+
test("模块名自身或点分前缀视为模块内 Key", () => {
|
|
6
|
+
expect(() =>
|
|
7
|
+
requireModuleNamespace("任务", "work-order", "work-order"),
|
|
8
|
+
).not.toThrow();
|
|
9
|
+
expect(() =>
|
|
10
|
+
requireModuleNamespace(
|
|
11
|
+
"任务",
|
|
12
|
+
"work-order.ticket.snapshot",
|
|
13
|
+
"work-order",
|
|
14
|
+
),
|
|
15
|
+
).not.toThrow();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("越界 Key 抛出中文错误", () => {
|
|
19
|
+
expect(() =>
|
|
20
|
+
requireModuleNamespace("上传用途", "other.attachment", "work-order"),
|
|
21
|
+
).toThrow("上传用途不属于模块 work-order:other.attachment");
|
|
22
|
+
expect(() =>
|
|
23
|
+
requireModuleNamespace("角色预设", "work-orderx.role", "work-order"),
|
|
24
|
+
).toThrow("角色预设不属于模块 work-order:work-orderx.role");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块命名空间约束:注册的 Key 必须等于模块名,或以 `<moduleName>.` 开头。
|
|
3
|
+
* 宿主在所有注册入口统一调用,越界注册在 Server 监听前抛错。
|
|
4
|
+
*/
|
|
5
|
+
export function requireModuleNamespace(
|
|
6
|
+
kind: string,
|
|
7
|
+
value: string,
|
|
8
|
+
moduleName: string,
|
|
9
|
+
): void {
|
|
10
|
+
if (value !== moduleName && !value.startsWith(`${moduleName}.`)) {
|
|
11
|
+
throw new Error(`${kind}不属于模块 ${moduleName}:${value}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ModuleActor } from "./actor.js";
|
|
2
|
+
import type { ModuleAuditEmitter } from "./audit-port.js";
|
|
3
|
+
import type { ModuleJobSubmitter } from "./jobs-port.js";
|
|
4
|
+
import type { ModuleStoragePort } from "./storage-port.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 模块路由 Handler 的受限请求上下文。
|
|
8
|
+
*
|
|
9
|
+
* 不暴露 Elysia 内部对象、数据库连接或任何 Repository;
|
|
10
|
+
* 模块只能经由 Actor 视图与受限 Port(Audit / Jobs / Storage)与平台交互。
|
|
11
|
+
*/
|
|
12
|
+
export interface ModuleRequestContext {
|
|
13
|
+
readonly requestId: string;
|
|
14
|
+
readonly actor: ModuleActor;
|
|
15
|
+
readonly params: Record<string, string | undefined>;
|
|
16
|
+
readonly query: Record<string, string | undefined>;
|
|
17
|
+
readonly body?: unknown;
|
|
18
|
+
readonly audit: ModuleAuditEmitter;
|
|
19
|
+
readonly jobs: ModuleJobSubmitter;
|
|
20
|
+
readonly storage: ModuleStoragePort;
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块贡献的角色预设。
|
|
3
|
+
*
|
|
4
|
+
* `key` 作为角色 Code,必须使用模块命名空间;`permissionKeys` 必须全部属于
|
|
5
|
+
* 本模块 Manifest `permissions` 声明(宿主在启动时校验)。预设随内置角色一起
|
|
6
|
+
* 同步入库:不存在则创建,权限或菜单漂移则收敛,不改变 4 个内置角色语义。
|
|
7
|
+
*/
|
|
8
|
+
export interface ModuleRolePreset {
|
|
9
|
+
readonly key: string;
|
|
10
|
+
readonly label: string;
|
|
11
|
+
readonly description?: string;
|
|
12
|
+
readonly dataScope?:
|
|
13
|
+
| "all"
|
|
14
|
+
| "department"
|
|
15
|
+
| "department-and-children"
|
|
16
|
+
| "self";
|
|
17
|
+
readonly permissionKeys: readonly string[];
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ModuleRequestContext } from "./request-context.js";
|
|
2
|
+
|
|
3
|
+
export type ModuleRouteMethod = "GET" | "POST" | "PUT" | "DELETE";
|
|
4
|
+
|
|
5
|
+
export type ModuleRouteHandler = (
|
|
6
|
+
context: ModuleRequestContext,
|
|
7
|
+
) => Promise<unknown> | unknown;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 模块贡献的 API 路由定义。
|
|
11
|
+
*
|
|
12
|
+
* `method` + `path` 必须与本模块 Manifest `apiPermissions` 中的声明一致
|
|
13
|
+
* (path 含 `/api` 前缀);宿主据此绑定 Guard(权限 / Feature / License),
|
|
14
|
+
* 无需模块手工接线。`permissionKey` 必须与 Manifest 绑定一致并属于本模块命名空间。
|
|
15
|
+
*
|
|
16
|
+
* 声明 `auditAction` 时,宿主在 Handler 成功返回后自动写入一条该动作的审计事件;
|
|
17
|
+
* 动作必须是本模块 Manifest `auditActions` 已注册的动作。
|
|
18
|
+
*/
|
|
19
|
+
export interface ModuleRouteDefinition {
|
|
20
|
+
readonly method: ModuleRouteMethod;
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly permissionKey: string;
|
|
23
|
+
readonly featureKey?: string;
|
|
24
|
+
readonly auditAction?: string;
|
|
25
|
+
readonly handler: ModuleRouteHandler;
|
|
26
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ModuleActor } from "./actor.js";
|
|
2
|
+
|
|
3
|
+
export interface ModuleSearchQuery {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly limit: number;
|
|
6
|
+
readonly cursor?: string;
|
|
7
|
+
readonly signal: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ModuleSearchContext {
|
|
11
|
+
readonly actor: ModuleActor;
|
|
12
|
+
readonly requestId: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ModuleSearchHit {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly resourceType: string;
|
|
18
|
+
readonly maskedTitle: string;
|
|
19
|
+
readonly maskedExcerpt?: string;
|
|
20
|
+
readonly score?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ModuleSearchPage {
|
|
24
|
+
readonly hits: ModuleSearchHit[];
|
|
25
|
+
readonly nextCursor?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ModuleSearchHealth {
|
|
29
|
+
readonly status: "healthy" | "degraded" | "unavailable";
|
|
30
|
+
readonly checkedAt: string;
|
|
31
|
+
readonly message?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 模块搜索 Provider。
|
|
36
|
+
*
|
|
37
|
+
* `key` 必须在本模块 Manifest `providers` 中声明;返回的标题与摘要必须是
|
|
38
|
+
* 已脱敏文本,明文访问与导出由目标资源与治理导出走各自 Guard。
|
|
39
|
+
*/
|
|
40
|
+
export interface ModuleSearchProvider {
|
|
41
|
+
readonly key: string;
|
|
42
|
+
readonly providerVersion: string;
|
|
43
|
+
search(
|
|
44
|
+
query: ModuleSearchQuery,
|
|
45
|
+
context: ModuleSearchContext,
|
|
46
|
+
): Promise<ModuleSearchPage>;
|
|
47
|
+
health(signal: AbortSignal): Promise<ModuleSearchHealth>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { OpenedFile, UploadSession } from "@southwind-ai/storage";
|
|
2
|
+
|
|
3
|
+
export interface ModuleUploadSessionInput {
|
|
4
|
+
readonly purpose: string;
|
|
5
|
+
readonly filename: string;
|
|
6
|
+
readonly mimeType: string;
|
|
7
|
+
readonly expectedByteSize: number;
|
|
8
|
+
readonly expectedSha256?: string;
|
|
9
|
+
readonly fileExpiresAt?: Date;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 模块受限 Upload / BlobRead Port。
|
|
14
|
+
*
|
|
15
|
+
* - `createUploadSession` 的 purpose 必须是本模块经 Host 注册的 Upload Policy,
|
|
16
|
+
* 跨模块或平台用途一律拒绝;Owner 由宿主以当前 Actor 身份补齐。
|
|
17
|
+
* - `openFile` 走 FileService 的 Owner 约束,且文件用途必须属于本模块。
|
|
18
|
+
* - `openPublicFile` 走 FileService 的 Purpose 约束,用途同样必须属于本模块。
|
|
19
|
+
* - 模块不允许直接接触 BlobStore 或 Upload Repository。
|
|
20
|
+
*/
|
|
21
|
+
export interface ModuleStoragePort {
|
|
22
|
+
createUploadSession(input: ModuleUploadSessionInput): Promise<UploadSession>;
|
|
23
|
+
openFile(fileId: string): Promise<OpenedFile>;
|
|
24
|
+
openPublicFile(fileId: string, purpose: string): Promise<OpenedFile>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type ModuleTaskResult = Record<string, unknown> | void;
|
|
2
|
+
|
|
3
|
+
export type ModuleTaskHandler = () =>
|
|
4
|
+
| Promise<ModuleTaskResult>
|
|
5
|
+
| ModuleTaskResult;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 模块定时任务的 Handler 注册。
|
|
9
|
+
*
|
|
10
|
+
* `taskKey` 必须在本模块 Manifest `tasks` 中声明;调度元数据(间隔、重试、
|
|
11
|
+
* Feature、权限)一律以 Manifest 为准,这里只提供执行体。
|
|
12
|
+
*/
|
|
13
|
+
export interface ModuleTaskHandlerRegistration {
|
|
14
|
+
readonly taskKey: string;
|
|
15
|
+
readonly handler: ModuleTaskHandler;
|
|
16
|
+
/** 判断执行结果是否为空转(用于调度指标统计),与平台任务语义一致。 */
|
|
17
|
+
readonly isIdleResult?: (value: ModuleTaskResult) => boolean;
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModuleAiJsonValue } from "./ai-operation-registration.js";
|
|
2
|
+
|
|
3
|
+
export interface ModuleAgentToolInvocation {
|
|
4
|
+
readonly runId: string;
|
|
5
|
+
readonly toolCallId: string;
|
|
6
|
+
readonly toolKey: string;
|
|
7
|
+
readonly arguments: Readonly<Record<string, ModuleAiJsonValue>>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ModuleAgentToolResult {
|
|
11
|
+
readonly value: ModuleAiJsonValue;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 宿主创建并绑定 Operation、Actor 与 Scope 的受限 Tool Host Port。
|
|
16
|
+
* 调用方不能传 Permission、Feature、License 或数据范围作为授权事实。
|
|
17
|
+
*/
|
|
18
|
+
export interface ModuleAgentToolHostPort {
|
|
19
|
+
execute(
|
|
20
|
+
invocation: ModuleAgentToolInvocation,
|
|
21
|
+
): Promise<ModuleAgentToolResult>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ModuleActor } from "./actor.js";
|
|
2
|
+
|
|
3
|
+
export interface ModuleToolContext {
|
|
4
|
+
readonly actor: ModuleActor;
|
|
5
|
+
readonly requestId: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ModuleToolResult {
|
|
9
|
+
readonly items: unknown[];
|
|
10
|
+
readonly total: number;
|
|
11
|
+
readonly page: number;
|
|
12
|
+
readonly pageSize: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 模块 Agent Tool 的 Handler 注册。
|
|
17
|
+
*
|
|
18
|
+
* `toolKey` 必须在本模块 Manifest `tools` 中声明;Guard(权限 / Feature /
|
|
19
|
+
* 数据范围)由宿主在 Agent 路由层统一执行。`scopeMode: "request"` 的 Handler
|
|
20
|
+
* 只会在持有服务端数据范围上下文的 Actor 下执行。
|
|
21
|
+
*/
|
|
22
|
+
export interface ModuleToolHandlerRegistration {
|
|
23
|
+
readonly toolKey: string;
|
|
24
|
+
readonly scopeMode: "platform" | "request";
|
|
25
|
+
execute(
|
|
26
|
+
query: Record<string, string | undefined>,
|
|
27
|
+
context: ModuleToolContext,
|
|
28
|
+
): Promise<ModuleToolResult> | ModuleToolResult;
|
|
29
|
+
}
|
|
@@ -5,6 +5,7 @@ export * from "./src/feature.js";
|
|
|
5
5
|
export * from "./src/license.js";
|
|
6
6
|
export * from "./src/license-catalog.js";
|
|
7
7
|
export * from "./src/migration.js";
|
|
8
|
+
export * from "./src/migration-bundle-validation.js";
|
|
8
9
|
export * from "./src/module-configuration.js";
|
|
9
10
|
export * from "./src/correlation.js";
|
|
10
11
|
export * from "./src/notification.js";
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
LICENSE_CATALOG,
|
|
4
|
+
parseLicenseCatalog,
|
|
5
|
+
parseLicenseCatalogJson,
|
|
6
|
+
resolveLicenseCatalog,
|
|
7
|
+
validateLicenseCatalog,
|
|
8
|
+
type LicenseCatalogDefinition,
|
|
9
|
+
} from "./license-catalog.js";
|
|
10
|
+
import { licenseVersionKey } from "./license.js";
|
|
3
11
|
|
|
4
12
|
describe("License Catalog", () => {
|
|
5
13
|
test("提供稳定且有序的默认产品版本", () => {
|
|
@@ -14,4 +22,68 @@ describe("License Catalog", () => {
|
|
|
14
22
|
),
|
|
15
23
|
).toBe(true);
|
|
16
24
|
});
|
|
25
|
+
|
|
26
|
+
test("默认目录自身通过校验", () => {
|
|
27
|
+
expect(() => validateLicenseCatalog(LICENSE_CATALOG)).not.toThrow();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("重复 Key 或重复 order 时校验报错", () => {
|
|
31
|
+
const duplicatedKey: LicenseCatalogDefinition[] = [
|
|
32
|
+
entry("basic", 10),
|
|
33
|
+
entry("basic", 20),
|
|
34
|
+
];
|
|
35
|
+
const duplicatedOrder: LicenseCatalogDefinition[] = [
|
|
36
|
+
entry("basic", 10),
|
|
37
|
+
entry("pro", 10),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
expect(() => validateLicenseCatalog(duplicatedKey)).toThrow(
|
|
41
|
+
"License 版本目录 Key 重复:basic",
|
|
42
|
+
);
|
|
43
|
+
expect(() => validateLicenseCatalog(duplicatedOrder)).toThrow(
|
|
44
|
+
"License 版本目录 order 重复:10",
|
|
45
|
+
);
|
|
46
|
+
expect(() => validateLicenseCatalog([])).toThrow(
|
|
47
|
+
"License 版本目录不能为空",
|
|
48
|
+
);
|
|
49
|
+
expect(() => validateLicenseCatalog([entry("", 10)])).toThrow(
|
|
50
|
+
"License 版本目录的 Key 与名称不能为空",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("resolveLicenseCatalog 未配置时回退默认目录,配置后校验并返回", () => {
|
|
55
|
+
expect(resolveLicenseCatalog()).toBe(LICENSE_CATALOG);
|
|
56
|
+
|
|
57
|
+
const custom = [entry("oem-basic", 10), entry("oem-pro", 20)];
|
|
58
|
+
expect(resolveLicenseCatalog(custom)).toBe(custom);
|
|
59
|
+
expect(() =>
|
|
60
|
+
resolveLicenseCatalog([entry("dup", 1), entry("dup", 2)]),
|
|
61
|
+
).toThrow("License 版本目录 Key 重复:dup");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("共享解析器统一归一化字段并拒绝结构漂移", () => {
|
|
65
|
+
expect(
|
|
66
|
+
parseLicenseCatalog([{ key: " oem ", label: " OEM 版 ", order: 10 }]),
|
|
67
|
+
).toEqual([{ key: "oem", label: "OEM 版", description: "", order: 10 }]);
|
|
68
|
+
expect(() =>
|
|
69
|
+
parseLicenseCatalog([{ key: "oem", label: "OEM", order: 1.5 }]),
|
|
70
|
+
).toThrow("order 必须是安全整数");
|
|
71
|
+
expect(() =>
|
|
72
|
+
parseLicenseCatalog([
|
|
73
|
+
{ key: "oem", label: "OEM", order: 10, legacy: true },
|
|
74
|
+
]),
|
|
75
|
+
).toThrow("License 版本目录包含未知字段:legacy");
|
|
76
|
+
expect(() => parseLicenseCatalogJson("invalid", "宿主目录")).toThrow(
|
|
77
|
+
"宿主目录 必须是合法 JSON 数组",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
17
80
|
});
|
|
81
|
+
|
|
82
|
+
function entry(key: string, order: number): LicenseCatalogDefinition {
|
|
83
|
+
return {
|
|
84
|
+
key: licenseVersionKey(key),
|
|
85
|
+
label: key || "占位",
|
|
86
|
+
description: `${key} 版本`,
|
|
87
|
+
order,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -7,6 +7,8 @@ export interface LicenseCatalogDefinition {
|
|
|
7
7
|
order: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const licenseCatalogFields = new Set(["key", "label", "description", "order"]);
|
|
11
|
+
|
|
10
12
|
export const LICENSE_CATALOG = [
|
|
11
13
|
{
|
|
12
14
|
key: licenseVersionKey("free-trial"),
|
|
@@ -31,3 +33,96 @@ export const LICENSE_CATALOG = [
|
|
|
31
33
|
export function licenseCatalogKeys(): ReadonlySet<LicenseVersionKey> {
|
|
32
34
|
return new Set(LICENSE_CATALOG.map(({ key }) => key));
|
|
33
35
|
}
|
|
36
|
+
|
|
37
|
+
/** 校验宿主配置的 License 版本目录:Key 与 order 均唯一,目录非空。 */
|
|
38
|
+
export function validateLicenseCatalog(
|
|
39
|
+
definitions: readonly LicenseCatalogDefinition[],
|
|
40
|
+
): void {
|
|
41
|
+
if (definitions.length === 0) {
|
|
42
|
+
throw new Error("License 版本目录不能为空");
|
|
43
|
+
}
|
|
44
|
+
const keys = new Set<string>();
|
|
45
|
+
const orders = new Set<number>();
|
|
46
|
+
for (const definition of definitions) {
|
|
47
|
+
if (!definition.key.trim() || !definition.label.trim()) {
|
|
48
|
+
throw new Error("License 版本目录的 Key 与名称不能为空");
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isSafeInteger(definition.order)) {
|
|
51
|
+
throw new Error(`License 版本 ${definition.key} 的 order 必须是安全整数`);
|
|
52
|
+
}
|
|
53
|
+
if (keys.has(definition.key)) {
|
|
54
|
+
throw new Error(`License 版本目录 Key 重复:${definition.key}`);
|
|
55
|
+
}
|
|
56
|
+
if (orders.has(definition.order)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`License 版本目录 order 重复:${definition.order}(${definition.key})`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
keys.add(definition.key);
|
|
62
|
+
orders.add(definition.order);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 从未知输入解析并归一化宿主 License 版本目录。 */
|
|
67
|
+
export function parseLicenseCatalog(
|
|
68
|
+
input: unknown,
|
|
69
|
+
): LicenseCatalogDefinition[] {
|
|
70
|
+
if (!Array.isArray(input)) {
|
|
71
|
+
throw new Error("License 版本目录必须是数组");
|
|
72
|
+
}
|
|
73
|
+
const catalog = input.map((entry, index) => {
|
|
74
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
75
|
+
throw new Error(`License 版本目录第 ${index + 1} 项必须是对象`);
|
|
76
|
+
}
|
|
77
|
+
const record = entry as Record<string, unknown>;
|
|
78
|
+
const unknownField = Object.keys(record).find(
|
|
79
|
+
(key) => !licenseCatalogFields.has(key),
|
|
80
|
+
);
|
|
81
|
+
if (unknownField) {
|
|
82
|
+
throw new Error(`License 版本目录包含未知字段:${unknownField}`);
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
typeof record.key !== "string" ||
|
|
86
|
+
typeof record.label !== "string" ||
|
|
87
|
+
(record.description !== undefined &&
|
|
88
|
+
typeof record.description !== "string") ||
|
|
89
|
+
typeof record.order !== "number"
|
|
90
|
+
) {
|
|
91
|
+
throw new Error(`License 版本目录第 ${index + 1} 项字段类型无效`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
key: licenseVersionKey(record.key.trim()),
|
|
95
|
+
label: record.label.trim(),
|
|
96
|
+
description: record.description?.trim() ?? "",
|
|
97
|
+
order: record.order,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
validateLicenseCatalog(catalog);
|
|
101
|
+
return catalog;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 解析环境变量 JSON,错误消息不包含原始配置内容。 */
|
|
105
|
+
export function parseLicenseCatalogJson(
|
|
106
|
+
raw: string,
|
|
107
|
+
source = "License 版本目录",
|
|
108
|
+
): LicenseCatalogDefinition[] {
|
|
109
|
+
let parsed: unknown;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(raw);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`${source} 必须是合法 JSON 数组`);
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(parsed)) {
|
|
116
|
+
throw new Error(`${source} 必须是合法 JSON 数组`);
|
|
117
|
+
}
|
|
118
|
+
return parseLicenseCatalog(parsed);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 有宿主配置时校验并返回,未配置时回退平台默认三级目录。 */
|
|
122
|
+
export function resolveLicenseCatalog(
|
|
123
|
+
configured?: readonly LicenseCatalogDefinition[],
|
|
124
|
+
): readonly LicenseCatalogDefinition[] {
|
|
125
|
+
if (!configured) return LICENSE_CATALOG;
|
|
126
|
+
validateLicenseCatalog(configured);
|
|
127
|
+
return configured;
|
|
128
|
+
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type MigrationBundle,
|
|
5
5
|
type MigrationDefinition,
|
|
6
6
|
type MigrationManifestSource,
|
|
7
|
-
} from "
|
|
7
|
+
} from "./migration.js";
|
|
8
8
|
|
|
9
9
|
export function normalizeManifestBundles(
|
|
10
10
|
modules: readonly MigrationManifestSource[],
|
|
@@ -126,6 +126,7 @@ export function validateAndOrderBundles(
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
validateMigrationDependencies(bundle, migrationOwner);
|
|
129
|
+
orderBundleMigrations(bundle);
|
|
129
130
|
}
|
|
130
131
|
return topologicalBundles(byModule);
|
|
131
132
|
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { workOrderModule } from "@southwind-ai/example-work-order";
|
|
2
|
-
import type { FeatureFlagDefinition } from "@southwind-ai/shared";
|
|
3
|
-
import type { ScheduledTaskDefinition } from "../scheduler/types.js";
|
|
4
|
-
import type { WorkOrderService } from "./service.js";
|
|
5
|
-
import { platformDataActor } from "../data-access/platform-actor.js";
|
|
6
|
-
|
|
7
|
-
export function createWorkOrderTaskDefinitions(
|
|
8
|
-
features: FeatureFlagDefinition[],
|
|
9
|
-
service: WorkOrderService,
|
|
10
|
-
): ScheduledTaskDefinition[] {
|
|
11
|
-
const manifest = workOrderModule();
|
|
12
|
-
const feature = features.find(({ key }) => key === "work-order.ticket");
|
|
13
|
-
if (!feature) return [];
|
|
14
|
-
const actor = platformDataActor(
|
|
15
|
-
"task_work_order_snapshot",
|
|
16
|
-
"工单运营快照任务",
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
return manifest.tasks.map((task) => ({
|
|
20
|
-
key: task.key,
|
|
21
|
-
label: task.label,
|
|
22
|
-
description: task.description,
|
|
23
|
-
intervalSeconds: task.intervalSeconds,
|
|
24
|
-
maxAttempts: task.maxAttempts,
|
|
25
|
-
retryDelaySeconds: task.retryDelaySeconds,
|
|
26
|
-
permissionKey: task.permissionKey,
|
|
27
|
-
feature,
|
|
28
|
-
handler: () => service.operationalSnapshot(actor),
|
|
29
|
-
}));
|
|
30
|
-
}
|