create-windy 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/dist/cli.js +1691 -1151
  2. package/package.json +2 -2
  3. package/template/.agents/skills/windy-business-module/SKILL.md +33 -1
  4. package/template/.windy-template.json +6 -2
  5. package/template/AGENTS.md +4 -0
  6. package/template/README.md +2 -0
  7. package/template/apps/agent-server/Dockerfile +1 -1
  8. package/template/apps/agent-server/README.md +1 -1
  9. package/template/apps/agent-server/native/EXTENSIONS.md +2 -2
  10. package/template/apps/agent-server/native/parallel-harness.ts +2 -0
  11. package/template/apps/agent-server/native/storage-lifecycle.test.ts +2 -1
  12. package/template/apps/agent-server/package.json +42 -41
  13. package/template/apps/agent-server/plugins/downloads/README.md +1 -1
  14. package/template/apps/agent-server/plugins/drop-overlay/README.md +1 -1
  15. package/template/apps/agent-server/plugins/settings/README.md +1 -1
  16. package/template/apps/agent-server/plugins/skill-library/client-chunks.test.ts +4 -1
  17. package/template/apps/agent-server/plugins/theme/README.md +5 -1
  18. package/template/apps/agent-server/plugins/theme/source-tokens.ts +0 -1
  19. package/template/apps/agent-server/plugins/theme/tokens.json +0 -4
  20. package/template/apps/agent-server/plugins/tool-catalog/README.md +6 -3
  21. package/template/apps/agent-server/plugins/tool-catalog/discovery.mjs +13 -10
  22. package/template/apps/agent-server/plugins/tool-catalog/discovery.test.ts +68 -6
  23. package/template/apps/agent-server/runtime/compatibility.ts +7 -1
  24. package/template/apps/agent-server/runtime/dsh-engine.ts +2 -0
  25. package/template/apps/agent-server/runtime/questions-workflow.nodecheck.ts +2 -0
  26. package/template/apps/server/Dockerfile +2 -1
  27. package/template/apps/server/package.json +11 -11
  28. package/template/apps/server/scripts/validate-module-composition-once.ts +2 -0
  29. package/template/apps/server/scripts/validate-module-composition.ts +8 -4
  30. package/template/apps/server/src/admin/routes.test.ts +29 -1
  31. package/template/apps/server/src/admin/routes.ts +20 -2
  32. package/template/apps/server/src/agent/client-session/completion.ts +1 -1
  33. package/template/apps/server/src/agent/client-session/manifest-admission.ts +1 -1
  34. package/template/apps/server/src/agent/client-session/request.ts +1 -1
  35. package/template/apps/server/src/agent/context-version-routes.ts +37 -13
  36. package/template/apps/server/src/agent/run-facade.ts +1 -1
  37. package/template/apps/server/src/agent/run-input-verification.ts +1 -1
  38. package/template/apps/server/src/agent/run-routes.ts +1 -14
  39. package/template/apps/server/src/agent/run-start-preparation.ts +1 -1
  40. package/template/apps/server/src/agent/runtime-bootstrap-input.ts +2 -0
  41. package/template/apps/server/src/agent/runtime-bootstrap.ts +49 -157
  42. package/template/apps/server/src/agent/runtime-disabled-composition.ts +2 -1
  43. package/template/apps/server/src/agent/session-invalidation/authorization-revocation.ts +54 -0
  44. package/template/apps/server/src/agent/session-runtime-composition.ts +1 -51
  45. package/template/apps/server/src/agent/unavailable-routes.ts +13 -2
  46. package/template/apps/server/src/agent-workflow/runtime-composition.ts +3 -0
  47. package/template/apps/server/src/agent-workflow/service.ts +17 -1
  48. package/template/apps/server/src/ai-artifact/media-resource-route.test.ts +134 -0
  49. package/template/apps/server/src/ai-artifact/routes.ts +1 -1
  50. package/template/apps/server/src/ai-artifact/runtime.ts +12 -0
  51. package/template/apps/server/src/ai-runtime-readiness/operation-port.test-support.ts +1 -1
  52. package/template/apps/server/src/ai-runtime-readiness/operation-port.ts +8 -0
  53. package/template/apps/server/src/ai-runtime-security/internal-token.ts +14 -0
  54. package/template/apps/server/src/app/antivirus-composition.test.ts +63 -0
  55. package/template/apps/server/src/app/antivirus-composition.ts +29 -0
  56. package/template/apps/server/src/app/dsh-workbench-socket-audit.test.ts +2 -1
  57. package/template/apps/server/src/configuration/definition.ts +15 -11
  58. package/template/apps/server/src/document/facade-support.ts +2 -2
  59. package/template/apps/server/src/document/facade.ts +2 -2
  60. package/template/apps/server/src/document/output-artifact-composition.ts +9 -5
  61. package/template/apps/server/src/document/output-artifact.test-support.ts +1 -1
  62. package/template/apps/server/src/embedding/facade.ts +1 -1
  63. package/template/apps/server/src/embedding/input.ts +1 -1
  64. package/template/apps/server/src/embedding/routes.ts +1 -1
  65. package/template/apps/server/src/index.ts +60 -60
  66. package/template/apps/server/src/license/routes.ts +4 -1
  67. package/template/apps/server/src/license/runtime-business-access.test.ts +261 -0
  68. package/template/apps/server/src/media-runtime/composition.ts +157 -0
  69. package/template/apps/server/src/media-runtime/media-only-composition.ts +44 -0
  70. package/template/apps/server/src/media-runtime/module-ai-port-factory.test.ts +69 -0
  71. package/template/apps/server/src/media-runtime/module-ai-port-factory.ts +72 -0
  72. package/template/apps/server/src/{agent → media-runtime}/runtime-readiness-sources.ts +14 -6
  73. package/template/apps/server/src/media-runtime/shared-composition.ts +104 -0
  74. package/template/apps/server/src/module-composition.test.ts +22 -0
  75. package/template/apps/server/src/module-composition.ts +48 -0
  76. package/template/apps/server/src/module-host/ai-operation-port.ts +97 -30
  77. package/template/apps/server/src/module-host/ai-operation-speech-normalize.test.ts +1 -1
  78. package/template/apps/server/src/module-host/http-runtime.ts +9 -3
  79. package/template/apps/server/src/module-host/static-route-authorization.test.ts +6 -3
  80. package/template/apps/server/src/module-host/storage-authorization-binding.test.ts +4 -0
  81. package/template/apps/server/src/module-runtime-validation.ts +2 -0
  82. package/template/apps/server/src/observability/diagnostic-index.ts +26 -1
  83. package/template/apps/server/src/observability/diagnostic-repository.ts +40 -11
  84. package/template/apps/server/src/platform-http-runtime.ts +1 -1
  85. package/template/apps/server/src/runtime-feature.ts +5 -1
  86. package/template/apps/server/src/runtime.ts +3 -0
  87. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +35 -4
  88. package/template/apps/server/src/settings/watermark-repository.ts +14 -0
  89. package/template/apps/server/src/settings/watermark-routes.test.ts +84 -2
  90. package/template/apps/server/src/settings/watermark-routes.ts +109 -0
  91. package/template/apps/server/src/settings/watermark-transfer.ts +79 -0
  92. package/template/apps/server/src/speech/facade-validation.ts +1 -1
  93. package/template/apps/server/src/speech/facade.ts +2 -2
  94. package/template/apps/server/src/startup-composition-validation.test.ts +32 -0
  95. package/template/apps/server/src/startup-finalization.ts +2 -0
  96. package/template/apps/server/src/startup-runtime.ts +77 -0
  97. package/template/apps/server/src/system/ai-runtime-build.ts +3 -0
  98. package/template/apps/server/src/system/ai-runtime-configuration.test.ts +36 -0
  99. package/template/apps/server/src/system/ai-runtime-configuration.ts +58 -0
  100. package/template/apps/server/src/system/diagnostic-routes.test.ts +45 -0
  101. package/template/apps/server/src/system/diagnostic-routes.ts +35 -0
  102. package/template/apps/server/src/system/environment-policy-reader.test.ts +74 -0
  103. package/template/apps/server/src/system/environment-policy-reader.ts +62 -0
  104. package/template/apps/server/src/system/feature-environment-assessment.test.ts +148 -0
  105. package/template/apps/server/src/system/feature-environment-assessment.ts +179 -0
  106. package/template/apps/server/src/system/host-diagnostics.test.ts +155 -0
  107. package/template/apps/server/src/system/host-diagnostics.ts +225 -0
  108. package/template/apps/server/src/system/menu-registry.test.ts +26 -0
  109. package/template/apps/server/src/system/menu-registry.ts +9 -7
  110. package/template/apps/server/src/system/operations.test.ts +5 -0
  111. package/template/apps/server/src/system/operations.ts +36 -12
  112. package/template/apps/server/src/system/role-authorization-postgres.integration.test.ts +181 -0
  113. package/template/apps/web/Dockerfile +7 -3
  114. package/template/apps/web/dev/dev-port.test.ts +6 -6
  115. package/template/apps/web/dev/dev-port.ts +1 -1
  116. package/template/apps/web/dev/start.ts +6 -1
  117. package/template/apps/web/index.html +1 -0
  118. package/template/apps/web/package.json +16 -16
  119. package/template/apps/web/runtime-server.test.ts +22 -1
  120. package/template/apps/web/runtime-server.ts +6 -0
  121. package/template/apps/web/src/App.vue +6 -3
  122. package/template/apps/web/src/assets/windy.svg +4 -0
  123. package/template/apps/web/src/components/auth/LoginForm.vue +2 -2
  124. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +14 -3
  125. package/template/apps/web/src/components/common/FormDialog.vue +1 -1
  126. package/template/apps/web/src/components/license/LicenseFlow.webtest.ts +3 -0
  127. package/template/apps/web/src/components/license/LicenseMachineCodeQr.vue +99 -0
  128. package/template/apps/web/src/components/license/LicenseMachineCodeQr.webtest.ts +83 -0
  129. package/template/apps/web/src/components/license/LicensePairingCard.vue +1 -1
  130. package/template/apps/web/src/components/license/LicenseStatusCard.vue +14 -2
  131. package/template/apps/web/src/composables/useLoginFlow.ts +2 -2
  132. package/template/apps/web/src/composables/usePlatformSettings.ts +1 -1
  133. package/template/apps/web/src/composables/useWatermarkSettings.ts +1 -0
  134. package/template/apps/web/src/layout/AppSidebar.vue +5 -9
  135. package/template/apps/web/src/layout/AppSidebar.webtest.ts +4 -1
  136. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +67 -6
  137. package/template/apps/web/src/pages/auth/LoginPage.vue +3 -12
  138. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +30 -0
  139. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +25 -14
  140. package/template/apps/web/src/pages/configuration/components/TimeZoneSelect.vue +55 -0
  141. package/template/apps/web/src/pages/profile/PersonalSettingsPage.vue +64 -11
  142. package/template/apps/web/src/pages/profile/PersonalSettingsPage.webtest.ts +21 -1
  143. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +7 -18
  144. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +9 -4
  145. package/template/apps/web/src/pages/settings/GlobalSettingsPage.vue +51 -6
  146. package/template/apps/web/src/pages/settings/GlobalSettingsPage.webtest.ts +19 -3
  147. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +1 -0
  148. package/template/apps/web/src/pages/settings/WatermarkYamlTransfer.vue +211 -0
  149. package/template/apps/web/src/pages/system/DiagnosticLookupPage.vue +18 -3
  150. package/template/apps/web/src/pages/system/DiagnosticLookupPage.webtest.ts +4 -1
  151. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +23 -0
  152. package/template/apps/web/src/pages/system/components/DiagnosticLogList.vue +120 -0
  153. package/template/apps/web/src/pages/system/components/OperationEnvironmentPanel.vue +237 -0
  154. package/template/apps/web/src/pages/system/components/OperationFeatureAssessmentPanel.vue +131 -0
  155. package/template/apps/web/src/pages/system/components/PagedSelectionList.vue +31 -24
  156. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +2 -1
  157. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +11 -2
  158. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +1 -0
  159. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +6 -2
  160. package/template/apps/web/src/pages/system/components/agent-skills/AgentSkillImportDialog.vue +18 -9
  161. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +18 -9
  162. package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +66 -34
  163. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +48 -33
  164. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +9 -2
  165. package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +23 -16
  166. package/template/apps/web/src/pages/system/components/dictionary-bulk/DictionaryBulkJobDetail.vue +10 -7
  167. package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +81 -57
  168. package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +6 -4
  169. package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +20 -4
  170. package/template/apps/web/src/pages/system/resource-config.ts +9 -0
  171. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +1 -1
  172. package/template/apps/web/src/router/disabled-feature-loading.webtest.ts +78 -0
  173. package/template/apps/web/src/router/index.ts +3 -6
  174. package/template/apps/web/src/router/manifest-routes.ts +2 -3
  175. package/template/apps/web/src/router/platform-admin-pages.ts +21 -62
  176. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -4
  177. package/template/apps/web/src/services/diagnostics-api.ts +13 -1
  178. package/template/apps/web/src/services/operations-api.ts +4 -0
  179. package/template/apps/web/src/services/platform-settings-api.ts +35 -1
  180. package/template/apps/web/vite.config.ts +9 -3
  181. package/template/deploy/environment/windy.environment.example.yaml +53 -0
  182. package/template/docker-compose.yml +4 -2
  183. package/template/docs/architecture/agent-admin-workbench-implementation.md +1 -1
  184. package/template/docs/architecture/agent-admin-workbench-plan.md +1 -1
  185. package/template/docs/architecture/agent-workflow.md +8 -0
  186. package/template/docs/architecture/durable-jobs.md +17 -0
  187. package/template/docs/architecture/embedded-dsh-integration.md +1 -1
  188. package/template/docs/development/dsh-plugins.md +1 -1
  189. package/template/docs/development/performance-engineering.md +24 -0
  190. package/template/docs/operations/admin-settings-yaml.md +29 -0
  191. package/template/docs/operations/dsh-agent-migration.md +2 -2
  192. package/template/docs/operations/environment-capabilities.md +185 -0
  193. package/template/docs/platform/module-configuration.md +18 -3
  194. package/template/docs/platform/platform-authentication.md +1 -1
  195. package/template/package.json +10 -8
  196. package/template/packages/config/package.json +2 -2
  197. package/template/packages/config/src/definition.ts +14 -4
  198. package/template/packages/config/src/environment-loader.ts +27 -3
  199. package/template/packages/config/src/platform-ai-runtime.test.ts +94 -0
  200. package/template/packages/config/src/platform.ts +5 -2
  201. package/template/packages/database/drizzle/0065_durable_job_lease_snapshot_lookup.sql +1 -0
  202. package/template/packages/database/drizzle/0066_agent_source_sequence_bigint.sql +3 -0
  203. package/template/packages/database/drizzle/0067_media_runtime_access.sql +46 -0
  204. package/template/packages/database/drizzle/meta/0065_snapshot.json +15370 -0
  205. package/template/packages/database/drizzle/meta/0066_snapshot.json +15370 -0
  206. package/template/packages/database/drizzle/meta/0067_snapshot.json +15370 -0
  207. package/template/packages/database/drizzle/meta/_journal.json +21 -0
  208. package/template/packages/database/package.json +3 -3
  209. package/template/packages/database/src/schema/agent-session.ts +4 -3
  210. package/template/packages/database/src/schema/durable-jobs.ts +4 -0
  211. package/template/packages/database/src/schema/index.ts +3 -1
  212. package/template/packages/database/src/schema-durable-recovery.test.ts +21 -1
  213. package/template/packages/database/src/schema-source-sequence.test.ts +24 -0
  214. package/template/packages/example-work-order/package.json +2 -2
  215. package/template/packages/modules/package.json +1 -1
  216. package/template/packages/modules/src/admin-ui-boundary.test.ts +33 -0
  217. package/template/packages/modules/src/composition.ts +11 -28
  218. package/template/packages/modules/src/manifest.ts +2 -0
  219. package/template/packages/modules/src/menu-composition.ts +24 -0
  220. package/template/packages/modules/src/system-admin-routes.ts +2 -2
  221. package/template/packages/modules/src/system-admin-ui-catalog.ts +6 -1
  222. package/template/packages/modules/src/system-ai-api-bindings.ts +0 -16
  223. package/template/packages/modules/src/system-api-control-plane.ts +9 -1
  224. package/template/packages/modules/src/system-api-permissions.ts +7 -0
  225. package/template/packages/modules/src/system-api-watermark.ts +21 -0
  226. package/template/packages/modules/src/system-audit-actions.ts +3 -1
  227. package/template/packages/modules/src/system-features.ts +21 -3
  228. package/template/packages/modules/src/system-media-api-bindings.ts +97 -0
  229. package/template/packages/modules/src/system-module-audit-selection.ts +2 -0
  230. package/template/packages/modules/src/system-module-catalog.test.ts +13 -6
  231. package/template/packages/modules/src/system-module-catalog.ts +6 -241
  232. package/template/packages/modules/src/system-module-definitions.ts +260 -0
  233. package/template/packages/modules/src/system-modules-access.test.ts +2 -0
  234. package/template/packages/modules/src/system-modules.test.ts +7 -1
  235. package/template/packages/modules/src/system-modules.ts +25 -13
  236. package/template/packages/modules/src/system-permissions.ts +12 -0
  237. package/template/packages/server-sdk/package.json +1 -1
  238. package/template/packages/shared/index.ts +3 -0
  239. package/template/packages/shared/package.json +1 -1
  240. package/template/packages/shared/src/environment-assessment.ts +68 -0
  241. package/template/packages/shared/src/environment-policy.ts +205 -0
  242. package/template/packages/shared/src/license-console-capabilities.ts +7 -0
  243. package/template/packages/shared/src/license.ts +1 -0
  244. package/template/packages/shared/src/module-configuration.ts +2 -1
  245. package/template/scripts/operations/collect-device-info.py +230 -0
  246. package/template/apps/server/src/agent/module-ai-port-factory.ts +0 -41
  247. /package/template/apps/server/src/{agent → ai-runtime-security}/execution-grants.ts +0 -0
  248. /package/template/apps/server/src/{agent → ai-runtime-security}/idempotency-key.test.ts +0 -0
  249. /package/template/apps/server/src/{agent → ai-runtime-security}/idempotency-key.ts +0 -0
  250. /package/template/apps/server/src/{agent → ai-runtime-security}/provider-input-governance.test.ts +0 -0
  251. /package/template/apps/server/src/{agent → ai-runtime-security}/provider-input-governance.ts +0 -0
  252. /package/template/apps/server/src/{agent → media-runtime}/required-environment.ts +0 -0
  253. /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
- const parentKey = this.validParentKey(
48
- existing.key,
49
- existing.parentKey,
50
- storedByKey,
51
- )
52
- ? existing.parentKey
53
- : definition.parentKey;
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"),