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
@@ -1,11 +1,15 @@
1
1
  <script setup lang="ts">
2
- import { computed, onMounted, shallowRef } from "vue";
2
+ import { computed, onBeforeUnmount, onMounted, shallowRef } from "vue";
3
+ import { onBeforeRouteLeave, RouterLink } from "vue-router";
3
4
  import {
4
5
  AtSignIcon,
6
+ KeyRoundIcon,
5
7
  LoaderCircleIcon,
6
8
  SaveIcon,
7
9
  UserRoundCogIcon,
8
10
  } from "@lucide/vue";
11
+ import UnsavedChangesDialog from "@/components/common/UnsavedChangesDialog.vue";
12
+ import { useUnsavedChangesConfirmation } from "@/composables/useUnsavedChangesConfirmation";
9
13
  import { Button } from "@/components/ui/button";
10
14
  import { Input } from "@/components/ui/input";
11
15
  import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
@@ -33,7 +37,33 @@ const canManage = computed(
33
37
  ) ?? false,
34
38
  );
35
39
 
36
- onMounted(load);
40
+ const confirmation = useUnsavedChangesConfirmation();
41
+ const dirty = computed(() => {
42
+ const saved = profile.value;
43
+ return Boolean(
44
+ saved &&
45
+ (displayName.value !== saved.displayName ||
46
+ email.value !== (saved.email || "") ||
47
+ phone.value !== (saved.phone || "")),
48
+ );
49
+ });
50
+
51
+ onBeforeRouteLeave(() => {
52
+ if (saving.value) return false;
53
+ return dirty.value ? confirmation.request(save) : true;
54
+ });
55
+
56
+ function beforeUnload(event: BeforeUnloadEvent) {
57
+ if (!dirty.value) return;
58
+ event.preventDefault();
59
+ event.returnValue = "";
60
+ }
61
+
62
+ onMounted(() => {
63
+ window.addEventListener("beforeunload", beforeUnload);
64
+ void load();
65
+ });
66
+ onBeforeUnmount(() => window.removeEventListener("beforeunload", beforeUnload));
37
67
 
38
68
  async function load() {
39
69
  loading.value = true;
@@ -46,11 +76,12 @@ async function load() {
46
76
  }
47
77
  }
48
78
 
49
- async function save() {
79
+ async function save(): Promise<boolean> {
80
+ if (!canManage.value || saving.value) return false;
50
81
  const name = displayName.value.trim();
51
82
  if (!name) {
52
83
  toast.error("姓名不能为空");
53
- return;
84
+ return false;
54
85
  }
55
86
  saving.value = true;
56
87
  try {
@@ -61,10 +92,12 @@ async function save() {
61
92
  phone: phone.value.trim() || undefined,
62
93
  }),
63
94
  );
64
- await Promise.all([auth.refresh(), access.refresh()]);
95
+ await Promise.allSettled([auth.refresh(), access.refresh()]);
65
96
  toast.success("个人资料已保存");
97
+ return true;
66
98
  } catch (error) {
67
99
  toast.error("个人资料保存失败", { description: messageOf(error) });
100
+ return false;
68
101
  } finally {
69
102
  saving.value = false;
70
103
  }
