create-windy 0.2.34 → 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 (237) hide show
  1. package/dist/cli.js +167 -17
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/AGENTS.md +5 -0
  5. package/template/README.md +3 -1
  6. package/template/apps/agent-server/Dockerfile +1 -0
  7. package/template/apps/agent-server/README.md +18 -5
  8. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  9. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  10. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  11. package/template/apps/agent-server/pyproject.toml +1 -1
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/app.py +1 -2
  17. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  18. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  19. package/template/apps/server/package.json +4 -1
  20. package/template/apps/server/src/agent/run-facade.ts +2 -7
  21. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  22. package/template/apps/server/src/application-services.ts +50 -0
  23. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  24. package/template/apps/server/src/audit/search-summary.ts +2 -6
  25. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  26. package/template/apps/server/src/auth/password.ts +30 -21
  27. package/template/apps/server/src/auth/routes.test.ts +44 -3
  28. package/template/apps/server/src/auth/routes.ts +6 -1
  29. package/template/apps/server/src/auth/runtime.ts +4 -1
  30. package/template/apps/server/src/auth/service.ts +25 -7
  31. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  32. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  33. package/template/apps/server/src/configuration/definition.ts +242 -12
  34. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  35. package/template/apps/server/src/configuration/repository.ts +27 -2
  36. package/template/apps/server/src/configuration/routes.ts +4 -1
  37. package/template/apps/server/src/configuration/service.test.ts +100 -0
  38. package/template/apps/server/src/configuration/service.ts +50 -10
  39. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  40. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  41. package/template/apps/server/src/example-modules.ts +4 -1
  42. package/template/apps/server/src/guards.ts +18 -5
  43. package/template/apps/server/src/http/request-source.test.ts +85 -0
  44. package/template/apps/server/src/http/request-source.ts +90 -0
  45. package/template/apps/server/src/index.ts +23 -6
  46. package/template/apps/server/src/license/routes.ts +2 -0
  47. package/template/apps/server/src/license/runtime-service.ts +10 -11
  48. package/template/apps/server/src/module-bootstrap.ts +2 -2
  49. package/template/apps/server/src/module-host/request-context.ts +6 -6
  50. package/template/apps/server/src/module-host/storage-scope.ts +2 -9
  51. package/template/apps/server/src/module-host/user-directory-port.ts +2 -6
  52. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  53. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  54. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  55. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  56. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  57. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  58. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  59. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  60. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  61. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  62. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  63. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  64. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  65. package/template/apps/server/src/notification/memory-support.ts +72 -0
  66. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  67. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  68. package/template/apps/server/src/notification/repository.test.ts +171 -0
  69. package/template/apps/server/src/notification/repository.ts +179 -32
  70. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  71. package/template/apps/server/src/notification/route-support.ts +225 -0
  72. package/template/apps/server/src/notification/routes.test.ts +182 -0
  73. package/template/apps/server/src/notification/routes.ts +225 -132
  74. package/template/apps/server/src/notification/service.test.ts +104 -0
  75. package/template/apps/server/src/notification/service.ts +140 -0
  76. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  77. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  78. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  79. package/template/apps/server/src/route-guards.ts +1 -0
  80. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  81. package/template/apps/server/src/runtime-development.ts +1 -0
  82. package/template/apps/server/src/runtime.test.ts +23 -9
  83. package/template/apps/server/src/runtime.ts +39 -2
  84. package/template/apps/server/src/scheduler/audit.ts +2 -11
  85. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  86. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  87. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  88. package/template/apps/server/src/scheduler/routes.ts +2 -7
  89. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  90. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  91. package/template/apps/server/src/search/service.ts +2 -6
  92. package/template/apps/server/src/settings/routes.ts +15 -3
  93. package/template/apps/server/src/settings/runtime.ts +10 -1
  94. package/template/apps/server/src/storage/runtime.ts +13 -0
  95. package/template/apps/server/src/system/audit-query.ts +33 -4
  96. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  97. package/template/apps/server/src/system/audit-routes.ts +6 -5
  98. package/template/apps/server/src/system/department-organization.ts +320 -0
  99. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  100. package/template/apps/server/src/system/department-routes.ts +114 -33
  101. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  102. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  103. package/template/apps/server/src/system/drizzle-system.ts +37 -9
  104. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  105. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  106. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  107. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  108. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  109. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  110. package/template/apps/server/src/system/route-helpers.ts +2 -5
  111. package/template/apps/server/src/system/route-types.ts +12 -3
  112. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  113. package/template/apps/server/src/system/routes.test.ts +10 -2
  114. package/template/apps/server/src/system/routes.ts +70 -36
  115. package/template/apps/server/src/system/seed.ts +7 -0
  116. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  117. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  118. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  119. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  120. package/template/apps/server/src/work-order/service.ts +42 -10
  121. package/template/apps/web/src/app/error-pages.ts +9 -0
  122. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  123. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  124. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  125. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  126. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  127. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  128. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  129. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  130. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  131. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  132. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  133. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  134. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  135. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  136. package/template/apps/web/src/lib/date-time.ts +22 -10
  137. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  138. package/template/apps/web/src/main.ts +14 -1
  139. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  140. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  141. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  142. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  143. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  144. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  145. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  146. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  147. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  148. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  149. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  150. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  151. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  152. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  153. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  154. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  155. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  156. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  157. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  158. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  159. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  160. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  161. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  162. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  163. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
  164. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  165. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  166. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  167. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  168. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  169. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  170. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  171. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  172. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  173. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  174. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  175. package/template/apps/web/src/router/error-navigation.ts +50 -0
  176. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  177. package/template/apps/web/src/router/index.ts +71 -0
  178. package/template/apps/web/src/router/index.webtest.ts +29 -0
  179. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  180. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  181. package/template/apps/web/src/services/auth-api.ts +5 -0
  182. package/template/apps/web/src/services/http.ts +43 -6
  183. package/template/apps/web/src/services/http.webtest.ts +70 -0
  184. package/template/apps/web/src/services/notification-api.ts +85 -2
  185. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  186. package/template/apps/web/src/services/search-api.ts +39 -0
  187. package/template/apps/web/src/services/system-api.ts +18 -0
  188. package/template/apps/web/vitest.config.ts +1 -0
  189. package/template/docker-compose.yml +66 -0
  190. package/template/docs/architecture/ai-runtime.md +14 -5
  191. package/template/docs/architecture/search-provider-protocol.md +38 -2
  192. package/template/docs/development/custom-error-pages.md +78 -0
  193. package/template/docs/development/custom-login-page.md +3 -3
  194. package/template/docs/platform/agent-runtime.md +32 -6
  195. package/template/docs/platform/audit-reliability.md +46 -2
  196. package/template/docs/platform/module-configuration.md +13 -4
  197. package/template/docs/platform/notifications.md +67 -11
  198. package/template/docs/platform/organization-membership.md +17 -3
  199. package/template/docs/platform/system-crud-api.md +16 -0
  200. package/template/docs/platform/web-system-crud.md +6 -2
  201. package/template/package.json +4 -0
  202. package/template/packages/config/index.test.ts +45 -0
  203. package/template/packages/config/package.json +1 -1
  204. package/template/packages/config/src/platform.ts +169 -48
  205. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  206. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  207. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  208. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  209. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  210. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  211. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  212. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  213. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  214. package/template/packages/database/package.json +1 -1
  215. package/template/packages/database/src/schema/identity.ts +7 -0
  216. package/template/packages/database/src/schema/notifications.ts +135 -0
  217. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  218. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  219. package/template/packages/modules/package.json +1 -1
  220. package/template/packages/modules/src/system-api-permissions.ts +54 -0
  221. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  222. package/template/packages/modules/src/system-features.ts +1 -1
  223. package/template/packages/modules/src/system-module-catalog.ts +2 -1
  224. package/template/packages/modules/src/system-modules.test.ts +13 -1
  225. package/template/packages/modules/src/system-modules.ts +12 -0
  226. package/template/packages/modules/src/system-permissions.ts +9 -5
  227. package/template/packages/server-sdk/package.json +1 -1
  228. package/template/packages/shared/package.json +1 -1
  229. package/template/packages/shared/src/audit.ts +25 -0
  230. package/template/packages/shared/src/module-configuration.ts +5 -0
  231. package/template/packages/shared/src/notification.ts +92 -1
  232. package/template/packages/shared/src/password.ts +7 -2
  233. package/template/packages/shared/src/platform-settings.ts +2 -0
  234. package/template/packages/storage/package.json +1 -1
  235. package/template/packages/storage/src/upload-service.test.ts +16 -0
  236. package/template/packages/storage/src/upload-service.ts +5 -1
  237. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -0,0 +1,106 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { createMemoryHistory, createRouter } from "vue-router";
