create-windy 0.2.33 → 0.3.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 (284) hide show
  1. package/dist/cli.js +173 -17
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +10 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +9 -0
  6. package/template/README.md +3 -1
  7. package/template/apps/agent-server/Dockerfile +1 -0
  8. package/template/apps/agent-server/README.md +28 -5
  9. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  10. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  11. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  12. package/template/apps/agent-server/pyproject.toml +1 -1
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/app.py +63 -3
  18. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
  21. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
  22. package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
  25. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  26. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  27. package/template/apps/agent-server/tests/test_api.py +16 -1
  28. package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
  29. package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
  30. package/template/apps/server/package.json +4 -1
  31. package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
  32. package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
  33. package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
  34. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  35. package/template/apps/server/src/agent/run-contracts.ts +2 -0
  36. package/template/apps/server/src/agent/run-facade.test.ts +52 -2
  37. package/template/apps/server/src/agent/run-facade.ts +38 -9
  38. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  39. package/template/apps/server/src/application-services.ts +50 -0
  40. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  41. package/template/apps/server/src/audit/search-summary.ts +2 -6
  42. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  43. package/template/apps/server/src/auth/password.ts +30 -21
  44. package/template/apps/server/src/auth/routes.test.ts +44 -3
  45. package/template/apps/server/src/auth/routes.ts +6 -1
  46. package/template/apps/server/src/auth/runtime.ts +4 -1
  47. package/template/apps/server/src/auth/service.ts +25 -7
  48. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  49. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  50. package/template/apps/server/src/configuration/definition.ts +242 -12
  51. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  52. package/template/apps/server/src/configuration/repository.ts +27 -2
  53. package/template/apps/server/src/configuration/routes.ts +4 -1
  54. package/template/apps/server/src/configuration/service.test.ts +100 -0
  55. package/template/apps/server/src/configuration/service.ts +50 -10
  56. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  57. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  58. package/template/apps/server/src/example-modules.ts +4 -1
  59. package/template/apps/server/src/guards.ts +18 -5
  60. package/template/apps/server/src/http/request-source.test.ts +85 -0
  61. package/template/apps/server/src/http/request-source.ts +90 -0
  62. package/template/apps/server/src/index.ts +45 -45
  63. package/template/apps/server/src/license/routes.ts +2 -0
  64. package/template/apps/server/src/license/runtime-service.ts +10 -11
  65. package/template/apps/server/src/module-bootstrap.ts +2 -2
  66. package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
  67. package/template/apps/server/src/module-host/initialize.ts +30 -44
  68. package/template/apps/server/src/module-host/request-context.ts +29 -23
  69. package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
  70. package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
  71. package/template/apps/server/src/module-host/route-declaration.ts +45 -0
  72. package/template/apps/server/src/module-host/route-installer.ts +27 -3
  73. package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
  74. package/template/apps/server/src/module-host/storage-port.ts +134 -1
  75. package/template/apps/server/src/module-host/storage-scope.ts +89 -0
  76. package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
  77. package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
  78. package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
  79. package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
  80. package/template/apps/server/src/module-host/user-directory-port.ts +139 -0
  81. package/template/apps/server/src/module-host.ts +4 -0
  82. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  83. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  84. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  85. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  86. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  87. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  88. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  89. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  90. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  91. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  92. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  93. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  94. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  95. package/template/apps/server/src/notification/memory-support.ts +72 -0
  96. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  97. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  98. package/template/apps/server/src/notification/repository.test.ts +171 -0
  99. package/template/apps/server/src/notification/repository.ts +179 -32
  100. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  101. package/template/apps/server/src/notification/route-support.ts +225 -0
  102. package/template/apps/server/src/notification/routes.test.ts +182 -0
  103. package/template/apps/server/src/notification/routes.ts +225 -132
  104. package/template/apps/server/src/notification/service.test.ts +104 -0
  105. package/template/apps/server/src/notification/service.ts +140 -0
  106. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  107. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  108. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  109. package/template/apps/server/src/route-guards.ts +1 -0
  110. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  111. package/template/apps/server/src/runtime-development.ts +2 -0
  112. package/template/apps/server/src/runtime.test.ts +23 -9
  113. package/template/apps/server/src/runtime.ts +39 -2
  114. package/template/apps/server/src/scheduler/audit.ts +2 -11
  115. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  116. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  117. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  118. package/template/apps/server/src/scheduler/routes.ts +2 -7
  119. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  120. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  121. package/template/apps/server/src/search/service.ts +2 -6
  122. package/template/apps/server/src/settings/routes.ts +15 -3
  123. package/template/apps/server/src/settings/runtime.ts +10 -1
  124. package/template/apps/server/src/storage/runtime.ts +13 -0
  125. package/template/apps/server/src/system/audit-query.ts +33 -4
  126. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  127. package/template/apps/server/src/system/audit-routes.ts +6 -5
  128. package/template/apps/server/src/system/built-in-roles.ts +1 -0
  129. package/template/apps/server/src/system/department-organization.ts +320 -0
  130. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  131. package/template/apps/server/src/system/department-routes.ts +114 -33
  132. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  133. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  134. package/template/apps/server/src/system/drizzle-system.ts +37 -9
  135. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  136. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  137. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  138. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  139. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  140. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  141. package/template/apps/server/src/system/route-helpers.ts +2 -5
  142. package/template/apps/server/src/system/route-types.ts +12 -3
  143. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  144. package/template/apps/server/src/system/routes.test.ts +10 -2
  145. package/template/apps/server/src/system/routes.ts +70 -36
  146. package/template/apps/server/src/system/seed.ts +7 -0
  147. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  148. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  149. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  150. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  151. package/template/apps/server/src/work-order/service.ts +42 -10
  152. package/template/apps/web/src/app/error-pages.ts +9 -0
  153. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  154. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  155. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  156. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  157. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  158. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  159. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  160. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  161. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  162. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  163. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  164. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  165. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  166. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  167. package/template/apps/web/src/lib/date-time.ts +22 -10
  168. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  169. package/template/apps/web/src/main.ts +14 -1
  170. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  171. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  172. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  173. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  174. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  175. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  176. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  177. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  178. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  179. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  180. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  181. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  182. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  183. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  184. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  185. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  186. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  187. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  188. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  189. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  190. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  191. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  192. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  193. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  194. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
  195. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  196. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  197. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  198. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  199. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  200. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  201. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  202. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  203. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  204. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  205. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  206. package/template/apps/web/src/router/error-navigation.ts +50 -0
  207. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  208. package/template/apps/web/src/router/index.ts +71 -0
  209. package/template/apps/web/src/router/index.webtest.ts +29 -0
  210. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  211. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  212. package/template/apps/web/src/services/auth-api.ts +5 -0
  213. package/template/apps/web/src/services/http.ts +43 -6
  214. package/template/apps/web/src/services/http.webtest.ts +70 -0
  215. package/template/apps/web/src/services/notification-api.ts +85 -2
  216. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  217. package/template/apps/web/src/services/search-api.ts +39 -0
  218. package/template/apps/web/src/services/system-api.ts +18 -0
  219. package/template/apps/web/vitest.config.ts +1 -0
  220. package/template/docker-compose.yml +68 -0
  221. package/template/docs/architecture/ai-egress.md +49 -7
  222. package/template/docs/architecture/ai-runtime.md +34 -6
  223. package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
  224. package/template/docs/architecture/object-storage.md +22 -2
  225. package/template/docs/architecture/search-provider-protocol.md +38 -2
  226. package/template/docs/development/custom-error-pages.md +78 -0
  227. package/template/docs/development/custom-login-page.md +3 -3
  228. package/template/docs/platform/agent-runtime.md +76 -7
  229. package/template/docs/platform/audit-reliability.md +46 -2
  230. package/template/docs/platform/business-user-directory.md +82 -0
  231. package/template/docs/platform/module-configuration.md +13 -4
  232. package/template/docs/platform/notifications.md +67 -11
  233. package/template/docs/platform/organization-membership.md +17 -3
  234. package/template/docs/platform/system-crud-api.md +16 -0
  235. package/template/docs/platform/web-system-crud.md +6 -2
  236. package/template/package.json +4 -0
  237. package/template/packages/config/index.test.ts +45 -0
  238. package/template/packages/config/package.json +1 -1
  239. package/template/packages/config/src/platform.ts +169 -48
  240. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  241. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  242. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  243. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  244. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  245. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  246. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  247. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  248. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  249. package/template/packages/database/package.json +1 -1
  250. package/template/packages/database/src/schema/identity.ts +7 -0
  251. package/template/packages/database/src/schema/notifications.ts +135 -0
  252. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  253. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  254. package/template/packages/modules/package.json +1 -1
  255. package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
  256. package/template/packages/modules/src/ai-operation-composition.ts +115 -0
  257. package/template/packages/modules/src/manifest.ts +26 -0
  258. package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
  259. package/template/packages/modules/src/system-api-permissions.ts +54 -0
  260. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  261. package/template/packages/modules/src/system-features.ts +6 -1
  262. package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
  263. package/template/packages/modules/src/system-module-catalog.ts +28 -4
  264. package/template/packages/modules/src/system-modules.test.ts +13 -1
  265. package/template/packages/modules/src/system-modules.ts +12 -0
  266. package/template/packages/modules/src/system-permissions.ts +11 -5
  267. package/template/packages/server-sdk/index.ts +11 -0
  268. package/template/packages/server-sdk/package.json +3 -2
  269. package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
  270. package/template/packages/server-sdk/src/module-host.ts +8 -0
  271. package/template/packages/server-sdk/src/request-context.ts +2 -0
  272. package/template/packages/server-sdk/src/storage-port.ts +66 -0
  273. package/template/packages/server-sdk/src/tool-registration.ts +6 -0
  274. package/template/packages/server-sdk/src/user-directory-port.ts +42 -0
  275. package/template/packages/shared/package.json +1 -1
  276. package/template/packages/shared/src/audit.ts +25 -0
  277. package/template/packages/shared/src/module-configuration.ts +5 -0
  278. package/template/packages/shared/src/notification.ts +92 -1
  279. package/template/packages/shared/src/password.ts +7 -2
  280. package/template/packages/shared/src/platform-settings.ts +2 -0
  281. package/template/packages/storage/package.json +1 -1
  282. package/template/packages/storage/src/upload-service.test.ts +16 -0
  283. package/template/packages/storage/src/upload-service.ts +5 -1
  284. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -0,0 +1,68 @@
