first commit

This commit is contained in:
npc0-hue
2026-07-11 14:56:10 +08:00
commit 7e05d0a4e7
660 changed files with 78119 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# Platform Web Routes
First-party routes must be declared here before page implementation.
## Required Navigation
- `/`: 平台概览(平台管理员默认落地页).
- `/servers`: 服务器管理(服主/服务器管理员默认落地页).
- `/servers/:serverId`: 服务器详情 route(日常运维工作台:概览、日志、配置、插件控制、AI 助手、操作历史).
- `/plugins`: 插件市场.
- `/users`: 用户管理.
- `/ai-providers`: AI 提供商管理.
- `/maintenance`: 系统维护(审计事件与运行节点).
- `/plugin-pages/:pluginId/:routeKey`: platform-hosted plugin page route.
## Role Scoping
Navigation entries are generated from the current user's capability set (`contracts/workspace.ts`):
- Platform administrators see 平台概览、服务器管理、插件市场、用户管理、AI 提供商管理、系统维护.
- Server owners and server administrators see only 服务器管理 and their server detail workspaces; unauthorized hashes redirect to the role default workspace.
- Default landing: platform administrators land on `/`, server owners/administrators land on `/servers`.
- Server creation opens inline from 服务器管理 (no separate `/servers/new` page).
Server and plugin detail flows must use routes, modals, or drawers. Do not build a fixed left-list/right-detail page.
+86
View File
@@ -0,0 +1,86 @@
import { describe, expect, it } from "vitest";
import { defaultPageForUser, firstPartyRoutes, hashForPage, navigationRoutesForUser, resolveRouteHash, routeForPage } from "./routes";
import { initialNavigationState } from "../stores/navigation";
import { capabilitiesForRoles, type CurrentUserView } from "../contracts/workspace";
const platformAdmin: CurrentUserView = {
id: "user-admin",
displayName: "Admin",
status: "active",
roles: ["platformAdmin"],
capabilities: capabilitiesForRoles(["platformAdmin"]),
profile: {},
source: "local"
};
const serverAdmin: CurrentUserView = {
id: "user-server",
displayName: "Server Operator",
status: "active",
roles: ["serverAdmin"],
capabilities: capabilitiesForRoles(["serverAdmin"]),
profile: {},
source: "local"
};
describe("console shell routes", () => {
it("resolves known hashes and paths", () => {
expect(resolveRouteHash("#/servers", platformAdmin).route.id).toBe("servers");
expect(resolveRouteHash("#/profile", platformAdmin).route.id).toBe("profileSettings");
expect(resolveRouteHash("#/aiProviders", platformAdmin).route.id).toBe("aiProviders");
expect(resolveRouteHash("#/ai-providers", platformAdmin).route.id).toBe("aiProviders");
expect(resolveRouteHash("#/unknown", platformAdmin).route.id).toBe("home");
expect(resolveRouteHash("#/plugins", platformAdmin).route.id).toBe("plugins");
});
it("resolves server detail hashes with params", () => {
const resolved = resolveRouteHash("#/servers/server-example-1", platformAdmin);
expect(resolved.route.id).toBe("serverDetail");
expect(resolved.params.serverId).toBe("server-example-1");
expect(hashForPage("serverDetail", { serverId: "server-example-1" })).toBe("#/servers/server-example-1");
});
it("keeps route metadata available for shell navigation", () => {
expect(firstPartyRoutes.map((route) => route.id)).toContain("maintenance");
expect(firstPartyRoutes.map((route) => route.id)).toContain("profileSettings");
expect(routeForPage("users")).toMatchObject({ label: "用户管理", hash: "#/users" });
expect(routeForPage("profileSettings")).toMatchObject({ label: "个人设置", hash: "#/profile" });
});
it("routes platform administrators to the platform overview by default", () => {
expect(defaultPageForUser(platformAdmin)).toBe("home");
expect(initialNavigationState(platformAdmin, "").pageId).toBe("home");
});
it("routes server owners and administrators to the server list by default", () => {
expect(defaultPageForUser(serverAdmin)).toBe("servers");
expect(initialNavigationState(serverAdmin, "").pageId).toBe("servers");
});
it("hides platform-only navigation from server-only users", () => {
const labels = navigationRoutesForUser(serverAdmin).map((route) => route.label);
expect(labels).toContain("服务器管理");
expect(labels).not.toContain("平台概览");
expect(labels).not.toContain("用户管理");
expect(labels).not.toContain("AI 提供商管理");
expect(labels).not.toContain("系统维护");
expect(labels).not.toContain("个人设置");
});
it("shows all platform areas to platform administrators", () => {
const labels = navigationRoutesForUser(platformAdmin).map((route) => route.label);
expect(labels).toEqual(["平台概览", "服务器管理", "插件市场", "用户管理", "AI 提供商管理", "系统维护"]);
});
it("allows every authenticated role to open profile settings", () => {
expect(initialNavigationState(platformAdmin, "#/profile").pageId).toBe("profileSettings");
expect(initialNavigationState(serverAdmin, "#/profile").pageId).toBe("profileSettings");
});
it("redirects unauthorized hashes to the role default workspace", () => {
expect(initialNavigationState(serverAdmin, "#/home").pageId).toBe("servers");
expect(initialNavigationState(serverAdmin, "#/users").pageId).toBe("servers");
expect(initialNavigationState(serverAdmin, "#/servers/server-1").pageId).toBe("serverDetail");
});
});
+141
View File
@@ -0,0 +1,141 @@
import type { PageId, PageParams, PageRoute } from "../contracts/page";
import type { CurrentUserView } from "../contracts/workspace";
import { isPlatformAdmin } from "../contracts/workspace";
export const firstPartyRoutes: PageRoute[] = [
{
id: "profileSettings",
label: "个人设置",
path: "/profile",
hash: "#/profile",
description: "个人资料与界面偏好",
requiredCapability: "profile.settings.manage",
showInNav: false
},
{
id: "home",
label: "平台概览",
path: "/",
hash: "#/home",
description: "平台运行健康概览",
requiredCapability: "platform.overview.read",
showInNav: true
},
{
id: "servers",
label: "服务器管理",
path: "/servers",
hash: "#/servers",
description: "服务器列表、状态与生命周期",
requiredCapability: "servers.read",
showInNav: true
},
{
id: "serverDetail",
label: "服务器详情",
path: "/servers/:serverId",
hash: "#/servers/:serverId",
description: "单服务器日常运维工作台",
requiredCapability: "servers.read",
showInNav: false
},
{
id: "plugins",
label: "插件市场",
path: "/plugins",
hash: "#/plugins",
description: "插件发现、安装状态与文档",
requiredCapability: "plugins.market.read",
showInNav: true
},
{
id: "users",
label: "用户管理",
path: "/users",
hash: "#/users",
description: "用户、角色和访问审核",
requiredCapability: "users.manage",
showInNav: true
},
{
id: "aiProviders",
label: "AI 提供商管理",
path: "/ai-providers",
hash: "#/aiProviders",
description: "平台中介模型配置",
requiredCapability: "aiProviders.manage",
showInNav: true
},
{
id: "maintenance",
label: "系统维护",
path: "/maintenance",
hash: "#/maintenance",
description: "审计事件与运行节点健康",
requiredCapability: "system.maintenance",
showInNav: true
}
];
const routesById = new Map(firstPartyRoutes.map((route) => [route.id, route]));
export interface ResolvedRoute {
route: PageRoute;
params: PageParams;
}
export function routeForPage(pageId: PageId): PageRoute {
return routesById.get(pageId) ?? firstPartyRoutes[0];
}
export function hashForPage(pageId: PageId, params: PageParams = {}): string {
const route = routeForPage(pageId);
if (pageId === "serverDetail" && params.serverId) {
return `#/servers/${encodeURIComponent(params.serverId)}`;
}
return route.hash;
}
export function defaultPageForUser(user: CurrentUserView): PageId {
return isPlatformAdmin(user) ? "home" : "servers";
}
export function navigationRoutesForUser(user: CurrentUserView): PageRoute[] {
return firstPartyRoutes.filter((route) => route.showInNav && user.capabilities.includes(route.requiredCapability));
}
export function canAccessRoute(user: CurrentUserView, pageId: PageId): boolean {
const route = routesById.get(pageId);
if (!route) {
return false;
}
return user.capabilities.includes(route.requiredCapability);
}
export function resolveRouteHash(hash: string, user?: CurrentUserView): ResolvedRoute {
const fallback: ResolvedRoute = {
route: routeForPage(user ? defaultPageForUser(user) : "servers"),
params: {}
};
const normalized = hash.replace(/^#/, "");
if (!normalized || normalized === "/") {
return fallback;
}
const segments = normalized.replace(/^\//, "").split("/").filter(Boolean);
if (segments.length === 0) {
return fallback;
}
if (segments[0] === "servers" && segments.length > 1) {
return { route: routeForPage("serverDetail"), params: { serverId: decodeURIComponent(segments[1]) } };
}
const key = segments[0];
const byId = routesById.get(key as PageId);
if (byId) {
return { route: byId, params: {} };
}
const byPath = firstPartyRoutes.find((route) => route.path === `/${key}`);
if (byPath) {
return { route: byPath, params: {} };
}
return fallback;
}