3
+ import { createApiClient } from "@/services/http";
4
+ import {
5
+ installApiFailureNavigation,
6
+ installUnexpectedErrorNavigation,
7
+ safeErrorReturnTo,
8
+ } from "./error-navigation";
9
+
10
+ function testRouter() {
11
+ return createRouter({
12
+ history: createMemoryHistory(),
13
+ routes: [
14
+ { path: "/", name: "home", component: {} },
15
+ { path: "/records", name: "records", component: {} },
16
+ { path: "/fallback", name: "fallback", component: {} },
17
+ {
18
+ path: "/error/internal",
19
+ name: "internal-error",
20
+ component: {},
21
+ meta: { errorPage: true },
22
+ },
23
+ {
24
+ path: "/error/maintenance",
25
+ name: "maintenance",
26
+ component: {},
27
+ meta: { errorPage: true },
28
+ },
29
+ {
30
+ path: "/error/network",
31
+ name: "network-error",
32
+ component: {},
33
+ meta: { errorPage: true },
34
+ },
35
+ ],
36
+ });
37
+ }
38
+
39
+ describe("统一错误导航", () => {
40
+ test("HTTP 失败类型映射到对应页面并保留 Request ID", async () => {
41
+ const router = testRouter();
42
+ await router.push("/records");
43
+
44
+ installApiFailureNavigation(router)({
45
+ kind: "maintenance",
46
+ requestId: "req-maintenance-1",
47
+ });
48
+ await vi.waitFor(() => {
49
+ expect(router.currentRoute.value.name).toBe("maintenance");
50
+ });
51
+
52
+ expect(router.currentRoute.value.query).toEqual({
53
+ from: "/records",
54
+ requestId: "req-maintenance-1",
55
+ });
56
+ });
57
+
58
+ test("页面运行异常进入 500,错误页内不再递归跳转", async () => {
59
+ const router = testRouter();
60
+ await router.push("/records");
61
+ const navigate = installUnexpectedErrorNavigation(router);
62
+
63
+ navigate("/records");
64
+ await vi.waitFor(() => {
65
+ expect(router.currentRoute.value.name).toBe("internal-error");
66
+ });
67
+
68
+ installApiFailureNavigation(router)({ kind: "network" });
69
+ await Promise.resolve();
70
+ expect(router.currentRoute.value.name).toBe("internal-error");
71
+ });
72
+
73
+ test("错误页地址和外部地址不会成为重试目标", () => {
74
+ expect(safeErrorReturnTo("/error/network")).toBe("/");
75
+ expect(safeErrorReturnTo("https://example.com")).toBe("/");
76
+ expect(safeErrorReturnTo("/records")).toBe("/records");
77
+ });
78
+
79
+ test("路由 Guard 内的服务失败最终进入全局维护页", async () => {
80
+ const router = testRouter();
81
+ const client = createApiClient({
82
+ apiBase: "/api",
83
+ fetcher: async () =>
84
+ Response.json(
85
+ { error: "MAINTENANCE" },
86
+ { status: 503, headers: { "x-request-id": "req-guard-1" } },
87
+ ),
88
+ });
89
+ client.setFailureHandler(installApiFailureNavigation(router));
90
+ router.beforeEach(async (to) => {
91
+ if (to.name !== "records") return true;
92
+ try {
93
+ await client.request("/guard");
94
+ return true;
95
+ } catch {
96
+ return { name: "fallback" };
97
+ }
98
+ });
99
+
100
+ await router.push("/records");
101
+
102
+ await vi.waitFor(() => {
103
+ expect(router.currentRoute.value.name).toBe("maintenance");
104
+ });
105
+ });
106
+ });
@@ -7,6 +7,7 @@ import {
7
7
  import DashboardPage from "@/pages/dashboard/DashboardPage.vue";
8
8
  import HomePage from "@/app/home/HomePage.vue";
9
9
  import { loginPageComponent } from "@/app/auth/login-page";
10
+ import { errorPageComponents } from "@/app/error-pages";
10
11
  import AccessDeniedPage from "@/pages/AccessDeniedPage.vue";
11
12
  import ServiceUnavailablePage from "@/pages/ServiceUnavailablePage.vue";
12
13
  import ChangePasswordPage from "@/pages/auth/ChangePasswordPage.vue";
@@ -39,6 +40,7 @@ import { useLicenseRuntime } from "@/composables/useLicenseRuntime";
39
40
  import { evaluateWebAccess, type WebAccessSnapshot } from "@/layout/navigation";
40
41
  import { isApiErrorStatus } from "@/services/http";
41
42
  import { loginRoute, safeReturnTo } from "./auth-navigation";
43
+ import { safeErrorReturnTo } from "./error-navigation";
42
44
  import {
43
45
  manifestAdminRoute,
44
46
  validateWebModuleComposition,
@@ -117,6 +119,54 @@ export const routes: RouteRecordRaw[] = [
117
119
  }),
118
120
  meta: { title: "服务暂不可用", layout: "auth", public: true },
119
121
  },
