---
phase: 02-rbac-ai
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - lib/permissions.ts
  - app/ai-model/page.tsx
autonomous: true
requirements:
  - CRED-FE-02
  - CRED-FE-03
must_haves:
  truths:
    - "PermissionModule union 含 'credential-slot' 字面量（第 14 项，紧随 'settings' 之后）"
    - "PERMISSION_MATRIX[\"超级管理员\"] 数组末尾含 'credential-slot'"
    - "PERMISSION_MATRIX[\"AI模型管理员\"] 数组末尾含 'credential-slot'"
    - "PERMISSION_MATRIX[\"内容管理员\"] / [\"卡牌管理员\"] / [\"查看者\"] / [\"管理员\"] 四个数组逐字不变（不含 'credential-slot'）"
    - "getModuleFromPath('/ai-model') 行为不变（pathMap 中 'ai-model': 'ai-model' 仍存在且无新增 credential-slot 路径映射）"
    - "app/ai-model/page.tsx 第 1 行为 'use client'，文件已转为 Client Component"
    - "app/ai-model/page.tsx 在 DashboardHeader 内含受 hasPermission('credential-slot') 收敛的「凭据槽位」Button（KeyRound 图标，variant='outline'）"
    - "未授权角色（查看者 / 内容管理员等）登录访问 /ai-model 时，凭据槽位 Button 在 DOM 中完全不存在（不仅是隐藏）"
    - "占位 Dialog 在 </Tabs> 之后、</DashboardShell> 之前，controlled mode（open + onOpenChange），DialogTitle 含中文「通用凭据槽位」+ DialogDescription 含「对话框真实内容由 Phase 3 落地」"
    - "useState<boolean>(false) 控制 isCredentialDialogOpen；点击 Button → setIsCredentialDialogOpen(true) 打开 Dialog"
    - "为避免 SSR 水合不匹配，hasPermission 调用走 mounted 守卫（复用 components/sidebar.tsx:83-104 模式）"
  artifacts:
    - path: "lib/permissions.ts"
      provides: "PermissionModule 14 项 union + 6 角色矩阵（其中 2 角色含 credential-slot）"
      contains: "'credential-slot'"
      min_lines: 120
    - path: "app/ai-model/page.tsx"
      provides: "Client Component，含凭据槽位入口 Button + 占位 Dialog + useState/useEffect/hasPermission/KeyRound 引用"
      contains: "use client"
      min_lines: 460
  key_links:
    - from: "app/ai-model/page.tsx"
      to: "lib/permissions.ts:hasPermission"
      via: "named import + 调用 hasPermission('credential-slot')"
      pattern: "hasPermission\\([\"']credential-slot[\"']\\)"
    - from: "app/ai-model/page.tsx Button onClick"
      to: "Dialog open prop"
      via: "useState<boolean> setIsCredentialDialogOpen"
      pattern: "setIsCredentialDialogOpen\\(true\\)"
    - from: "lib/permissions.ts PermissionModule union"
      to: "PERMISSION_MATRIX 角色数组"
      via: "TS literal 校验 + Record<RoleName, PermissionModule[]>"
      pattern: "[\"']credential-slot[\"']"
---

<objective>
落地 Phase 2 的两条核心需求：
1. **CRED-FE-02**：扩展 `lib/permissions.ts` 的 RBAC，让 `'credential-slot'` 模块仅对「超级管理员」与「AI模型管理员」开放
2. **CRED-FE-03**：在 `/ai-model` 页面渲染受 `hasPermission('credential-slot')` 收敛的「凭据槽位」入口 Button，点击触发占位 Dialog 打开

Purpose：让授权运营立即能在大模型管理页看到入口控件，未授权角色彻底看不到（DOM 中完全不存在）；为 Phase 3 的真实表单落地预留 Dialog 挂载点。
Output：`lib/permissions.ts`（修改）+ `app/ai-model/page.tsx`（修改）。无新依赖、不动 lockfile。
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/02-rbac-ai/02-CONTEXT.md
@.planning/phases/02-rbac-ai/02-RESEARCH.md
@CLAUDE.md
@lib/permissions.ts
@app/ai-model/page.tsx
@components/dashboard-header.tsx
@components/ui/button.tsx
@components/ui/dialog.tsx
@components/sidebar.tsx
</context>

