---
phase: 02-rbac-ai
plan: 01
subsystem: rbac + ai-model-page
tags: [rbac, permissions, ai-model, dialog, client-component]
requires:
  - lib/api/credential-slot.ts:CredentialSlot（Phase 1 已交付，本 plan 暂未直接消费，留给 Phase 3）
  - components/ui/dialog.tsx:Dialog/DialogContent/DialogHeader/DialogTitle/DialogDescription
  - components/ui/button.tsx:Button(variant="outline")
  - components/dashboard-header.tsx:DashboardHeader（children 单 slot）
  - components/sidebar.tsx:mounted 守卫模式（line 83-104）
  - lucide-react:KeyRound
provides:
  - PermissionModule literal "credential-slot"（lib/permissions.ts）
  - PERMISSION_MATRIX 中超级管理员/AI模型管理员可访问 credential-slot
  - /ai-model 页面入口 Button（受 hasPermission 收敛）
  - 占位 Dialog 挂载点（Phase 3 落实表单内容）
affects:
  - lib/permissions.ts
  - app/ai-model/page.tsx
tech-stack:
  added: []
  patterns:
    - mounted 守卫（避免 SSR 水合警告，复用 components/sidebar.tsx 同模式）
    - hasPermission(...) && JSX 收敛（DOM 中完全不存在，非仅 hidden）
    - Dialog controlled mode（open + onOpenChange + useState 配对）
key-files:
  created: []
  modified:
    - lib/permissions.ts
    - app/ai-model/page.tsx
decisions:
  - 凭据槽位入口 Button 与原「添加新模型」Button 用 div.flex.items-center.gap-2 包裹后作为 DashboardHeader children 单节点传入（DashboardHeader 本身只渲染单 children slot，无 gap）
  - 入口 Button variant="outline"（与现有页面其他次要按钮如「查看详情」「试听示例」视觉一致）
  - 图标用 KeyRound（lucide-react 0.454.0 锁定版本支持，凭据语义最贴切）
  - 占位 Dialog 内联在 page.tsx，不抽离单独组件（Phase 3 才抽到 components/ai-model/CredentialSlotDialog.tsx）
  - mounted 守卫复用 components/sidebar.tsx:83-104 的 useState(false) + useEffect(setMounted(true)) 模式
metrics:
  duration: ~6min
  completed: 2026-05-08
  tasks: 2
  files_changed: 2
  lines_added: 53
  lines_removed: 6
---

# Phase 2 Plan 01：扩展 RBAC 矩阵 + /ai-model 页面凭据槽位入口 Summary

## 一句话

把 `'credential-slot'` 模块加入 PermissionModule union 与「超级管理员/AI模型管理员」角色矩阵，同时把 `/ai-model` 页面转为 Client Component 并加上受 `mounted && hasPermission('credential-slot')` 收敛的「凭据槽位」入口 Button + 占位 Dialog（DialogTitle「通用凭据槽位」+ DialogDescription「对话框真实内容由 Phase 3 落地」）。

## 完成的需求

- ✅ **CRED-FE-02** RBAC 模块声明：PermissionModule 14 项 union + 6 角色矩阵（其中超级管理员、AI模型管理员含 credential-slot）；其他 4 角色逐字未动
- ✅ **CRED-FE-03** /ai-model 页面入口：Button + 占位 Dialog 已落地，未授权角色 DOM 中完全不存在；点击触发占位 Dialog 打开

## 执行的任务

### Task 1：扩展 lib/permissions.ts RBAC（commit `d60dd89`）

**改动 4 处（与 PLAN 一致）：**

1. PermissionModule union 末尾追加 `| "credential-slot";`（第 14 项）
2. 「超级管理员」数组末尾追加 `"credential-slot"`
3. 「AI模型管理员」数组末尾追加 `"credential-slot"`
4. 文件顶部权限矩阵注释表新增「凭据槽位」行（与代码同步）

**未动：**
- 内容管理员 / 卡牌管理员 / 查看者 / 管理员 4 个角色数组逐字不变
- `getModuleFromPath` 函数体（pathMap 不含 `credential-slot`）
- `getUserRole` / `getAllowedModules` / `hasPermission` / `hasPathPermission` 函数体

**验证：**
- `grep -nE "['\"]credential-slot['\"]" lib/permissions.ts` 命中 3 行（union literal + 2 角色数组）
- `grep -n "credential-slot" lib/permissions.ts` 命中 4 行（含注释）
- `npx tsc --noEmit` 整体 67 条存量错误，无新错误指向 lib/permissions.ts

### Task 2：app/ai-model/page.tsx 加入口 Button + 占位 Dialog（commit `0bcaa39`）

**改动 5 处（与 PLAN 一致）：**