122
+ {
123
+ path: "/error/internal",
124
+ name: "internal-error",
125
+ component: errorPageComponents.internal,
126
+ props: (route) => ({
127
+ kind: "internal",
128
+ from: safeErrorReturnTo(String(route.query.from || "/")),
129
+ requestId: requestIdFrom(route.query.requestId),
130
+ }),
131
+ meta: {
132
+ title: "页面运行异常",
133
+ layout: "auth",
134
+ public: true,
135
+ errorPage: true,
136
+ },
137
+ },
138
+ {
139
+ path: "/error/maintenance",
140
+ name: "maintenance",
141
+ component: errorPageComponents.maintenance,
142
+ props: (route) => ({
143
+ kind: "maintenance",
144
+ from: safeErrorReturnTo(String(route.query.from || "/")),
145
+ requestId: requestIdFrom(route.query.requestId),
146
+ }),
147
+ meta: {
148
+ title: "服务维护中",
149
+ layout: "auth",
150
+ public: true,
151
+ errorPage: true,
152
+ },
153
+ },
154
+ {
155
+ path: "/error/network",
156
+ name: "network-error",
157
+ component: errorPageComponents.network,
158
+ props: (route) => ({
159
+ kind: "network",
160
+ from: safeErrorReturnTo(String(route.query.from || "/")),
161
+ requestId: requestIdFrom(route.query.requestId),
162
+ }),
163
+ meta: {
164
+ title: "网络异常",
165
+ layout: "auth",
166
+ public: true,
167
+ errorPage: true,
168
+ },
169
+ },
120
170
  systemRoute("system.user", "users"),
