create-windy 0.9.1 → 0.10.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/dist/cli.js +1769 -1277
- package/package.json +2 -2
- package/template/.agents/skills/windy-business-module/SKILL.md +33 -1
- package/template/.windy-template.json +6 -2
- package/template/AGENTS.md +4 -0
- package/template/README.md +2 -0
- package/template/apps/agent-server/Dockerfile +1 -1
- package/template/apps/agent-server/README.md +1 -1
- package/template/apps/agent-server/native/EXTENSIONS.md +2 -2
- package/template/apps/agent-server/native/parallel-harness.ts +2 -0
- package/template/apps/agent-server/native/storage-lifecycle.test.ts +2 -1
- package/template/apps/agent-server/package.json +42 -41
- package/template/apps/agent-server/plugins/downloads/README.md +1 -1
- package/template/apps/agent-server/plugins/drop-overlay/README.md +1 -1
- package/template/apps/agent-server/plugins/settings/README.md +1 -1
- package/template/apps/agent-server/plugins/skill-library/client-chunks.test.ts +4 -1
- package/template/apps/agent-server/plugins/theme/README.md +5 -1
- package/template/apps/agent-server/plugins/theme/source-tokens.ts +0 -1
- package/template/apps/agent-server/plugins/theme/tokens.json +0 -4
- package/template/apps/agent-server/plugins/tool-catalog/README.md +6 -3
- package/template/apps/agent-server/plugins/tool-catalog/discovery.mjs +13 -10
- package/template/apps/agent-server/plugins/tool-catalog/discovery.test.ts +68 -6
- package/template/apps/agent-server/runtime/compatibility.ts +7 -1
- package/template/apps/agent-server/runtime/dsh-engine.ts +2 -0
- package/template/apps/agent-server/runtime/questions-workflow.nodecheck.ts +2 -0
- package/template/apps/server/Dockerfile +2 -1
- package/template/apps/server/package.json +11 -11
- package/template/apps/server/scripts/validate-module-composition-once.ts +2 -0
- package/template/apps/server/scripts/validate-module-composition.ts +8 -4
- package/template/apps/server/src/admin/routes.test.ts +29 -1
- package/template/apps/server/src/admin/routes.ts +20 -2
- package/template/apps/server/src/agent/client-session/completion.ts +1 -1
- package/template/apps/server/src/agent/client-session/manifest-admission.ts +1 -1
- package/template/apps/server/src/agent/client-session/request.ts +1 -1
- package/template/apps/server/src/agent/context-version-routes.ts +37 -13
- package/template/apps/server/src/agent/run-facade.ts +1 -1
- package/template/apps/server/src/agent/run-input-verification.ts +1 -1
- package/template/apps/server/src/agent/run-routes.ts +1 -14
- package/template/apps/server/src/agent/run-start-preparation.ts +1 -1
- package/template/apps/server/src/agent/runtime-bootstrap-input.ts +2 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +49 -157
- package/template/apps/server/src/agent/runtime-disabled-composition.ts +2 -1
- package/template/apps/server/src/agent/session-invalidation/authorization-revocation.ts +54 -0
- package/template/apps/server/src/agent/session-runtime-composition.ts +1 -51
- package/template/apps/server/src/agent/unavailable-routes.ts +13 -2
- package/template/apps/server/src/agent-workflow/runtime-composition.ts +3 -0
- package/template/apps/server/src/agent-workflow/service.ts +17 -1
- package/template/apps/server/src/ai-artifact/media-resource-route.test.ts +134 -0
- package/template/apps/server/src/ai-artifact/routes.ts +1 -1
- package/template/apps/server/src/ai-artifact/runtime.ts +12 -0
- package/template/apps/server/src/ai-runtime-readiness/operation-port.test-support.ts +1 -1
- package/template/apps/server/src/ai-runtime-readiness/operation-port.ts +8 -0
- package/template/apps/server/src/ai-runtime-security/internal-token.ts +14 -0
- package/template/apps/server/src/app/antivirus-composition.test.ts +63 -0
- package/template/apps/server/src/app/antivirus-composition.ts +29 -0
- package/template/apps/server/src/app/dsh-workbench-socket-audit.test.ts +2 -1
- package/template/apps/server/src/configuration/definition.ts +15 -11
- package/template/apps/server/src/document/facade-support.ts +2 -2
- package/template/apps/server/src/document/facade.ts +2 -2
- package/template/apps/server/src/document/output-artifact-composition.ts +9 -5
- package/template/apps/server/src/document/output-artifact.test-support.ts +1 -1
- package/template/apps/server/src/embedding/facade.ts +1 -1
- package/template/apps/server/src/embedding/input.ts +1 -1
- package/template/apps/server/src/embedding/routes.ts +1 -1
- package/template/apps/server/src/index.ts +60 -60
- package/template/apps/server/src/license/routes.ts +4 -1
- package/template/apps/server/src/license/runtime-business-access.test.ts +261 -0
- package/template/apps/server/src/media-runtime/composition.ts +157 -0
- package/template/apps/server/src/media-runtime/media-only-composition.ts +44 -0
- package/template/apps/server/src/media-runtime/module-ai-port-factory.test.ts +69 -0
- package/template/apps/server/src/media-runtime/module-ai-port-factory.ts +72 -0
- package/template/apps/server/src/{agent → media-runtime}/runtime-readiness-sources.ts +14 -6
- package/template/apps/server/src/media-runtime/shared-composition.ts +104 -0
- package/template/apps/server/src/module-composition.test.ts +22 -0
- package/template/apps/server/src/module-composition.ts +48 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +97 -30
- package/template/apps/server/src/module-host/ai-operation-speech-normalize.test.ts +1 -1
- package/template/apps/server/src/module-host/http-runtime.ts +9 -3
- package/template/apps/server/src/module-host/static-route-authorization.test.ts +6 -3
- package/template/apps/server/src/module-host/storage-authorization-binding.test.ts +4 -0
- package/template/apps/server/src/module-runtime-validation.ts +2 -0
- package/template/apps/server/src/observability/diagnostic-index.ts +26 -1
- package/template/apps/server/src/observability/diagnostic-repository.ts +40 -11
- package/template/apps/server/src/platform-http-runtime.ts +1 -1
- package/template/apps/server/src/runtime-feature.ts +5 -1
- package/template/apps/server/src/runtime.ts +3 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +35 -4
- package/template/apps/server/src/settings/watermark-repository.ts +14 -0
- package/template/apps/server/src/settings/watermark-routes.test.ts +84 -2
- package/template/apps/server/src/settings/watermark-routes.ts +109 -0
- package/template/apps/server/src/settings/watermark-transfer.ts +79 -0
- package/template/apps/server/src/speech/facade-validation.ts +1 -1
- package/template/apps/server/src/speech/facade.ts +2 -2
- package/template/apps/server/src/startup-composition-validation.test.ts +32 -0
- package/template/apps/server/src/startup-finalization.ts +2 -0
- package/template/apps/server/src/startup-runtime.ts +77 -0
- package/template/apps/server/src/system/ai-runtime-build.ts +3 -0
- package/template/apps/server/src/system/ai-runtime-configuration.test.ts +36 -0
- package/template/apps/server/src/system/ai-runtime-configuration.ts +58 -0
- package/template/apps/server/src/system/diagnostic-routes.test.ts +45 -0
- package/template/apps/server/src/system/diagnostic-routes.ts +35 -0
- package/template/apps/server/src/system/environment-policy-reader.test.ts +74 -0
- package/template/apps/server/src/system/environment-policy-reader.ts +62 -0
- package/template/apps/server/src/system/feature-environment-assessment.test.ts +148 -0
- package/template/apps/server/src/system/feature-environment-assessment.ts +179 -0
- package/template/apps/server/src/system/host-diagnostics.test.ts +155 -0
- package/template/apps/server/src/system/host-diagnostics.ts +225 -0
- package/template/apps/server/src/system/menu-registry.test.ts +26 -0
- package/template/apps/server/src/system/menu-registry.ts +9 -7
- package/template/apps/server/src/system/operations.test.ts +5 -0
- package/template/apps/server/src/system/operations.ts +36 -12
- package/template/apps/server/src/system/role-authorization-postgres.integration.test.ts +181 -0
- package/template/apps/web/Dockerfile +7 -3
- package/template/apps/web/dev/dev-port.test.ts +6 -6
- package/template/apps/web/dev/dev-port.ts +1 -1
- package/template/apps/web/dev/start.ts +6 -1
- package/template/apps/web/index.html +1 -0
- package/template/apps/web/package.json +16 -16
- package/template/apps/web/runtime-server.test.ts +22 -1
- package/template/apps/web/runtime-server.ts +6 -0
- package/template/apps/web/src/App.vue +6 -3
- package/template/apps/web/src/assets/windy.svg +4 -0
- package/template/apps/web/src/components/auth/LoginForm.vue +2 -2
- package/template/apps/web/src/components/auth/PasswordChangeForm.vue +14 -3
- package/template/apps/web/src/components/common/FormDialog.vue +1 -1
- package/template/apps/web/src/components/license/LicenseFlow.webtest.ts +3 -0
- package/template/apps/web/src/components/license/LicenseMachineCodeQr.vue +99 -0
- package/template/apps/web/src/components/license/LicenseMachineCodeQr.webtest.ts +83 -0
- package/template/apps/web/src/components/license/LicensePairingCard.vue +1 -1
- package/template/apps/web/src/components/license/LicenseStatusCard.vue +14 -2
- package/template/apps/web/src/composables/useLoginFlow.ts +2 -2
- package/template/apps/web/src/composables/usePlatformSettings.ts +1 -1
- package/template/apps/web/src/composables/useWatermarkSettings.ts +1 -0
- package/template/apps/web/src/layout/AppSidebar.vue +5 -9
- package/template/apps/web/src/layout/AppSidebar.webtest.ts +4 -1
- package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +67 -6
- package/template/apps/web/src/pages/auth/LoginPage.vue +3 -12
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +30 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +25 -14
- package/template/apps/web/src/pages/configuration/components/TimeZoneSelect.vue +55 -0
- package/template/apps/web/src/pages/profile/PersonalSettingsPage.vue +64 -11
- package/template/apps/web/src/pages/profile/PersonalSettingsPage.webtest.ts +21 -1
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +7 -18
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +9 -4
- package/template/apps/web/src/pages/settings/GlobalSettingsPage.vue +51 -6
- package/template/apps/web/src/pages/settings/GlobalSettingsPage.webtest.ts +19 -3
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +1 -0
- package/template/apps/web/src/pages/settings/WatermarkYamlTransfer.vue +211 -0
- package/template/apps/web/src/pages/system/DiagnosticLookupPage.vue +18 -3
- package/template/apps/web/src/pages/system/DiagnosticLookupPage.webtest.ts +4 -1
- package/template/apps/web/src/pages/system/SystemOperationsPage.vue +23 -0
- package/template/apps/web/src/pages/system/components/DiagnosticLogList.vue +120 -0
- package/template/apps/web/src/pages/system/components/OperationEnvironmentPanel.vue +237 -0
- package/template/apps/web/src/pages/system/components/OperationFeatureAssessmentPanel.vue +131 -0
- package/template/apps/web/src/pages/system/components/PagedSelectionList.vue +31 -24
- package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +2 -1
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +11 -2
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +1 -0
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +6 -2
- package/template/apps/web/src/pages/system/components/agent-skills/AgentSkillImportDialog.vue +18 -9
- package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +18 -9
- package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +66 -34
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +48 -33
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +9 -2
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +23 -16
- package/template/apps/web/src/pages/system/components/dictionary-bulk/DictionaryBulkJobDetail.vue +10 -7
- package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +81 -57
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +6 -4
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +20 -4
- package/template/apps/web/src/pages/system/resource-config.ts +9 -0
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +1 -1
- package/template/apps/web/src/router/disabled-feature-loading.webtest.ts +78 -0
- package/template/apps/web/src/router/index.ts +3 -6
- package/template/apps/web/src/router/manifest-routes.ts +2 -3
- package/template/apps/web/src/router/platform-admin-pages.ts +21 -62
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -4
- package/template/apps/web/src/services/diagnostics-api.ts +13 -1
- package/template/apps/web/src/services/operations-api.ts +4 -0
- package/template/apps/web/src/services/platform-settings-api.ts +35 -1
- package/template/apps/web/vite.config.ts +9 -3
- package/template/deploy/environment/windy.environment.example.yaml +53 -0
- package/template/docker-compose.yml +4 -2
- package/template/docs/architecture/agent-admin-workbench-implementation.md +1 -1
- package/template/docs/architecture/agent-admin-workbench-plan.md +1 -1
- package/template/docs/architecture/agent-workflow.md +8 -0
- package/template/docs/architecture/durable-jobs.md +17 -0
- package/template/docs/architecture/embedded-dsh-integration.md +1 -1
- package/template/docs/development/dsh-plugins.md +1 -1
- package/template/docs/development/performance-engineering.md +24 -0
- package/template/docs/operations/admin-settings-yaml.md +29 -0
- package/template/docs/operations/dsh-agent-migration.md +2 -2
- package/template/docs/operations/environment-capabilities.md +185 -0
- package/template/docs/platform/module-configuration.md +18 -3
- package/template/docs/platform/platform-authentication.md +1 -1
- package/template/package.json +10 -8
- package/template/packages/config/package.json +2 -2
- package/template/packages/config/src/definition.ts +14 -4
- package/template/packages/config/src/environment-loader.ts +27 -3
- package/template/packages/config/src/platform-ai-runtime.test.ts +94 -0
- package/template/packages/config/src/platform.ts +5 -2
- package/template/packages/database/drizzle/0065_durable_job_lease_snapshot_lookup.sql +1 -0
- package/template/packages/database/drizzle/0066_agent_source_sequence_bigint.sql +3 -0
- package/template/packages/database/drizzle/0067_media_runtime_access.sql +46 -0
- package/template/packages/database/drizzle/meta/0065_snapshot.json +15370 -0
- package/template/packages/database/drizzle/meta/0066_snapshot.json +15370 -0
- package/template/packages/database/drizzle/meta/0067_snapshot.json +15370 -0
- package/template/packages/database/drizzle/meta/_journal.json +21 -0
- package/template/packages/database/package.json +3 -3
- package/template/packages/database/src/schema/agent-session.ts +4 -3
- package/template/packages/database/src/schema/durable-jobs.ts +4 -0
- package/template/packages/database/src/schema/index.ts +3 -1
- package/template/packages/database/src/schema-durable-recovery.test.ts +21 -1
- package/template/packages/database/src/schema-source-sequence.test.ts +24 -0
- package/template/packages/example-work-order/package.json +2 -2
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/admin-ui-boundary.test.ts +33 -0
- package/template/packages/modules/src/composition.ts +11 -28
- package/template/packages/modules/src/manifest.ts +2 -0
- package/template/packages/modules/src/menu-composition.ts +24 -0
- package/template/packages/modules/src/system-admin-routes.ts +2 -2
- package/template/packages/modules/src/system-admin-ui-catalog.ts +6 -1
- package/template/packages/modules/src/system-ai-api-bindings.ts +0 -16
- package/template/packages/modules/src/system-api-control-plane.ts +9 -1
- package/template/packages/modules/src/system-api-permissions.ts +7 -0
- package/template/packages/modules/src/system-api-watermark.ts +21 -0
- package/template/packages/modules/src/system-audit-actions.ts +3 -1
- package/template/packages/modules/src/system-features.ts +21 -3
- package/template/packages/modules/src/system-media-api-bindings.ts +97 -0
- package/template/packages/modules/src/system-module-audit-selection.ts +2 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +13 -6
- package/template/packages/modules/src/system-module-catalog.ts +6 -241
- package/template/packages/modules/src/system-module-definitions.ts +260 -0
- package/template/packages/modules/src/system-modules-access.test.ts +2 -0
- package/template/packages/modules/src/system-modules.test.ts +7 -1
- package/template/packages/modules/src/system-modules.ts +25 -13
- package/template/packages/modules/src/system-permissions.ts +12 -0
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/environment-assessment.ts +68 -0
- package/template/packages/shared/src/environment-policy.ts +205 -0
- package/template/packages/shared/src/license-console-capabilities.ts +7 -0
- package/template/packages/shared/src/license.ts +1 -0
- package/template/packages/shared/src/module-configuration.ts +2 -1
- package/template/scripts/operations/collect-device-info.py +230 -0
- package/template/apps/server/src/agent/module-ai-port-factory.ts +0 -41
- /package/template/apps/server/src/{agent → ai-runtime-security}/execution-grants.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/idempotency-key.test.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/idempotency-key.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/provider-input-governance.test.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/provider-input-governance.ts +0 -0
- /package/template/apps/server/src/{agent → media-runtime}/required-environment.ts +0 -0
- /package/template/apps/server/src/{agent → media-runtime}/runtime-readiness-sources.test.ts +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { FeatureFlagDefinition } from "@southwind-ai/shared";
|
|
2
|
+
import type { EnvironmentSnapshot } from "../../../../packages/shared/src/environment-assessment.js";
|
|
3
|
+
import type {
|
|
4
|
+
EnvironmentAssessmentSnapshot,
|
|
5
|
+
FeatureEnvironmentAssessment,
|
|
6
|
+
FeatureEnvironmentPolicy,
|
|
7
|
+
} from "../../../../packages/shared/src/environment-policy.js";
|
|
8
|
+
import type { EnvironmentPolicyRead } from "./environment-policy-reader.js";
|
|
9
|
+
|
|
10
|
+
export interface FeatureAssessmentInput {
|
|
11
|
+
environment: EnvironmentSnapshot;
|
|
12
|
+
definitions: readonly FeatureFlagDefinition[];
|
|
13
|
+
resolveEnabled: (definition: FeatureFlagDefinition) => Promise<boolean>;
|
|
14
|
+
environmentValues?: Record<string, string | undefined>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Advisory only: never changes features, licenses, running tasks, or build contents. */
|
|
18
|
+
export async function assessFeatureEnvironment(
|
|
19
|
+
source: EnvironmentPolicyRead,
|
|
20
|
+
input: FeatureAssessmentInput,
|
|
21
|
+
): Promise<EnvironmentAssessmentSnapshot> {
|
|
22
|
+
if (source.status !== "available") return { ...source, features: [] };
|
|
23
|
+
const definitions = new Map(
|
|
24
|
+
input.definitions.map((item) => [String(item.key), item]),
|
|
25
|
+
);
|
|
26
|
+
const features = await Promise.all(
|
|
27
|
+
source.policy.features.map(async (policy) => {
|
|
28
|
+
const definition = definitions.get(policy.key);
|
|
29
|
+
if (!definition)
|
|
30
|
+
return result(
|
|
31
|
+
policy,
|
|
32
|
+
"not-packaged",
|
|
33
|
+
"当前装配未注册此功能;请核对功能 Key 与交付模块。",
|
|
34
|
+
[],
|
|
35
|
+
);
|
|
36
|
+
let enabled: boolean;
|
|
37
|
+
try {
|
|
38
|
+
enabled = await input.resolveEnabled(definition);
|
|
39
|
+
} catch {
|
|
40
|
+
return result(
|
|
41
|
+
policy,
|
|
42
|
+
"unknown",
|
|
43
|
+
"功能开关状态读取失败;未修改配置。",
|
|
44
|
+
[],
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return assessRequirements(policy, input, enabled);
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
status: "available",
|
|
52
|
+
message:
|
|
53
|
+
"评估只生成建议;请管理员核对证据后在功能管理中确认关闭。条件满足不代表模型或业务已验收。",
|
|
54
|
+
features,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function result(
|
|
58
|
+
policy: FeatureEnvironmentPolicy,
|
|
59
|
+
status: FeatureEnvironmentAssessment["status"],
|
|
60
|
+
message: string,
|
|
61
|
+
evidence: string[],
|
|
62
|
+
): FeatureEnvironmentAssessment {
|
|
63
|
+
return {
|
|
64
|
+
key: policy.key,
|
|
65
|
+
title: policy.title,
|
|
66
|
+
status,
|
|
67
|
+
message,
|
|
68
|
+
evidence,
|
|
69
|
+
...(policy.recommendedEnabled === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: { recommendedEnabled: policy.recommendedEnabled }),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function assessRequirements(
|
|
75
|
+
policy: FeatureEnvironmentPolicy,
|
|
76
|
+
input: FeatureAssessmentInput,
|
|
77
|
+
enabled: boolean,
|
|
78
|
+
) {
|
|
79
|
+
const process = input.environment.process;
|
|
80
|
+
const evidence = [
|
|
81
|
+
`当前功能开关:${enabled ? "开启" : "关闭"}`,
|
|
82
|
+
`应用进程:${process.platform}/${process.architecture};此信息不代表宿主机硬件`,
|
|
83
|
+
`声明执行方式:${policy.execution}`,
|
|
84
|
+
];
|
|
85
|
+
if (
|
|
86
|
+
policy.supportedPlatforms?.length &&
|
|
87
|
+
!policy.supportedPlatforms.includes(process.platform)
|
|
88
|
+
)
|
|
89
|
+
return result(
|
|
90
|
+
policy,
|
|
91
|
+
"software",
|
|
92
|
+
"当前应用运行平台不在声明支持范围;更换硬件不一定能解决。",
|
|
93
|
+
evidence,
|
|
94
|
+
);
|
|
95
|
+
if (
|
|
96
|
+
policy.supportedArchitectures?.length &&
|
|
97
|
+
!policy.supportedArchitectures.includes(process.architecture)
|
|
98
|
+
)
|
|
99
|
+
return result(
|
|
100
|
+
policy,
|
|
101
|
+
"software",
|
|
102
|
+
"当前应用架构不在声明的软件支持范围;需匹配的软件构建。",
|
|
103
|
+
evidence,
|
|
104
|
+
);
|
|
105
|
+
const missing = (policy.requiredEnvironmentKeys ?? []).filter(
|
|
106
|
+
(key) => !(input.environmentValues ?? {})[key]?.trim(),
|
|
107
|
+
);
|
|
108
|
+
if (missing.length)
|
|
109
|
+
return result(policy, "configuration", "所需配置未就绪;不能归因于硬件。", [
|
|
110
|
+
...evidence,
|
|
111
|
+
`缺少配置项:${missing.join("、")}`,
|
|
112
|
+
]);
|
|
113
|
+
const needsHost =
|
|
114
|
+
policy.execution !== "remote" &&
|
|
115
|
+
(policy.execution === "local-gpu" || policy.minMemoryGiB !== undefined);
|
|
116
|
+
if (needsHost) {
|
|
117
|
+
const host = input.environment.host;
|
|
118
|
+
if (host.status !== "available" || !host.report)
|
|
119
|
+
return result(
|
|
120
|
+
policy,
|
|
121
|
+
"unknown",
|
|
122
|
+
"缺少有效宿主机证据;容器中未检测到设备不等于宿主机没有设备。",
|
|
123
|
+
evidence,
|
|
124
|
+
);
|
|
125
|
+
evidence.push(`宿主机采集时间:${host.report.collectedAt}`);
|
|
126
|
+
const facts = host.report.facts;
|
|
127
|
+
if (policy.minMemoryGiB !== undefined) {
|
|
128
|
+
if (facts.memoryTotalBytes === null)
|
|
129
|
+
return result(
|
|
130
|
+
policy,
|
|
131
|
+
"unknown",
|
|
132
|
+
"宿主机内存数据未知,无法判断最低内存条件。",
|
|
133
|
+
evidence,
|
|
134
|
+
);
|
|
135
|
+
evidence.push(
|
|
136
|
+
`宿主机总内存:${(facts.memoryTotalBytes / 1024 ** 3).toFixed(1)} GiB;要求至少 ${policy.minMemoryGiB} GiB`,
|
|
137
|
+
);
|
|
138
|
+
if (facts.memoryTotalBytes < policy.minMemoryGiB * 1024 ** 3)
|
|
139
|
+
return result(
|
|
140
|
+
policy,
|
|
141
|
+
"hardware",
|
|
142
|
+
"宿主机总内存低于声明的最低要求。",
|
|
143
|
+
evidence,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (policy.execution === "local-gpu") {
|
|
147
|
+
if (facts.gpuCount === undefined)
|
|
148
|
+
return result(
|
|
149
|
+
policy,
|
|
150
|
+
"unknown",
|
|
151
|
+
"未取得有效 NVIDIA GPU 查询;需核对设备、驱动、权限与采集工具。",
|
|
152
|
+
evidence,
|
|
153
|
+
);
|
|
154
|
+
evidence.push(`NVIDIA GPU 数量:${facts.gpuCount}`);
|
|
155
|
+
if (facts.gpuCount === 0)
|
|
156
|
+
return result(
|
|
157
|
+
policy,
|
|
158
|
+
"hardware",
|
|
159
|
+
"有效宿主机查询未返回所需 GPU;本地 GPU 执行条件不满足。",
|
|
160
|
+
evidence,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!enabled)
|
|
165
|
+
return result(
|
|
166
|
+
policy,
|
|
167
|
+
"disabled",
|
|
168
|
+
"功能当前已关闭;环境评估不会自动开启。",
|
|
169
|
+
evidence,
|
|
170
|
+
);
|
|
171
|
+
return result(
|
|
172
|
+
policy,
|
|
173
|
+
"available",
|
|
174
|
+
policy.execution === "none"
|
|
175
|
+
? "声明条件已满足;仍以实际业务验收为准。"
|
|
176
|
+
: "声明条件已满足;模型 readiness、驱动兼容、容器配额及性能尚需单独验收。",
|
|
177
|
+
evidence,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
HOST_PROBE_KEYS,
|
|
7
|
+
type HostDiagnosticsReport,
|
|
8
|
+
} from "@southwind-ai/shared";
|
|
9
|
+
import {
|
|
10
|
+
createEnvironmentProbe,
|
|
11
|
+
parseHostDiagnostics,
|
|
12
|
+
readHostDiagnostics,
|
|
13
|
+
} from "./host-diagnostics.js";
|
|
14
|
+
|
|
15
|
+
const now = Date.parse("2026-11-06T12:00:00Z");
|
|
16
|
+
function fixture(): HostDiagnosticsReport {
|
|
17
|
+
return {
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
scope: "host",
|
|
20
|
+
collector: "windy-host-diagnostics-v1",
|
|
21
|
+
collectedAt: new Date(now).toISOString(),
|
|
22
|
+
facts: {
|
|
23
|
+
os: "linux",
|
|
24
|
+
kernel: "6.1",
|
|
25
|
+
architecture: "aarch64",
|
|
26
|
+
cpuLogicalCount: 8,
|
|
27
|
+
memoryTotalBytes: 16 * 1024 ** 3,
|
|
28
|
+
},
|
|
29
|
+
probes: Object.fromEntries(
|
|
30
|
+
HOST_PROBE_KEYS.map((key) => [
|
|
31
|
+
key,
|
|
32
|
+
{
|
|
33
|
+
status: "missing",
|
|
34
|
+
exitCode: null,
|
|
35
|
+
output: "",
|
|
36
|
+
truncated: false,
|
|
37
|
+
},
|
|
38
|
+
]),
|
|
39
|
+
) as HostDiagnosticsReport["probes"],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const directories: string[] = [];
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
await Promise.all(
|
|
45
|
+
directories
|
|
46
|
+
.splice(0)
|
|
47
|
+
.map((dir) => rm(dir, { recursive: true, force: true })),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("valid host report preserves unknown GPU and strips undeclared data", () => {
|
|
52
|
+
const input = { ...fixture(), secret: "not-returned" };
|
|
53
|
+
const result = parseHostDiagnostics(input, now);
|
|
54
|
+
expect(result.status).toBe("available");
|
|
55
|
+
expect(result.report?.facts.gpuCount).toBeUndefined();
|
|
56
|
+
expect(JSON.stringify(result)).not.toContain("not-returned");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("version, scope, future date, over-budget output and missing probes fail closed", () => {
|
|
60
|
+
for (const patch of [
|
|
61
|
+
{ schemaVersion: 2 },
|
|
62
|
+
{ scope: "process" },
|
|
63
|
+
{ collectedAt: "invalid" },
|
|
64
|
+
{ collectedAt: new Date(now + 600_000).toISOString() },
|
|
65
|
+
{ probes: {} },
|
|
66
|
+
]) {
|
|
67
|
+
expect(parseHostDiagnostics({ ...fixture(), ...patch }, now).status).toBe(
|
|
68
|
+
"invalid",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const report = fixture();
|
|
72
|
+
report.probes.cpu.output = "x".repeat(16385);
|
|
73
|
+
expect(parseHostDiagnostics(report, now).status).toBe("invalid");
|
|
74
|
+
expect(
|
|
75
|
+
parseHostDiagnostics(fixture(), now + 25 * 60 * 60 * 1000).status,
|
|
76
|
+
).toBe("stale");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("bounded file read refuses symlink and oversized file and handles absence", async () => {
|
|
80
|
+
const directory = await mkdtemp(join(tmpdir(), "windy-host-report-"));
|
|
81
|
+
directories.push(directory);
|
|
82
|
+
const path = join(directory, "report.json");
|
|
83
|
+
await writeFile(path, JSON.stringify(fixture()));
|
|
84
|
+
expect((await readHostDiagnostics(path, now)).status).toBe("available");
|
|
85
|
+
const link = join(directory, "link.json");
|
|
86
|
+
await symlink(path, link);
|
|
87
|
+
expect((await readHostDiagnostics(link, now)).status).toBe("unreadable");
|
|
88
|
+
await writeFile(path, "x".repeat(256 * 1024 + 1));
|
|
89
|
+
expect((await readHostDiagnostics(path, now)).status).toBe("invalid");
|
|
90
|
+
expect(
|
|
91
|
+
(await readHostDiagnostics(join(directory, "missing"), now)).status,
|
|
92
|
+
).toBe("unreadable");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("without a mount only process scope is reported", async () => {
|
|
96
|
+
const snapshot = await createEnvironmentProbe({ WINDY_CONTAINERIZED: "1" })();
|
|
97
|
+
expect(snapshot.process.scope).toBe("process");
|
|
98
|
+
expect(snapshot.process.containerized).toBe(true);
|
|
99
|
+
expect(snapshot.host.status).toBe("not-configured");
|
|
100
|
+
expect(snapshot.host.report).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("deployment metadata is whitelisted and cannot carry arbitrary manifest contents", () => {
|
|
104
|
+
const result = parseHostDiagnostics(
|
|
105
|
+
{
|
|
106
|
+
...fixture(),
|
|
107
|
+
deployment: {
|
|
108
|
+
status: "available",
|
|
109
|
+
releaseId: "20261106",
|
|
110
|
+
gitSha: "abcdef123",
|
|
111
|
+
platform: "linux/arm64",
|
|
112
|
+
secret: "never-return",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
now,
|
|
116
|
+
);
|
|
117
|
+
expect(result.report?.deployment).toEqual({
|
|
118
|
+
status: "available",
|
|
119
|
+
releaseId: "20261106",
|
|
120
|
+
gitSha: "abcdef123",
|
|
121
|
+
platform: "linux/arm64",
|
|
122
|
+
});
|
|
123
|
+
expect(
|
|
124
|
+
parseHostDiagnostics(
|
|
125
|
+
{ ...fixture(), deployment: { status: "available", gitSha: "invalid" } },
|
|
126
|
+
now,
|
|
127
|
+
).status,
|
|
128
|
+
).toBe("invalid");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("GPU count is not trusted when its probe is missing, failed or truncated", () => {
|
|
132
|
+
for (const status of [
|
|
133
|
+
"missing",
|
|
134
|
+
"failed",
|
|
135
|
+
"permission-denied",
|
|
136
|
+
"timeout",
|
|
137
|
+
"unsupported",
|
|
138
|
+
] as const) {
|
|
139
|
+
const report = fixture();
|
|
140
|
+
report.facts.gpuCount = 0;
|
|
141
|
+
report.probes.gpu.status = status;
|
|
142
|
+
expect(
|
|
143
|
+
parseHostDiagnostics(report, now).report?.facts.gpuCount,
|
|
144
|
+
).toBeUndefined();
|
|
145
|
+
}
|
|
146
|
+
const report = fixture();
|
|
147
|
+
report.facts.gpuCount = 0;
|
|
148
|
+
report.probes.gpu.status = "ok";
|
|
149
|
+
report.probes.gpu.truncated = true;
|
|
150
|
+
expect(
|
|
151
|
+
parseHostDiagnostics(report, now).report?.facts.gpuCount,
|
|
152
|
+
).toBeUndefined();
|
|
153
|
+
report.probes.gpu.truncated = false;
|
|
154
|
+
expect(parseHostDiagnostics(report, now).report?.facts.gpuCount).toBe(0);
|
|
155
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { cpus, totalmem } from "node:os";
|
|
4
|
+
import {
|
|
5
|
+
HOST_PROBE_KEYS,
|
|
6
|
+
type EnvironmentSnapshot,
|
|
7
|
+
type HostDiagnosticsReport,
|
|
8
|
+
type HostDiagnosticProbe,
|
|
9
|
+
} from "@southwind-ai/shared";
|
|
10
|
+
|
|
11
|
+
const MAX_FILE_BYTES = 256 * 1024;
|
|
12
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
const statuses = new Set([
|
|
14
|
+
"ok",
|
|
15
|
+
"missing",
|
|
16
|
+
"permission-denied",
|
|
17
|
+
"timeout",
|
|
18
|
+
"failed",
|
|
19
|
+
"unsupported",
|
|
20
|
+
]);
|
|
21
|
+
type HostState = EnvironmentSnapshot["host"];
|
|
22
|
+
export type EnvironmentProbe = () => Promise<EnvironmentSnapshot>;
|
|
23
|
+
|
|
24
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
25
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
26
|
+
? (value as Record<string, unknown>)
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
function text(value: unknown, max: number): value is string {
|
|
30
|
+
return typeof value === "string" && value.length <= max;
|
|
31
|
+
}
|
|
32
|
+
function count(value: unknown): value is number {
|
|
33
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Constructs a bounded whitelist projection; unknown fields are never returned to clients. */
|
|
37
|
+
export function parseHostDiagnostics(
|
|
38
|
+
value: unknown,
|
|
39
|
+
now = Date.now(),
|
|
40
|
+
): HostState {
|
|
41
|
+
const data = record(value);
|
|
42
|
+
const facts = record(data?.facts);
|
|
43
|
+
const probes = record(data?.probes);
|
|
44
|
+
if (
|
|
45
|
+
!data ||
|
|
46
|
+
data.schemaVersion !== 1 ||
|
|
47
|
+
data.scope !== "host" ||
|
|
48
|
+
data.collector !== "windy-host-diagnostics-v1" ||
|
|
49
|
+
!text(data.collectedAt, 64) ||
|
|
50
|
+
!facts ||
|
|
51
|
+
!probes ||
|
|
52
|
+
!text(facts.os, 128) ||
|
|
53
|
+
!(facts.distribution === undefined || text(facts.distribution, 256)) ||
|
|
54
|
+
!text(facts.kernel, 256) ||
|
|
55
|
+
!text(facts.architecture, 64) ||
|
|
56
|
+
!count(facts.cpuLogicalCount) ||
|
|
57
|
+
!(facts.memoryTotalBytes === null || count(facts.memoryTotalBytes)) ||
|
|
58
|
+
!(facts.gpuCount === undefined || count(facts.gpuCount))
|
|
59
|
+
) {
|
|
60
|
+
return invalid();
|
|
61
|
+
}
|
|
62
|
+
const collectedAt = Date.parse(data.collectedAt);
|
|
63
|
+
if (!Number.isFinite(collectedAt) || collectedAt > now + 5 * 60 * 1000)
|
|
64
|
+
return invalid();
|
|
65
|
+
if (now - collectedAt > MAX_AGE_MS)
|
|
66
|
+
return {
|
|
67
|
+
status: "stale",
|
|
68
|
+
message: "宿主机诊断文件超过 24 小时,请在宿主机重新采集。",
|
|
69
|
+
};
|
|
70
|
+
const selected = {} as Record<
|
|
71
|
+
(typeof HOST_PROBE_KEYS)[number],
|
|
72
|
+
HostDiagnosticProbe
|
|
73
|
+
>;
|
|
74
|
+
for (const key of HOST_PROBE_KEYS) {
|
|
75
|
+
const probe = record(probes[key]);
|
|
76
|
+
if (
|
|
77
|
+
!probe ||
|
|
78
|
+
typeof probe.status !== "string" ||
|
|
79
|
+
!statuses.has(probe.status) ||
|
|
80
|
+
!text(probe.output, 16384) ||
|
|
81
|
+
typeof probe.truncated !== "boolean" ||
|
|
82
|
+
!(
|
|
83
|
+
probe.exitCode === null ||
|
|
84
|
+
(typeof probe.exitCode === "number" &&
|
|
85
|
+
Number.isSafeInteger(probe.exitCode))
|
|
86
|
+
)
|
|
87
|
+
) {
|
|
88
|
+
return invalid();
|
|
89
|
+
}
|
|
90
|
+
selected[key] = {
|
|
91
|
+
status: probe.status as HostDiagnosticProbe["status"],
|
|
92
|
+
output: probe.output,
|
|
93
|
+
exitCode: probe.exitCode as number | null,
|
|
94
|
+
truncated: probe.truncated,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const deployment = parseDeployment(data.deployment);
|
|
98
|
+
if (deployment === null) return invalid();
|
|
99
|
+
const report: HostDiagnosticsReport = {
|
|
100
|
+
schemaVersion: 1,
|
|
101
|
+
scope: "host",
|
|
102
|
+
collector: "windy-host-diagnostics-v1",
|
|
103
|
+
collectedAt: data.collectedAt,
|
|
104
|
+
facts: {
|
|
105
|
+
os: facts.os,
|
|
106
|
+
...(facts.distribution ? { distribution: facts.distribution } : {}),
|
|
107
|
+
kernel: facts.kernel,
|
|
108
|
+
architecture: facts.architecture,
|
|
109
|
+
cpuLogicalCount: facts.cpuLogicalCount,
|
|
110
|
+
memoryTotalBytes: facts.memoryTotalBytes,
|
|
111
|
+
...(facts.gpuCount !== undefined &&
|
|
112
|
+
selected.gpu.status === "ok" &&
|
|
113
|
+
!selected.gpu.truncated
|
|
114
|
+
? { gpuCount: facts.gpuCount }
|
|
115
|
+
: {}),
|
|
116
|
+
},
|
|
117
|
+
probes: selected,
|
|
118
|
+
...(deployment ? { deployment } : {}),
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
status: "available",
|
|
122
|
+
message: "已读取宿主机采集文件;这是采集时快照,不是实时监控。",
|
|
123
|
+
report,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseDeployment(
|
|
128
|
+
value: unknown,
|
|
129
|
+
): HostDiagnosticsReport["deployment"] | null {
|
|
130
|
+
if (value === undefined) return undefined;
|
|
131
|
+
const item = record(value);
|
|
132
|
+
if (
|
|
133
|
+
!item ||
|
|
134
|
+
!["available", "invalid", "unreadable"].includes(String(item.status))
|
|
135
|
+
)
|
|
136
|
+
return null;
|
|
137
|
+
if (item.status !== "available")
|
|
138
|
+
return { status: item.status as "invalid" | "unreadable" };
|
|
139
|
+
const patterns = {
|
|
140
|
+
releaseId: /^[A-Za-z0-9._-]{1,128}$/,
|
|
141
|
+
gitSha: /^[a-fA-F0-9]{7,64}$/,
|
|
142
|
+
platform: /^[A-Za-z0-9._/-]{1,64}$/,
|
|
143
|
+
};
|
|
144
|
+
const result: NonNullable<HostDiagnosticsReport["deployment"]> = {
|
|
145
|
+
status: "available",
|
|
146
|
+
};
|
|
147
|
+
let count = 0;
|
|
148
|
+
for (const key of ["releaseId", "gitSha", "platform"] as const) {
|
|
149
|
+
const field = item[key];
|
|
150
|
+
if (field === undefined) continue;
|
|
151
|
+
if (typeof field !== "string" || !patterns[key].test(field)) return null;
|
|
152
|
+
result[key] = field;
|
|
153
|
+
count++;
|
|
154
|
+
}
|
|
155
|
+
return count ? result : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function invalid(): HostState {
|
|
159
|
+
return {
|
|
160
|
+
status: "invalid",
|
|
161
|
+
message: "宿主机诊断文件格式、大小或采集时间无效。",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function readHostDiagnostics(
|
|
166
|
+
path: string | undefined,
|
|
167
|
+
now = Date.now(),
|
|
168
|
+
): Promise<HostState> {
|
|
169
|
+
if (!path)
|
|
170
|
+
return {
|
|
171
|
+
status: "not-configured",
|
|
172
|
+
message: "未挂载宿主机诊断文件;以下运行进程信息不能代表宿主机。",
|
|
173
|
+
};
|
|
174
|
+
let file: Awaited<ReturnType<typeof open>> | undefined;
|
|
175
|
+
try {
|
|
176
|
+
file = await open(
|
|
177
|
+
path,
|
|
178
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
179
|
+
);
|
|
180
|
+
const stat = await file.stat();
|
|
181
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return invalid();
|
|
182
|
+
const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
|
|
183
|
+
let bytes = 0;
|
|
184
|
+
while (bytes < buffer.length) {
|
|
185
|
+
const read = await file.read(buffer, bytes, buffer.length - bytes, null);
|
|
186
|
+
if (!read.bytesRead) break;
|
|
187
|
+
bytes += read.bytesRead;
|
|
188
|
+
}
|
|
189
|
+
if (bytes > MAX_FILE_BYTES) return invalid();
|
|
190
|
+
try {
|
|
191
|
+
return parseHostDiagnostics(
|
|
192
|
+
JSON.parse(buffer.subarray(0, bytes).toString("utf8")),
|
|
193
|
+
now,
|
|
194
|
+
);
|
|
195
|
+
} catch {
|
|
196
|
+
return invalid();
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
return {
|
|
200
|
+
status: "unreadable",
|
|
201
|
+
message: "无法读取宿主机诊断文件,请检查只读挂载与文件权限。",
|
|
202
|
+
};
|
|
203
|
+
} finally {
|
|
204
|
+
await file?.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createEnvironmentProbe(
|
|
209
|
+
env: Record<string, string | undefined> = process.env,
|
|
210
|
+
): EnvironmentProbe {
|
|
211
|
+
return async () => ({
|
|
212
|
+
generatedAt: new Date().toISOString(),
|
|
213
|
+
process: {
|
|
214
|
+
scope: "process",
|
|
215
|
+
platform: process.platform,
|
|
216
|
+
architecture: process.arch,
|
|
217
|
+
nodeVersion: process.version,
|
|
218
|
+
...(process.versions.bun ? { bunVersion: process.versions.bun } : {}),
|
|
219
|
+
containerized: env.WINDY_CONTAINERIZED === "1",
|
|
220
|
+
logicalCpuCount: cpus().length,
|
|
221
|
+
memoryTotalBytes: totalmem(),
|
|
222
|
+
},
|
|
223
|
+
host: await readHostDiagnostics(env.WINDY_HOST_DIAGNOSTICS_FILE),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -39,6 +39,32 @@ describe("ManifestMenuRegistry", () => {
|
|
|
39
39
|
// @windy-module system.settings end
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
// @windy-module system.agent begin
|
|
43
|
+
test("同步时将旧运维目录内的 AI 入口移入新分组", async () => {
|
|
44
|
+
const repositories = createSystemRepositories(createFoundationSnapshot());
|
|
45
|
+
const registry = repositories.menuRegistry;
|
|
46
|
+
await registry.synchronize(actor);
|
|
47
|
+
await registry.updatePlacement(
|
|
48
|
+
"system.agent-workbench",
|
|
49
|
+
{ parentKey: "system.operations", visible: false },
|
|
50
|
+
actor,
|
|
51
|
+
);
|
|
52
|
+
await registry.updatePlacement(
|
|
53
|
+
"system.agent-debug",
|
|
54
|
+
{ parentKey: "system.identity" },
|
|
55
|
+
actor,
|
|
56
|
+
);
|
|
57
|
+
await registry.synchronize(actor);
|
|
58
|
+
expect(await registry.get("system.agent-workbench")).toMatchObject({
|
|
59
|
+
parentKey: "system.ai",
|
|
60
|
+
visible: false,
|
|
61
|
+
});
|
|
62
|
+
expect(await registry.get("system.agent-debug")).toMatchObject({
|
|
63
|
+
parentKey: "system.identity",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
// @windy-module system.agent end
|
|
67
|
+
|
|
42
68
|
test("支持调整父目录和同级顺序并拒绝循环父级", async () => {
|
|
43
69
|
const repositories = createSystemRepositories(createFoundationSnapshot());
|
|
44
70
|
await repositories.menuRegistry.synchronize(actor);
|
|
@@ -44,13 +44,15 @@ export class ManifestMenuRegistry {
|
|
|
44
44
|
await this.repository.create(definition, actor);
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
// Move existing AI entries that still use their former default group.
|
|
48
|
+
const moveToAiGroup =
|
|
49
|
+
definition.parentKey === "system.ai" &&
|
|
50
|
+
existing.parentKey === "system.operations";
|
|
51
|
+
const parentKey =
|
|
52
|
+
!moveToAiGroup &&
|
|
53
|
+
this.validParentKey(existing.key, existing.parentKey, storedByKey)
|
|
54
|
+
? existing.parentKey
|
|
55
|
+
: definition.parentKey;
|
|
54
56
|
await this.repository.update(
|
|
55
57
|
existing.id,
|
|
56
58
|
{
|
|
@@ -198,6 +198,11 @@ describe("system operation routes", () => {
|
|
|
198
198
|
app,
|
|
199
199
|
runtime,
|
|
200
200
|
createFoundationSnapshot().features,
|
|
201
|
+
{
|
|
202
|
+
environmentProbe: async () => {
|
|
203
|
+
throw new Error("unauthorized host read");
|
|
204
|
+
},
|
|
205
|
+
},
|
|
201
206
|
);
|
|
202
207
|
const guardContext = await runtime.createGuardContext(
|
|
203
208
|
new Request("http://localhost/api/system/operations"),
|