<interfaces>
<!-- 执行器无需再去翻代码：以下都是已读完整文件后摘出的关键 contract -->

### `lib/permissions.ts` 当前结构（VERIFIED 全文 123 行）

```ts
// line 17（保持不变）
export type RoleName = "超级管理员" | "内容管理员" | "AI模型管理员" | "卡牌管理员" | "查看者" | "管理员";

// line 21-34（union 当前 13 项，待 +1）
export type PermissionModule =
  | "dashboard"
  | "users"
  | "permissions"
  | "ai-model"
  | "outfits"
  | "props"
  | "home-decor"
  | "food"
  | "songs"
  | "dances"
  | "achievements"
  | "affinity"
  | "settings";

// line 37-60（矩阵当前 6 角色）
const PERMISSION_MATRIX: Record<RoleName, PermissionModule[]> = {
  超级管理员: [
    "dashboard", "users", "permissions", "ai-model",
    "outfits", "props", "home-decor", "food",
    "songs", "dances", "achievements", "affinity", "settings",
  ],
  内容管理员: [
    "dashboard", "outfits", "props", "home-decor", "food",
    "songs", "dances", "achievements", "affinity",
  ],
  AI模型管理员: [
    "dashboard", "ai-model",
  ],
  卡牌管理员: [
    "dashboard", "outfits", "props", "home-decor", "food",
  ],
  查看者: [
    "dashboard",
  ],
  管理员: [
    "dashboard",
  ],
};

// line 92-113（getModuleFromPath，本 phase 完全不动）
export function getModuleFromPath(pathname: string): PermissionModule | null {
  const segment = pathname.replace(/^\//, "").split("/")[0];
  const pathMap: Record<string, PermissionModule> = {
    "": "dashboard",
    "ai-model": "ai-model",
    // ... 其余 11 条
  };
  return pathMap[segment] ?? null;
}

// line 85-87（hasPermission，签名稳定，仅验证 'credential-slot' 字面量在 union 中合法）
export function hasPermission(module: PermissionModule): boolean {
  return getAllowedModules().includes(module);
}
```

### `app/ai-model/page.tsx` 当前结构（VERIFIED 全文 446 行）

```tsx
// line 1-7（当前 import；缺 "use client"，是 Server Component）
import { DashboardShell } from "@/components/dashboard-shell"
import { DashboardHeader } from "@/components/dashboard-header"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Brain, Mic, Database, Plus, Sparkles, Edit, Play, Sliders, User } from "lucide-react"

// line 8-16（DashboardHeader 区域，含现有「添加新模型」按钮）
export default function AIModelPage() {
  return (
    <DashboardShell>
      <DashboardHeader heading="大模型管理" text="管理洛天依的AI模型、语音和知识库">
        <Button className="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 transition-all duration-300 shadow-md hover:shadow-lg">
          <Plus className="mr-2 h-4 w-4" />
          添加新模型
        </Button>
      </DashboardHeader>

      <Tabs defaultValue="framework" className="space-y-4">
        ...
      </Tabs>
    </DashboardShell>  // line 443
  )
}
```

### `components/dashboard-header.tsx`（VERIFIED 全文 20 行）

```tsx
// line 8-19：children 容器是 flex row（items-center justify-between）
export function DashboardHeader({ heading, text, children }: DashboardHeaderProps) {
  return (
    <div className="flex items-center justify-between px-2 mb-8">
      <div className="grid gap-1">
        <h1 className="...">{heading}</h1>
        {text && <div className="text-lg text-muted-foreground">{text}</div>}
      </div>
      {children}   {/* ← 直接渲染 children，本身不是 flex 容器 */}
    </div>
  )
}
```

**关键判断**：外层是 `flex items-center justify-between`，左侧是 heading 容器、右侧 children 直接渲染。当 children 是**多个** Button 时，多个 Button 会被同等参与 flex 横排，但**之间无 gap**。**结论**：在 page.tsx 把两个 Button 用 `<div className="flex items-center gap-2">` 包起来，作为 children 单一节点传入，避免视觉粘连。