121
171
  systemRoute("system.role", "roles"),
122
172
  systemRoute("system.department", "departments"),
@@ -145,6 +195,21 @@ export const routes: RouteRecordRaw[] = [
145
195
  component: ModuleConfigurationPage,
146
196
  }),
147
197
  // @windy-module system.configuration end
198
+ {
199
+ path: "/:pathMatch(.*)*",
200
+ name: "not-found",
201
+ component: errorPageComponents.notFound,
202
+ props: (route) => ({
203
+ kind: "not-found",
204
+ from: route.fullPath,
205
+ }),
206
+ meta: {
207
+ title: "页面不存在",
208
+ layout: "auth",
209
+ public: true,
210
+ errorPage: true,
211
+ },
212
+ },
148
213
  ];
149
214
 
150
215
  validateWebModuleComposition(routes);
@@ -257,4 +322,10 @@ function serviceUnavailableRoute(from: string, reason: string) {
257
322
  };
258
323
  }
259
324
 
325
+ function requestIdFrom(value: unknown): string | undefined {
326
+ if (typeof value !== "string") return undefined;
327
+ const requestId = value.trim();
328
+ return requestId && requestId.length <= 128 ? requestId : undefined;
329
+ }
330
+
260
331
  export const router = createAppRouter();
@@ -13,6 +13,35 @@ import {
13
13
  } from "./router-test-fixtures";
