refactor(modules): update classes, course-plans, diagnostic, questions, settings, student, layout
- Update classes data-access (invitations, main) for invitation management - Update course-plans actions, data-access, and types - Update diagnostic data-access for report queries - Update questions data-access for question bank queries - Update settings actions, ai-provider-settings-card, data-access, and types - Update student course-filters, student-courses-view, student-schedule-filters, student-schedule-view - Update layout app-sidebar, site-header, and navigation config
This commit is contained in:
@@ -5,7 +5,11 @@ import { revalidatePath } from "next/cache"
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
|
||||
import type { ActionState } from "@/shared/types/action-state"
|
||||
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard"
|
||||
import {
|
||||
requirePermission,
|
||||
PermissionDeniedError,
|
||||
getAuthContext,
|
||||
} from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { encryptAiApiKey, getAiErrorMessage, testAiProviderById, testAiProviderConfig } from "@/shared/lib/ai"
|
||||
|
||||
@@ -15,13 +19,15 @@ import {
|
||||
deleteAiProvider as deleteAiProviderRecord,
|
||||
getAiProviderForUpdate,
|
||||
getAiProviderSummaries as fetchAiProviderSummaries,
|
||||
getAiProviderSummariesForUser,
|
||||
updateAiProvider,
|
||||
} from "./data-access"
|
||||
import type { AiProviderSummary } from "./types"
|
||||
import type { AiProviderSummary, AiProviderVisibility } from "./types"
|
||||
|
||||
export type { AiProviderSummary } from "./types"
|
||||
|
||||
const ProviderSchema = z.enum(["zhipu", "openai", "gemini", "custom"])
|
||||
const VisibilitySchema = z.enum(["public", "private"])
|
||||
|
||||
const AiProviderFormSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
@@ -30,6 +36,7 @@ const AiProviderFormSchema = z.object({
|
||||
model: z.string().min(1),
|
||||
apiKey: z.string().min(1).optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
visibility: VisibilitySchema.optional(),
|
||||
})
|
||||
|
||||
const AiProviderTestSchema = AiProviderFormSchema.extend({
|
||||
@@ -44,9 +51,15 @@ const AiProviderTestSchema = AiProviderFormSchema.extend({
|
||||
}
|
||||
})
|
||||
|
||||
const ensureUser = async (): Promise<{ id: string }> => {
|
||||
const ctx = await requirePermission(Permissions.AI_CONFIGURE)
|
||||
return { id: ctx.userId }
|
||||
/**
|
||||
* 校验当前用户身份,返回 { id, isAdmin }
|
||||
*
|
||||
* - 所有 AI_CHAT 用户均可访问(用于管理自己的 private provider)
|
||||
* - isAdmin 标识是否拥有 AI_CONFIGURE 权限(可管理 public provider 与他人 private)
|
||||
*/
|
||||
const ensureUser = async (): Promise<{ id: string; isAdmin: boolean }> => {
|
||||
const ctx = await requirePermission(Permissions.AI_CHAT)
|
||||
return { id: ctx.userId, isAdmin: ctx.permissions.includes(Permissions.AI_CONFIGURE) }
|
||||
}
|
||||
|
||||
const normalizeBaseUrl = (value: string | undefined): string | null => {
|
||||
@@ -58,10 +71,18 @@ const normalizeBaseUrl = (value: string | undefined): string | null => {
|
||||
.replace(/\/chat\/completions$/i, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户可见的 AI Provider 列表
|
||||
*
|
||||
* - 管理员:返回所有 public + private 记录
|
||||
* - 普通用户:返回 public + 自己创建的 private 记录
|
||||
*/
|
||||
export async function getAiProviderSummaries(): Promise<ActionState<AiProviderSummary[]>> {
|
||||
try {
|
||||
await ensureUser()
|
||||
const data = await fetchAiProviderSummaries()
|
||||
const user = await ensureUser()
|
||||
const data = user.isAdmin
|
||||
? await fetchAiProviderSummaries()
|
||||
: await getAiProviderSummariesForUser(user.id)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
if (error instanceof PermissionDeniedError) return { success: false, message: error.message }
|
||||
@@ -85,10 +106,17 @@ export async function upsertAiProviderAction(
|
||||
return { success: false, message: "Base URL is required for this provider" }
|
||||
}
|
||||
|
||||
// 可见性规则:
|
||||
// - 管理员可创建/更新 public 或 private
|
||||
// - 普通用户只能创建/更新 private
|
||||
const requestedVisibility: AiProviderVisibility = payload.visibility ?? "private"
|
||||
const visibility: AiProviderVisibility =
|
||||
user.isAdmin ? requestedVisibility : "private"
|
||||
|
||||
// Parallelize default-count and existing-provider queries
|
||||
const [defaultCount, existing] = await Promise.all([
|
||||
countDefaultAiProviders(),
|
||||
payload.id ? getAiProviderForUpdate(payload.id) : Promise.resolve(null),
|
||||
payload.id ? getAiProviderForUpdate(payload.id, user.id) : Promise.resolve(null),
|
||||
])
|
||||
const hasDefault = defaultCount > 0
|
||||
|
||||
@@ -114,11 +142,13 @@ export async function upsertAiProviderAction(
|
||||
apiKeyEncrypted: encrypted,
|
||||
apiKeyLast4: last4,
|
||||
isDefault: isNextDefault,
|
||||
visibility,
|
||||
updatedBy: user.id,
|
||||
},
|
||||
payload.isDefault === true
|
||||
)
|
||||
|
||||
revalidatePath("/admin/ai-settings")
|
||||
revalidatePath("/settings")
|
||||
return { success: true, message: "AI provider updated", data: id }
|
||||
}
|
||||
@@ -141,16 +171,19 @@ export async function upsertAiProviderAction(
|
||||
apiKeyEncrypted: encrypted,
|
||||
apiKeyLast4: last4,
|
||||
isDefault: shouldMakeDefault,
|
||||
visibility,
|
||||
createdBy: user.id,
|
||||
updatedBy: user.id,
|
||||
},
|
||||
shouldMakeDefault
|
||||
)
|
||||
|
||||
revalidatePath("/admin/ai-settings")
|
||||
revalidatePath("/settings")
|
||||
return { success: true, message: "AI provider created", data: id }
|
||||
} catch (error) {
|
||||
if (error instanceof PermissionDeniedError) return { success: false, message: error.message }
|
||||
console.error("[upsertAiProviderAction] Failed to save AI provider:", error)
|
||||
return { success: false, message: "Failed to save AI provider" }
|
||||
}
|
||||
}
|
||||
@@ -190,18 +223,26 @@ const DeleteAiProviderSchema = z.object({
|
||||
/**
|
||||
* 删除 AI Provider
|
||||
*
|
||||
* 权限规则:
|
||||
* - 管理员(AI_CONFIGURE):可删除任意 Provider
|
||||
* - 普通用户(AI_CHAT):仅可删除自己创建的 Provider
|
||||
*
|
||||
* 如果删除的是默认 Provider,自动将最新的一条记录设为默认(若存在)。
|
||||
*/
|
||||
export async function deleteAiProviderAction(
|
||||
input: z.infer<typeof DeleteAiProviderSchema>
|
||||
): Promise<ActionState<null>> {
|
||||
try {
|
||||
await ensureUser()
|
||||
const user = await ensureUser()
|
||||
const parsed = DeleteAiProviderSchema.safeParse(input)
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Invalid provider id" }
|
||||
}
|
||||
await deleteAiProviderRecord(parsed.data.id)
|
||||
// 管理员不传 userId(可删除任意);普通用户传 userId 做所有权校验
|
||||
await deleteAiProviderRecord(
|
||||
parsed.data.id,
|
||||
user.isAdmin ? undefined : user.id
|
||||
)
|
||||
revalidatePath("/admin/ai-settings")
|
||||
revalidatePath("/settings")
|
||||
return { success: true, message: "AI provider deleted", data: null }
|
||||
@@ -210,3 +251,18 @@ export async function deleteAiProviderAction(
|
||||
return { success: false, message: "Failed to delete AI provider" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前用户是否拥有 AI_CONFIGURE 权限(管理员)
|
||||
*
|
||||
* 供 UI 层决定是否显示 public 可见性选项。
|
||||
*/
|
||||
export async function canConfigurePublicAiProvider(): Promise<ActionState<boolean>> {
|
||||
try {
|
||||
const ctx = await getAuthContext()
|
||||
return { success: true, data: ctx.permissions.includes(Permissions.AI_CONFIGURE) }
|
||||
} catch (error) {
|
||||
if (error instanceof PermissionDeniedError) return { success: false, message: error.message, data: false }
|
||||
return { success: false, message: "Failed to check permission", data: false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, useTransition, type ReactElement } from "react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { z } from "zod"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -39,9 +40,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/components/ui/select"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { deleteAiProviderAction, getAiProviderSummaries, testAiProviderAction, upsertAiProviderAction, type AiProviderSummary } from "@/modules/settings/actions"
|
||||
|
||||
const ProviderSchema = z.enum(["zhipu", "openai", "gemini", "custom"])
|
||||
const VisibilitySchema = z.enum(["public", "private"])
|
||||
|
||||
const AiProviderFormSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
@@ -50,19 +53,26 @@ const AiProviderFormSchema = z.object({
|
||||
model: z.string().min(1, "Model is required"),
|
||||
apiKey: z.string().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
visibility: VisibilitySchema.optional(),
|
||||
})
|
||||
|
||||
type AiProviderFormValues = z.infer<typeof AiProviderFormSchema>
|
||||
|
||||
const NEW_PROVIDER_VALUE = "__new__"
|
||||
|
||||
type AiProviderSettingsCardProps = {
|
||||
onProvidersChanged?: (rows: AiProviderSummary[]) => void
|
||||
initialMode?: "new" | "first"
|
||||
isAdmin?: boolean
|
||||
currentUserId?: string
|
||||
}
|
||||
|
||||
export function AiProviderSettingsCard({
|
||||
onProvidersChanged,
|
||||
initialMode = "first",
|
||||
}: {
|
||||
onProvidersChanged?: (rows: AiProviderSummary[]) => void
|
||||
initialMode?: "new" | "first"
|
||||
}) {
|
||||
isAdmin = false,
|
||||
currentUserId,
|
||||
}: AiProviderSettingsCardProps) {
|
||||
const t = useTranslations("settings.ai.providers")
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [providers, setProviders] = useState<AiProviderSummary[]>([])
|
||||
@@ -80,6 +90,7 @@ export function AiProviderSettingsCard({
|
||||
model: "",
|
||||
apiKey: "",
|
||||
isDefault: false,
|
||||
visibility: "private",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -108,6 +119,7 @@ export function AiProviderSettingsCard({
|
||||
model: "",
|
||||
apiKey: "",
|
||||
isDefault: false,
|
||||
visibility: "private",
|
||||
})
|
||||
}, [form])
|
||||
|
||||
@@ -138,6 +150,7 @@ export function AiProviderSettingsCard({
|
||||
model: next.model,
|
||||
apiKey: "",
|
||||
isDefault: next.isDefault,
|
||||
visibility: next.visibility,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
@@ -163,6 +176,7 @@ export function AiProviderSettingsCard({
|
||||
model: next.model,
|
||||
apiKey: "",
|
||||
isDefault: next.isDefault,
|
||||
visibility: next.visibility,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -193,6 +207,7 @@ export function AiProviderSettingsCard({
|
||||
model: values.model.trim(),
|
||||
apiKey: apiKey || undefined,
|
||||
isDefault: values.isDefault ?? false,
|
||||
visibility: values.visibility,
|
||||
}
|
||||
const result = await testAiProviderAction(payload)
|
||||
if (result.success) {
|
||||
@@ -220,6 +235,7 @@ export function AiProviderSettingsCard({
|
||||
model: values.model.trim(),
|
||||
apiKey: values.apiKey?.trim() || undefined,
|
||||
isDefault: values.isDefault ?? false,
|
||||
visibility: values.visibility,
|
||||
}
|
||||
const result = await upsertAiProviderAction(payload)
|
||||
if (result.success) {
|
||||
@@ -245,6 +261,7 @@ export function AiProviderSettingsCard({
|
||||
model: next.model,
|
||||
apiKey: "",
|
||||
isDefault: next.isDefault,
|
||||
visibility: next.visibility,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -278,6 +295,7 @@ export function AiProviderSettingsCard({
|
||||
model: next.model,
|
||||
apiKey: "",
|
||||
isDefault: next.isDefault,
|
||||
visibility: next.visibility,
|
||||
})
|
||||
} else {
|
||||
resetToNew()
|
||||
@@ -291,6 +309,34 @@ export function AiProviderSettingsCard({
|
||||
})
|
||||
}
|
||||
|
||||
const renderProviderLabel = (item: AiProviderSummary): string => {
|
||||
const parts = [item.provider, "·", item.model]
|
||||
if (item.isDefault) parts.push(`(${t("setDefault")})`)
|
||||
return parts.join(" ")
|
||||
}
|
||||
|
||||
const renderVisibilityBadge = (item: AiProviderSummary): ReactElement => {
|
||||
const isOwner = currentUserId !== undefined && item.createdBy === currentUserId
|
||||
return (
|
||||
<span className="ml-2 inline-flex gap-1">
|
||||
{item.visibility === "public" ? (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-4">
|
||||
{t("badgePublic")}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4">
|
||||
{t("badgePrivate")}
|
||||
</Badge>
|
||||
)}
|
||||
{isOwner ? (
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4 text-muted-foreground">
|
||||
{t("badgeOwner")}
|
||||
</Badge>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -312,7 +358,7 @@ export function AiProviderSettingsCard({
|
||||
<SelectItem value={NEW_PROVIDER_VALUE}>{t("createNew")}</SelectItem>
|
||||
{providers.map((item) => (
|
||||
<SelectItem key={item.id} value={item.id}>
|
||||
{item.provider} · {item.model}
|
||||
{renderProviderLabel(item)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -320,10 +366,13 @@ export function AiProviderSettingsCard({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("keyStatus")}</Label>
|
||||
<div className="rounded-md border px-3 py-2 text-sm text-muted-foreground">
|
||||
{selectedProvider?.apiKeyLast4
|
||||
? `${t("stored")} • ****${selectedProvider.apiKeyLast4}`
|
||||
: t("noKey")}
|
||||
<div className="flex items-center rounded-md border px-3 py-2 text-sm text-muted-foreground">
|
||||
{selectedProvider ? renderVisibilityBadge(selectedProvider) : null}
|
||||
<span className="ml-auto">
|
||||
{selectedProvider?.apiKeyLast4
|
||||
? `${t("stored")} • ****${selectedProvider.apiKeyLast4}`
|
||||
: t("noKey")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -374,6 +423,36 @@ export function AiProviderSettingsCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="visibility"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("visibility")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value ?? "private"}
|
||||
onValueChange={field.onChange}
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="private">{t("visibilityPrivateLabel")}</SelectItem>
|
||||
{isAdmin ? (
|
||||
<SelectItem value="public">{t("visibilityPublicLabel")}</SelectItem>
|
||||
) : null}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{isAdmin ? t("visibilityDesc") : t("visibilityReadOnly")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isDefault"
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import "server-only"
|
||||
|
||||
import { count, desc, eq } from "drizzle-orm"
|
||||
import { count, desc, eq, or } from "drizzle-orm"
|
||||
|
||||
import { db } from "@/shared/db"
|
||||
import { aiProviders, passwordSecurity, users } from "@/shared/db/schema"
|
||||
|
||||
import type { AiProviderExisting, AiProviderName, AiProviderSummary } from "./types"
|
||||
import type {
|
||||
AiProviderExisting,
|
||||
AiProviderName,
|
||||
AiProviderSummary,
|
||||
AiProviderVisibility,
|
||||
} from "./types"
|
||||
|
||||
// --- AI Provider operations ---
|
||||
|
||||
/**
|
||||
* 获取所有 AI Provider(管理员视图)
|
||||
*
|
||||
* 返回所有 public 与 private 记录,供管理员在 /admin/ai-settings 中管理。
|
||||
*/
|
||||
export async function getAiProviderSummaries(): Promise<AiProviderSummary[]> {
|
||||
const rows = await db
|
||||
.select({
|
||||
@@ -18,6 +28,8 @@ export async function getAiProviderSummaries(): Promise<AiProviderSummary[]> {
|
||||
model: aiProviders.model,
|
||||
apiKeyLast4: aiProviders.apiKeyLast4,
|
||||
isDefault: aiProviders.isDefault,
|
||||
visibility: aiProviders.visibility,
|
||||
createdBy: aiProviders.createdBy,
|
||||
updatedAt: aiProviders.updatedAt,
|
||||
})
|
||||
.from(aiProviders)
|
||||
@@ -25,6 +37,41 @@ export async function getAiProviderSummaries(): Promise<AiProviderSummary[]> {
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户可见的 AI Provider(用户视图)
|
||||
*
|
||||
* 规则:
|
||||
* - public Provider:全员可见
|
||||
* - private Provider:仅创建者可见
|
||||
*
|
||||
* @param userId 当前用户 ID
|
||||
*/
|
||||
export async function getAiProviderSummariesForUser(
|
||||
userId: string
|
||||
): Promise<AiProviderSummary[]> {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: aiProviders.id,
|
||||
provider: aiProviders.provider,
|
||||
baseUrl: aiProviders.baseUrl,
|
||||
model: aiProviders.model,
|
||||
apiKeyLast4: aiProviders.apiKeyLast4,
|
||||
isDefault: aiProviders.isDefault,
|
||||
visibility: aiProviders.visibility,
|
||||
createdBy: aiProviders.createdBy,
|
||||
updatedAt: aiProviders.updatedAt,
|
||||
})
|
||||
.from(aiProviders)
|
||||
.where(
|
||||
or(
|
||||
eq(aiProviders.visibility, "public"),
|
||||
eq(aiProviders.createdBy, userId)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(aiProviders.updatedAt))
|
||||
return rows
|
||||
}
|
||||
|
||||
export async function countDefaultAiProviders(): Promise<number> {
|
||||
const [row] = await db
|
||||
.select({ value: count() })
|
||||
@@ -33,18 +80,35 @@ export async function countDefaultAiProviders(): Promise<number> {
|
||||
return Number(row?.value ?? 0)
|
||||
}
|
||||
|
||||
export async function getAiProviderForUpdate(id: string): Promise<AiProviderExisting | null> {
|
||||
/**
|
||||
* 获取 Provider 用于更新(管理员或创建者视图)
|
||||
*
|
||||
* 管理员可访问任意 Provider;非管理员仅能访问自己创建的 private Provider。
|
||||
*/
|
||||
export async function getAiProviderForUpdate(
|
||||
id: string,
|
||||
userId?: string
|
||||
): Promise<AiProviderExisting | null> {
|
||||
const [row] = await db
|
||||
.select({
|
||||
id: aiProviders.id,
|
||||
apiKeyEncrypted: aiProviders.apiKeyEncrypted,
|
||||
apiKeyLast4: aiProviders.apiKeyLast4,
|
||||
isDefault: aiProviders.isDefault,
|
||||
visibility: aiProviders.visibility,
|
||||
createdBy: aiProviders.createdBy,
|
||||
})
|
||||
.from(aiProviders)
|
||||
.where(eq(aiProviders.id, id))
|
||||
.limit(1)
|
||||
return row ?? null
|
||||
if (!row) return null
|
||||
|
||||
// 未传 userId 视为管理员视图(向后兼容)
|
||||
if (userId === undefined) return row
|
||||
|
||||
// 非管理员只能更新自己创建的 Provider
|
||||
if (row.createdBy !== userId) return null
|
||||
return row
|
||||
}
|
||||
|
||||
export async function updateAiProvider(
|
||||
@@ -56,6 +120,7 @@ export async function updateAiProvider(
|
||||
apiKeyEncrypted: string
|
||||
apiKeyLast4: string | null
|
||||
isDefault: boolean
|
||||
visibility: AiProviderVisibility
|
||||
updatedBy: string
|
||||
},
|
||||
resetOtherDefaults: boolean
|
||||
@@ -73,6 +138,7 @@ export async function updateAiProvider(
|
||||
apiKeyEncrypted: data.apiKeyEncrypted,
|
||||
apiKeyLast4: data.apiKeyLast4,
|
||||
isDefault: data.isDefault,
|
||||
visibility: data.visibility,
|
||||
updatedBy: data.updatedBy,
|
||||
})
|
||||
.where(eq(aiProviders.id, id))
|
||||
@@ -88,6 +154,7 @@ export async function createAiProvider(
|
||||
apiKeyEncrypted: string
|
||||
apiKeyLast4: string | null
|
||||
isDefault: boolean
|
||||
visibility: AiProviderVisibility
|
||||
createdBy: string
|
||||
updatedBy: string
|
||||
},
|
||||
@@ -105,6 +172,7 @@ export async function createAiProvider(
|
||||
apiKeyEncrypted: data.apiKeyEncrypted,
|
||||
apiKeyLast4: data.apiKeyLast4,
|
||||
isDefault: data.isDefault,
|
||||
visibility: data.visibility,
|
||||
createdBy: data.createdBy,
|
||||
updatedBy: data.updatedBy,
|
||||
})
|
||||
@@ -115,11 +183,20 @@ export async function createAiProvider(
|
||||
* 删除 AI Provider
|
||||
*
|
||||
* 如果删除的是默认 Provider,自动将最新的一条记录设为默认(若存在)。
|
||||
*
|
||||
* @param id Provider ID
|
||||
* @param userId 当前用户 ID(用于所有权校验;未传则不校验,仅管理员路径使用)
|
||||
*/
|
||||
export async function deleteAiProvider(id: string): Promise<{ wasDefault: boolean }> {
|
||||
export async function deleteAiProvider(
|
||||
id: string,
|
||||
userId?: string
|
||||
): Promise<{ wasDefault: boolean }> {
|
||||
return await db.transaction(async (tx) => {
|
||||
const [existing] = await tx
|
||||
.select({ isDefault: aiProviders.isDefault })
|
||||
.select({
|
||||
isDefault: aiProviders.isDefault,
|
||||
createdBy: aiProviders.createdBy,
|
||||
})
|
||||
.from(aiProviders)
|
||||
.where(eq(aiProviders.id, id))
|
||||
.limit(1)
|
||||
@@ -128,6 +205,11 @@ export async function deleteAiProvider(id: string): Promise<{ wasDefault: boolea
|
||||
return { wasDefault: false }
|
||||
}
|
||||
|
||||
// 所有权校验:非创建者不能删除(管理员路径不传 userId)
|
||||
if (userId !== undefined && existing.createdBy !== userId) {
|
||||
return { wasDefault: false }
|
||||
}
|
||||
|
||||
await tx.delete(aiProviders).where(eq(aiProviders.id, id))
|
||||
|
||||
// 如果删除的是默认 Provider,自动选一条最新的设为默认
|
||||
|
||||
@@ -7,6 +7,13 @@ import type {
|
||||
|
||||
export type AiProviderName = "zhipu" | "openai" | "gemini" | "custom"
|
||||
|
||||
/**
|
||||
* AI 服务商可见性
|
||||
* - public: 管理员发布,全员可用
|
||||
* - private: 仅创建者可见
|
||||
*/
|
||||
export type AiProviderVisibility = "public" | "private"
|
||||
|
||||
export interface AiProviderSummary {
|
||||
id: string
|
||||
provider: AiProviderName
|
||||
@@ -14,6 +21,8 @@ export interface AiProviderSummary {
|
||||
model: string
|
||||
apiKeyLast4: string | null
|
||||
isDefault: boolean
|
||||
visibility: AiProviderVisibility
|
||||
createdBy: string | null
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
@@ -22,6 +31,8 @@ export interface AiProviderExisting {
|
||||
apiKeyEncrypted: string
|
||||
apiKeyLast4: string | null
|
||||
isDefault: boolean
|
||||
visibility: AiProviderVisibility
|
||||
createdBy: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user