### `components/ui/dialog.tsx` 关键导出（VERIFIED line 111-122）

```ts
export {
  Dialog,             // = DialogPrimitive.Root（直通，支持 controlled props）
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  // ... DialogFooter / Portal / Overlay / Close / Trigger 本 phase 不用
}
```

**Controlled mode 用法**：`<Dialog open={state} onOpenChange={setState}>`，内部用 `<DialogContent>` 包 `<DialogHeader>{<DialogTitle/> + <DialogDescription/>}</DialogHeader>`，本 phase 不放 footer / form。

### `components/ui/button.tsx` 关键 variants（VERIFIED line 11-21）

variant 取值：`default | destructive | outline | secondary | ghost | link`
size 取值：`default | sm | lg | icon`

本 phase 入口 Button：`variant="outline"`、`size` 默认（与 CONTEXT.md 锁定一致；与现有页面其他「查看详情」「试听示例」等次要按钮的 variant="outline" 视觉一致）。

### `components/sidebar.tsx` mounted 守卫模式（VERIFIED line 83-104）

```tsx
"use client"
import { useState, useEffect } from "react"
import { hasPermission } from "@/lib/permissions"

const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) }, [])

// 然后用 {mounted && hasPermission(...) && <X/>} 包裹任何依赖 localStorage 的 UI
```

**为什么必须 mounted 守卫**：`getUserRole()` 在 SSR 阶段（typeof window === "undefined"）一律 fallback「查看者」；hydration 后才能读到真实 role。不加守卫会出现「超管刷新页面瞬间按钮闪一下消失再出现」的水合警告。
</interfaces>

<tasks>