1. line 1 加 `"use client"` 指令（文件转为 Client Component）
2. 新增 imports：`useState, useEffect` (react)、Dialog 子组件 5 个 (`@/components/ui/dialog`)、`KeyRound` (lucide-react)、`hasPermission` (`@/lib/permissions`)
3. 函数体顶部加 `mounted` + `isCredentialDialogOpen` 两个 `useState(false)` + 一个 `useEffect(() => setMounted(true), [])`（复用 sidebar.tsx 的 mounted 守卫模式）
4. DashboardHeader children 改为 `<div className="flex items-center gap-2">` 包裹，含原「添加新模型」Button + 新增 `{mounted && hasPermission("credential-slot") && <Button variant="outline" onClick={() => setIsCredentialDialogOpen(true)}><KeyRound .../>凭据槽位</Button>}`
5. `</Tabs>` 之后、`</DashboardShell>` 之前插入 controlled mode 占位 Dialog（DialogTitle「通用凭据槽位」+ DialogDescription「对话框真实内容由 Phase 3 落地」）

**未动：**
- Tabs / TabsContent / Card 等所有原有内容（line 18-441，对应改动后 line 60-470）逐字未动
- 不新建 `components/ai-model/CredentialSlotDialog.tsx`
- 不引入 sonner / useToast / `getCredentialSlot` / `updateCredentialSlot`
- package.json / yarn.lock / package-lock.json / pnpm-lock.yaml 全部未动

**验证：**
- `head -n 1 app/ai-model/page.tsx` 输出 `"use client"`
- `grep` 命中 KeyRound×2 + 凭据槽位×2 + 通用凭据槽位×1 + hasPermission("credential-slot")×1 + setIsCredentialDialogOpen×3 + 对话框真实内容由 Phase 3 落地×1
- `npx tsc --noEmit` 无新错误指向 app/ai-model/page.tsx

## Plan 级整体验证

| # | 校验项 | 结果 |
|---|--------|------|
| 1 | `npx tsc --noEmit` 不引入指向 lib/permissions.ts / app/ai-model/page.tsx 的新错误 | ✅ 0 条 |
| 2 | `grep -nE "['\"]credential-slot['\"]" lib/permissions.ts` 命中 3 行 | ✅ |
| 3 | 4 个不应含 credential-slot 的角色数组逐字未变 | ✅ |
| 4 | `getModuleFromPath` pathMap 中无新增 credential-slot 路径映射 | ✅ |
| 5 | `head -n 1 app/ai-model/page.tsx` = `"use client"` | ✅ |
| 6 | KeyRound / 凭据槽位 / 通用凭据槽位 / hasPermission(credential-slot) / setIsCredentialDialogOpen / 对话框真实内容由 Phase 3 落地 全部命中 | ✅（综合 ≥10 条） |
| 7 | `git diff --stat package.json yarn.lock package-lock.json pnpm-lock.yaml` 0 行输出（不引入新依赖） | ✅ |

## 偏离 PLAN 之处

无 — Plan 02-01 完全按照 02-01-PLAN.md 的 5 处改动逐字执行；mounted 守卫模式严格复刻 components/sidebar.tsx:83-104。

## 已知遗留 / 移交事项

- **修改记录**：本 plan 未触动 `docs/修改记录.md`，由 Plan 02-02 在收尾任务中统一为 Phase 2 写一条条目
- **占位 Dialog**：仅含 DialogTitle + DialogDescription，无 Footer / 表单 / 提交按钮；表单与提交逻辑由 Phase 3 / CRED-FE-04 落地
- **后端联调**：Phase 3 才会真正调用 `getCredentialSlot()` / `updateCredentialSlot()`；本 plan 不涉及任何后端调用

## 提交历史

| Task | Commit | 描述 |
|------|--------|------|
| 1 | d60dd89 | feat(02-01): 扩展 RBAC 矩阵增加 credential-slot 模块 |
| 2 | 0bcaa39 | feat(02-01): /ai-model 页面新增凭据槽位入口 Button + 占位 Dialog |

## Self-Check

**文件存在性：**
- `lib/permissions.ts`：FOUND（126 行；123 → 126，+3 行 = union +1 / 超管 +1 / AI模型管理员 +1，注释表 +1 与原 -1 净 0；实际 4 处改动结果总 +3 行而非 +4 是因为最早 union 末项 `"settings";` 替换为 `"settings"` + 新增一行 `| "credential-slot";`，注释表本身只增 1 行，三个数组各 +1 但其中一个原行被改为多行）
- `app/ai-model/page.tsx`：FOUND（488 行；446 → 488，+42 行 = imports +13 / state +5 / DashboardHeader 重构 +7 / Dialog +14 - 其他微调）

**Commit 存在性：**
- d60dd89：FOUND（git log）
- 0bcaa39：FOUND（git log）

## Self-Check: PASSED
