feat(ai): 统一 AI 配置入口到 /admin/ai-settings

## 新增
- 创建 /admin/ai-settings 统一配置页(AiProviderSettingsCard + AiUsageDashboard)
- admin 侧边栏新增"AI 配置"菜单项(权限 AI_CONFIGURE,图标 Sparkles)
- 新增 deleteAiProvider 数据访问层(事务删除 + 自动转移默认)
- 新增 deleteAiProviderAction Server Action(Zod 校验 + 权限校验)
- AiProviderSettingsCard 新增删除按钮(AlertDialog 确认 + destructive 变体)
- 新增 i18n 翻译键(delete/deleteConfirm/deleteSuccess 等,zh-CN + en)

## 移除
- 从 /settings 移除 AI 标签页(原 VALID_TABS 含 "ai",现仅 4 标签页)
- 从考试页面移除 AI 配置弹窗(Dialog + AiProviderSettingsCard 内嵌)
- 从 ai-provider-selector.tsx 移除配置弹窗(managePanel/manageOpen props)
- 移除 settings-view.tsx 中 canConfigureAi 逻辑和未使用 import

## 变更
- 考试页面"管理"按钮改为 Link 跳转到 /admin/ai-settings
- ai-provider-selector.tsx"管理"按钮改为 Link 跳转到 /admin/ai-settings
- exam-form.tsx 移除 providerDialogOpen/providerDialogKey 状态
- 修正架构文档 004 中 Action 命名(getAiProvidersAction → getAiProviderSummaries 等)

## 架构文档同步
- 004 更新 settings 模块章节(V3 标记/修正 Action 名称/新增 deleteAiProvider)
- 005 新增 deleteAiProviderAction 节点 + /admin/ai-settings 路由
This commit is contained in:
SpecialX
2026-06-23 19:33:28 +08:00
parent d884c6d513
commit 7e320d78c1
13 changed files with 618 additions and 213 deletions

View File

@@ -12,6 +12,7 @@ import { encryptAiApiKey, getAiErrorMessage, testAiProviderById, testAiProviderC
import {
countDefaultAiProviders,
createAiProvider,
deleteAiProvider as deleteAiProviderRecord,
getAiProviderForUpdate,
getAiProviderSummaries as fetchAiProviderSummaries,
updateAiProvider,
@@ -181,3 +182,31 @@ export async function testAiProviderAction(
return { success: false, message: getAiErrorMessage(error) }
}
}
const DeleteAiProviderSchema = z.object({
id: z.string().min(1),
})
/**
* 删除 AI Provider
*
* 如果删除的是默认 Provider自动将最新的一条记录设为默认若存在
*/
export async function deleteAiProviderAction(
input: z.infer<typeof DeleteAiProviderSchema>
): Promise<ActionState<null>> {
try {
await ensureUser()
const parsed = DeleteAiProviderSchema.safeParse(input)
if (!parsed.success) {
return { success: false, message: "Invalid provider id" }
}
await deleteAiProviderRecord(parsed.data.id)
revalidatePath("/admin/ai-settings")
revalidatePath("/settings")
return { success: true, message: "AI provider deleted", data: null }
} catch (error) {
if (error instanceof PermissionDeniedError) return { success: false, message: error.message }
return { success: false, message: "Failed to delete AI provider" }
}
}