<task type="auto" tdd="false">
  <name>任务 1：扩展 lib/permissions.ts RBAC（PermissionModule union +1 / 矩阵 +2 角色）</name>
  <files>lib/permissions.ts</files>

  <read_first>
    1. **必读**：`lib/permissions.ts` 完整 1-123 行（确认 union 当前 13 项 + 6 角色数组当前内容）
    2. **必读**：`.planning/phases/02-rbac-ai/02-CONTEXT.md` 的「Locked Decisions / CRED-FE-02 RBAC 模块声明」段（4 条 bullet）
    3. **必读**：`.planning/phases/02-rbac-ai/02-RESEARCH.md` 「`lib/permissions.ts` 改动 patch」段（line 358-435 完整 before/after diff）
  </read_first>

  <action>
    用 Edit 工具对 `lib/permissions.ts` 做**4 处** old_string → new_string 替换；其他内容**逐字不动**。

    **改动 1：`PermissionModule` union（line 21-34）追加 `'credential-slot'`**

    old_string（完全照搬 line 21-34，含分号）：
    ```ts
    export type PermissionModule =
      | "dashboard"
      | "users"
      | "permissions"
      | "ai-model"
      | "outfits"
      | "props"
      | "home-decor"
      | "food"
      | "songs"
      | "dances"
      | "achievements"
      | "affinity"
      | "settings";
    ```

    new_string：
    ```ts
    export type PermissionModule =
      | "dashboard"
      | "users"
      | "permissions"
      | "ai-model"
      | "outfits"
      | "props"
      | "home-decor"
      | "food"
      | "songs"
      | "dances"
      | "achievements"
      | "affinity"
      | "settings"
      | "credential-slot";
    ```

    **改动 2：「超级管理员」数组末尾追加 `"credential-slot"`（line 38-42）**

    old_string：
    ```ts
      超级管理员: [
        "dashboard", "users", "permissions", "ai-model",
        "outfits", "props", "home-decor", "food",
        "songs", "dances", "achievements", "affinity", "settings",
      ],
    ```

    new_string：
    ```ts
      超级管理员: [
        "dashboard", "users", "permissions", "ai-model",
        "outfits", "props", "home-decor", "food",
        "songs", "dances", "achievements", "affinity", "settings",
        "credential-slot",
      ],
    ```

    **改动 3：「AI模型管理员」数组末尾追加 `"credential-slot"`（line 47-49）**

    old_string：
    ```ts
      AI模型管理员: [
        "dashboard", "ai-model",
      ],
    ```

    new_string：
    ```ts
      AI模型管理员: [
        "dashboard", "ai-model",
        "credential-slot",
      ],
    ```

    **改动 4（可选 / 推荐）：更新文件顶部「权限矩阵对照表」注释（line 1-15），新增一行让文档与代码同步**

    old_string（完全照搬 line 4-14）：
    ```ts
     * 权限矩阵对照表：
     * | 模块         | 超级管理员 | 内容管理员 | AI模型管理员 | 卡牌管理员 | 查看者 |
     * |-------------|-----------|-----------|------------|-----------|-------|
     * | 仪表盘查看    | ✓         | ✓         | ✓          | ✓         | ✓     |
     * | 用户管理     | ✓         |           |            |           |       |
     * | 角色权限管理  | ✓         |           |            |           |       |
     * | AI模型管理   | ✓         |           | ✓          |           |       |
     * | 服装管理     | ✓         | ✓         |            | ✓         |       |
     * | 道具管理     | ✓         | ✓         |            | ✓         |       |
     * | 歌曲管理     | ✓         | ✓         |            |           |       |
     * | 系统设置     | ✓         |           |            |           |       |
    ```

    new_string：
    ```ts
     * 权限矩阵对照表：
     * | 模块         | 超级管理员 | 内容管理员 | AI模型管理员 | 卡牌管理员 | 查看者 |
     * |-------------|-----------|-----------|------------|-----------|-------|
     * | 仪表盘查看    | ✓         | ✓         | ✓          | ✓         | ✓     |
     * | 用户管理     | ✓         |           |            |           |       |
     * | 角色权限管理  | ✓         |           |            |           |       |
     * | AI模型管理   | ✓         |           | ✓          |           |       |
     * | 凭据槽位     | ✓         |           | ✓          |           |       |
     * | 服装管理     | ✓         | ✓         |            | ✓         |       |
     * | 道具管理     | ✓         | ✓         |            | ✓         |       |
     * | 歌曲管理     | ✓         | ✓         |            |           |       |
     * | 系统设置     | ✓         |           |            |           |       |
    ```

    **明确不要做的事**：
    - 不要动「内容管理员」「卡牌管理员」「查看者」「管理员」4 个角色的数组（逐字不变）
    - 不要动 `getModuleFromPath` 的 `pathMap`（不要新增 `'credential-slot'` 路径映射，凭据槽位是 `/ai-model` 子能力不占独立路由）
    - 不要动 `getUserRole` / `getAllowedModules` / `hasPermission` / `hasPathPermission` 四个函数体
    - 不要新增 import / export
    - 不要重排数组顺序（仅在数组**末尾**追加）
  </action>

  <acceptance_criteria>
    - `grep -nE "['\"]credential-slot['\"]" lib/permissions.ts` 命中 **3 行**（union literal + 「超级管理员」数组 + 「AI模型管理员」数组）
    - `grep -n "credential-slot" lib/permissions.ts` 命中**总共 4 行**（含 1 行注释表新增的「凭据槽位」行 = 4；若改动 4 跳过则为 3）
    - `grep -n "getModuleFromPath" lib/permissions.ts` 仍命中函数定义（行号可能不变），且 `grep -n '"ai-model": "ai-model"' lib/permissions.ts` 仍命中 1 行
    - `grep -nE "(内容管理员|卡牌管理员|查看者|管理员):" lib/permissions.ts` 各角色后的数组**不**含 `credential-slot`（人工核对 4 个数组逐字与原文一致）
    - 文件总行数变化：union +1 行、超管数组 +1 行、AI模型管理员数组 +1 行，注释表 +1 行 = 总 +4 行（123 → 127；若跳过改动 4 则 123 → 126）
  </acceptance_criteria>

  <verify>
    <automated>
    cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && npx tsc --noEmit 2>&1 | grep -E "lib/permissions\.ts" | wc -l
    # 预期：0（lib/permissions.ts 在改动后零类型错误；存量错误数与 Phase 1 的 67 条一致或更少；不能引入新的指向 lib/permissions.ts 的错误）
    </automated>
    <automated>
    cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -cE "['\"]credential-slot['\"]" lib/permissions.ts
    # 预期：3
    </automated>
  </verify>

  <done>
    - `lib/permissions.ts` PermissionModule union 含 14 项（最后一项是 `"credential-slot"`）
    - 「超级管理员」数组末尾含 `"credential-slot"`，「AI模型管理员」数组末尾含 `"credential-slot"`
    - 其他 4 角色数组逐字不变
    - `getModuleFromPath` 函数体完全不变
    - `npx tsc --noEmit` 不引入指向 `lib/permissions.ts` 的新错误
  </done>