@@ -86,9 +119,7 @@ function messageOf(error: unknown): string {
86
119
  <main class="mx-auto grid w-full max-w-3xl gap-6">
87
120
  <header class="space-y-2">
88
121
  <h1 class="text-3xl font-semibold tracking-tight">个人设置</h1>
89
- <p class="text-muted-foreground">
90
- 维护当前账号的姓名与联系方式;账号名由管理员统一管理。
91
- </p>
122
+ <p class="text-muted-foreground">维护当前账号的个人资料与登录密码。</p>
92
123
  </header>
93
124
 
94
125
  <section
@@ -126,7 +157,7 @@ function messageOf(error: unknown): string {
126
157
  <span>姓名</span>
127
158
  <Input
128
159
  v-model="displayName"
129
- :disabled="!canManage"
160
+ :disabled="!canManage || saving"
130
161
  maxlength="120"
131
162
  autocomplete="name"
132
163
  />
@@ -136,7 +167,7 @@ function messageOf(error: unknown): string {
136
167
  <Input
137
168
  v-model="email"
138
169
  type="email"
139
- :disabled="!canManage"
170
+ :disabled="!canManage || saving"
140
171
  maxlength="160"
141
172
  autocomplete="email"
142
173
  placeholder="name@example.com"
@@ -147,7 +178,7 @@ function messageOf(error: unknown): string {
147
178
  <Input
148
179
  v-model="phone"
149
180
  type="tel"
150
- :disabled="!canManage"
181
+ :disabled="!canManage || saving"
151
182
  maxlength="32"
152
183
  autocomplete="tel"
153
184
  placeholder="请输入联系电话"
@@ -173,5 +204,27 @@ function messageOf(error: unknown): string {
173
204
  <Button variant="outline" @click="load">重新加载</Button>
174
205
  </div>
175
206
  </section>
207
+ <section
208
+ class="flex flex-wrap items-center justify-between gap-4 rounded-xl border bg-card p-6 text-card-foreground shadow-sm"
209
+ >
210
+ <div class="space-y-1">
211
+ <h2 class="font-semibold">登录密码</h2>
212
+ <p class="text-sm text-muted-foreground">
213
+ 修改成功后,所有会话都会退出。
214
+ </p>
215
+ </div>
216
+ <Button as-child variant="outline">
217
+ <RouterLink :to="{ name: 'change-password' }">
218
+ <KeyRoundIcon data-icon="inline-start" />
219
+ 修改密码
220
+ </RouterLink>
221
+ </Button>
222
+ </section>
223
+ <UnsavedChangesDialog
224
+ :open="confirmation.open.value"
225
+ :saving="confirmation.saving.value"
226
+ @cancel="confirmation.cancel"
227
+ @save="confirmation.save"
228
+ />
176
229
  </main>
177
230
  </template>
@@ -1,5 +1,6 @@
1
1
  import { beforeEach, describe, expect, test, vi } from "vitest";
2
2
  import { flushPromises, mount } from "@vue/test-utils";
3
+ import { createMemoryHistory, createRouter } from "vue-router";
3
4
  import PersonalSettingsPage from "./PersonalSettingsPage.vue";
4
5
 
5
6
  const mocks = vi.hoisted(() => ({
@@ -55,7 +56,22 @@ describe("PersonalSettingsPage", () => {
55
56
  });
56
57
 
57
58
  test("加载资料后保存修改并刷新会话与授权快照", async () => {
58
- const wrapper = mount(PersonalSettingsPage);
59
+ const router = createRouter({
60
+ history: createMemoryHistory(),
61
+ routes: [
62
+ { path: "/profile", component: { template: "<div />" } },
63
+ {
64
+ path: "/change-password",
65
+ name: "change-password",
66
+ component: { template: "<div />" },
67
+ },
68
+ ],
69
+ });
70
+ await router.push("/profile");
71
+ await router.isReady();
72
+ const wrapper = mount(PersonalSettingsPage, {
73
+ global: { plugins: [router] },
74
+ });
59
75
  await flushPromises();
60
76
  const inputs = wrapper.findAll("input");
61
77
 
@@ -74,5 +90,9 @@ describe("PersonalSettingsPage", () => {
74
90
  expect(mocks.refreshAuth).toHaveBeenCalledOnce();
75
91
  expect(mocks.refreshAccess).toHaveBeenCalledOnce();
76
92
  expect(mocks.success).toHaveBeenCalledWith("个人资料已保存");
93
+ await wrapper.get('a[href="/change-password"]').trigger("click");
94
+ await flushPromises();
95
+ expect(router.currentRoute.value.name).toBe("change-password");
96
+ wrapper.unmount();
77
97
  });
78
98
  });
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { ImageIcon, SettingsIcon, Trash2Icon, UploadIcon } from "@lucide/vue";
2
+ import windyLogo from "@/assets/windy.svg";
3
+ import { SettingsIcon, Trash2Icon, UploadIcon } from "@lucide/vue";
3
4
  import { computed, onMounted, shallowRef } from "vue";
4
5
  import { Button } from "@/components/ui/button";
5
6
  import { Input } from "@/components/ui/input";
@@ -9,6 +10,7 @@ import { usePlatformSettings } from "@/composables/usePlatformSettings";
9
10
  import { maxLocalLogoBytes, readLocalLogo } from "./logo-file";
10
11
 
11
12
  const settings = usePlatformSettings();
13
+ const { saving } = settings;
12
14
  const access = useAccessSnapshot();
13
15
  const toast = useAppToast();
14
16
  const softwareName = shallowRef("");
@@ -172,23 +174,16 @@ defineExpose({
172
174
  <div class="flex items-center gap-3">
173
175
  <div
174
176
  class="flex size-12 items-center justify-center overflow-hidden rounded-lg"
175
- :class="
176
- logoUrl
177
- ? 'bg-transparent text-foreground'
178
- : 'bg-primary text-primary-foreground'
179
- "
180
177
  >
181
178
  <img
182
- v-if="logoUrl"
183
- :src="logoUrl"
179
+ :src="logoUrl || windyLogo"
184
180
  alt="Logo 预览"
185
181
  class="size-full object-contain"
186
182
  />
187
- <ImageIcon v-else class="size-5" />
188
183
  </div>
189
184
  <div class="min-w-0">
190
185
  <p class="truncate font-semibold">
191
- {{ softwareName || "Windy Platform" }}
186
+ {{ softwareName || "Windy" }}
192
187
  </p>
193
188
  <p class="line-clamp-2 text-sm text-muted-foreground">
194
189
  {{ description || "企业级 Dashboard 产品基座" }}
@@ -200,14 +195,8 @@ defineExpose({
200
195
  <p v-if="!canManage" class="text-sm text-muted-foreground">
201
196
  当前账号只有查看权限。
202
197
  </p>
203
- <Button
204
- v-else
205
- :disabled="settings.saving.value || readingLogo"
206
- @click="save"
207
- >
208
- <SettingsIcon />{{
209
- settings.saving.value ? "保存中…" : "保存基本信息"
210
- }}
198
+ <Button v-else :disabled="saving || readingLogo" @click="save">
199
+ <SettingsIcon />{{ saving ? "保存中…" : "保存基本信息" }}
211
200
  </Button>
212
201
  </div>
213
202
  </div>
@@ -1,8 +1,10 @@
1
- import { flushPromises, mount } from "@vue/test-utils";
2
- import { beforeEach, describe, expect, test, vi } from "vitest";
1
+ import { enableAutoUnmount, flushPromises, mount } from "@vue/test-utils";
2
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
3
3
  import { shallowRef } from "vue";
4
4
  import BrandingSettingsSection from "./BrandingSettingsSection.vue";
5
5
 
6
+ enableAutoUnmount(afterEach);
7
+
6
8
  const mocks = vi.hoisted(() => ({
7
9
  refresh: vi.fn(),
8
10
  save: vi.fn(),
@@ -59,8 +61,11 @@ describe("品牌基本信息", () => {
59
61
  );
60
62
  expect(wrapper.text()).toContain("编码内容不在输入框中显示");
61
63
  const preview = wrapper.get('img[alt="Logo 预览"]').element.parentElement;
62
- expect(preview?.classList.contains("bg-transparent")).toBe(true);
63
- expect(preview?.classList.contains("bg-primary")).toBe(false);
64
+ expect(
65
+ [...preview!.classList].filter(
66
+ (name) => name.startsWith("bg-") && name !== "bg-transparent",
67
+ ),
68
+ ).toEqual([]);
64
69
  });
65
70
 
66
71
  test("外部或站内 Logo 地址仍可编辑", async () => {
@@ -8,12 +8,17 @@ import {
8
8
  } from "vue-router";
9
9
  import UnsavedChangesDialog from "@/components/common/UnsavedChangesDialog.vue";
10
10
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
11
+ import type { PlatformWatermarkPolicy } from "@southwind-ai/shared";
12
+ import { useAuthSession } from "@/composables/useAuthSession";
13
+ import { useWatermarkSettings } from "@/composables/useWatermarkSettings";
11
14
  import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
12
15
  import { useUnsavedChangesConfirmation } from "@/composables/useUnsavedChangesConfirmation";
13
16
  import { evaluateWebAccess } from "@/layout/navigation";
14
17
  import BrandingSettingsSection from "./BrandingSettingsSection.vue";
15
18
  import WatermarkSettingsSection from "./WatermarkSettingsSection.vue";
16
19
 
20
+ import WatermarkYamlTransfer from "./WatermarkYamlTransfer.vue";
21
+
17
22
  type SettingsTab = "basic" | "watermark";
18
23
 
19
24
  interface SettingsSectionHandle {
@@ -24,11 +29,19 @@ interface SettingsSectionHandle {
24
29
  const route = useRoute();
25
30
  const router = useRouter();
26
31
  const access = useAccessSnapshot();
32
+ const watermark = useWatermarkSettings(useAuthSession().actor);
27
33
  const confirmation = useUnsavedChangesConfirmation();
28
34
  const confirmOpen = confirmation.open;
29
35
  const confirmSaving = confirmation.saving;
30
36
  const basicSection = shallowRef<SettingsSectionHandle | null>(null);
31
- const watermarkSection = shallowRef<SettingsSectionHandle | null>(null);
37
+ const watermarkSection = shallowRef<
38
+ (SettingsSectionHandle & { applyPolicyToForm(): void }) | null
39
+ >(null);
40
+
41
+ function onYamlImported(policy: PlatformWatermarkPolicy) {
42
+ watermark.applyImportedPolicy(policy);
43
+ watermarkSection.value?.applyPolicyToForm();
44
+ }
32
45
  const canReadBasic = computed(() => {
33
46
  const snapshot = access.snapshot.value;
34
47
  return Boolean(
@@ -49,6 +62,19 @@ const canReadWatermark = computed(() => {
49
62
  }).allowed,
50
63
  );
51
64
  });
65
+ const canManageWatermark = computed(() => {
66
+ const snapshot = access.snapshot.value;
67
+ return Boolean(
68
+ snapshot &&
69
+ evaluateWebAccess(snapshot, {
70
+ permissionKey: "system.watermark.manage",
71
+ featureKey: "system.watermark",
72
+ }).allowed,
73
+ );
74
+ });
75
+ const canOpenBasic = computed(
76
+ () => canReadBasic.value || canReadWatermark.value,
77
+ );
52
78
  const activeTab = shallowRef<SettingsTab | undefined>(resolveTab(route.hash));
53
79
  onBeforeRouteUpdate(async (to) => {
54
80
  const target = resolveTab(to.hash);
@@ -100,14 +126,14 @@ async function normalizeCurrentHash() {
100
126
 
101
127
  function resolveTab(hash: string): SettingsTab | undefined {
102
128
  if (hash === "#watermark" && canReadWatermark.value) return "watermark";
103
- if (hash === "#basic" && canReadBasic.value) return "basic";
129
+ if (hash === "#basic" && canOpenBasic.value) return "basic";
104
130
  if (canReadBasic.value) return "basic";
105
131
  if (canReadWatermark.value) return "watermark";
106
132
  return undefined;
107
133
  }
108
134
 
109
135
  function isAccessible(tab: SettingsTab) {
110
- return tab === "basic" ? canReadBasic.value : canReadWatermark.value;
136
+ return tab === "basic" ? canOpenBasic.value : canReadWatermark.value;
111
137
  }
112
138
 
113
139
  function tabHash(tab: SettingsTab) {
@@ -158,7 +184,7 @@ function handleBeforeUnload(event: BeforeUnloadEvent) {
158
184
  class="h-auto w-full justify-start border-b px-0 pb-2"
159
185
  >
160
186
  <TabsTrigger
161
- v-if="canReadBasic"
187
+ v-if="canOpenBasic"
162
188
  value="basic"
163
189
  class="min-w-24 flex-none px-4 py-2"
164
190
  >
@@ -172,8 +198,27 @@ function handleBeforeUnload(event: BeforeUnloadEvent) {
172
198
  水印设置
173
199
  </TabsTrigger>
174
200
  </TabsList>
175
- <TabsContent v-if="canReadBasic" value="basic" class="mt-0">
176
- <BrandingSettingsSection ref="basicSection" />
201
+ <TabsContent v-if="canOpenBasic" value="basic" class="mt-0">
202
+ <BrandingSettingsSection v-if="canReadBasic" ref="basicSection" />
203
+ <section
204
+ v-if="canReadWatermark"
205
+ class="mt-5 space-y-3 rounded-xl border bg-card p-6"
206
+ >
207
+ <h2 class="text-xl font-semibold">配置导入与导出</h2>
208
+ <p class="text-sm text-muted-foreground">
209
+ 通过 YAML 备份或迁移页面水印配置。
210
+ </p>
211
+ <WatermarkYamlTransfer
212
+ :can-manage="canManageWatermark"
213
+ :import-disabled="
214
+ !canManageWatermark ||
215
+ currentIsDirty() ||
216
+ watermark.loading.value ||
217
+ watermark.saving.value
218
+ "
219
+ @imported="onYamlImported"
220
+ />
221
+ </section>
177
222
  </TabsContent>
178
223
  <TabsContent v-if="canReadWatermark" value="watermark" class="mt-0">
179
224
  <WatermarkSettingsSection ref="watermarkSection" />
@@ -4,6 +4,7 @@ import { defineComponent, h, shallowRef } from "vue";
4
4
  import { createMemoryHistory, createRouter, RouterView } from "vue-router";
5
5
  import type { WebAccessSnapshot } from "@/layout/navigation";
6
6
  import GlobalSettingsPage from "./GlobalSettingsPage.vue";
7
+ import WatermarkYamlTransfer from "./WatermarkYamlTransfer.vue";
7
8
 
8
9
  const snapshot = shallowRef<WebAccessSnapshot>();
9
10
  const ensureLoaded = vi.fn(async () => snapshot.value!);
@@ -84,15 +85,30 @@ describe("全局设置分类", () => {
84
85
  expect(wrapper.text()).not.toContain("基本信息表单");
85
86
  });
86
87
 
87
- test("只有水印权限时直接落到可访问分类", async () => {
88
+ test("只有水印权限时可从基本信息导出 YAML,但不显示品牌表单", async () => {
88
89
  snapshot.value = createSnapshot(["system.watermark.read"]);
89
90
  const { wrapper, router } = await mountPage("/settings#basic");
90
91
 
91
- expect(router.currentRoute.value.fullPath).toBe("/settings#watermark");
92
- expect(wrapper.text()).toContain("水印表单");
92
+ expect(router.currentRoute.value.fullPath).toBe("/settings#basic");
93
+ expect(wrapper.text()).toContain("导出 YAML");
94
+ expect(wrapper.text()).not.toContain("导入 YAML");
93
95
  expect(wrapper.text()).not.toContain("基本信息表单");
94
96
  });
95
97
 
98
+ test("基本信息只提供一个 YAML 入口,未保存编辑时禁止导入", async () => {
99
+ snapshot.value = createSnapshot([
100
+ "system.settings.read",
101
+ "system.watermark.read",
102
+ "system.watermark.manage",
103
+ ]);
104
+ const { wrapper } = await mountPage();
105
+ expect(wrapper.findAllComponents(WatermarkYamlTransfer)).toHaveLength(1);
106
+ const transfer = wrapper.getComponent(WatermarkYamlTransfer);
107
+ expect(transfer.props("importDisabled")).toBe(false);
108
+ await wrapper.get('[data-testid="dirty-basic"]').trigger("click");
109
+ expect(transfer.props("importDisabled")).toBe(true);
110
+ });
111
+
96
112
  test("访问快照加载完成后再恢复有权限的深链接", async () => {
97
113
  snapshot.value = undefined;
98
114
  ensureLoaded.mockImplementation(async () => {
@@ -163,6 +163,7 @@ function toggleContentField(field: WatermarkContentField, checked: boolean) {
163
163
 
164
164
  defineExpose({
165
165
  isDirty: () => isDirty.value,
166
+ applyPolicyToForm,
166
167
  save,
167
168
  });
168
169
  </script>
@@ -0,0 +1,211 @@
1
+ <script setup lang="ts">
2
+ import { LoaderCircleIcon } from "@lucide/vue";
3
+ import { shallowRef } from "vue";
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogTrigger,
13
+ } from "@/components/ui/dialog";
14
+ import { useAppToast } from "@/composables/useAppToast";
15
+ import {
16
+ exportWatermarkYaml,
17
+ importWatermarkYaml,
18
+ previewWatermarkYaml,
19
+ type PlatformWatermarkPolicy,
20
+ type WatermarkYamlPreview,
21
+ } from "@/services/platform-settings-api";
22
+
23
+ defineProps<{ canManage: boolean; importDisabled: boolean }>();
24
+ const emit = defineEmits<{ imported: [policy: PlatformWatermarkPolicy] }>();
25
+ const toast = useAppToast();
26
+ const open = shallowRef(false);
27
+ const exporting = shallowRef(false);
28
+ const busy = shallowRef<"preview" | "import">();
29
+ const error = shallowRef("");
30
+ const yaml = shallowRef("");
31
+ const preview = shallowRef<WatermarkYamlPreview>();
32
+
33
+ const labels = {
34
+ enabled: "显示页面水印",
35
+ density: "密度",
36
+ contentFields: "显示字段",
37
+ opacityPercent: "不透明度(%)",
38
+ fontSize: "字号(px)",
39
+ };
40
+ const valueLabels: Readonly<Record<string, string>> = {
41
+ low: "低",
42
+ medium: "中",
43
+ high: "高",
44
+ username: "用户名",
45
+ displayName: "姓名",
46
+ phone: "手机号",
47
+ email: "邮箱",
48
+ custom: "目标实例的自定义内容",
49
+ };
50
+
51
+ function format(value: WatermarkYamlPreview["changes"][number]["before"]) {
52
+ if (typeof value === "boolean") return value ? "开启" : "关闭";
53
+ if (Array.isArray(value))
54
+ return value.map((item) => valueLabels[item] ?? item).join("、");
55
+ return valueLabels[String(value)] ?? value;
56
+ }
57
+
58
+ async function download() {
59
+ exporting.value = true;
60
+ try {
61
+ const url = URL.createObjectURL(await exportWatermarkYaml());
62
+ const link = document.createElement("a");
63
+ link.href = url;
64
+ link.download = "windy-admin-settings.yaml";
65
+ link.click();
66
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
67
+ } catch (failure) {
68
+ toast.error("导出 YAML 失败", { description: message(failure) });
69
+ } finally {
70
+ exporting.value = false;
71
+ }
72
+ }
73
+
74
+ function setOpen(value: boolean) {
75
+ if (busy.value) return;
76
+ open.value = value;
77
+ if (value) return;
78
+ yaml.value = "";
79
+ error.value = "";
80
+ preview.value = undefined;
81
+ }
82
+
83
+ async function readFile(event: Event) {
84
+ const input = event.target as HTMLInputElement;
85
+ const file = input.files?.[0];
86
+ input.value = "";
87
+ if (!file || busy.value) return;
88
+ yaml.value = "";
89
+ preview.value = undefined;
90
+ if (file.size > 8192) {
91
+ error.value = "YAML 文件不能超过 8 KiB";
92
+ return;
93
+ }
94
+ busy.value = "preview";
95
+ error.value = "";
96
+ try {
97
+ yaml.value = await file.text();
98
+ preview.value = await previewWatermarkYaml(yaml.value);
99
+ } catch (failure) {
100
+ error.value = message(failure);
101
+ } finally {
102
+ busy.value = undefined;
103
+ }
104
+ }
105
+
106
+ async function apply() {
107
+ if (busy.value || !preview.value?.changes.length) return;
108
+ busy.value = "import";
109
+ error.value = "";
110
+ try {
111
+ const policy = await importWatermarkYaml(
112
+ yaml.value,
113
+ preview.value.revision,
114
+ );
115
+ emit("imported", policy);
116
+ toast.success("水印配置已导入");
117
+ busy.value = undefined;
118
+ setOpen(false);
119
+ } catch (failure) {
120
+ error.value = message(failure);
121
+ preview.value = undefined;
122
+ } finally {
123
+ busy.value = undefined;
124
+ }
125
+ }
126
+
127
+ function message(failure: unknown) {
128
+ return failure instanceof Error ? failure.message : "操作失败,请重试";
129
+ }
130
+ </script>
131
+
132
+ <template>
133
+ <div class="flex flex-wrap gap-2">
134
+ <Button variant="outline" :disabled="exporting" @click="download">
135
+ {{ exporting ? "导出中…" : "导出 YAML" }}
136
+ </Button>
137
+ <Dialog v-if="canManage" :open="open" @update:open="setOpen">
138
+ <DialogTrigger as-child>
139
+ <Button variant="outline" :disabled="importDisabled">导入 YAML</Button>
140
+ </DialogTrigger>
141
+ <DialogContent
142
+ :show-close-button="false"
143
+ @escape-key-down.prevent
144
+ @interact-outside.prevent
145
+ >
146
+ <DialogHeader>
147
+ <DialogTitle>导入水印配置</DialogTitle>
148
+ <DialogDescription>
149
+ 选择 YAML 文件,校验通过后核对差异。确认后配置才会生效。
150
+ </DialogDescription>
151
+ </DialogHeader>
152
+ <label class="grid gap-2">
153
+ 配置文件(最多 8 KiB)
154
+ <input
155
+ type="file"
156
+ accept=".yaml,.yml"
157
+ :disabled="!!busy"
158
+ @change="readFile"
159
+ />
160
+ </label>
161
+ <p v-if="error" role="alert" class="text-destructive">{{ error }}</p>
162
+ <p v-if="busy === 'preview'" role="status">正在校验文件…</p>
163
+ <template v-if="preview">
164
+ <p role="status">
165
+ {{
166
+ preview.changes.length
167
+ ? "校验通过,请核对以下变更。"
168
+ : "校验通过,配置没有变化。"
169
+ }}
170
+ </p>
171
+ <div v-if="preview.changes.length" class="overflow-auto">
172
+ <table class="w-full text-left">
173
+ <thead>
174
+ <tr>
175
+ <th scope="col">配置项</th>
176
+ <th scope="col">当前值</th>
177
+ <th scope="col">导入值</th>
178
+ </tr>
179
+ </thead>
180
+ <tbody>
181
+ <tr
182
+ v-for="change in preview.changes"
183
+ :key="change.key"
184
+ class="border-t"
185
+ >
186
+ <th scope="row" class="py-2 font-medium">
187
+ {{ labels[change.key] }}
188
+ </th>
189
+ <td>{{ format(change.before) }}</td>
190
+ <td>{{ format(change.after) }}</td>
191
+ </tr>
192
+ </tbody>
193
+ </table>
194
+ </div>
195
+ </template>
196
+ <DialogFooter>
197
+ <Button variant="outline" :disabled="!!busy" @click="setOpen(false)"
198
+ >取消</Button
199
+ >
200
+ <Button
201
+ :disabled="!!busy || !preview?.changes.length || importDisabled"
202
+ @click="apply"
203
+ >
204
+ <LoaderCircleIcon v-if="busy === 'import'" class="animate-spin" />
205
+ {{ busy === "import" ? "导入中…" : "确认导入" }}
206
+ </Button>
207
+ </DialogFooter>
208
+ </DialogContent>
209
+ </Dialog>
210
+ </div>
211
+ </template>