功能修改

This commit is contained in:
npc0-hue
2026-07-20 16:42:33 +08:00
parent 48b8ad8d6c
commit a0e69417db
224 changed files with 22015 additions and 884 deletions
+4 -1
View File
@@ -11,7 +11,7 @@ First-party routes must be declared here before page implementation.
- `/users`: 用户管理.
- `/ai-providers`: AI 提供商管理.
- `/maintenance`: 系统维护(审计事件与运行节点).
- `/plugin-pages/:pluginId/:routeKey`: platform-hosted plugin page route.
- `/plugin-pages/:pluginId/:routeKey?serverInstanceId=:serverId`: platform-hosted plugin page route with optional server context; IDs are URL encoded and the route is not shown in primary navigation.
## Role Scoping
@@ -23,3 +23,6 @@ Navigation entries are generated from the current user's capability set (`contra
- 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.
# Server Detail lifecycle route
The `serverDetail` route owns the Client Manager workspace as an overview operations surface. It is a detail workflow, not a permanent split pane: list pages remain full-width and destructive lifecycle commands use shared confirmation dialogs. Route state does not contain component secrets or Run session material.
+8
View File
@@ -41,6 +41,14 @@ describe("console shell routes", () => {
expect(hashForPage("serverDetail", { serverId: "server-example-1" })).toBe("#/servers/server-example-1");
});
it("round-trips hosted plugin page hashes with server context", () => {
const hash = hashForPage("pluginPage", { pluginId: "game.scum", routeKey: "operations", serverId: "server/scum-1" });
expect(hash).toBe("#/plugin-pages/game.scum/operations?serverInstanceId=server%2Fscum-1");
const resolved = resolveRouteHash(hash, platformAdmin);
expect(resolved.route).toMatchObject({ id: "pluginPage", showInNav: false, requiredCapability: "servers.read" });
expect(resolved.params).toEqual({ pluginId: "game.scum", routeKey: "operations", serverId: "server/scum-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");
+30 -1
View File
@@ -39,6 +39,15 @@ export const firstPartyRoutes: PageRoute[] = [
requiredCapability: "servers.read",
showInNav: false
},
{
id: "pluginPage",
label: "插件页面",
path: "/plugin-pages/:pluginId/:routeKey",
hash: "#/plugin-pages/:pluginId/:routeKey",
description: "平台托管的插件运维页面",
requiredCapability: "servers.read",
showInNav: false
},
{
id: "plugins",
label: "插件市场",
@@ -93,6 +102,14 @@ export function hashForPage(pageId: PageId, params: PageParams = {}): string {
if (pageId === "serverDetail" && params.serverId) {
return `#/servers/${encodeURIComponent(params.serverId)}`;
}
if (pageId === "pluginPage" && params.pluginId && params.routeKey) {
const query = new URLSearchParams();
if (params.serverId) {
query.set("serverInstanceId", params.serverId);
}
const suffix = query.toString();
return `#/plugin-pages/${encodeURIComponent(params.pluginId)}/${encodeURIComponent(params.routeKey)}${suffix ? `?${suffix}` : ""}`;
}
return route.hash;
}
@@ -121,13 +138,25 @@ export function resolveRouteHash(hash: string, user?: CurrentUserView): Resolved
if (!normalized || normalized === "/") {
return fallback;
}
const segments = normalized.replace(/^\//, "").split("/").filter(Boolean);
const [normalizedPath, rawQuery = ""] = normalized.split("?", 2);
const segments = normalizedPath.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]) } };
}
if (segments[0] === "plugin-pages" && segments.length > 2) {
const serverId = new URLSearchParams(rawQuery).get("serverInstanceId") ?? undefined;
return {
route: routeForPage("pluginPage"),
params: {
pluginId: decodeURIComponent(segments[1]),
routeKey: decodeURIComponent(segments[2]),
serverId
}
};
}
const key = segments[0];
const byId = routesById.get(key as PageId);
if (byId) {