</task>

<task type="auto" tdd="false">
  <name>任务 2：app/ai-model/page.tsx 加 "use client"、入口 Button、占位 Dialog</name>
  <files>app/ai-model/page.tsx</files>

  <read_first>
    1. **必读**：`app/ai-model/page.tsx` 完整 1-446 行（确认 line 1-7 import 块、line 8-16 DashboardHeader 段、line 442 `</Tabs>`、line 443 `</DashboardShell>`）
    2. **必读**：`components/sidebar.tsx` line 83-104（mounted 守卫模式样板，复用其结构）
    3. **必读**：`.planning/phases/02-rbac-ai/02-CONTEXT.md` 的「CRED-FE-03 /ai-model 页面入口」段（8 条 bullet）+ 「Claude's Discretion」段
    4. **必读**：`.planning/phases/02-rbac-ai/02-RESEARCH.md` 的「Code Examples / `app/ai-model/page.tsx` 改动骨架」段（line 439-512）+ 「Common Pitfalls」5 条
    5. **依赖任务 1 完成**：`lib/permissions.ts` 的 PermissionModule union 必须已含 `'credential-slot'`，否则本任务的 `hasPermission('credential-slot')` 调用会被 TS 报错
  </read_first>

  <action>
    对 `app/ai-model/page.tsx` 做 **5 处**精确改动；其他内容（Tabs / Card / 现有 Button）**逐字不动**。

    **改动 1：line 1 顶部新增 `"use client"` 指令（必须在所有 import 之前）**

    old_string（line 1-2，完整照搬现状）：
    ```tsx
    import { DashboardShell } from "@/components/dashboard-shell"
    import { DashboardHeader } from "@/components/dashboard-header"
    ```

    new_string：
    ```tsx
    "use client"

    import { useState, useEffect } from "react"
    import { DashboardShell } from "@/components/dashboard-shell"
    import { DashboardHeader } from "@/components/dashboard-header"
    ```

    **改动 2：扩展 lucide-react import（line 6 现状）+ 新增 Dialog import + hasPermission import**

    old_string：
    ```tsx
    import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
    import { Brain, Mic, Database, Plus, Sparkles, Edit, Play, Sliders, User } from "lucide-react"
    ```

    new_string：
    ```tsx
    import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
    import {
      Dialog,
      DialogContent,
      DialogDescription,
      DialogHeader,
      DialogTitle,
    } from "@/components/ui/dialog"
    import { Brain, Mic, Database, Plus, Sparkles, Edit, Play, Sliders, User, KeyRound } from "lucide-react"
    import { hasPermission } from "@/lib/permissions"
    ```

    **改动 3：函数体顶部加 useState / useEffect（mounted 守卫 + Dialog 开关）**

    old_string（line 8-11 完整照搬）：
    ```tsx
    export default function AIModelPage() {
      return (
        <DashboardShell>
          <DashboardHeader heading="大模型管理" text="管理洛天依的AI模型、语音和知识库">
    ```

    new_string：
    ```tsx
    export default function AIModelPage() {
      const [mounted, setMounted] = useState(false)
      const [isCredentialDialogOpen, setIsCredentialDialogOpen] = useState(false)

      useEffect(() => {
        setMounted(true)
      }, [])

      return (
        <DashboardShell>
          <DashboardHeader heading="大模型管理" text="管理洛天依的AI模型、语音和知识库">
    ```

    **改动 4：在 DashboardHeader 内部、line 16 `</DashboardHeader>` 之前用 flex 容器把现有「添加新模型」Button 与新增的「凭据槽位」Button 包起来（dashboard-header 的 children 是单 slot，多 Button 需自行加 gap）**

    old_string（line 12-16 完整照搬）：
    ```tsx
          <DashboardHeader heading="大模型管理" text="管理洛天依的AI模型、语音和知识库">
            <Button className="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 transition-all duration-300 shadow-md hover:shadow-lg">
              <Plus className="mr-2 h-4 w-4" />
              添加新模型
            </Button>
          </DashboardHeader>
    ```

    new_string：
    ```tsx
          <DashboardHeader heading="大模型管理" text="管理洛天依的AI模型、语音和知识库">
            <div className="flex items-center gap-2">
              <Button className="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 transition-all duration-300 shadow-md hover:shadow-lg">
                <Plus className="mr-2 h-4 w-4" />
                添加新模型
              </Button>
              {mounted && hasPermission("credential-slot") && (
                <Button
                  variant="outline"
                  onClick={() => setIsCredentialDialogOpen(true)}
                >
                  <KeyRound className="mr-2 h-4 w-4" />
                  凭据槽位
                </Button>
              )}
            </div>
          </DashboardHeader>
    ```

    **改动 5：在 `</Tabs>`（line 442）之后、`</DashboardShell>`（line 443）之前插入占位 Dialog**

    old_string（line 442-444 完整照搬，含末尾 `}`）：
    ```tsx
          </Tabs>
        </DashboardShell>
      )
    ```

    new_string：
    ```tsx
          </Tabs>

          <Dialog
            open={isCredentialDialogOpen}
            onOpenChange={setIsCredentialDialogOpen}
          >
            <DialogContent>
              <DialogHeader>
                <DialogTitle>通用凭据槽位</DialogTitle>
                <DialogDescription>
                  对话框真实内容由 Phase 3 落地
                </DialogDescription>
              </DialogHeader>
            </DialogContent>
          </Dialog>
        </DashboardShell>
      )
    ```

    **明确不要做的事**：
    - 不要新建 `components/ai-model/CredentialSlotDialog.tsx`（Phase 3 才抽离）
    - 不要给 Dialog 加表单 / 输入框 / 按钮 / 提交逻辑（Phase 3 落地）
    - 不要 import sonner / useToast（Phase 3 落地）
    - 不要 import `getCredentialSlot` / `updateCredentialSlot`（Phase 3 落地）
    - 不要动 Tabs / TabsContent / Card 任何子内容（line 18-441 全部保留逐字不变）
    - 不要给 Button 加 `disabled` / loading state
    - 不要把「添加新模型」Button 的 className 风格删掉或修改
    - 如果 `KeyRound` 在 lucide-react 中报错（编译时 import 失败），降级到 `Lock`（同包内图标），但**必须先尝试 KeyRound**（lucide-react ^0.454.0 已锁，KeyRound 自 0.298+ 即存在）
  </action>

  <acceptance_criteria>
    - `head -n 1 app/ai-model/page.tsx` 输出 `"use client"`
    - `grep -n "useState" app/ai-model/page.tsx` 命中 import 行 + 至少 2 处调用（mounted + isCredentialDialogOpen）
    - `grep -n "useEffect" app/ai-model/page.tsx` 命中 import 行 + 至少 1 处调用
    - `grep -n "KeyRound" app/ai-model/page.tsx` 命中 ≥2（import + JSX）
    - `grep -nE 'hasPermission\(["'\'']credential-slot' app/ai-model/page.tsx` 命中 ≥1
    - `grep -n "凭据槽位" app/ai-model/page.tsx` 命中 ≥2（Button 文案 + DialogTitle）
    - `grep -n "通用凭据槽位" app/ai-model/page.tsx` 命中 ≥1（DialogTitle）
    - `grep -n "对话框真实内容由 Phase 3 落地" app/ai-model/page.tsx` 命中 1
    - `grep -n "setIsCredentialDialogOpen(true)" app/ai-model/page.tsx` 命中 1
    - `grep -n "isCredentialDialogOpen" app/ai-model/page.tsx` 命中 ≥3（useState + onClick + Dialog open prop）
    - `grep -nE 'variant=["'\'']outline["'\'']' app/ai-model/page.tsx` 命中 ≥1（凭据槽位 Button；可能有更多匹配如其他卡片内的 outline 按钮，无所谓）
    - `grep -n "from \"@/components/ui/dialog\"" app/ai-model/page.tsx` 命中 1
    - `grep -n "from \"@/lib/permissions\"" app/ai-model/page.tsx` 命中 1
    - `grep -nE "添加新模型" app/ai-model/page.tsx` 仍命中 ≥2（保留原有按钮文案；DashboardHeader 内 + CardFooter 内）
  </acceptance_criteria>

  <verify>
    <automated>
    cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && npx tsc --noEmit 2>&1 | grep -E "app/ai-model/page\.tsx" | wc -l
    # 预期：0（新文件零类型错误；沿用 Phase 1 已建立的判定模式：tsc 整体退出码 2，但 grep 过滤后不指向 app/ai-model/page.tsx）
    </automated>
    <automated>
    cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && head -n 1 app/ai-model/page.tsx
    # 预期：包含 "use client"（精确字符串："use client"）
    </automated>
    <automated>
    cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -cE "(KeyRound|凭据槽位|通用凭据槽位|hasPermission\\([\"']credential-slot|setIsCredentialDialogOpen|对话框真实内容由 Phase 3 落地)" app/ai-model/page.tsx
    # 预期：≥10（KeyRound×2 + 凭据槽位×2 + 通用凭据槽位×1 + hasPermission(credential-slot)×1 + setIsCredentialDialogOpen×3 + 对话框真实内容由 Phase 3 落地×1）
    </automated>
  </verify>

  <done>
    - 文件 line 1 是 `"use client"`
    - 新增 5 个 import：`useState`、`useEffect`（react）+ Dialog 子组件 5 个（@/components/ui/dialog）+ `KeyRound`（lucide-react）+ `hasPermission`（@/lib/permissions）
    - 函数体顶部含 `mounted` + `isCredentialDialogOpen` 两个 useState + 1 个 useEffect 设 mounted
    - DashboardHeader children 改为 `<div className="flex items-center gap-2">` 包两个 Button：保留原「添加新模型」Button + 新增 `{mounted && hasPermission("credential-slot") && <Button variant="outline" onClick={() => setIsCredentialDialogOpen(true)}><KeyRound .../>凭据槽位</Button>}`
    - `</Tabs>` 之后、`</DashboardShell>` 之前插入 controlled mode `<Dialog open={isCredentialDialogOpen} onOpenChange={setIsCredentialDialogOpen}>` + `<DialogContent><DialogHeader><DialogTitle>通用凭据槽位</DialogTitle><DialogDescription>对话框真实内容由 Phase 3 落地</DialogDescription></DialogHeader></DialogContent></Dialog>`
    - Tabs / TabsContent / Card 等所有原有内容（line 18-441）逐字不变
    - `npx tsc --noEmit` 不引入指向 `app/ai-model/page.tsx` 的新错误
  </done>
