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.
- package/dist/cli.js +167 -17
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +5 -0
- package/template/README.md +3 -1
- package/template/apps/agent-server/Dockerfile +1 -0
- package/template/apps/agent-server/README.md +18 -5
- package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
- package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
- package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
- package/template/apps/agent-server/pyproject.toml +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
- package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +1 -2
- package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
- package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
- package/template/apps/server/package.json +4 -1
- package/template/apps/server/src/agent/run-facade.ts +2 -7
- package/template/apps/server/src/agent/tool-registry.ts +2 -6
- package/template/apps/server/src/application-services.ts +50 -0
- package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
- package/template/apps/server/src/audit/search-summary.ts +2 -6
- package/template/apps/server/src/auth/credential-restriction.ts +1 -0
- package/template/apps/server/src/auth/password.ts +30 -21
- package/template/apps/server/src/auth/routes.test.ts +44 -3
- package/template/apps/server/src/auth/routes.ts +6 -1
- package/template/apps/server/src/auth/runtime.ts +4 -1
- package/template/apps/server/src/auth/service.ts +25 -7
- package/template/apps/server/src/configuration/bootstrap.ts +20 -1
- package/template/apps/server/src/configuration/definition.test.ts +71 -0
- package/template/apps/server/src/configuration/definition.ts +242 -12
- package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
- package/template/apps/server/src/configuration/repository.ts +27 -2
- package/template/apps/server/src/configuration/routes.ts +4 -1
- package/template/apps/server/src/configuration/service.test.ts +100 -0
- package/template/apps/server/src/configuration/service.ts +50 -10
- package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
- package/template/apps/server/src/data-governance/export/service.ts +2 -6
- package/template/apps/server/src/example-modules.ts +4 -1
- package/template/apps/server/src/guards.ts +18 -5
- package/template/apps/server/src/http/request-source.test.ts +85 -0
- package/template/apps/server/src/http/request-source.ts +90 -0
- package/template/apps/server/src/index.ts +23 -6
- package/template/apps/server/src/license/routes.ts +2 -0
- package/template/apps/server/src/license/runtime-service.ts +10 -11
- package/template/apps/server/src/module-bootstrap.ts +2 -2
- package/template/apps/server/src/module-host/request-context.ts +6 -6
- package/template/apps/server/src/module-host/storage-scope.ts +2 -9
- package/template/apps/server/src/module-host/user-directory-port.ts +2 -6
- package/template/apps/server/src/notification/channel-adapter.ts +46 -0
- package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
- package/template/apps/server/src/notification/delivery-policy.ts +37 -0
- package/template/apps/server/src/notification/delivery-repository.ts +198 -0
- package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
- package/template/apps/server/src/notification/delivery-worker.ts +88 -0
- package/template/apps/server/src/notification/drizzle-management.ts +180 -0
- package/template/apps/server/src/notification/drizzle-query.ts +160 -0
- package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
- package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
- package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
- package/template/apps/server/src/notification/extension-routes.ts +111 -0
- package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
- package/template/apps/server/src/notification/memory-support.ts +72 -0
- package/template/apps/server/src/notification/preference-repository.ts +95 -0
- package/template/apps/server/src/notification/recipient-directory.ts +104 -0
- package/template/apps/server/src/notification/repository.test.ts +171 -0
- package/template/apps/server/src/notification/repository.ts +179 -32
- package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
- package/template/apps/server/src/notification/route-support.ts +225 -0
- package/template/apps/server/src/notification/routes.test.ts +182 -0
- package/template/apps/server/src/notification/routes.ts +225 -132
- package/template/apps/server/src/notification/service.test.ts +104 -0
- package/template/apps/server/src/notification/service.ts +140 -0
- package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
- package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
- package/template/apps/server/src/persistence.integration.test.ts +41 -2
- package/template/apps/server/src/route-guards.ts +1 -0
- package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
- package/template/apps/server/src/runtime-development.ts +1 -0
- package/template/apps/server/src/runtime.test.ts +23 -9
- package/template/apps/server/src/runtime.ts +39 -2
- package/template/apps/server/src/scheduler/audit.ts +2 -11
- package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
- package/template/apps/server/src/scheduler/definitions.ts +7 -0
- package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
- package/template/apps/server/src/scheduler/routes.ts +2 -7
- package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
- package/template/apps/server/src/search/opensearch-client.ts +145 -0
- package/template/apps/server/src/search/service.ts +2 -6
- package/template/apps/server/src/settings/routes.ts +15 -3
- package/template/apps/server/src/settings/runtime.ts +10 -1
- package/template/apps/server/src/storage/runtime.ts +13 -0
- package/template/apps/server/src/system/audit-query.ts +33 -4
- package/template/apps/server/src/system/audit-routes.test.ts +58 -4
- package/template/apps/server/src/system/audit-routes.ts +6 -5
- package/template/apps/server/src/system/department-organization.ts +320 -0
- package/template/apps/server/src/system/department-routes.test.ts +207 -48
- package/template/apps/server/src/system/department-routes.ts +114 -33
- package/template/apps/server/src/system/drizzle-repository.ts +15 -7
- package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
- package/template/apps/server/src/system/drizzle-system.ts +37 -9
- package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
- package/template/apps/server/src/system/identity-route-config.ts +7 -2
- package/template/apps/server/src/system/mutation-runner.ts +23 -0
- package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
- package/template/apps/server/src/system/resource-access-response.ts +12 -6
- package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
- package/template/apps/server/src/system/route-helpers.ts +2 -5
- package/template/apps/server/src/system/route-types.ts +12 -3
- package/template/apps/server/src/system/routes-validation.test.ts +26 -0
- package/template/apps/server/src/system/routes.test.ts +10 -2
- package/template/apps/server/src/system/routes.ts +70 -36
- package/template/apps/server/src/system/seed.ts +7 -0
- package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
- package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
- package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
- package/template/apps/server/src/work-order/search-provider.ts +60 -1
- package/template/apps/server/src/work-order/service.ts +42 -10
- package/template/apps/web/src/app/error-pages.ts +9 -0
- package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
- package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
- package/template/apps/web/src/components/common/FormDialog.vue +16 -2
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
- package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
- package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
- package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
- package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
- package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
- package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
- package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
- package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
- package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
- package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
- package/template/apps/web/src/lib/date-time.ts +22 -10
- package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
- package/template/apps/web/src/main.ts +14 -1
- package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
- package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
- package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
- package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
- package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
- package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
- package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
- package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
- package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
- package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
- package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
- package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
- package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
- package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
- package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
- package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
- package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
- package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
- package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
- package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
- package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
- package/template/apps/web/src/pages/system/resource-config.ts +12 -4
- package/template/apps/web/src/router/error-navigation.ts +50 -0
- package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
- package/template/apps/web/src/router/index.ts +71 -0
- package/template/apps/web/src/router/index.webtest.ts +29 -0
- package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/auth-api.ts +5 -0
- package/template/apps/web/src/services/http.ts +43 -6
- package/template/apps/web/src/services/http.webtest.ts +70 -0
- package/template/apps/web/src/services/notification-api.ts +85 -2
- package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
- package/template/apps/web/src/services/search-api.ts +39 -0
- package/template/apps/web/src/services/system-api.ts +18 -0
- package/template/apps/web/vitest.config.ts +1 -0
- package/template/docker-compose.yml +66 -0
- package/template/docs/architecture/ai-runtime.md +14 -5
- package/template/docs/architecture/search-provider-protocol.md +38 -2
- package/template/docs/development/custom-error-pages.md +78 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/platform/agent-runtime.md +32 -6
- package/template/docs/platform/audit-reliability.md +46 -2
- package/template/docs/platform/module-configuration.md +13 -4
- package/template/docs/platform/notifications.md +67 -11
- package/template/docs/platform/organization-membership.md +17 -3
- package/template/docs/platform/system-crud-api.md +16 -0
- package/template/docs/platform/web-system-crud.md +6 -2
- package/template/package.json +4 -0
- package/template/packages/config/index.test.ts +45 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/config/src/platform.ts +169 -48
- package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
- package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
- package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
- package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
- package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
- package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
- package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
- package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/schema/identity.ts +7 -0
- package/template/packages/database/src/schema/notifications.ts +135 -0
- package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
- package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-api-permissions.ts +54 -0
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +1 -1
- package/template/packages/modules/src/system-module-catalog.ts +2 -1
- package/template/packages/modules/src/system-modules.test.ts +13 -1
- package/template/packages/modules/src/system-modules.ts +12 -0
- package/template/packages/modules/src/system-permissions.ts +9 -5
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/audit.ts +25 -0
- package/template/packages/shared/src/module-configuration.ts +5 -0
- package/template/packages/shared/src/notification.ts +92 -1
- package/template/packages/shared/src/password.ts +7 -2
- package/template/packages/shared/src/platform-settings.ts +2 -0
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage/src/upload-service.test.ts +16 -0
- package/template/packages/storage/src/upload-service.ts +5 -1
- 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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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)}`,
|