14
14
 
15
15
  describe("Web 路由强制访问", () => {
16
+ test("未知地址无需权限快照即可进入统一 404", async () => {
17
+ const router = createAppRouter(
18
+ createMemoryHistory(),
19
+ {
20
+ async refresh() {
21
+ throw new Error("404 不应读取权限快照");
22
+ },
23
+ },
24
+ // @windy-module system.license begin
25
+ {
26
+ async refresh() {
27
+ throw new Error("404 不应读取 License 快照");
28
+ },
29
+ },
30
+ // @windy-module system.license end
31
+ authProvider(),
32
+ );
33
+
34
+ await router.push("/unknown/business/page");
35
+ await router.isReady();
36
+
37
+ expect(router.currentRoute.value.name).toBe("not-found");
38
+ expect(router.currentRoute.value.params.pathMatch).toEqual([
39
+ "unknown",
40
+ "business",
41
+ "page",
42
+ ]);
43
+ });
44
+
16
45
  // @windy-module system.license begin
17
46
  test("普通业务账号可进入首页且不会加载管理面或 License 快照", async () => {
18
47
  let accessRefreshes = 0;
@@ -9,6 +9,10 @@ describe("Manifest 菜单与 Router 契约", () => {
9
9
  expect(publicRoutes.map((route) => route.path)).toEqual([
10
10
  "/login",
11
11
  "/service-unavailable",
12
+ "/error/internal",
13
+ "/error/maintenance",
14
+ "/error/network",
15
+ "/:pathMatch(.*)*",
12
16
  ]);
13
17
  expect(routes.find((route) => route.path === "/")?.meta?.public).not.toBe(
14
18
  true,
@@ -13,6 +13,18 @@ describe("Router shared-scaffold 中性契约", () => {
13
13
  expect(fixtureSource).toContain("collectRouteAccess");
14
14
  });
15
15
 
16
+ test("平台错误路由使用 project-owned 业务选择 seam", () => {
17
+ expect(routerSource).toContain(
18
+ 'import { errorPageComponents } from "@/app/error-pages";',
19
+ );
20
+ expect(routerSource).toContain("component: errorPageComponents.notFound");
21
+ expect(routerSource).toContain("component: errorPageComponents.internal");
22
+ expect(routerSource).toContain(
23
+ "component: errorPageComponents.maintenance",
24
+ );
25
+ expect(routerSource).toContain("component: errorPageComponents.network");
26
+ });
27
+
16
28
  test("测试快照只从项目注入路由递归派生访问键", () => {
17
29
  expect(collectRouteAccess([])).toEqual({
18
30
  permissionKeys: [],
@@ -1,4 +1,5 @@
1
1
  import type { WebAccessActor } from "@/layout/navigation";
2
+ import type { PasswordPolicy } from "@southwind-ai/shared";
2
3
  import { apiRequest } from "./http";
3
4
 
4
5
  export interface LoginCredentials {
@@ -70,3 +71,7 @@ export function changeOwnPassword(input: PasswordChangeInput): Promise<{
70
71
  { suppressUnauthorizedHandler: true },
71
72
  );
72
73
  }
74
+
75
+ export function loadPasswordPolicy(): Promise<PasswordPolicy> {
76
+ return apiRequest<PasswordPolicy>("/auth/password/policy");
77
+ }
@@ -8,6 +8,7 @@ export interface ApiPage<T> {
8
8
  export interface ApiRequestPolicy {
9
9
  omitDevelopmentAuth?: boolean;
10
10
  suppressUnauthorizedHandler?: boolean;
11
+ suppressFailureHandler?: boolean;
11
12
  }
12
13
 
13
14
  export interface DevelopmentAuthStatus {
@@ -21,12 +22,18 @@ export interface ApiClientOptions {
21
22
  developmentToken?: string;
22
23
  }
23
24
 
25
+ export interface ApiFailure {
26
+ kind: "internal" | "maintenance" | "network";
27
+ requestId?: string;
28
+ }
29
+
24
30
  export class ApiError extends Error {
25
31
  constructor(
26
32
  message: string,
27
33
  readonly status: number,
28
34
  readonly code?: string,
29
35
  readonly reason?: string,
36
+ readonly requestId?: string,
30
37
  ) {
31
38
  super(message);
32
39
  this.name = "ApiError";
@@ -38,6 +45,7 @@ export function createApiClient(options: ApiClientOptions) {
38
45
  const configuredToken = options.developmentToken?.trim() || undefined;
39
46
  let developmentAuthActive = Boolean(configuredToken);
40
47
  let unauthorizedHandler: (() => void) | undefined;
48
+ let failureHandler: ((failure: ApiFailure) => void) | undefined;
41
49
 
42
50
  async function response(
43
51
  path: string,
@@ -56,17 +64,31 @@ export function createApiClient(options: ApiClientOptions) {
56
64
  ) {
57
65
  headers.set("authorization", `Bearer ${configuredToken}`);
58
66
  }
59
- const result = await fetcher(`${options.apiBase}${path}`, {
60
- ...init,
61
- credentials: init.credentials || "same-origin",
62
- headers,
63
- });
67
+ let result: Response;
68
+ try {
69
+ result = await fetcher(`${options.apiBase}${path}`, {
70
+ ...init,
71
+ credentials: init.credentials || "same-origin",
72
+ headers,
73
+ });
74
+ } catch (error) {
75
+ if (!policy.suppressFailureHandler && !isAbortError(error)) {
76
+ failureHandler?.({ kind: "network" });
77
+ }
78
+ throw error;
79
+ }
64
80
 
65
81
  if (!result.ok) {
66
82
  const error = await readApiError(result);
67
83
  if (result.status === 401 && !policy.suppressUnauthorizedHandler) {
68
84
  unauthorizedHandler?.();
69
85
  }
86
+ if (!policy.suppressFailureHandler && result.status >= 500) {
87
+ failureHandler?.({
88
+ kind: result.status === 503 ? "maintenance" : "internal",
89
+ requestId: error.requestId,
90
+ });
91
+ }
70
92
  throw error;
71
93
  }
72
94
  return result;
@@ -88,6 +110,9 @@ export function createApiClient(options: ApiClientOptions) {
88
110
  setUnauthorizedHandler(handler: (() => void) | undefined) {
89
111
  unauthorizedHandler = handler;
90
112
  },
113
+ setFailureHandler(handler: ((failure: ApiFailure) => void) | undefined) {
114
+ failureHandler = handler;
115
+ },
91
116
  developmentAuthStatus(): DevelopmentAuthStatus {
92
117
  return {
93
118
  configured: Boolean(configuredToken),
@@ -116,9 +141,16 @@ async function readApiError(response: Response): Promise<ApiError> {
116
141
  response.status,
117
142
  payload.error,
118
143
  payload.reason,
144
+ response.headers.get("x-request-id") || undefined,
119
145
  );
120
146
  } catch {
121
- return new ApiError(response.statusText || "请求失败", response.status);
147
+ return new ApiError(
148
+ response.statusText || "请求失败",
149
+ response.status,
150
+ undefined,
151
+ undefined,
152
+ response.headers.get("x-request-id") || undefined,
153
+ );
122
154
  }
123
155
  }
124
156
 
@@ -137,9 +169,14 @@ const client = createApiClient({
137
169
  export const apiRequest = client.request;
138
170
  export const apiResponse = client.response;
139
171
  export const setUnauthorizedHandler = client.setUnauthorizedHandler;
172
+ export const setApiFailureHandler = client.setFailureHandler;
140
173
  export const developmentAuthStatus = client.developmentAuthStatus;
141
174
  export const suspendDevelopmentAuth = client.suspendDevelopmentAuth;
142
175
 
143
176
  export function isApiErrorStatus(error: unknown, status: number): boolean {
144
177
  return error instanceof ApiError && error.status === status;
145
178
  }
179
+
180
+ function isAbortError(error: unknown): boolean {
181
+ return error instanceof Error && error.name === "AbortError";
182
+ }
@@ -70,4 +70,74 @@ describe("Web HTTP 会话策略", () => {
70
70
  });
71
71
  expect(unauthorized).toHaveBeenCalledTimes(1);
72
72
  });
73
+
74
+ test("5xx 映射统一错误类型并保留响应 Request ID", async () => {
75
+ const failures = vi.fn();
76
+ const fetcher = vi
77
+ .fn<typeof fetch>()
78
+ .mockResolvedValueOnce(
79
+ Response.json(
80
+ { error: "INTERNAL_ERROR" },
81
+ {
82
+ status: 500,
83
+ headers: { "x-request-id": "req-internal-1" },
84
+ },
85
+ ),
86
+ )
87
+ .mockResolvedValueOnce(
88
+ Response.json(
89
+ { error: "MAINTENANCE" },
90
+ {
91
+ status: 503,
92
+ headers: { "x-request-id": "req-maintenance-1" },
93
+ },
94
+ ),
95
+ );
96
+ const client = createApiClient({ apiBase: "/api", fetcher });
97
+ client.setFailureHandler(failures);
98
+
99
+ await expect(client.request("/first")).rejects.toMatchObject({
100
+ status: 500,
101
+ requestId: "req-internal-1",
102
+ });
103
+ await expect(client.request("/second")).rejects.toMatchObject({
104
+ status: 503,
105
+ requestId: "req-maintenance-1",
106
+ });
107
+ expect(failures.mock.calls).toEqual([
108
+ [{ kind: "internal", requestId: "req-internal-1" }],
109
+ [{ kind: "maintenance", requestId: "req-maintenance-1" }],
110
+ ]);
111
+ });
112
+
113
+ test("传输失败进入网络异常,调用方可显式抑制全局错误页", async () => {
114
+ const failures = vi.fn();
115
+ const fetcher = vi.fn<typeof fetch>(async () => {
116
+ throw new TypeError("Failed to fetch");
117
+ });
118
+ const client = createApiClient({ apiBase: "/api", fetcher });
119
+ client.setFailureHandler(failures);
120
+
121
+ await expect(client.request("/first")).rejects.toThrow("Failed to fetch");
122
+ await expect(
123
+ client.request("/second", {}, { suppressFailureHandler: true }),
124
+ ).rejects.toThrow("Failed to fetch");
125
+
126
+ expect(failures).toHaveBeenCalledTimes(1);
127
+ expect(failures).toHaveBeenCalledWith({ kind: "network" });
128
+ });
129
+
130
+ test("调用方主动取消请求不会误跳网络异常页", async () => {
131
+ const failures = vi.fn();
132
+ const fetcher = vi.fn<typeof fetch>(async () => {
133
+ throw new DOMException("aborted", "AbortError");
134
+ });
135
+ const client = createApiClient({ apiBase: "/api", fetcher });
136
+ client.setFailureHandler(failures);
137
+
138
+ await expect(client.request("/cancelled")).rejects.toMatchObject({
139
+ name: "AbortError",
140
+ });
141
+ expect(failures).not.toHaveBeenCalled();
142
+ });
73
143
  });
@@ -1,16 +1,39 @@
1
1
  import type {
2
2
  MarkNotificationReadResult,
3
+ NotificationInboxFilters,
3
4
  NotificationInboxItem,
5
+ NotificationAudienceOptions,
6
+ NotificationDelivery,
7
+ NotificationPreference,
8
+ NotificationRevision,
4
9
  PageResult,
5
10
  PlatformNotification,
6
11
  PublishNotificationInput,
12
+ SetNotificationFavoriteResult,
13
+ UpdateNotificationInput,
14
+ UpdateNotificationPreferenceInput,
15
+ UpdateNotificationResult,
7
16
  } from "@southwind-ai/shared";
8
17
  import { DEFAULT_PAGE_SIZE } from "@southwind-ai/shared";
9
18
  import { apiRequest } from "./http";
10
19
 
11
- export function listNotifications(page = 1, pageSize = DEFAULT_PAGE_SIZE) {
20
+ export function listNotifications(
21
+ page = 1,
22
+ pageSize = DEFAULT_PAGE_SIZE,
23
+ filters: NotificationInboxFilters = {},
24
+ ) {
25
+ const params = new URLSearchParams({
26
+ page: String(page),
27
+ pageSize: String(pageSize),
28
+ });
29
+ if (filters.q) params.set("q", filters.q);
30
+ if (filters.category) params.set("category", filters.category);
31
+ if (filters.read) params.set("read", filters.read);
32
+ if (filters.favorite !== undefined) {
33
+ params.set("favorite", String(filters.favorite));
34
+ }
12
35
  return apiRequest<PageResult<NotificationInboxItem>>(
13
- `/notifications?page=${page}&pageSize=${pageSize}`,
36
+ `/notifications?${params.toString()}`,
14
37
  );
15
38
  }
16
39
 
@@ -21,6 +44,20 @@ export function markNotificationRead(id: string) {
21
44
  );
22
45
  }
23
46
 
47
+ export function favoriteNotification(id: string) {
48
+ return apiRequest<SetNotificationFavoriteResult>(
49
+ `/notifications/${encodeURIComponent(id)}/favorite`,
50
+ { method: "PUT" },
51
+ );
52
+ }
53
+
54
+ export function unfavoriteNotification(id: string) {
55
+ return apiRequest<SetNotificationFavoriteResult>(
56
+ `/notifications/${encodeURIComponent(id)}/favorite`,
57
+ { method: "DELETE" },
58
+ );
59
+ }
60
+
24
61
  export function listManagedNotifications(
25
62
  page = 1,
26
63
  pageSize = DEFAULT_PAGE_SIZE,
@@ -43,6 +80,52 @@ export function publishNotification(input: PublishNotificationInput) {
43
80
  });
44
81
  }
45
82
 
83
+ export function getNotificationPreference() {
84
+ return apiRequest<NotificationPreference>("/notifications/preferences");
85
+ }
86
+
87
+ export function updateNotificationPreference(
88
+ input: UpdateNotificationPreferenceInput,
89
+ ) {
90
+ return apiRequest<NotificationPreference>("/notifications/preferences", {
91
+ method: "PUT",
92
+ body: JSON.stringify(input),
93
+ });
94
+ }
95
+
96
+ export function getNotificationAudienceOptions() {
97
+ return apiRequest<NotificationAudienceOptions>(
98
+ "/system/notifications/audience-options",
99
+ );
100
+ }
101
+
102
+ export function listNotificationDeliveries(id: string) {
103
+ return apiRequest<NotificationDelivery[]>(
104
+ `/system/notifications/${encodeURIComponent(id)}/deliveries`,
105
+ );
106
+ }
107
+
108
+ export function updateNotification(id: string, input: UpdateNotificationInput) {
109
+ return apiRequest<UpdateNotificationResult>(
110
+ `/system/notifications/${encodeURIComponent(id)}`,
111
+ { method: "PUT", body: JSON.stringify(input) },
112
+ );
113
+ }
114
+
115
+ export function listNotificationRevisions(
116
+ id: string,
117
+ page = 1,
118
+ pageSize = DEFAULT_PAGE_SIZE,
119
+ ) {
120
+ const params = new URLSearchParams({
121
+ page: String(page),
122
+ pageSize: String(pageSize),
123
+ });
124
+ return apiRequest<PageResult<NotificationRevision>>(
125
+ `/system/notifications/${encodeURIComponent(id)}/revisions?${params.toString()}`,
126
+ );
127
+ }
128
+
46
129
  export function archiveNotification(id: string) {
47
130
  return apiRequest<PlatformNotification>(
48
131
  `/system/notifications/${encodeURIComponent(id)}`,