</task>

</tasks>

<verification>
## Plan 级整体验证

执行完两个任务后，运行下列**4 条**整体校验：

### 1. TS 编译（不引入新错误）
```bash
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
npx tsc --noEmit 2>&1 | tee /tmp/tsc.log
echo "新文件错误数（应为 0）："
grep -E "(lib/permissions\.ts|app/ai-model/page\.tsx)" /tmp/tsc.log | wc -l
```
**判定**：tsc 整体退出码可能为 2（存量 67 条错误，与 Phase 1 一致，本 phase 无关），但 grep 过滤后**0 条**指向 `lib/permissions.ts` 或 `app/ai-model/page.tsx`。

### 2. RBAC 矩阵正确性（5+1 角色逐一确认）
```bash
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin

# 总计 'credential-slot' 命中数（union literal + 2 个角色数组 + 可能 1 行注释表 = 3 或 4）
grep -nE "['\"]credential-slot['\"]" lib/permissions.ts

# 反向校验：4 个不应该含 credential-slot 的角色数组
# 提取每个角色定义后的数组内容，检查不含 credential-slot
awk '/内容管理员: \[/,/\],/' lib/permissions.ts | grep "credential-slot" | wc -l   # 期望 0
awk '/卡牌管理员: \[/,/\],/' lib/permissions.ts | grep "credential-slot" | wc -l   # 期望 0
awk '/查看者: \[/,/\],/' lib/permissions.ts | grep "credential-slot" | wc -l        # 期望 0
awk '/管理员: \[/,/\],/' lib/permissions.ts | tail -n +2 | grep "credential-slot" | wc -l  # 期望 0（tail 跳过第一个匹配避免「AI模型管理员」干扰）

# getModuleFromPath 不变
grep -n '"ai-model": "ai-model"' lib/permissions.ts   # 期望 1 行命中
grep -n "credential-slot" lib/permissions.ts | grep "pathMap\|getModuleFromPath" | wc -l   # 期望 0（不在路径映射函数体中）
```