1
+ import { readonly, shallowRef, watch, type Ref } from "vue";
2
+ import { searchPlatform, type SearchResultGroup } from "@/services/search-api";
3
+
4
+ const SEARCH_DELAY_MS = 250;
5
+
6
+ export function useGlobalSearch(query: Ref<string>) {
7
+ const groups = shallowRef<readonly SearchResultGroup[]>([]);
8
+ const loading = shallowRef(false);
9
+ const partial = shallowRef(false);
10
+ const error = shallowRef("");
11
+ let cancelActiveRequest = () => {};
12
+
13
+ watch(
14
+ () => query.value.trim(),
15
+ (keyword, _, onCleanup) => {
16
+ cancelActiveRequest();
17
+ groups.value = [];
18
+ partial.value = false;
19
+ error.value = "";
20
+ if (!keyword) {
21
+ loading.value = false;
22
+ return;
23
+ }
24
+
25
+ loading.value = true;
26
+ const controller = new AbortController();
27
+ const timer = window.setTimeout(() => {
28
+ void searchPlatform(keyword, controller.signal)
29
+ .then((result) => {
30
+ groups.value = result.groups;
31
+ partial.value = result.partial;
32
+ })
33
+ .catch((cause: unknown) => {
34
+ if (controller.signal.aborted) return;
35
+ error.value =
36
+ cause instanceof Error ? cause.message : "业务搜索暂不可用";
37
+ })
38
+ .finally(() => {
39
+ if (!controller.signal.aborted) loading.value = false;
40
+ });
41
+ }, SEARCH_DELAY_MS);
42
+
43
+ const cancel = () => {
44
+ window.clearTimeout(timer);
45
+ controller.abort();
46
+ };
47
+ cancelActiveRequest = cancel;
48
+ onCleanup(cancel);
49
+ },
50
+ );
51
+
52
+ function reset() {
53
+ cancelActiveRequest();
54
+ cancelActiveRequest = () => {};
55
+ groups.value = [];
56
+ partial.value = false;
57
+ error.value = "";
58
+ loading.value = false;
59
+ }
60
+
61
+ return {
62
+ groups: readonly(groups),
63
+ loading: readonly(loading),
64
+ partial: readonly(partial),
65
+ error: readonly(error),
66
+ reset,
67
+ };
68
+ }
@@ -1,4 +1,6 @@
1
1
  import { computed, readonly, shallowRef } from "vue";
