create-windy 0.3.18 → 0.4.0
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 +28 -13
- package/dist/cli.js +788 -280
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +34 -10
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +18 -8
- package/template/README.md +32 -22
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug.py +38 -5
- package/template/apps/agent-server/tests/test_adk_debug_result_polling.py +139 -0
- package/template/apps/server/Dockerfile +1 -0
- package/template/apps/server/package.json +1 -0
- package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
- package/template/apps/server/scripts/validate-module-composition.ts +28 -0
- package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
- package/template/apps/server/src/agent/routes.test.ts +20 -7
- package/template/apps/server/src/agent/routes.ts +14 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
- package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
- package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
- package/template/apps/server/src/app/server-composition.ts +56 -0
- package/template/apps/server/src/application-services.ts +2 -0
- package/template/apps/server/src/bulk-data/service.ts +14 -0
- package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
- package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
- package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
- package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
- package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
- package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
- package/template/apps/server/src/feature/routes.ts +33 -15
- package/template/apps/server/src/foundation-activation.test.ts +60 -0
- package/template/apps/server/src/foundation.ts +64 -5
- package/template/apps/server/src/guards.test.ts +23 -0
- package/template/apps/server/src/guards.ts +12 -0
- package/template/apps/server/src/index.ts +28 -34
- package/template/apps/server/src/installed-business-modules.ts +13 -24
- package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
- package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
- package/template/apps/server/src/license/activation.test.ts +30 -0
- package/template/apps/server/src/license/activation.ts +26 -0
- package/template/apps/server/src/license/restriction.test.ts +16 -0
- package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
- package/template/apps/server/src/license/routes.ts +20 -4
- package/template/apps/server/src/license/runtime-service.test.ts +10 -2
- package/template/apps/server/src/module-bootstrap.ts +14 -23
- package/template/apps/server/src/module-composition.test.ts +43 -2
- package/template/apps/server/src/module-composition.ts +29 -0
- package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
- package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
- package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
- package/template/apps/server/src/module-host/durable-context.ts +42 -2
- package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
- package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
- package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
- package/template/apps/server/src/module-host/initialize.test.ts +78 -2
- package/template/apps/server/src/module-host/initialize.ts +15 -4
- package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
- package/template/apps/server/src/module-host/project-attachments.ts +58 -0
- package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
- package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
- package/template/apps/server/src/module-host/project-composition.ts +311 -0
- package/template/apps/server/src/module-host/request-context.ts +3 -0
- package/template/apps/server/src/module-host/route-declaration.ts +15 -6
- package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
- package/template/apps/server/src/module-host/route-installer.ts +12 -2
- package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
- package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
- package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
- package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
- package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
- package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
- package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
- package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
- package/template/apps/server/src/module-host/task-definitions.ts +1 -0
- package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
- package/template/apps/server/src/module-host.test.ts +2 -0
- package/template/apps/server/src/module-preflight.test.ts +289 -0
- package/template/apps/server/src/module-preflight.ts +96 -0
- package/template/apps/server/src/module-runtime-validation.ts +1 -0
- package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
- package/template/apps/server/src/platform-http-runtime.ts +32 -7
- package/template/apps/server/src/route-guards.test.ts +22 -0
- package/template/apps/server/src/route-guards.ts +10 -3
- package/template/apps/server/src/runtime-audit-query.ts +50 -0
- package/template/apps/server/src/runtime-feature.test.ts +46 -0
- package/template/apps/server/src/runtime-feature.ts +5 -0
- package/template/apps/server/src/runtime.ts +10 -49
- package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
- package/template/apps/server/src/scheduler/audit.ts +1 -1
- package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
- package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
- package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
- package/template/apps/server/src/scheduler/service.test.ts +1 -0
- package/template/apps/server/src/scheduler/service.ts +18 -21
- package/template/apps/server/src/scheduler/types.ts +5 -1
- package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
- package/template/apps/server/src/startup-composition-validation.ts +54 -0
- package/template/apps/server/src/startup-preflight.test.ts +36 -0
- package/template/apps/server/src/startup-preflight.ts +37 -0
- package/template/apps/server/src/system/audit-routes.test.ts +9 -3
- package/template/apps/server/src/system/department-routes.test.ts +5 -3
- package/template/apps/server/src/system/feature-routes.test.ts +4 -3
- package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
- package/template/apps/server/src/system/interface-catalog.ts +3 -0
- package/template/apps/server/src/system/menu-routes.test.ts +5 -3
- package/template/apps/server/src/system/route-test-runtime.ts +24 -0
- package/template/apps/server/src/system/routes-validation.test.ts +7 -3
- package/template/apps/server/src/system/routes.test.ts +21 -11
- package/template/apps/server/src/system/routes.ts +11 -2
- package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
- package/template/apps/server/src/work-order/routes.test.ts +8 -0
- package/template/apps/server/src/work-order/task.test.ts +6 -1
- package/template/apps/web/Dockerfile +5 -0
- package/template/apps/web/package.json +1 -0
- package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
- package/template/apps/web/src/app/web-composition.ts +24 -0
- package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
- package/template/apps/web/src/layout/AppSidebar.vue +8 -1
- package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
- package/template/apps/web/src/layout/navigation.ts +29 -4
- package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
- package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
- package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
- package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
- package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
- package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
- package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
- package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
- package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
- package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
- package/template/apps/web/src/router/business-routes.ts +32 -45
- package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
- package/template/apps/web/src/router/index.ts +64 -54
- package/template/apps/web/src/router/manifest-routes.ts +27 -7
- package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
- package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
- package/template/apps/web/src/router/web-composition-host.ts +20 -0
- package/template/apps/web/src/services/work-order-api.ts +16 -0
- package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
- package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
- package/template/apps/web/vite.config.ts +22 -1
- package/template/docs/architecture/migration-bundle.md +10 -7
- package/template/docs/architecture/module-manifest-composition.md +85 -3
- package/template/docs/architecture/search-provider-protocol.md +6 -1
- package/template/docs/architecture/server-module-interface.md +70 -7
- package/template/docs/development/business-web-routes.md +53 -50
- package/template/docs/development/module-lifecycle.md +48 -54
- package/template/docs/platform/agent-runtime.md +2 -0
- package/template/docs/platform/navigation-registry.md +5 -4
- package/template/package.json +1 -1
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/package.json +1 -1
- package/template/packages/example-work-order/package.json +6 -0
- package/template/packages/example-work-order/src/admin-page.ts +78 -0
- package/template/packages/example-work-order/src/admin.ts +35 -0
- package/template/packages/example-work-order/src/manifest.test.ts +62 -5
- package/template/packages/example-work-order/src/manifest.ts +54 -7
- package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
- package/template/packages/example-work-order/src/server-module.ts +2 -2
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/index.ts +4 -0
- package/template/packages/modules/package.json +3 -2
- package/template/packages/modules/src/admin-ui-entry.ts +8 -0
- package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
- package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
- package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
- package/template/packages/modules/src/capability-audit-validation.ts +38 -0
- package/template/packages/modules/src/capability-guard-validation.ts +148 -0
- package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
- package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
- package/template/packages/modules/src/composition.test.ts +74 -9
- package/template/packages/modules/src/composition.ts +8 -0
- package/template/packages/modules/src/manifest.ts +1 -0
- package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
- package/template/packages/modules/src/module-host-range.test.ts +57 -0
- package/template/packages/modules/src/module-host-range.ts +125 -0
- package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
- package/template/packages/modules/src/project-module-assembly.ts +126 -0
- package/template/packages/modules/src/system-admin-routes.ts +6 -5
- package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
- package/template/packages/modules/src/system-agent-tools.ts +2 -2
- package/template/packages/modules/src/system-api-binding.ts +43 -9
- package/template/packages/modules/src/system-api-control-plane.ts +202 -0
- package/template/packages/modules/src/system-api-data-governance.ts +4 -2
- package/template/packages/modules/src/system-api-operational.ts +57 -8
- package/template/packages/modules/src/system-api-permissions.ts +115 -21
- package/template/packages/modules/src/system-api-watermark.ts +3 -0
- package/template/packages/modules/src/system-audit-actions.ts +7 -0
- package/template/packages/modules/src/system-data-governance.ts +5 -2
- package/template/packages/modules/src/system-features.ts +31 -0
- package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
- package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
- package/template/packages/modules/src/system-module-catalog.ts +68 -7
- package/template/packages/modules/src/system-module-tasks.ts +8 -1
- package/template/packages/modules/src/system-modules-access.test.ts +4 -0
- package/template/packages/modules/src/system-modules.test.ts +10 -2
- package/template/packages/modules/src/system-modules.ts +6 -5
- package/template/packages/modules/src/system-permissions.ts +4 -0
- package/template/packages/modules/user/module.ts +19 -2
- package/template/packages/server-sdk/index.ts +8 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/durable-handler.ts +16 -1
- package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
- package/template/packages/server-sdk/src/module-adapter.ts +59 -0
- package/template/packages/server-sdk/src/route-definition.ts +3 -2
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/audit.test.ts +7 -0
- package/template/packages/shared/src/audit.ts +16 -0
- package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
- package/template/packages/shared/src/rbac.ts +2 -0
- package/template/apps/web/src/app/business-routes.ts +0 -22
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ModuleDurableHandlerContext,
|
|
12
12
|
} from "@southwind-ai/server-sdk";
|
|
13
13
|
import { ModuleBackgroundAiError } from "@southwind-ai/server-sdk";
|
|
14
|
+
import { evaluateAccessGuards } from "../access-guards.js";
|
|
14
15
|
import {
|
|
15
16
|
captureAuthorizationScope,
|
|
16
17
|
hashAuthorizationScope,
|
|
@@ -48,6 +49,7 @@ export function createModuleDurableContextualizer(
|
|
|
48
49
|
claim: {
|
|
49
50
|
job: {
|
|
50
51
|
id: string;
|
|
52
|
+
handlerKey: string;
|
|
51
53
|
requestedBy: string;
|
|
52
54
|
authorizationScope?: AuthorizationScopeSnapshot;
|
|
53
55
|
};
|
|
@@ -76,6 +78,39 @@ export function createModuleDurableContextualizer(
|
|
|
76
78
|
request,
|
|
77
79
|
actor,
|
|
78
80
|
);
|
|
81
|
+
const binding = services.hosts.durableHandlerOf(
|
|
82
|
+
input.moduleName,
|
|
83
|
+
input.claim.job.handlerKey,
|
|
84
|
+
);
|
|
85
|
+
if (!binding) {
|
|
86
|
+
throw denied("MODULE_DURABLE_CAPABILITY_MISSING", {
|
|
87
|
+
handlerKey: input.claim.job.handlerKey,
|
|
88
|
+
moduleName: input.moduleName,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const feature = services.hosts
|
|
92
|
+
.manifestOf(input.moduleName)
|
|
93
|
+
.features.find(({ key }) => key === binding.featureKey);
|
|
94
|
+
if (!feature) {
|
|
95
|
+
throw denied("MODULE_DURABLE_CAPABILITY_MISSING", {
|
|
96
|
+
featureKey: binding.featureKey,
|
|
97
|
+
handlerKey: binding.key,
|
|
98
|
+
moduleName: input.moduleName,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const access = await evaluateAccessGuards(services.runtime, guardContext, {
|
|
102
|
+
permission: { permissionKey: binding.permissionKey },
|
|
103
|
+
feature,
|
|
104
|
+
});
|
|
105
|
+
if (!access.decision.allowed) {
|
|
106
|
+
throw denied("MODULE_DURABLE_GUARD_DENIED", {
|
|
107
|
+
auditAction: binding.auditAction,
|
|
108
|
+
featureKey: binding.featureKey,
|
|
109
|
+
handlerKey: binding.key,
|
|
110
|
+
permissionKey: binding.permissionKey,
|
|
111
|
+
reason: access.decision.reason,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
79
114
|
const storage = services.storage
|
|
80
115
|
? createModuleStoragePort(
|
|
81
116
|
services.storage,
|
|
@@ -121,6 +156,11 @@ function unavailableBackgroundAi(moduleName: string): ModuleBackgroundAiPort {
|
|
|
121
156
|
};
|
|
122
157
|
}
|
|
123
158
|
|
|
124
|
-
function denied(code: string) {
|
|
125
|
-
return new DurableHandlerError(
|
|
159
|
+
function denied(code: string, details?: Record<string, unknown>) {
|
|
160
|
+
return new DurableHandlerError(
|
|
161
|
+
code,
|
|
162
|
+
"模块后台任务授权已失效",
|
|
163
|
+
false,
|
|
164
|
+
details,
|
|
165
|
+
);
|
|
126
166
|
}
|
|
@@ -4,30 +4,39 @@ import {
|
|
|
4
4
|
type DurableHandlerDefinition,
|
|
5
5
|
type DurableHandlerRegistry,
|
|
6
6
|
} from "@southwind-ai/jobs";
|
|
7
|
+
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
7
8
|
import type {
|
|
8
9
|
ModuleDurableHandlerContext,
|
|
9
10
|
ModuleDurableHandlerDefinition,
|
|
10
11
|
} from "@southwind-ai/server-sdk";
|
|
11
12
|
import { ModuleBackgroundAiError } from "@southwind-ai/server-sdk";
|
|
13
|
+
import type { RegisteredModuleDurableHandler } from "./initialize-contracts.js";
|
|
12
14
|
|
|
13
15
|
export interface DurableHandlerRegistrationState {
|
|
14
16
|
readonly handlerKeys: Set<string>;
|
|
15
17
|
readonly handlerInstallers: Array<(registry: DurableHandlerRegistry) => void>;
|
|
18
|
+
readonly durableHandlers: RegisteredModuleDurableHandler[];
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export function registerModuleDurableHandler<T>(
|
|
19
|
-
|
|
22
|
+
manifest: ModuleManifest,
|
|
20
23
|
state: DurableHandlerRegistrationState,
|
|
21
24
|
definition: ModuleDurableHandlerDefinition<T>,
|
|
22
25
|
) {
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
const moduleName = manifest.name;
|
|
27
|
+
const binding = durableHandlerBinding(definition);
|
|
28
|
+
validateDurableHandlerCapability(manifest, binding);
|
|
29
|
+
if (state.handlerKeys.has(binding.key)) {
|
|
30
|
+
throw new Error(`任务 handler 重复注册:${binding.key}`);
|
|
25
31
|
}
|
|
26
|
-
state.handlerKeys.add(
|
|
32
|
+
state.handlerKeys.add(binding.key);
|
|
33
|
+
state.durableHandlers.push(
|
|
34
|
+
Object.freeze({ moduleName, definition: binding }),
|
|
35
|
+
);
|
|
27
36
|
state.handlerInstallers.push((registry) => {
|
|
28
37
|
registry.register(
|
|
29
38
|
{
|
|
30
|
-
key:
|
|
39
|
+
key: binding.key,
|
|
31
40
|
payload: definition.payload,
|
|
32
41
|
async execute(payload, context) {
|
|
33
42
|
try {
|
|
@@ -52,6 +61,88 @@ export function registerModuleDurableHandler<T>(
|
|
|
52
61
|
});
|
|
53
62
|
}
|
|
54
63
|
|
|
64
|
+
function validateDurableHandlerCapability(
|
|
65
|
+
manifest: ModuleManifest,
|
|
66
|
+
definition: DurableHandlerBinding,
|
|
67
|
+
): void {
|
|
68
|
+
if (!manifest.features.some(({ key }) => key === definition.featureKey)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`任务 handler Feature 未在 Manifest 注册:${definition.featureKey}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
!manifest.permissions.some(({ key }) => key === definition.permissionKey)
|
|
75
|
+
) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`任务 handler Permission 未在 Manifest 注册:${definition.permissionKey}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
!manifest.auditActions.some((action) =>
|
|
82
|
+
typeof action === "string"
|
|
83
|
+
? action === definition.auditAction
|
|
84
|
+
: action.key === definition.auditAction,
|
|
85
|
+
)
|
|
86
|
+
) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`任务 handler Audit Action 未在 Manifest 注册:${definition.auditAction}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
!manifestCapabilities(manifest).some((capability) =>
|
|
93
|
+
sameCapability(capability, definition),
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`任务 handler 未绑定 Manifest 已声明能力:${definition.key}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface ManifestCapability {
|
|
103
|
+
readonly featureKey?: string;
|
|
104
|
+
readonly permissionKey: string;
|
|
105
|
+
readonly auditAction?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type DurableHandlerBinding = Pick<
|
|
109
|
+
ModuleDurableHandlerDefinition<unknown>,
|
|
110
|
+
"key" | "featureKey" | "permissionKey" | "auditAction"
|
|
111
|
+
>;
|
|
112
|
+
|
|
113
|
+
function durableHandlerBinding(
|
|
114
|
+
definition: DurableHandlerBinding,
|
|
115
|
+
): DurableHandlerBinding {
|
|
116
|
+
return Object.freeze({
|
|
117
|
+
key: definition.key,
|
|
118
|
+
featureKey: definition.featureKey,
|
|
119
|
+
permissionKey: definition.permissionKey,
|
|
120
|
+
auditAction: definition.auditAction,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function manifestCapabilities(
|
|
125
|
+
manifest: ModuleManifest,
|
|
126
|
+
): readonly ManifestCapability[] {
|
|
127
|
+
return [
|
|
128
|
+
...manifest.apiPermissions,
|
|
129
|
+
...manifest.tasks,
|
|
130
|
+
...manifest.tools,
|
|
131
|
+
...(manifest.aiOperations ?? []),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function sameCapability(
|
|
136
|
+
registered: ManifestCapability,
|
|
137
|
+
definition: DurableHandlerBinding,
|
|
138
|
+
): boolean {
|
|
139
|
+
return (
|
|
140
|
+
registered.featureKey === definition.featureKey &&
|
|
141
|
+
registered.permissionKey === definition.permissionKey &&
|
|
142
|
+
registered.auditAction === definition.auditAction
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
55
146
|
function requireModuleDurableContext(
|
|
56
147
|
context: Parameters<DurableHandlerDefinition<unknown>["execute"]>[1],
|
|
57
148
|
moduleName: string,
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ModuleHost } from "@southwind-ai/server-sdk";
|
|
3
|
+
import { featureKey } from "@southwind-ai/shared";
|
|
4
|
+
import { initializeModuleHosts } from "./initialize.js";
|
|
5
|
+
import {
|
|
6
|
+
governedResourceRouteKeys,
|
|
7
|
+
validateHostedModuleImplementations,
|
|
8
|
+
} from "./implementation-completeness.js";
|
|
9
|
+
import { manifest } from "./test-fixtures.js";
|
|
10
|
+
|
|
11
|
+
describe("Server Module 实现完整性", () => {
|
|
12
|
+
test("Manifest 声明的 API、任务和 Provider 必须由同一安装单元实现", async () => {
|
|
13
|
+
const complete = completeRouteModule();
|
|
14
|
+
const completeHosts = await initializeModuleHosts({
|
|
15
|
+
modules: [complete.manifest],
|
|
16
|
+
installations: [complete],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(() =>
|
|
20
|
+
validateHostedModuleImplementations([complete], completeHosts),
|
|
21
|
+
).not.toThrow();
|
|
22
|
+
|
|
23
|
+
const incomplete = {
|
|
24
|
+
manifest: complete.manifest,
|
|
25
|
+
setup() {},
|
|
26
|
+
};
|
|
27
|
+
const incompleteHosts = await initializeModuleHosts({
|
|
28
|
+
modules: [incomplete.manifest],
|
|
29
|
+
installations: [incomplete],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(() =>
|
|
33
|
+
validateHostedModuleImplementations([incomplete], incompleteHosts),
|
|
34
|
+
).toThrow("API Route GET /api/catalog/items 缺少 Handler");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("治理资源注册派生读取、明文和四个导出 Handler Key", () => {
|
|
38
|
+
expect(
|
|
39
|
+
governedResourceRouteKeys({
|
|
40
|
+
readPath: "/api/people/persons/:id",
|
|
41
|
+
revealPath: "/api/people/persons/:id/plaintext",
|
|
42
|
+
export: {
|
|
43
|
+
basePath: "/api/people/persons/exports",
|
|
44
|
+
validateQuery: () => ({}),
|
|
45
|
+
readPage: () => ({ records: [] }),
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
).toEqual([
|
|
49
|
+
"GET /api/people/persons/:id",
|
|
50
|
+
"POST /api/people/persons/:id/plaintext",
|
|
51
|
+
"POST /api/people/persons/exports",
|
|
52
|
+
"GET /api/people/persons/exports/:id",
|
|
53
|
+
"POST /api/people/persons/exports/:id/cancel",
|
|
54
|
+
"GET /api/people/persons/exports/:id/download",
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("Durable Handler 的 Host 声明与 Registry 安装必须双向一致", async () => {
|
|
59
|
+
const capabilityKey = "reports.refresh";
|
|
60
|
+
const permissionKey = "reports.refresh.execute";
|
|
61
|
+
const moduleManifest = manifest("reports", {
|
|
62
|
+
features: [
|
|
63
|
+
{
|
|
64
|
+
key: featureKey(capabilityKey),
|
|
65
|
+
label: "报表刷新",
|
|
66
|
+
module: "reports",
|
|
67
|
+
visible: "visible",
|
|
68
|
+
enabled: true,
|
|
69
|
+
stage: "stable",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
permissions: [
|
|
73
|
+
{
|
|
74
|
+
key: permissionKey,
|
|
75
|
+
label: "执行报表刷新",
|
|
76
|
+
module: "reports",
|
|
77
|
+
moduleLabel: "报表",
|
|
78
|
+
resource: "refresh",
|
|
79
|
+
resourceLabel: "刷新",
|
|
80
|
+
action: "execute",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
tasks: [
|
|
84
|
+
{
|
|
85
|
+
key: capabilityKey,
|
|
86
|
+
label: "报表刷新",
|
|
87
|
+
description: "刷新报表。",
|
|
88
|
+
featureKey: capabilityKey,
|
|
89
|
+
permissionKey,
|
|
90
|
+
auditAction: permissionKey,
|
|
91
|
+
intervalSeconds: 60,
|
|
92
|
+
maxAttempts: 3,
|
|
93
|
+
retryDelaySeconds: 5,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
auditActions: [permissionKey],
|
|
97
|
+
});
|
|
98
|
+
const installation = {
|
|
99
|
+
manifest: moduleManifest,
|
|
100
|
+
setup(host: ModuleHost) {
|
|
101
|
+
moduleSetup(host);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const hosts = await initializeModuleHosts({
|
|
105
|
+
modules: [moduleManifest],
|
|
106
|
+
installations: [installation],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(() =>
|
|
110
|
+
validateHostedModuleImplementations([installation], hosts),
|
|
111
|
+
).not.toThrow();
|
|
112
|
+
expect(() =>
|
|
113
|
+
validateHostedModuleImplementations([installation], {
|
|
114
|
+
...hosts,
|
|
115
|
+
installDurableHandlers() {},
|
|
116
|
+
}),
|
|
117
|
+
).toThrow("Durable Handler reports:reports.refresh 缺少 Handler");
|
|
118
|
+
|
|
119
|
+
function moduleSetup(host: ModuleHost) {
|
|
120
|
+
host.registerTaskHandlers([
|
|
121
|
+
{ taskKey: capabilityKey, handler: () => undefined },
|
|
122
|
+
]);
|
|
123
|
+
host.registerDurableHandler({
|
|
124
|
+
key: capabilityKey,
|
|
125
|
+
featureKey: capabilityKey,
|
|
126
|
+
permissionKey,
|
|
127
|
+
auditAction: permissionKey,
|
|
128
|
+
payload: { parse: () => ({}) },
|
|
129
|
+
execute: () => undefined,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
function completeRouteModule() {
|
|
136
|
+
const capabilityKey = "catalog.item";
|
|
137
|
+
const permissionKey = "catalog.item.read";
|
|
138
|
+
const moduleManifest = manifest("catalog", {
|
|
139
|
+
features: [
|
|
140
|
+
{
|
|
141
|
+
key: featureKey(capabilityKey),
|
|
142
|
+
label: "目录项",
|
|
143
|
+
module: "catalog",
|
|
144
|
+
visible: "visible",
|
|
145
|
+
enabled: true,
|
|
146
|
+
stage: "stable",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
permissions: [
|
|
150
|
+
{
|
|
151
|
+
key: permissionKey,
|
|
152
|
+
label: "查看目录",
|
|
153
|
+
module: "catalog",
|
|
154
|
+
moduleLabel: "目录",
|
|
155
|
+
resource: "item",
|
|
156
|
+
resourceLabel: "目录项",
|
|
157
|
+
action: "read",
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
apiPermissions: [
|
|
161
|
+
{
|
|
162
|
+
method: "GET",
|
|
163
|
+
path: "/api/catalog/items",
|
|
164
|
+
permissionKey,
|
|
165
|
+
featureKey: capabilityKey,
|
|
166
|
+
auditAction: "data.read",
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
auditActions: ["data.read"],
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
manifest: moduleManifest,
|
|
173
|
+
setup(host: ModuleHost) {
|
|
174
|
+
host.registerRoutes([
|
|
175
|
+
{
|
|
176
|
+
method: "GET",
|
|
177
|
+
path: "/api/catalog/items",
|
|
178
|
+
permissionKey,
|
|
179
|
+
featureKey: capabilityKey,
|
|
180
|
+
handler: () => ({ items: [] }),
|
|
181
|
+
},
|
|
182
|
+
]);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { DurableHandlerRegistry } from "@southwind-ai/jobs";
|
|
2
|
+
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
3
|
+
import type {
|
|
4
|
+
InstallableServerModule,
|
|
5
|
+
ModuleGovernedResourceRegistration,
|
|
6
|
+
} from "@southwind-ai/server-sdk";
|
|
7
|
+
import type { InitializedModuleHosts } from "./initialize-contracts.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 校验经 Module Host 安装的业务模块是否实现了 Manifest 中所有 Server 能力。
|
|
11
|
+
*
|
|
12
|
+
* Admin Route 页面由 Web 构建门禁校验;这里覆盖 API、任务、Tool、AI Operation
|
|
13
|
+
* 与 Provider,且只检查 Host 安装单元,不把平台静态实现伪装成业务 Handler。
|
|
14
|
+
*/
|
|
15
|
+
export function validateHostedModuleImplementations(
|
|
16
|
+
installations: readonly InstallableServerModule<ModuleManifest>[],
|
|
17
|
+
hosts: InitializedModuleHosts,
|
|
18
|
+
): void {
|
|
19
|
+
for (const installation of installations) {
|
|
20
|
+
const moduleName = installation.manifest.name;
|
|
21
|
+
assertComplete(
|
|
22
|
+
"API Route",
|
|
23
|
+
installation.manifest.apiPermissions.map(
|
|
24
|
+
({ method, path }) => `${method.toUpperCase()} ${path}`,
|
|
25
|
+
),
|
|
26
|
+
hosts.routes
|
|
27
|
+
.filter((entry) => entry.moduleName === moduleName)
|
|
28
|
+
.map(
|
|
29
|
+
({ definition }) =>
|
|
30
|
+
`${definition.method.toUpperCase()} ${definition.path}`,
|
|
31
|
+
)
|
|
32
|
+
.concat(
|
|
33
|
+
hosts.governedResources
|
|
34
|
+
.filter((entry) => entry.moduleName === moduleName)
|
|
35
|
+
.flatMap(({ registration }) =>
|
|
36
|
+
governedResourceRouteKeys(registration),
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
assertComplete(
|
|
41
|
+
"定时任务",
|
|
42
|
+
installation.manifest.tasks.map(({ key }) => key),
|
|
43
|
+
hosts.taskHandlers
|
|
44
|
+
.filter((entry) => entry.moduleName === moduleName)
|
|
45
|
+
.map(({ registration }) => registration.taskKey),
|
|
46
|
+
);
|
|
47
|
+
assertComplete(
|
|
48
|
+
"Agent Tool",
|
|
49
|
+
installation.manifest.tools.map(({ key }) => key),
|
|
50
|
+
[...hosts.toolHandlers, ...hosts.sandboxToolHandlers]
|
|
51
|
+
.filter((entry) => entry.moduleName === moduleName)
|
|
52
|
+
.map(({ registration }) => registration.toolKey),
|
|
53
|
+
);
|
|
54
|
+
assertComplete(
|
|
55
|
+
"AI Operation",
|
|
56
|
+
(installation.manifest.aiOperations ?? []).map(({ key }) => key),
|
|
57
|
+
hosts.aiOperationHandlers
|
|
58
|
+
.filter((entry) => entry.moduleName === moduleName)
|
|
59
|
+
.map(({ registration }) => registration.operationKey),
|
|
60
|
+
);
|
|
61
|
+
assertComplete(
|
|
62
|
+
"Provider",
|
|
63
|
+
installation.manifest.providers.map(({ key }) => key),
|
|
64
|
+
hosts.searchProviders
|
|
65
|
+
.filter((entry) => entry.moduleName === moduleName)
|
|
66
|
+
.map(({ provider }) => provider.key),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
validateDurableHandlerInstallations(hosts);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function governedResourceRouteKeys(
|
|
73
|
+
registration: Pick<
|
|
74
|
+
ModuleGovernedResourceRegistration,
|
|
75
|
+
"readPath" | "revealPath" | "export"
|
|
76
|
+
>,
|
|
77
|
+
): string[] {
|
|
78
|
+
return [
|
|
79
|
+
`GET ${registration.readPath}`,
|
|
80
|
+
`POST ${registration.revealPath}`,
|
|
81
|
+
...(registration.export
|
|
82
|
+
? [
|
|
83
|
+
`POST ${registration.export.basePath}`,
|
|
84
|
+
`GET ${registration.export.basePath}/:id`,
|
|
85
|
+
`POST ${registration.export.basePath}/:id/cancel`,
|
|
86
|
+
`GET ${registration.export.basePath}/:id/download`,
|
|
87
|
+
]
|
|
88
|
+
: []),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertComplete(
|
|
93
|
+
label: string,
|
|
94
|
+
declared: readonly string[],
|
|
95
|
+
registered: readonly string[],
|
|
96
|
+
): void {
|
|
97
|
+
const implementations = new Set(registered);
|
|
98
|
+
for (const key of declared) {
|
|
99
|
+
if (!implementations.has(key)) {
|
|
100
|
+
throw new Error(`${label} ${key} 缺少 Handler`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateDurableHandlerInstallations(
|
|
106
|
+
hosts: InitializedModuleHosts,
|
|
107
|
+
): void {
|
|
108
|
+
const registry = new DurableHandlerRegistry();
|
|
109
|
+
hosts.installDurableHandlers(registry);
|
|
110
|
+
const hosted = hosts.durableHandlers.map(
|
|
111
|
+
({ moduleName, definition }) => `${moduleName}:${definition.key}`,
|
|
112
|
+
);
|
|
113
|
+
const installed = registry.keys().map((key) => {
|
|
114
|
+
const handler = registry.get(key);
|
|
115
|
+
return `${handler?.moduleName ?? ""}:${key}`;
|
|
116
|
+
});
|
|
117
|
+
assertSameRegistrations("Durable Handler", hosted, installed);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function assertSameRegistrations(
|
|
121
|
+
label: string,
|
|
122
|
+
hosted: readonly string[],
|
|
123
|
+
installed: readonly string[],
|
|
124
|
+
): void {
|
|
125
|
+
assertComplete(label, hosted, installed);
|
|
126
|
+
const declarations = new Set(hosted);
|
|
127
|
+
for (const key of installed) {
|
|
128
|
+
if (!declarations.has(key)) {
|
|
129
|
+
throw new Error(`${label} ${key} 未在 Host 注册`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { ModuleManifest } from "@southwind-ai/modules";
|
|
|
3
3
|
import type {
|
|
4
4
|
InstallableServerModule,
|
|
5
5
|
ModuleAiOperationHandlerRegistration,
|
|
6
|
+
ModuleDurableHandlerDefinition,
|
|
6
7
|
ModuleInitializer,
|
|
7
8
|
ModuleGovernedResourceRegistration,
|
|
8
9
|
ModuleRolePreset,
|
|
@@ -40,6 +41,14 @@ export interface RegisteredModuleAiOperationHandler {
|
|
|
40
41
|
readonly registration: ModuleAiOperationHandlerRegistration;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
export interface RegisteredModuleDurableHandler {
|
|
45
|
+
readonly moduleName: string;
|
|
46
|
+
readonly definition: Pick<
|
|
47
|
+
ModuleDurableHandlerDefinition<unknown>,
|
|
48
|
+
"key" | "featureKey" | "permissionKey" | "auditAction"
|
|
49
|
+
>;
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
export interface RegisteredModuleGovernedResource {
|
|
44
53
|
readonly moduleName: string;
|
|
45
54
|
readonly registration: ModuleGovernedResourceRegistration;
|
|
@@ -73,6 +82,7 @@ export interface InitializedModuleHosts {
|
|
|
73
82
|
readonly toolHandlers: readonly RegisteredModuleToolHandler[];
|
|
74
83
|
readonly sandboxToolHandlers: readonly RegisteredModuleSandboxToolHandler[];
|
|
75
84
|
readonly aiOperationHandlers: readonly RegisteredModuleAiOperationHandler[];
|
|
85
|
+
readonly durableHandlers: readonly RegisteredModuleDurableHandler[];
|
|
76
86
|
readonly governedResources: readonly RegisteredModuleGovernedResource[];
|
|
77
87
|
readonly rolePresets: readonly RegisteredModuleRolePreset[];
|
|
78
88
|
readonly searchProviders: readonly RegisteredModuleSearchProvider[];
|
|
@@ -83,5 +93,9 @@ export interface InitializedModuleHosts {
|
|
|
83
93
|
resourceFileAccessPoliciesOf(
|
|
84
94
|
moduleName: string,
|
|
85
95
|
): readonly ModuleResourceFileAccessPolicy[];
|
|
96
|
+
durableHandlerOf(
|
|
97
|
+
moduleName: string,
|
|
98
|
+
handlerKey: string,
|
|
99
|
+
): RegisteredModuleDurableHandler["definition"] | undefined;
|
|
86
100
|
installDurableHandlers(registry: DurableHandlerRegistry): void;
|
|
87
101
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { DurableHandlerRegistry } from "@southwind-ai/jobs";
|
|
3
|
+
import { featureKey } from "@southwind-ai/shared";
|
|
3
4
|
import type {
|
|
4
5
|
ModuleInitializer,
|
|
5
6
|
ModuleSandboxToolHost,
|
|
@@ -15,6 +16,9 @@ describe("模块 Host 初始化", () => {
|
|
|
15
16
|
await Promise.resolve();
|
|
16
17
|
host.registerDurableHandler({
|
|
17
18
|
key: "alpha.refresh",
|
|
19
|
+
featureKey: "alpha.refresh",
|
|
20
|
+
permissionKey: "alpha.refresh.execute",
|
|
21
|
+
auditAction: "alpha.refresh.execute",
|
|
18
22
|
payload: { parse: () => ({}) },
|
|
19
23
|
execute: () => undefined,
|
|
20
24
|
});
|
|
@@ -25,7 +29,7 @@ describe("模块 Host 初始化", () => {
|
|
|
25
29
|
});
|
|
26
30
|
|
|
27
31
|
const initialized = await initializeModuleHosts({
|
|
28
|
-
modules: [
|
|
32
|
+
modules: [durableManifest("alpha"), manifest("beta")],
|
|
29
33
|
initializers: [first, second],
|
|
30
34
|
});
|
|
31
35
|
expect(events).toEqual(["alpha:start", "alpha:end", "beta"]);
|
|
@@ -33,6 +37,13 @@ describe("模块 Host 初始化", () => {
|
|
|
33
37
|
const registry = new DurableHandlerRegistry();
|
|
34
38
|
initialized.installDurableHandlers(registry);
|
|
35
39
|
expect(registry.keys()).toEqual(["alpha.refresh"]);
|
|
40
|
+
expect(
|
|
41
|
+
initialized.durableHandlerOf("alpha", "alpha.refresh"),
|
|
42
|
+
).toMatchObject({
|
|
43
|
+
featureKey: "alpha.refresh",
|
|
44
|
+
permissionKey: "alpha.refresh.execute",
|
|
45
|
+
auditAction: "alpha.refresh.execute",
|
|
46
|
+
});
|
|
36
47
|
});
|
|
37
48
|
|
|
38
49
|
test("未知模块、错误命名空间和重复注册立即失败", async () => {
|
|
@@ -76,11 +87,14 @@ describe("模块 Host 初始化", () => {
|
|
|
76
87
|
|
|
77
88
|
await expect(
|
|
78
89
|
initializeModuleHosts({
|
|
79
|
-
modules: [
|
|
90
|
+
modules: [durableManifest("known")],
|
|
80
91
|
initializers: [
|
|
81
92
|
initializer("known", (host) => {
|
|
82
93
|
const definition = {
|
|
83
94
|
key: "known.refresh",
|
|
95
|
+
featureKey: "known.refresh",
|
|
96
|
+
permissionKey: "known.refresh.execute",
|
|
97
|
+
auditAction: "known.refresh.execute",
|
|
84
98
|
payload: { parse: () => ({}) },
|
|
85
99
|
execute: () => undefined,
|
|
86
100
|
};
|
|
@@ -92,6 +106,26 @@ describe("模块 Host 初始化", () => {
|
|
|
92
106
|
).rejects.toThrow("任务 handler 重复注册:known.refresh");
|
|
93
107
|
});
|
|
94
108
|
|
|
109
|
+
test("Durable Handler 必须绑定 Manifest 已声明能力", async () => {
|
|
110
|
+
await expect(
|
|
111
|
+
initializeModuleHosts({
|
|
112
|
+
modules: [durableManifest("known")],
|
|
113
|
+
initializers: [
|
|
114
|
+
initializer("known", (host) => {
|
|
115
|
+
host.registerDurableHandler({
|
|
116
|
+
key: "known.refresh",
|
|
117
|
+
featureKey: "known.missing",
|
|
118
|
+
permissionKey: "known.refresh.execute",
|
|
119
|
+
auditAction: "known.refresh.execute",
|
|
120
|
+
payload: { parse: () => ({}) },
|
|
121
|
+
execute: () => undefined,
|
|
122
|
+
});
|
|
123
|
+
}),
|
|
124
|
+
],
|
|
125
|
+
}),
|
|
126
|
+
).rejects.toThrow("任务 handler Feature 未在 Manifest 注册:known.missing");
|
|
127
|
+
});
|
|
128
|
+
|
|
95
129
|
test("安装单元的 Manifest 必须来自同一平台能力目录", async () => {
|
|
96
130
|
await expect(
|
|
97
131
|
initializeModuleHosts({
|
|
@@ -238,3 +272,45 @@ function policy(purpose: string) {
|
|
|
238
272
|
signature: "none" as const,
|
|
239
273
|
};
|
|
240
274
|
}
|
|
275
|
+
|
|
276
|
+
function durableManifest(name: string) {
|
|
277
|
+
const capabilityKey = `${name}.refresh`;
|
|
278
|
+
const permissionKey = `${capabilityKey}.execute`;
|
|
279
|
+
return manifest(name, {
|
|
280
|
+
features: [
|
|
281
|
+
{
|
|
282
|
+
key: featureKey(capabilityKey),
|
|
283
|
+
label: "刷新",
|
|
284
|
+
module: name,
|
|
285
|
+
visible: "visible",
|
|
286
|
+
enabled: true,
|
|
287
|
+
stage: "stable",
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
permissions: [
|
|
291
|
+
{
|
|
292
|
+
key: permissionKey,
|
|
293
|
+
label: "执行刷新",
|
|
294
|
+
module: name,
|
|
295
|
+
moduleLabel: name,
|
|
296
|
+
resource: "refresh",
|
|
297
|
+
resourceLabel: "刷新",
|
|
298
|
+
action: "execute",
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
tasks: [
|
|
302
|
+
{
|
|
303
|
+
key: capabilityKey,
|
|
304
|
+
label: "刷新",
|
|
305
|
+
description: "刷新模块数据。",
|
|
306
|
+
featureKey: capabilityKey,
|
|
307
|
+
permissionKey,
|
|
308
|
+
auditAction: permissionKey,
|
|
309
|
+
intervalSeconds: 60,
|
|
310
|
+
maxAttempts: 3,
|
|
311
|
+
retryDelaySeconds: 5,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
auditActions: [permissionKey],
|
|
315
|
+
});
|
|
316
|
+
}
|