### 3. 入口控件 + 占位 Dialog 完整性（11 条 specifics）
```bash
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin

# specifics 1-5 来自 lib/permissions.ts，已在上面 grep
# specifics 6-9, 11 来自 app/ai-model/page.tsx：

head -n 1 app/ai-model/page.tsx                                              # 期望 "use client"
grep -cE "['\"]credential-slot['\"]" app/ai-model/page.tsx                  # 期望 ≥1（hasPermission 调用）
grep -c "KeyRound" app/ai-model/page.tsx                                    # 期望 ≥2（import + JSX）
grep -c "凭据槽位" app/ai-model/page.tsx                                     # 期望 ≥2（Button + DialogTitle）
grep -c "通用凭据槽位" app/ai-model/page.tsx                                  # 期望 1（DialogTitle）
grep -c "对话框真实内容由 Phase 3 落地" app/ai-model/page.tsx                  # 期望 1
grep -c "setIsCredentialDialogOpen" app/ai-model/page.tsx                   # 期望 ≥3（useState + onClick + 通过 onOpenChange 间接传引用）
grep -c "useState" app/ai-model/page.tsx                                    # 期望 ≥3（import + 2 调用）
grep -cE "from \"@/components/ui/dialog\"" app/ai-model/page.tsx           # 期望 1
grep -cE "from \"@/lib/permissions\"" app/ai-model/page.tsx                # 期望 1
```

