feat: add agent admin settings access
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-17 23:16:52 +08:00
parent 57b784f542
commit ea5a189ae0
15 changed files with 104 additions and 28 deletions

View File

@ -13,6 +13,12 @@ export default async function AdminLayout({
getNavLinks(),
getTutorialVideos(),
]);
const settingsHref =
session?.user?.type === "admin"
? "/settings"
: session?.user?.type === "agent_admin"
? "/settings/agent-workflow"
: null;
return (
<div className="flex min-h-screen flex-col bg-background">
@ -20,7 +26,8 @@ export default async function AdminLayout({
siteName={siteSettings.siteName}
siteLogo={siteSettings.siteLogo || undefined}
siteLogoDark={siteSettings.siteLogoDark || undefined}
isAdmin={session?.user?.type === "admin"}
isAdmin={session?.user?.type === "admin" || session?.user?.type === "agent_admin"}
settingsHref={settingsHref}
customLinks={navLinks.map((link) => ({
id: link.id,
name: link.name,
@ -47,4 +54,3 @@ export default async function AdminLayout({
</div>
);
}

View File

@ -128,7 +128,7 @@ type User = {
email: string;
name: string | null;
department: string | null;
role: "guest" | "regular" | "admin";
role: "guest" | "regular" | "admin" | "agent_admin";
isHighVip: boolean;
isActive: boolean;
monthlyImageLimit: number | null;
@ -141,6 +141,7 @@ const ROLE_LABELS: Record<User["role"], string> = {
guest: "访客用户",
regular: "普通用户",
admin: "管理员",
agent_admin: "智能体管理员",
};
type ToolConfig = {
@ -3172,6 +3173,7 @@ export function AdminSettingsClient() {
<SelectItem value="guest">访客</SelectItem>
<SelectItem value="regular">普通</SelectItem>
<SelectItem value="admin">管理员</SelectItem>
<SelectItem value="agent_admin">智能体管理员</SelectItem>
</SelectContent>
</Select>
<Button

View File

@ -119,7 +119,15 @@ function createWorkflowStepDraft(
};
}
export function AgentWorkflowSettingsClient() {
interface AgentWorkflowSettingsClientProps {
backHref?: string;
backLabel?: string;
}
export function AgentWorkflowSettingsClient({
backHref = "/settings",
backLabel = "返回设置",
}: AgentWorkflowSettingsClientProps = {}) {
const { data, isLoading, mutate } = useSWR<{
agentWorkflowUiConfig?: AgentWorkflowUiConfig;
agentWorkflowMaterialApiConfig?: AgentWorkflowMaterialApiConfig;
@ -403,9 +411,9 @@ export function AgentWorkflowSettingsClient() {
<div className="flex items-center gap-2">
<Button asChild variant="outline">
<Link href="/settings">
<Link href={backHref}>
<ArrowLeft className="mr-2 h-4 w-4" />
返回设置
{backLabel}
</Link>
</Button>
<Button onClick={handleSave} disabled={isSaving}>

View File

@ -1,15 +1,19 @@
import { requireAdmin } from "@/lib/middleware/admin";
import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
import { redirect } from "next/navigation";
import { AgentWorkflowSettingsClient } from "./agent-workflow-settings-client";
export const dynamic = "force-dynamic";
export default async function AgentWorkflowSettingsPage() {
try {
await requireAdmin();
} catch {
const session = await requireAgentWorkflowAdmin().catch(() => null);
if (!session) {
redirect("/");
}
return <AgentWorkflowSettingsClient />;
return (
<AgentWorkflowSettingsClient
backHref={session.user.type === "admin" ? "/settings" : "/"}
backLabel={session.user.type === "admin" ? "返回设置" : "返回首页"}
/>
);
}

View File

@ -1,3 +1,4 @@
import { auth } from "@/app/(auth)/auth";
import { requireAdmin } from "@/lib/middleware/admin";
import { redirect } from "next/navigation";
import { AdminSettingsClient } from "./admin-settings-client";
@ -6,6 +7,11 @@ import { AdminSettingsClient } from "./admin-settings-client";
export const dynamic = "force-dynamic";
export default async function AdminSettingsPage() {
const session = await auth().catch(() => null);
if (session?.user?.type === "agent_admin") {
redirect("/settings/agent-workflow");
}
try {
await requireAdmin();
} catch {
@ -14,4 +20,3 @@ export default async function AdminSettingsPage() {
return <AdminSettingsClient />;
}

View File

@ -11,7 +11,7 @@ import { db } from "@/lib/db";
import { loginToken, user as userTable } from "@/lib/db/schema";
import { eq, and, gt } from "drizzle-orm";
export type UserType = "guest" | "regular" | "admin";
export type UserType = "guest" | "regular" | "admin" | "agent_admin";
declare module "next-auth" {
interface Session extends DefaultSession {

View File

@ -40,6 +40,12 @@ async function SidebarWrapper({ children }: { children: React.ReactNode }) {
const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true";
const homeVariant =
homeVariantSetting?.value === "lovart" ? "lovart" : "classic";
const settingsHref =
session?.user?.type === "admin"
? "/settings"
: session?.user?.type === "agent_admin"
? "/settings/agent-workflow"
: null;
return (
<ChatShell
@ -49,7 +55,8 @@ async function SidebarWrapper({ children }: { children: React.ReactNode }) {
siteName={siteSettings.siteName}
siteLogo={siteSettings.siteLogo || undefined}
siteLogoDark={siteSettings.siteLogoDark || undefined}
isAdmin={session?.user?.type === "admin"}
isAdmin={session?.user?.type === "admin" || session?.user?.type === "agent_admin"}
settingsHref={settingsHref}
customLinks={navLinks.map((link) => ({
id: link.id,
name: link.name,

View File

@ -10,7 +10,7 @@ import { ChatSDKError } from "@/lib/errors";
const updateUserSchema = z.object({
id: z.string().uuid(),
role: z.enum(["guest", "regular", "admin"]).optional(),
role: z.enum(["guest", "regular", "admin", "agent_admin"]).optional(),
isHighVip: z.boolean().optional(),
isActive: z.boolean().optional(),
email: z.string().email().optional(),

View File

@ -4,6 +4,7 @@ import { db } from "@/lib/db";
import { siteSettings, systemSettings } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { auth } from "@/app/(auth)/auth";
import { isAgentWorkflowAdminType, isSiteAdminType } from "@/lib/middleware/admin";
import {
defaultMembershipConfig,
defaultMembershipUserOverrides,
@ -209,7 +210,8 @@ function normalizeAgentCategoryTreeConfig(value: unknown): AgentCategoryTreeConf
export async function GET() {
try {
const session = await auth().catch(() => null);
const isAdmin = session?.user?.type === "admin";
const isAdmin = isSiteAdminType(session?.user?.type);
const canManageAgentWorkflow = isAgentWorkflowAdminType(session?.user?.type);
const [
settings,
notificationSetting,
@ -405,7 +407,7 @@ export async function GET() {
const agentWorkflowUiConfig = normalizeAgentWorkflowUiConfig(
agentWorkflowUiConfigSetting?.value
);
const agentWorkflowMaterialApiConfig = isAdmin
const agentWorkflowMaterialApiConfig = canManageAgentWorkflow
? normalizeAgentWorkflowMaterialApiConfig(agentWorkflowMaterialApiConfigSetting?.value)
: normalizeAgentWorkflowMaterialApiConfigForPublic(
agentWorkflowMaterialApiConfigSetting?.value
@ -669,7 +671,7 @@ const updateSchema = z.object({
.optional(),
});
// PUT /api/site-settings - 更新网站设置(仅管理员)
// PUT /api/site-settings - 更新网站设置(管理员 / 智能体管理员)
export async function PUT(request: NextRequest) {
try {
const session = await auth();
@ -678,11 +680,25 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (session.user.type !== "admin") {
const isAdmin = isSiteAdminType(session.user.type);
const canManageAgentWorkflow = isAgentWorkflowAdminType(session.user.type);
if (!canManageAgentWorkflow) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const body = await request.json();
if (!isAdmin) {
const allowedKeys = new Set([
"agentWorkflowUiConfig",
"agentWorkflowMaterialApiConfig",
"agentWorkflowBrandModelConfig",
]);
const hasRestrictedKeys = Object.keys(body || {}).some((key) => !allowedKeys.has(key));
if (hasRestrictedKeys) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
}
const data = updateSchema.parse(body);
const {
homeVariant,

View File

@ -32,6 +32,7 @@ type ChatShellProps = {
siteLogo?: string;
siteLogoDark?: string;
isAdmin: boolean;
settingsHref?: string | null;
customLinks: CustomLink[];
toolbox1Name: string;
toolbox1Enabled: boolean;
@ -52,6 +53,7 @@ export function ChatShell({
siteLogo,
siteLogoDark,
isAdmin,
settingsHref = null,
customLinks,
toolbox1Name,
toolbox1Enabled,
@ -76,6 +78,7 @@ export function ChatShell({
siteLogo={siteLogo}
siteLogoDark={siteLogoDark}
isAdmin={isAdmin}
settingsHref={settingsHref}
customLinks={customLinks}
toolbox1Name={toolbox1Name}
toolbox1Enabled={toolbox1Enabled}

View File

@ -1599,13 +1599,15 @@ export function LovartHome({
</HoverCardContent>
</HoverCard>
) : null}
{user?.type === "admin" ? (
{user?.type === "admin" || user?.type === "agent_admin" ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
onClick={() => router.push("/settings")}
onClick={() =>
router.push(user?.type === "agent_admin" ? "/settings/agent-workflow" : "/settings")
}
title="系统设置"
>
<Settings className="h-[18px] w-[18px]" />

View File

@ -48,6 +48,7 @@ type TopNavbarProps = {
siteLogo?: string;
siteLogoDark?: string;
isAdmin?: boolean;
settingsHref?: string | null;
customLinks?: CustomLink[];
toolbox1Name?: string;
toolbox1Enabled?: boolean;
@ -64,6 +65,7 @@ export function TopNavbar({
siteLogo,
siteLogoDark,
isAdmin = false,
settingsHref = null,
customLinks = [],
toolbox1Name = "工具箱",
toolbox1Enabled = true,
@ -490,16 +492,16 @@ export function TopNavbar({
</a>
</div>
{/* 系统设置(仅管理员可见)*/}
{isAdmin && (
{/* 系统设置(管理员 / 智能体管理员可见)*/}
{(isAdmin || settingsHref) && (
<div className="pb-4">
<a
href="/settings"
href={settingsHref || "/settings"}
className={navItemBaseClass}
title="系统设置"
onClick={(e) => {
e.preventDefault();
window.location.href = "/settings";
window.location.href = settingsHref || "/settings";
}}
>
<Settings className="h-5 w-5" />

View File

@ -1587,7 +1587,7 @@ export async function updateUser({
}: {
id: string;
email?: string;
role?: "guest" | "regular" | "admin";
role?: "guest" | "regular" | "admin" | "agent_admin";
isHighVip?: boolean;
isActive?: boolean;
monthlyImageLimit?: number | null;

View File

@ -21,7 +21,7 @@ export const user = pgTable("User", {
name: varchar("name", { length: 100 }), // 用户姓名(用于显示)
department: varchar("department", { length: 256 }), // 部门信息
dingtalkUserId: varchar("dingtalkUserId", { length: 128 }), // 钉钉用户ID
role: varchar("role", { enum: ["guest", "regular", "admin"] })
role: varchar("role", { enum: ["guest", "regular", "admin", "agent_admin"] })
.notNull()
.default("regular"),
isHighVip: boolean("isHighVip").notNull().default(false),

View File

@ -1,6 +1,14 @@
import { auth } from "@/app/(auth)/auth";
import { ChatSDKError } from "@/lib/errors";
export function isSiteAdminType(type?: string | null) {
return type === "admin";
}
export function isAgentWorkflowAdminType(type?: string | null) {
return type === "admin" || type === "agent_admin";
}
export async function requireAdmin() {
const session = await auth();
@ -8,10 +16,23 @@ export async function requireAdmin() {
throw new ChatSDKError("unauthorized:admin");
}
if (session.user.type !== "admin") {
if (!isSiteAdminType(session.user.type)) {
throw new ChatSDKError("forbidden:admin");
}
return session;
}
export async function requireAgentWorkflowAdmin() {
const session = await auth();
if (!session?.user) {
throw new ChatSDKError("unauthorized:admin");
}
if (!isAgentWorkflowAdminType(session.user.type)) {
throw new ChatSDKError("forbidden:admin");
}
return session;
}