2
+ import type { PlatformBrandingSettings } from "@southwind-ai/shared";
3
+ import { setDateTimeZone } from "@/lib/date-time";
2
4
  // @windy-module system.settings begin
3
5
  import {
4
6
  loadPublicBranding,
@@ -6,13 +8,10 @@ import {
6
8
  } from "@/services/platform-settings-api";
7
9
  // @windy-module system.settings end
8
10
 
9
- interface PlatformBrandingSettings {
10
- softwareName: string;
11
- logoUrl?: string;
12
- description?: string;
13
- }
14
-
15
- const fallback: PlatformBrandingSettings = { softwareName: "Windy Platform" };
11
+ const fallback: PlatformBrandingSettings = {
12
+ softwareName: "Windy Platform",
13
+ timeZone: "Asia/Shanghai",
14
+ };
16
15
  const branding = shallowRef<PlatformBrandingSettings>({ ...fallback });
17
16
  const loading = shallowRef(false);
18
17
  const saving = shallowRef(false);
@@ -65,7 +64,9 @@ function applyBranding(next: PlatformBrandingSettings) {
65
64
  softwareName: next.softwareName.trim() || fallback.softwareName,
66
65
  logoUrl: next.logoUrl?.trim() || undefined,
67
66
  description: next.description?.trim() || undefined,
67
+ timeZone: next.timeZone?.trim() || fallback.timeZone,
68
68
  };
69
+ setDateTimeZone(branding.value.timeZone || fallback.timeZone!);
69
70
  document.title = branding.value.softwareName;
70
71
  loaded = true;
71
72
  return branding.value;
@@ -0,0 +1,58 @@
1
+ <script setup lang="ts">
2
+ import { LoaderCircleIcon } from "@lucide/vue";
3
+ import type { SearchResultGroup } from "@/services/search-api";
4
+
5
+ defineProps<{
6
+ groups: readonly SearchResultGroup[];
7
+ loading: boolean;
8
+ partial: boolean;
9
+ error: string;
10
+ }>();
11
+
12
+ const emit = defineEmits<{
13
+ select: [routeTemplate: string, params: Record<string, string>];
14
+ }>();
15
+ </script>
16
+
17
+ <template>
18
+ <section
19
+ v-if="loading || error || partial || groups.length"
20
+ class="border-t px-2 pb-2 pt-3"
21
+ aria-label="业务搜索结果"
22
+ >
23
+ <div class="mb-2 flex items-center justify-between px-3">
24
+ <p class="text-xs font-medium text-muted-foreground">业务数据</p>
25
+ <LoaderCircleIcon
26
+ v-if="loading"
27
+ class="size-3.5 animate-spin text-muted-foreground"
28
+ aria-label="正在搜索业务数据"
29
+ />
30
+ </div>
31
+ <p v-if="error" class="px-3 py-3 text-sm text-destructive">
32
+ {{ error }}
33
+ </p>
34
+ <p v-else-if="partial" class="px-3 pb-2 text-xs text-amber-600">
35
+ 部分业务搜索源暂不可用,以下结果可能不完整。
36
+ </p>
37
+ <div v-for="group in groups" :key="group.providerKey" class="mb-2">
38
+ <p class="px-3 py-1 text-xs text-muted-foreground">
39
+ {{ group.label }}
40
+ </p>
41
+ <button
42
+ v-for="hit in group.hits"
43
+ :key="`${group.providerKey}:${hit.id}`"
44
+ type="button"
45
+ class="flex w-full flex-col rounded-md px-3 py-2 text-left text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground"
46
+ @click="emit('select', hit.deepLink.routeTemplate, hit.deepLink.params)"
47
+ >
48
+ <span class="truncate font-medium">{{ hit.maskedTitle }}</span>
49
+ <span
50
+ v-if="hit.maskedExcerpt"
51
+ class="mt-0.5 line-clamp-1 text-xs text-muted-foreground"
52
+ >
53
+ {{ hit.maskedExcerpt }}
54
+ </span>
55
+ </button>
56
+ </div>
57
+ </section>
58
+ </template>
@@ -22,6 +22,8 @@ import {
22
22
  } from "@/components/ui/dialog";
23
23
  import { Input } from "@/components/ui/input";
24
24
  import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
25
+ import { useGlobalSearch } from "@/composables/useGlobalSearch";
26
+ import GlobalSearchResults from "./GlobalSearchResults.vue";
25
27
  import { authorizedSidebarMenus } from "./navigation";
26
28
 
27
29
  interface SearchableNavigationItem {
@@ -36,6 +38,7 @@ const route = useRoute();
36
38
  const access = useAccessSnapshot();
37
39
  const open = shallowRef(false);
38
40
  const query = shallowRef("");
41
+ const businessSearch = useGlobalSearch(query);
39
42
 
40
43
  const items = computed(() =>
41
44
  access.snapshot.value
@@ -55,8 +58,12 @@ const results = computed(() => {
55
58
  });
56
59
 
57
60
  watch(open, async (isOpen) => {
58
- if (!isOpen) return;
61
+ if (!isOpen) {
62
+ businessSearch.reset();
63
+ return;
64
+ }
59
65
  query.value = "";
66
+ businessSearch.reset();
60
67
  await nextTick();
61
68
  document.querySelector<HTMLInputElement>("[data-navigation-search]")?.focus();
62
69
  });
@@ -79,6 +86,19 @@ async function select(item: SearchableNavigationItem) {
79
86
  if (route.path !== item.path) await router.push(item.path);
80
87
  }
81
88
 
89
+ async function selectBusiness(
90
+ routeTemplate: string,
91
+ params: Record<string, string>,
92
+ ) {
93
+ const path = Object.entries(params).reduce(
94
+ (result, [key, value]) =>
95
+ result.replace(`:${key}`, encodeURIComponent(value)),
96
+ routeTemplate,
97
+ );
98
+ open.value = false;
99
+ if (route.path !== path) await router.push(path);
100
+ }
101
+
82
102
  function selectFirst() {
83
103
  const first = results.value[0];
84
104
  if (first) void select(first);
@@ -114,9 +134,9 @@ function flattenMenus(
114
134
  </DialogTrigger>
115
135
  <DialogContent class="gap-0 overflow-hidden p-0 sm:max-w-xl">
116
136
  <DialogHeader class="border-b px-5 py-4 text-left">
117
- <DialogTitle>搜索平台功能</DialogTitle>
137
+ <DialogTitle>全局搜索</DialogTitle>
118
138
  <DialogDescription
119
- >搜索当前账号有权访问的菜单,按 Enter 打开首项。</DialogDescription
139
+ >同时搜索当前账号有权访问的菜单与业务数据。</DialogDescription
120
140
  >
121
141
  </DialogHeader>
122
142
  <div class="shrink-0 border-b p-4">
@@ -128,17 +148,26 @@ function flattenMenus(
128
148
  v-model="query"
129
149
  data-navigation-search
130
150
  class="pl-9"
131
- placeholder="输入功能名称或路径"
151
+ placeholder="输入功能名称、路径或业务关键词"
132
152
  @keyup.enter="selectFirst"
133
153
  />
134
154
  </div>
135
155
  </div>
136
156
  <ModalBody class="p-2">
137
157
  <p
138
- v-if="results.length === 0"
158
+ v-if="
159
+ query.trim() &&
160
+ results.length === 0 &&
161
+ businessSearch.groups.value.length === 0 &&
162
+ !businessSearch.loading.value &&
163
+ !businessSearch.error.value
164
+ "
139
165
  class="px-3 py-10 text-center text-sm text-muted-foreground"
140
166
  >
141
- 没有找到可访问的功能
167
+ 没有找到可访问的菜单或业务数据
168
+ </p>
169
+ <p class="px-3 py-1 text-xs font-medium text-muted-foreground">
170
+ 平台导航
142
171
  </p>
143
172
  <button
144
173
  v-for="item in results"
@@ -157,6 +186,13 @@ function flattenMenus(
157
186
  item.path
158
187
  }}</span>
159
188
  </button>
189
+ <GlobalSearchResults
190
+ :groups="businessSearch.groups.value"
191
+ :loading="businessSearch.loading.value"
192
+ :partial="businessSearch.partial.value"
193
+ :error="businessSearch.error.value"
194
+ @select="selectBusiness"
195
+ />
160
196
  </ModalBody>
161
197
  <div class="shrink-0 border-t px-5 py-2 text-xs text-muted-foreground">
162
198
  快捷键 Ctrl/⌘ K
@@ -5,6 +5,10 @@ import { createMemoryHistory, createRouter } from "vue-router";
5
5
  import type { WebAccessSnapshot } from "./navigation";
6
6
  import NavigationSearchDialog from "./NavigationSearchDialog.vue";
7
7
 
8
+ const searchMocks = vi.hoisted(() => ({
9
+ search: vi.fn(),
10
+ }));
11
+
8
12
  const snapshot = shallowRef<WebAccessSnapshot>({
9
13
  actor: {
10
14
  id: "user_1",
@@ -40,10 +44,14 @@ const snapshot = shallowRef<WebAccessSnapshot>({
40
44
  vi.mock("@/composables/useAccessSnapshot", () => ({
41
45
  useAccessSnapshot: () => ({ snapshot }),
42
46
  }));
47
+ vi.mock("@/services/search-api", () => ({
48
+ searchPlatform: searchMocks.search,
49
+ }));
43
50
 
44
51
  afterEach(() => {
45
52
  document.body.innerHTML = "";
46
53
  vi.clearAllMocks();
54
+ vi.useRealTimers();
47
55
  });
48
56
 
49
57
  describe("NavigationSearchDialog", () => {
@@ -81,4 +89,67 @@ describe("NavigationSearchDialog", () => {
81
89
  expect(router.currentRoute.value.path).toBe("/system/roles");
82
90
  wrapper.unmount();
83
91
  });
92
+
93
+ test("输入关键词后展示授权 SearchProvider 结果并打开业务深链", async () => {
94
+ vi.useFakeTimers();
95
+ searchMocks.search.mockResolvedValue({
96
+ groups: [
97
+ {
98
+ providerKey: "case.record.search",
99
+ label: "案件",
100
+ hits: [
101
+ {
102
+ id: "case_1",
103
+ resourceType: "case",
104
+ maskedTitle: "案…一",
105
+ maskedExcerpt: "涉…",
106
+ providerKey: "case.record.search",
107
+ deepLink: {
108
+ surface: "business",
109
+ routeTemplate: "/cases/:id",
110
+ params: { id: "case_1" },
111
+ },
112
+ },
113
+ ],
114
+ },
115
+ ],
116
+ partial: false,
117
+ failures: [],
118
+ });
119
+ const router = createRouter({
120
+ history: createMemoryHistory(),
121
+ routes: [
122
+ { path: "/", component: { template: "<div />" } },
123
+ { path: "/cases/:id", component: { template: "<div />" } },
124
+ ],
125
+ });
126
+ await router.push("/");
127
+ await router.isReady();
128
+ const wrapper = mount(NavigationSearchDialog, {
129
+ attachTo: document.body,
130
+ global: { plugins: [router] },
131
+ });
132
+ await wrapper.get('button[aria-label="搜索平台功能"]').trigger("click");
133
+ await flushPromises();
134
+ const input = document.body.querySelector<HTMLInputElement>(
135
+ "[data-navigation-search]",
136
+ )!;
137
+ input.value = "案件";
138
+ input.dispatchEvent(new Event("input", { bubbles: true }));
139
+ await vi.advanceTimersByTimeAsync(250);
140
+ await flushPromises();
141
+
142
+ expect(searchMocks.search).toHaveBeenCalledWith(
143
+ "案件",
144
+ expect.any(AbortSignal),
145
+ );
146
+ const result = Array.from(document.body.querySelectorAll("button")).find(
147
+ (button) => button.textContent?.includes("案…一"),
148
+ );
149
+ expect(result).toBeDefined();
150
+ result?.click();
151
+ await flushPromises();
152
+ expect(router.currentRoute.value.path).toBe("/cases/case_1");
153
+ wrapper.unmount();
154
+ });
84
155
  });
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { NotificationInboxItem } from "@southwind-ai/shared";
3
- import { BellIcon } from "@lucide/vue";
3
+ import { BellIcon, SettingsIcon } from "@lucide/vue";
4
4
  import { computed, onMounted, shallowRef } from "vue";
5
5
  import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
6
6
  import { Button } from "@/components/ui/button";
@@ -20,9 +20,13 @@ import {
20
20
  } from "@/components/ui/sheet";
21
21
  import { formatDateTime } from "@/lib/date-time";
22
22
  import { evaluateWebAccess } from "@/layout/navigation";
23
+ import NotificationMenuItem from "./NotificationMenuItem.vue";
24
+ import NotificationPreferencesDialog from "./NotificationPreferencesDialog.vue";
23
25
  import {
26
+ favoriteNotification,
24
27
  listNotifications,
25
28
  markNotificationRead,
29
+ unfavoriteNotification,
26
30
  } from "@/services/notification-api";
27
31
 
28
32
  const notifications = shallowRef<NotificationInboxItem[]>([]);
@@ -31,6 +35,8 @@ const selected = shallowRef<NotificationInboxItem>();
31
35
  const detailOpen = shallowRef(false);
32
36
  const loading = shallowRef(false);
33
37
  const error = shallowRef("");
38
+ const preferencesOpen = shallowRef(false);
39
+ const favoritePendingIds = shallowRef<ReadonlySet<string>>(new Set());
34
40
  const unreadCount = computed(
35
41
  () => notifications.value.filter(({ read }) => !read).length,
36
42
  );
@@ -83,6 +89,52 @@ async function openNotification(notification: NotificationInboxItem) {
83
89
  );
84
90
  }
85
91
  }
92
+
93
+ async function toggleFavorite(notification: NotificationInboxItem) {
94
+ if (favoritePendingIds.value.has(notification.id)) return;
95
+ const previous = {
96
+ favorite: notification.favorite,
97
+ favoritedAt: notification.favoritedAt,
98
+ };
99
+ const desired = !notification.favorite;
100
+ setFavoritePending(notification.id, true);
101
+ patchNotification(notification.id, {
102
+ favorite: desired,
103
+ favoritedAt: desired ? new Date().toISOString() : undefined,
104
+ });
105
+ try {
106
+ const result = desired
107
+ ? await favoriteNotification(notification.id)
108
+ : await unfavoriteNotification(notification.id);
109
+ patchNotification(notification.id, {
110
+ favorite: result.favorite,
111
+ favoritedAt: result.favoritedAt,
112
+ });
113
+ } catch {
114
+ patchNotification(notification.id, previous);
115
+ } finally {
116
+ setFavoritePending(notification.id, false);
117
+ }
118
+ }
119
+
120
+ function patchNotification(
121
+ id: string,
122
+ patch: Pick<NotificationInboxItem, "favorite" | "favoritedAt">,
123
+ ) {
124
+ notifications.value = notifications.value.map((item) =>
125
+ item.id === id ? { ...item, ...patch } : item,
126
+ );
127
+ if (selected.value?.id === id) {
128
+ selected.value = { ...selected.value, ...patch };
129
+ }
130
+ }
131
+
132
+ function setFavoritePending(id: string, pending: boolean) {
133
+ const next = new Set(favoritePendingIds.value);
134
+ if (pending) next.add(id);
135
+ else next.delete(id);
136
+ favoritePendingIds.value = next;
137
+ }
86
138
  </script>
87
139
 
88
140
  <template>
@@ -105,43 +157,31 @@ async function openNotification(notification: NotificationInboxItem) {
105
157
  <DropdownMenuContent align="end" class="w-80 p-0">
106
158
  <DropdownMenuLabel class="flex items-center justify-between px-4 py-3">
107
159
  <span>站内通知</span>
108
- <span class="text-xs font-normal text-muted-foreground">
109
- {{ unreadCount }} 条未读
160
+ <span class="flex items-center gap-2">
161
+ <span class="text-xs font-normal text-muted-foreground">
162
+ {{ unreadCount }} 条未读
163
+ </span>
164
+ <Button
165
+ variant="ghost"
166
+ size="icon-sm"
167
+ aria-label="通知偏好"
168
+ @click.stop="preferencesOpen = true"
169
+ >
170
+ <SettingsIcon />
171
+ </Button>
110
172
  </span>
111
173
  </DropdownMenuLabel>
112
174
  <DropdownMenuSeparator />
113
175
 
114
176
  <div v-if="notifications.length" class="max-h-96 overflow-y-auto py-1">
115
- <button
177
+ <NotificationMenuItem
116
178
  v-for="notification in notifications"
117
179
  :key="notification.id"
118
- :data-notification-id="notification.id"
119
- type="button"
120
- class="flex w-full gap-3 px-4 py-3 text-left outline-none transition-colors hover:bg-muted focus-visible:bg-muted"
121
- @click="openNotification(notification)"
122
- >
123
- <span
124
- class="mt-1.5 size-2 shrink-0 rounded-full"
125
- :class="notification.read ? 'bg-muted-foreground/30' : 'bg-primary'"
126
- aria-hidden="true"
127
- />
128
- <span class="min-w-0 flex-1 space-y-1">
129
- <span class="flex items-center justify-between gap-3">
130
- <span class="truncate text-sm font-medium text-foreground">
131
- {{ notification.title }}
132
- </span>
133
- <span class="shrink-0 text-xs text-muted-foreground">
134
- {{ formatDateTime(notification.publishedAt) }}
135
- </span>
136
- </span>
137
- <span class="line-clamp-2 text-xs leading-5 text-muted-foreground">
138
- {{ notification.summary }}
139
- </span>
140
- <span class="text-xs text-muted-foreground">
141
- {{ notification.category }}
142
- </span>
143
- </span>
144
- </button>
180
+ :item="notification"
181
+ :favorite-pending="favoritePendingIds.has(notification.id)"
182
+ @open="openNotification"
183
+ @toggle-favorite="toggleFavorite"
184
+ />
145
185
  </div>
146
186
 
147
187
  <div v-else class="px-4 py-8 text-center">
@@ -171,4 +211,6 @@ async function openNotification(notification: NotificationInboxItem) {
171
211
  </article>
172
212
  </SheetContent>
173
213
  </Sheet>
214
+
215
+ <NotificationPreferencesDialog v-model:open="preferencesOpen" />
174
216
  </template>
@@ -20,6 +20,7 @@ const mocks = vi.hoisted(() => ({
20
20
  updatedAt: "2026-07-14T08:00:00.000Z",
21
21
  updatedBy: "admin",
22
22
  read: false,
23
+ favorite: false,
23
24
  },
24
25
  ],
25
26
  total: 1,
@@ -31,11 +32,29 @@ const mocks = vi.hoisted(() => ({
31
32
  alreadyRead: false,
32
33
  readAt: "2026-07-14T08:01:00.000Z",
33
34
  })),
35
+ favorite: vi.fn(async () => ({
36
+ success: true,
37
+ favorite: true,
38
+ changed: true,
39
+ favoritedAt: "2026-07-14T08:02:00.000Z",
40
+ })),
41
+ unfavorite: vi.fn(async () => ({
42
+ success: true,
43
+ favorite: false,
44
+ changed: true,
45
+ })),
46
+ beforeEach: vi.fn(() => vi.fn()),
47
+ }));
48
+
49
+ vi.mock("vue-router", () => ({
50
+ useRouter: () => ({ beforeEach: mocks.beforeEach }),
34
51
  }));
35
52
 
36
53
  vi.mock("@/services/notification-api", () => ({
37
54
  listNotifications: mocks.list,
38
55
  markNotificationRead: mocks.markRead,
56
+ favoriteNotification: mocks.favorite,
57
+ unfavoriteNotification: mocks.unfavorite,
39
58
  }));
40
59
 
41
60
  vi.mock("@/composables/useAccessSnapshot", () => ({
@@ -69,6 +88,9 @@ describe("站内通知", () => {
69
88
  mocks.allowed = true;
70
89
  mocks.list.mockClear();
71
90
  mocks.markRead.mockClear();
91
+ mocks.favorite.mockClear();
92
+ mocks.unfavorite.mockClear();
93
+ mocks.beforeEach.mockClear();
72
94
  });
73
95
 
74
96
  test("Feature 或阅读权限不可用时隐藏入口且不请求通知接口", async () => {
@@ -86,7 +108,7 @@ describe("站内通知", () => {
86
108
  await wrapper.get('button[aria-label="站内通知"]').trigger("click");
87
109
 
88
110
  const item = document.body.querySelector<HTMLButtonElement>(
89
- "[data-notification-id='notice_1']",
111
+ "[data-notification-id='notice_1'] > button",
90
112
  );
91
113
  expect(item).not.toBeNull();
92
114
  item?.click();
@@ -99,4 +121,25 @@ describe("站内通知", () => {
99
121
  expect(mocks.markRead).toHaveBeenCalledWith("notice_1");
100
122
  wrapper.unmount();
101
123
  });
124
+
125
+ test("收藏更新不改变通知已读状态", async () => {
126
+ const wrapper = mount(NotificationMenu, { attachTo: document.body });
127
+ await flushPromises();
128
+ await wrapper.get('button[aria-label="站内通知"]').trigger("click");
129
+
130
+ const favorite = document.body.querySelector<HTMLButtonElement>(
131
+ "[data-notification-id='notice_1'] button[aria-label='收藏通知']",
132
+ );
133
+ favorite?.click();
134
+ await flushPromises();
135
+
136
+ expect(mocks.favorite).toHaveBeenCalledWith("notice_1");
137
+ expect(mocks.markRead).not.toHaveBeenCalled();
138
+ expect(
139
+ document.body.querySelector(
140
+ "[data-notification-id='notice_1'] button[aria-label='取消收藏通知']",
141
+ ),
142
+ ).not.toBeNull();
143
+ wrapper.unmount();
144
+ });
102
145
  });
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ import type { NotificationInboxItem } from "@southwind-ai/shared";
3
+ import { StarIcon } from "@lucide/vue";
4
+ import { Button } from "@/components/ui/button";
5
+ import { formatDateTime } from "@/lib/date-time";
6
+
7
+ defineProps<{
8
+ item: NotificationInboxItem;
9
+ favoritePending: boolean;
10
+ }>();
11
+
12
+ const emit = defineEmits<{
13
+ open: [item: NotificationInboxItem];
14
+ toggleFavorite: [item: NotificationInboxItem];
15
+ }>();
16
+ </script>
17
+
18
+ <template>
19
+ <div
20
+ :data-notification-id="item.id"
21
+ class="flex items-start gap-1 px-2 transition-colors hover:bg-muted"
22
+ >
23
+ <button
24
+ type="button"
25
+ class="flex min-w-0 flex-1 gap-3 px-2 py-3 text-left outline-none focus-visible:bg-muted"
26
+ @click="emit('open', item)"
27
+ >
28
+ <span
29
+ class="mt-1.5 size-2 shrink-0 rounded-full"
30
+ :class="item.read ? 'bg-muted-foreground/30' : 'bg-primary'"
31
+ aria-hidden="true"
32
+ />
33
+ <span class="min-w-0 flex-1 space-y-1">
34
+ <span class="flex items-center justify-between gap-3">
35
+ <span class="truncate text-sm font-medium text-foreground">
36
+ {{ item.title }}
37
+ </span>
38
+ <span class="shrink-0 text-xs text-muted-foreground">
39
+ {{ formatDateTime(item.publishedAt) }}
40
+ </span>
41
+ </span>
42
+ <span class="line-clamp-2 text-xs leading-5 text-muted-foreground">
43
+ {{ item.summary }}
44
+ </span>
45
+ <span class="text-xs text-muted-foreground">{{ item.category }}</span>
46
+ </span>
47
+ </button>
48
+ <Button
49
+ variant="ghost"
50
+ size="icon-sm"
51
+ class="mt-2 shrink-0"
52
+ :disabled="favoritePending"
53
+ :aria-label="item.favorite ? '取消收藏通知' : '收藏通知'"
54
+ @click="emit('toggleFavorite', item)"
55
+ >
56
+ <StarIcon
57
+ :class="item.favorite ? 'fill-current text-primary' : undefined"
58
+ />
59
+ </Button>
60
+ </div>
61
+ </template>