### 4. 不引入新依赖
```bash
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
git diff --stat package.json yarn.lock package-lock.json pnpm-lock.yaml 2>/dev/null
# 期望：0 行输出（4 个文件均未改动）
```

**整体判定**：4 条全部通过 → Plan 02-01 收尾，可移交 Plan 02-02（修改记录追加）。
</verification>

<success_criteria>
**ROADMAP.md Phase 2 Success Criteria 4 条对照（前 3 条由 Plan 02-01 + Plan 02-02 共同覆盖；第 4 条由本 plan 独立覆盖）：**

1. ✅ `lib/permissions.ts` PermissionModule union 含 `'credential-slot'`；超级管理员 + AI模型管理员含、其他角色不含；`hasPermission('credential-slot')` 在两类账户下返回 true，其他角色 false
2. ✅ `getModuleFromPath('/ai-model')` 行为不变，无新菜单项（不动 sidebar）
3. ✅ `/ai-model` 页面工具栏区域可见明确的「凭据槽位」入口控件（在 DashboardHeader children 内、与「添加新模型」按钮同行右侧）；未授权角色 DOM 中不存在
4. ✅ 入口控件可见性走 `hasPermission('credential-slot')`，不直接读 `localStorage.user_role`；点击入口控件触发占位 Dialog 打开（DialogTitle「通用凭据槽位」+ DialogDescription「对话框真实内容由 Phase 3 落地」）
</success_criteria>

<output>
完成后由 Plan 02-02 在收尾任务中创建 `.planning/phases/02-rbac-ai/02-01-SUMMARY.md`（与 Plan 02-02 的 SUMMARY 各自独立）。
</output>
