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.
Files changed (253) hide show
  1. package/dist/cli.js +1769 -1277
  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,99 @@
1
+ <script setup lang="ts">
2
+ import { onBeforeUnmount, shallowRef, watch } from "vue";
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ Sheet,
6
+ SheetContent,
7
+ SheetDescription,
8
+ SheetHeader,
9
+ SheetTitle,
10
+ SheetTrigger,
11
+ } from "@/components/ui/sheet";
12
+
13
+ const props = defineProps<{ machineCode: string }>();
14
+ const visible = shallowRef(false);
15
+ const loading = shallowRef(false);
16
+ const image = shallowRef("");
17
+ const error = shallowRef("");
18
+ let generation = 0;
19
+
20
+ function reset(): void {
21
+ generation++;
22
+ visible.value = false;
23
+ loading.value = false;
24
+ image.value = "";
25
+ error.value = "";
26
+ }
27
+ watch(() => props.machineCode, reset, { flush: "sync" });
28
+ onBeforeUnmount(reset);
29
+
30
+ async function show(): Promise<void> {
31
+ if (loading.value || !props.machineCode) return;
32
+ const current = ++generation;
33
+ const machineCode = props.machineCode;
34
+ visible.value = true;
35
+ loading.value = true;
36
+ error.value = "";
37
+ try {
38
+ const { default: QRCode } = await import("qrcode");
39
+ if (current !== generation) return;
40
+ const data = await QRCode.toDataURL(machineCode, {
41
+ width: 320,
42
+ margin: 4,
43
+ errorCorrectionLevel: "M",
44
+ });
45
+ if (current === generation) image.value = data;
46
+ } catch {
47
+ if (current === generation)
48
+ error.value = "二维码生成失败,请重试或返回授权页复制设备码。";
49
+ } finally {
50
+ if (current === generation) loading.value = false;
51
+ }
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <Sheet
57
+ v-if="machineCode"
58
+ :open="visible"
59
+ @update:open="$event ? show() : reset()"
60
+ >
61
+ <SheetTrigger as-child>
62
+ <Button
63
+ type="button"
64
+ variant="outline"
65
+ size="sm"
66
+ class="mt-2"
67
+ data-testid="machine-code-qr-toggle"
68
+ >
69
+ 显示设备码二维码
70
+ </Button>
71
+ </SheetTrigger>
72
+ <SheetContent class="w-full overflow-y-auto sm:max-w-sm">
73
+ <SheetHeader>
74
+ <SheetTitle>设备码二维码</SheetTitle>
75
+ <SheetDescription
76
+ >请运维扫码获取设备码,用于授权签发。</SheetDescription
77
+ >
78
+ </SheetHeader>
79
+ <div class="space-y-2 px-4 pb-4" aria-live="polite">
80
+ <p v-if="loading" class="text-muted-foreground">正在生成二维码…</p>
81
+ <img
82
+ v-if="image"
83
+ :src="image"
84
+ width="320"
85
+ height="320"
86
+ class="aspect-square h-auto w-80 max-w-full rounded-md border bg-white"
87
+ alt="设备码二维码,供运维扫码读取设备码"
88
+ data-testid="machine-code-qr"
89
+ />
90
+ <template v-if="error">
91
+ <p role="alert" class="text-sm text-destructive">{{ error }}</p>
92
+ <Button type="button" variant="outline" size="sm" @click="show">
93
+ 重试
94
+ </Button>
95
+ </template>
96
+ </div>
97
+ </SheetContent>
98
+ </Sheet>
99
+ </template>
@@ -0,0 +1,83 @@
1
+ import {
2
+ DOMWrapper,
3
+ enableAutoUnmount,
4
+ flushPromises,
5
+ mount,
6
+ } from "@vue/test-utils";
7
+ import { afterEach, beforeEach, expect, test, vi } from "vitest";
8
+ import LicenseMachineCodeQr from "./LicenseMachineCodeQr.vue";
9
+
10
+ enableAutoUnmount(afterEach);
11
+ afterEach(() => document.body.replaceChildren());
12
+ const body = () => new DOMWrapper(document.body);
13
+
14
+ const render = vi.hoisted(() => vi.fn());
15
+ vi.mock("qrcode", () => ({ default: { toDataURL: render } }));
16
+ beforeEach(() =>
17
+ render.mockReset().mockResolvedValue("data:image/png;base64,qr"),
18
+ );
19
+
20
+ test("按需编码完整设备码,关闭抽屉后移除图片", async () => {
21
+ const machineCode = "machine:设备/123-ABC";
22
+ const wrapper = mount(LicenseMachineCodeQr, {
23
+ attachTo: document.body,
24
+ props: { machineCode },
25
+ });
26
+ expect(render).not.toHaveBeenCalled();
27
+ await wrapper.get("button").trigger("click");
28
+ await flushPromises();
29
+ expect(render).toHaveBeenCalledWith(
30
+ machineCode,
31
+ expect.objectContaining({ margin: 4 }),
32
+ );
33
+ expect(body().get("img").attributes("src")).toBe("data:image/png;base64,qr");
34
+ await body().get('[data-slot="sheet-close"]').trigger("click");
35
+ await flushPromises();
36
+ expect(body().find("img").exists()).toBe(false);
37
+ wrapper.unmount();
38
+ });
39
+
40
+ test("设备码变化后丢弃未完成的旧二维码", async () => {
41
+ let finish: (value: string) => void = () => {};
42
+ render.mockImplementationOnce(
43
+ () =>
44
+ new Promise<string>((resolve) => {
45
+ finish = resolve;
46
+ }),
47
+ );
48
+ const wrapper = mount(LicenseMachineCodeQr, {
49
+ attachTo: document.body,
50
+ props: { machineCode: "old" },
51
+ });
52
+ await wrapper.get("button").trigger("click");
53
+ await flushPromises();
54
+ await wrapper.setProps({ machineCode: "new" });
55
+ finish("data:image/png;base64,old");
56
+ await flushPromises();
57
+ expect(body().find("img").exists()).toBe(false);
58
+ await wrapper.get("button").trigger("click");
59
+ await flushPromises();
60
+ expect(render).toHaveBeenLastCalledWith("new", expect.any(Object));
61
+ wrapper.unmount();
62
+ });
63
+
64
+ test("生成失败允许重试,空设备码无入口", async () => {
65
+ render.mockRejectedValueOnce(new Error("failed"));
66
+ const wrapper = mount(LicenseMachineCodeQr, {
67
+ attachTo: document.body,
68
+ props: { machineCode: "machine" },
69
+ });
70
+ await wrapper.get("button").trigger("click");
71
+ await flushPromises();
72
+ expect(body().get('[role="alert"]').text()).toContain("生成失败");
73
+ await body()
74
+ .findAll("button")
75
+ .find((button) => button.text() === "重试")!
76
+ .trigger("click");
77
+ await flushPromises();
78
+ expect(body().find("img").exists()).toBe(true);
79
+ await wrapper.setProps({ machineCode: "" });
80
+ expect(wrapper.find("button").exists()).toBe(false);
81
+ expect(body().find("img").exists()).toBe(false);
82
+ wrapper.unmount();
83
+ });
@@ -30,7 +30,7 @@ const statusText = computed(() => {
30
30
  if (status === "consumed") return "配对激活完成,新 License 已生效。";
31
31
  return model.session.value
32
32
  ? "等待手机扫描并完成签发…"
33
- : "客户端无需连接互联网;手机扫码签发后会自动回传。";
33
+ : "客户服务器无需访问互联网。手机须能访问签发中心和本系统的内网 HTTPS 地址,才能扫码签发并回传。";
34
34
  });
35
35
 
36
36
  async function confirm(): Promise<void> {
@@ -1,14 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
+ import { usePlatformSettings } from "@/composables/usePlatformSettings";
3
4
  import { Badge } from "@/components/ui/badge";
4
5
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
6
  import type { PlatformLicenseSnapshot } from "@/services/license-api";
7
+ import LicenseMachineCodeQr from "./LicenseMachineCodeQr.vue";
6
8
  import { formatLicenseDateTime } from "./license-display";
7
9
 
8
10
  const props = defineProps<{
9
11
  snapshot: PlatformLicenseSnapshot;
10
12
  }>();
11
13
 
14
+ const { softwareName } = usePlatformSettings();
15
+
12
16
  const statusLabel = computed(() => {
13
17
  const labels: Record<PlatformLicenseSnapshot["status"], string> = {
14
18
  valid: "有效",
@@ -53,10 +57,18 @@ const expiresAtLabel = computed(() =>
53
57
  <p class="break-all font-mono" data-testid="machine-code">
54
58
  {{ snapshot.machineCode }}
55
59
  </p>
60
+ <LicenseMachineCodeQr
61
+ v-if="snapshot.machineCode"
62
+ :machine-code="snapshot.machineCode"
63
+ />
64
+ </div>
65
+ <div v-if="snapshot.canViewDetails">
66
+ <p class="text-muted-foreground">软件名称</p>
67
+ <p>{{ softwareName }}</p>
56
68
  </div>
57
69
  <div v-if="snapshot.canViewDetails">
58
- <p class="text-muted-foreground">产品</p>
59
- <p>{{ snapshot.productCode }}</p>
70
+ <p class="text-muted-foreground">产品标识</p>
71
+ <p class="break-all font-mono">{{ snapshot.productCode }}</p>
60
72
  </div>
61
73
  <template v-if="snapshot.canViewDetails && snapshot.license">
62
74
  <div>
@@ -26,7 +26,7 @@ export function useLoginFlow() {
26
26
  // @windy-module system.license end
27
27
  resetSessionContext();
28
28
  const returnTo = safeReturnTo(route.query.returnTo);
29
- // @windy-module system.agent begin
29
+ // @windy-module system.agent-ui begin
30
30
  if (
31
31
  new URL(returnTo, window.location.origin).pathname.replace(
32
32
  /\/$/,
@@ -36,7 +36,7 @@ export function useLoginFlow() {
36
36
  window.location.assign(returnTo);
37
37
  return;
38
38
  }
39
- // @windy-module system.agent end
39
+ // @windy-module system.agent-ui end
40
40
  await router.replace(returnTo);
41
41
  } catch {
42
42
  submitError.value = auth.error.value || "登录失败,请稍后重试";
@@ -9,7 +9,7 @@ import {
9
9
  // @windy-module system.settings end
10
10
 
11
11
  const fallback: PlatformBrandingSettings = {
12
- softwareName: "Windy Platform",
12
+ softwareName: "Windy",
13
13
  timeZone: "Asia/Shanghai",
14
14
  };
15
15
  const branding = shallowRef<PlatformBrandingSettings>({ ...fallback });
@@ -44,6 +44,7 @@ export function useWatermarkSettings(
44
44
  refresh: () => load(true, "public"),
45
45
  loadManaged: () => load(true, "managed"),
46
46
  save,
47
+ applyImportedPolicy: applyPolicy,
47
48
  };
48
49
  }
49
50
 
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import windyLogo from "@/assets/windy.svg";
2
3
  import type { MenuNode } from "@southwind-ai/shared";
3
4
  import {
4
5
  ActivityIcon,
@@ -16,6 +17,7 @@ import {
16
17
  PanelLeftIcon,
17
18
  SettingsIcon,
18
19
  ShieldCheckIcon,
20
+ SparklesIcon,
19
21
  StampIcon,
20
22
  TicketIcon,
21
23
  UsersIcon,
@@ -62,6 +64,7 @@ const iconMap: Record<string, Component> = {
62
64
  settings: SettingsIcon,
63
65
  users: UsersIcon,
64
66
  "shield-check": ShieldCheckIcon,
67
+ sparkles: SparklesIcon,
65
68
  stamp: StampIcon,
66
69
  flag: FlagIcon,
67
70
  "key-round": KeyRoundIcon,
@@ -196,20 +199,13 @@ function findMenu(menus: MenuNode[], key: string): MenuNode | undefined {
196
199
  >
197
200
  <div
198
201
  data-testid="sidebar-brand-logo"
199
- class="flex size-8 max-w-full shrink-0 items-center justify-center overflow-hidden rounded-lg [&>svg]:size-5 group-data-[collapsible=icon]:size-4 group-data-[collapsible=icon]:rounded-sm group-data-[collapsible=icon]:[&>svg]:size-4"
200
- :class="
201
- platform.branding.value.logoUrl
202
- ? 'bg-transparent text-sidebar-foreground'
203
- : 'bg-primary text-primary-foreground'
204
- "
202
+ class="flex size-8 max-w-full shrink-0 items-center justify-center overflow-hidden rounded-lg group-data-[collapsible=icon]:size-4 group-data-[collapsible=icon]:rounded-sm"
205
203
  >
206
204
  <img
207
- v-if="platform.branding.value.logoUrl"
208
- :src="platform.branding.value.logoUrl"
205
+ :src="platform.branding.value.logoUrl || windyLogo"
209
206
  alt=""
210
207
  class="size-full object-contain"
211
208
  />
212
- <BlocksIcon v-else />
213
209
  </div>
214
210
  <div class="min-w-0 group-data-[collapsible=icon]:hidden">
215
211
  <p class="truncate text-sm font-semibold">
@@ -96,7 +96,10 @@ describe("AppSidebar", () => {
96
96
  expect(brandLink.classes()).toContain("group-data-[collapsible=icon]:p-0");
97
97
  expect(logo.classes()).toContain("group-data-[collapsible=icon]:size-4");
98
98
  expect(logo.classes()).toContain("overflow-hidden");
99
- expect(logo.classes()).toContain("bg-transparent");
99
+ expect(logo.classes().filter((name) => name.startsWith("bg-"))).toEqual([]);
100
+ expect(logo.get("img").attributes("src")).toBe(
101
+ state.branding.value.logoUrl,
102
+ );
100
103
  expect(logo.classes()).not.toContain("bg-primary");
101
104
  wrapper.unmount();
102
105
  });
@@ -1,7 +1,16 @@
1
1
  <script setup lang="ts">
2
- import { useRouter } from "vue-router";
3
- import { onMounted, shallowRef } from "vue";
2
+ import { onBeforeRouteLeave, RouterLink, useRouter } from "vue-router";
3
+ import {
4
+ computed,
5
+ onBeforeUnmount,
6
+ onMounted,
7
+ shallowRef,
8
+ useTemplateRef,
9
+ } from "vue";
4
10
  import { PASSWORD_POLICY, type PasswordPolicy } from "@southwind-ai/shared";
11
+ import { Button } from "@/components/ui/button";
12
+ import UnsavedChangesDialog from "@/components/common/UnsavedChangesDialog.vue";
13
+ import { useUnsavedChangesConfirmation } from "@/composables/useUnsavedChangesConfirmation";
5
14
  import PasswordChangeForm from "@/components/auth/PasswordChangeForm.vue";
6
15
  import {
7
16
  Card,
@@ -22,7 +31,42 @@ const auth = useAuthSession();
22
31
  const router = useRouter();
23
32
  const policy = shallowRef<PasswordPolicy>(PASSWORD_POLICY);
24
33
 
34
+ const form = useTemplateRef("passwordForm");
35
+ const confirmation = useUnsavedChangesConfirmation();
36
+ const required = computed(
37
+ () => auth.actor.value?.credentialState === "must-change-password",
38
+ );
39
+ const completed = shallowRef(false);
40
+ const confirmationError = shallowRef<string>();
41
+ const loginDestination = { name: "login", query: { passwordChanged: "1" } };
42
+
43
+ onBeforeRouteLeave(async () => {
44
+ if (flow.loading.value) return false;
45
+ if (completed.value || !form.value?.isDirty()) return true;
46
+ confirmationError.value = undefined;
47
+ const saved = await confirmation.request(async () => {
48
+ const input = form.value?.getInput();
49
+ if (!input) {
50
+ confirmationError.value = "请取消并补全当前密码、新密码与确认密码。";
51
+ return false;
52
+ }
53
+ const saved = await save(input);
54
+ if (!saved) confirmationError.value = flow.submitError.value;
55
+ return saved;
56
+ });
57
+ return saved ? loginDestination : false;
58
+ });
59
+
60
+ function beforeUnload(event: BeforeUnloadEvent) {
61
+ if (completed.value || !form.value?.isDirty()) return;
62
+ event.preventDefault();
63
+ event.returnValue = "";
64
+ }
65
+
66
+ onBeforeUnmount(() => window.removeEventListener("beforeunload", beforeUnload));
67
+
25
68
  onMounted(async () => {
69
+ window.addEventListener("beforeunload", beforeUnload);
26
70
  try {
27
71
  policy.value = await loadPasswordPolicy();
28
72
  } catch {
@@ -30,10 +74,15 @@ onMounted(async () => {
30
74
  }
31
75
  });
32
76
 
33
- async function submit(input: PasswordChangeInput) {
34
- if (!(await flow.submit(input))) return;
77
+ async function save(input: PasswordChangeInput) {
78
+ if (!(await flow.submit(input))) return false;
79
+ completed.value = true;
35
80
  auth.clear();
36
- await router.replace({ name: "login", query: { passwordChanged: "1" } });
81
+ return true;
82
+ }
83
+
84
+ async function submit(input: PasswordChangeInput) {
85
+ if (await save(input)) await router.replace(loginDestination);
37
86
  }
38
87
  </script>
39
88
 
@@ -45,17 +94,29 @@ async function submit(input: PasswordChangeInput) {
45
94
  <CardHeader>
46
95
  <CardTitle>修改登录密码</CardTitle>
47
96
  <CardDescription>
48
- 如密码由管理员重置,请先完成修改。成功后全部会话都会退出,请使用新密码重新登录。
97
+ <span v-if="required">请先完成密码修改。</span>
98
+ 成功后全部会话都会退出,请使用新密码重新登录。
49
99
  </CardDescription>
50
100
  </CardHeader>
51
101
  <CardContent>
52
102
  <PasswordChangeForm
103
+ ref="passwordForm"
53
104
  :loading="flow.loading.value"
54
105
  :error="flow.submitError.value"
55
106
  :policy="policy"
56
107
  @submit="submit"
57
108
  />
109
+ <Button v-if="!required" as-child variant="ghost" class="mt-3 w-full">
110
+ <RouterLink to="/admin/profile">返回个人设置</RouterLink>
111
+ </Button>
58
112
  </CardContent>
59
113
  </Card>
114
+ <UnsavedChangesDialog
115
+ :open="confirmation.open.value"
116
+ :saving="confirmation.saving.value"
117
+ :description="confirmationError"
118
+ @cancel="confirmation.cancel"
119
+ @save="confirmation.save"
120
+ />
60
121
  </main>
61
122
  </template>
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import windyLogo from "@/assets/windy.svg";
2
3
  import DevelopmentAuthWarning from "@/components/auth/DevelopmentAuthWarning.vue";
3
4
  import LoginForm from "@/components/auth/LoginForm.vue";
4
5
  import {
@@ -10,7 +11,6 @@ import {
10
11
  } from "@/components/ui/card";
11
12
  import { useLoginFlow } from "@/composables/useLoginFlow";
12
13
  import { usePlatformSettings } from "@/composables/usePlatformSettings";
13
- import { BlocksIcon } from "@lucide/vue";
14
14
  import { developmentAuthStatus } from "@/services/http";
15
15
 
16
16
  const flow = useLoginFlow();
@@ -24,21 +24,12 @@ const developmentAuth = developmentAuthStatus();
24
24
  >
25
25
  <div class="w-full max-w-md space-y-4">
26
26
  <div class="flex items-center justify-center gap-3">
27
- <span
28
- class="flex size-10 items-center justify-center rounded-xl"
29
- :class="
30
- platform.branding.value.logoUrl
31
- ? 'bg-transparent text-foreground'
32
- : 'bg-primary text-primary-foreground'
33
- "
34
- >
27
+ <span class="flex size-10 items-center justify-center rounded-xl">
35
28
  <img
36
- v-if="platform.branding.value.logoUrl"
37
- :src="platform.branding.value.logoUrl"
29
+ :src="platform.branding.value.logoUrl || windyLogo"
38
30
  alt=""
39
31
  class="size-full rounded-xl object-contain"
40
32
  />
41
- <BlocksIcon v-else />
42
33
  </span>
43
34
  <div>
44
35
  <p class="text-lg font-semibold">{{ platform.softwareName.value }}</p>
@@ -15,6 +15,7 @@ import {
15
15
  } from "@/components/ui/card";
16
16
  import { Input } from "@/components/ui/input";
17
17
  import { Switch } from "@/components/ui/switch";
18
+ import TimeZoneSelect from "./TimeZoneSelect.vue";
18
19
 
19
20
  const props = defineProps<{
20
21
  summary: ModuleConfigSummary;
@@ -35,6 +36,17 @@ function update(path: string, value: ModuleConfigValue) {
35
36
  emit("update:values", { ...props.values, [path]: value });
36
37
  }
37
38
 
39
+ const sizeFormatter = new Intl.NumberFormat("zh-CN", {
40
+ maximumFractionDigits: 2,
41
+ });
42
+
43
+ function formatByteSize(value: ModuleConfigValue | undefined): string {
44
+ if (typeof value !== "number") return "";
45
+ return value >= 1024 ** 2
46
+ ? `${sizeFormatter.format(value / 1024 ** 2)} MiB`
47
+ : `${sizeFormatter.format(value / 1024)} KiB`;
48
+ }
49
+
38
50
  function updateInteger(path: string, event: Event) {
39
51
  const value = Number((event.currentTarget as HTMLInputElement).value);
40
52
  if (Number.isSafeInteger(value)) update(path, value);
@@ -71,6 +83,14 @@ function updateInteger(path: string, event: Event) {
71
83
  <p class="text-xs text-muted-foreground">
72
84
  {{ field.description }}
73
85
  </p>
86
+ <p
87
+ v-if="field.unit === 'bytes'"
88
+ :id="`config-unit-${field.path}`"
89
+ class="text-xs text-muted-foreground"
90
+ >
91
+ 输入单位为字节,当前输入约为
92
+ {{ formatByteSize(values[field.path]) }}。
93
+ </p>
74
94
  <p class="text-xs font-medium text-muted-foreground">
75
95
  {{
76
96
  field.applyMode === "restart"
@@ -91,12 +111,22 @@ function updateInteger(path: string, event: Event) {
91
111
  v-if="field.input === 'integer'"
92
112
  :id="`config-${field.path}`"
93
113
  type="number"
114
+ :aria-describedby="
115
+ field.unit === 'bytes' ? `config-unit-${field.path}` : undefined
116
+ "
94
117
  :min="field.minimum"
95
118
  :max="field.maximum"
96
119
  :model-value="String(values[field.path] ?? '')"
97
120
  :disabled="!canManage || saving"
98
121
  @input="updateInteger(field.path, $event)"
99
122
  />
123
+ <TimeZoneSelect
124
+ v-if="field.input === 'time-zone'"
125
+ :id="`config-${field.path}`"
126
+ :model-value="String(values[field.path] ?? '')"
127
+ :disabled="!canManage || saving"
128
+ @update:model-value="update(field.path, $event)"
129
+ />
100
130
  <Input
101
131
  v-if="field.input === 'text'"
102
132
  :id="`config-${field.path}`"
@@ -1,4 +1,12 @@
1
1
  <script setup lang="ts">
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectTrigger,
7
+ SelectValue,
8
+ } from "@/components/ui/select";
9
+
2
10
  import type {
3
11
  ModuleConfigValues,
4
12
  ModuleConfigVersion,
@@ -64,8 +72,7 @@ async function refresh() {
64
72
  }
65
73
  }
66
74
 
67
- async function requestModule(event: Event) {
68
- const moduleKey = (event.currentTarget as HTMLSelectElement).value;
75
+ async function requestModule(moduleKey: string) {
69
76
  if (moduleKey === configurations.selectedModuleKey.value) return;
70
77
  if (!(await confirmChanges())) return;
71
78
  try {
@@ -193,20 +200,24 @@ function serialize(current: ModuleConfigValues): string {
193
200
  >
194
201
  <label class="grid min-w-72 gap-2 text-sm font-medium">
195
202
  <span>配置模块</span>
196
- <select
197
- class="h-9 rounded-md border border-input bg-background px-3 text-sm"
198
- :value="configurations.selectedModuleKey.value"
203
+ <Select
204
+ :model-value="configurations.selectedModuleKey.value"
199
205
  :disabled="configurations.loading.value"
200
- @change="requestModule"
206
+ @update:model-value="requestModule(String($event))"
201
207
  >
202
- <option
203
- v-for="summary in configurations.summaries.value"
204
- :key="summary.moduleKey"
205
- :value="summary.moduleKey"
206
- >
207
- {{ summary.label }}({{ summary.moduleKey }})
208
- </option>
209
- </select>
208
+ <SelectTrigger aria-label="配置模块" class="w-full"
209
+ ><SelectValue placeholder="选择配置模块"
210
+ /></SelectTrigger>
211
+ <SelectContent>
212
+ <SelectItem
213
+ v-for="summary in configurations.summaries.value"
214
+ :key="summary.moduleKey"
215
+ :value="summary.moduleKey"
216
+ >
217
+ {{ summary.label }}({{ summary.moduleKey }})
218
+ </SelectItem>
219
+ </SelectContent>
220
+ </Select>
210
221
  </label>
211
222
  <button
212
223
  type="button"
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ const props = defineProps<{
5
+ id: string;
6
+ modelValue: string;
7
+ disabled?: boolean;
8
+ }>();
9
+ const emit = defineEmits<{ "update:modelValue": [value: string] }>();
10
+
11
+ const now = new Date();
12
+ const supportedZones = Intl.supportedValuesOf?.("timeZone") ?? [];
13
+ const options = computed(() => {
14
+ const zones = new Set(["UTC", "Asia/Shanghai", ...supportedZones]);
15
+ if (props.modelValue) zones.add(props.modelValue);
16
+
17
+ return [...zones].sort().map((zone) => ({
18
+ value: zone,
19
+ label: formatZone(zone),
20
+ }));
21
+ });
22
+
23
+ function formatZone(zone: string): string {
24
+ try {
25
+ const offset = new Intl.DateTimeFormat("en", {
26
+ timeZone: zone,
27
+ timeZoneName: "longOffset",
28
+ })
29
+ .formatToParts(now)
30
+ .find(({ type }) => type === "timeZoneName")?.value;
31
+ return offset ? `${zone}(${offset.replace("GMT", "UTC")})` : zone;
32
+ } catch {
33
+ return zone;
34
+ }
35
+ }
36
+ </script>
37
+
38
+ <template>
39
+ <select
40
+ :id="id"
41
+ :value="modelValue"
42
+ :disabled="disabled"
43
+ class="h-9 w-full rounded-md border border-input bg-background px-3 text-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
44
+ @change="
45
+ emit(
46
+ 'update:modelValue',
47
+ ($event.currentTarget as HTMLSelectElement).value,
48
+ )
49
+ "
50
+ >
51
+ <option v-for="option in options" :key="option.value" :value="option.value">
52
+ {{ option.label }}
53
+ </option>
54
+ </select>
55
+ </template>