31 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-rbac-ai 01 execute 1
lib/permissions.ts
app/ai-model/page.tsx
true
CRED-FE-02
CRED-FE-03
truths artifacts key_links
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') 收敛的「凭据槽位」ButtonKeyRound 图标variant='outline'
未授权角色(查看者 / 内容管理员等)登录访问 /ai-model 时,凭据槽位 Button 在 DOM 中完全不存在(不仅是隐藏)
占位 Dialog 在 </Tabs> 之后、</DashboardShell> 之前controlled modeopen + onOpenChangeDialogTitle 含中文「通用凭据槽位」+ DialogDescription 含「对话框真实内容由 Phase 3 落地」
useState<boolean>(false) 控制 isCredentialDialogOpen点击 Button → setIsCredentialDialogOpen(true) 打开 Dialog
为避免 SSR 水合不匹配hasPermission 调用走 mounted 守卫(复用 components/sidebar.tsx:83-104 模式)
path provides contains min_lines
lib/permissions.ts PermissionModule 14 项 union + 6 角色矩阵(其中 2 角色含 credential-slot 'credential-slot' 120
path provides contains min_lines
app/ai-model/page.tsx Client Component含凭据槽位入口 Button + 占位 Dialog + useState/useEffect/hasPermission/KeyRound 引用 use client 460
from to via pattern
app/ai-model/page.tsx lib/permissions.ts:hasPermission named import + 调用 hasPermission('credential-slot') hasPermission(["']credential-slot["'])
from to via pattern
app/ai-model/page.tsx Button onClick Dialog open prop useState<boolean> setIsCredentialDialogOpen setIsCredentialDialogOpen(true)
from to via pattern
lib/permissions.ts PermissionModule union PERMISSION_MATRIX 角色数组 TS literal 校验 + Record<RoleName, PermissionModule[]> ["']credential-slot["']
落地 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 挂载点。 Outputlib/permissions.ts(修改)+ app/ai-model/page.tsx(修改)。无新依赖、不动 lockfile。

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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

lib/permissions.ts 当前结构VERIFIED 全文 123 行)

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

// line 21-34union 当前 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-113getModuleFromPath本 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-87hasPermission签名稳定仅验证 'credential-slot' 字面量在 union 中合法)
export function hasPermission(module: PermissionModule): boolean {
  return getAllowedModules().includes(module);
}

app/ai-model/page.tsx 当前结构VERIFIED 全文 446 行)

// 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-16DashboardHeader 区域,含现有「添加新模型」按钮)
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.tsxVERIFIED 全文 20 行)

// line 8-19children 容器是 flex rowitems-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

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 关键 variantsVERIFIED line 11-21

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

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

components/sidebar.tsx mounted 守卫模式VERIFIED line 83-104

"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。不加守卫会出现「超管刷新页面瞬间按钮闪一下消失再出现」的水合警告。

任务 1扩展 lib/permissions.ts RBACPermissionModule union +1 / 矩阵 +2 角色) lib/permissions.ts

<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.mdlib/permissions.ts 改动 patch」段line 358-435 完整 before/after diff </read_first>

用 Edit 工具对 `lib/permissions.ts` 做**4 处** old_string → new_string 替换;其他内容**逐字不动**。
**改动 1`PermissionModule` unionline 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
- 不要重排数组顺序(仅在数组**末尾**追加)

<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>

cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && npx tsc --noEmit 2>&1 | grep -E "lib/permissions\.ts" | wc -l # 预期0lib/permissions.ts 在改动后零类型错误;存量错误数与 Phase 1 的 67 条一致或更少;不能引入新的指向 lib/permissions.ts 的错误) cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -cE "['\"]credential-slot['\"]" lib/permissions.ts # 预期3 - `lib/permissions.ts` PermissionModule union 含 14 项(最后一项是 `"credential-slot"` - 「超级管理员」数组末尾含 `"credential-slot"`「AI模型管理员」数组末尾含 `"credential-slot"` - 其他 4 角色数组逐字不变 - `getModuleFromPath` 函数体完全不变 - `npx tsc --noEmit` 不引入指向 `lib/permissions.ts` 的新错误 任务 2app/ai-model/page.tsx 加 "use client"、入口 Button、占位 Dialog app/ai-model/page.tsx

<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-104mounted 守卫模式样板,复用其结构) 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>

对 `app/ai-model/page.tsx` 做 **5 处**精确改动其他内容Tabs / Card / 现有 Button**逐字不动**。
**改动 1line 1 顶部新增 `"use client"` 指令(必须在所有 import 之前)**

old_stringline 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 importline 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 / useEffectmounted 守卫 + Dialog 开关)**

old_stringline 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_stringline 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_stringline 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 / useToastPhase 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+ 即存在)

<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 命中 ≥2import + JSX - grep -nE 'hasPermission\(["'\'']credential-slot' app/ai-model/page.tsx 命中 ≥1 - grep -n "凭据槽位" app/ai-model/page.tsx 命中 ≥2Button 文案 + DialogTitle - grep -n "通用凭据槽位" app/ai-model/page.tsx 命中 ≥1DialogTitle - 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 命中 ≥3useState + 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>

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 cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && head -n 1 app/ai-model/page.tsx # 预期:包含 "use client"(精确字符串:"use client" cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -cE "(KeyRound|凭据槽位|通用凭据槽位|hasPermission\\([\"']credential-slot|setIsCredentialDialogOpen|对话框真实内容由 Phase 3 落地)" app/ai-model/page.tsx # 预期≥10KeyRound×2 + 凭据槽位×2 + 通用凭据槽位×1 + hasPermission(credential-slot)×1 + setIsCredentialDialogOpen×3 + 对话框真实内容由 Phase 3 落地×1 - 文件 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 改为 `
` 包两个 Button保留原「添加新模型」Button + 新增 `{mounted && hasPermission("credential-slot") && setIsCredentialDialogOpen(true)}>凭据槽位}` - `` 之后、`` 之前插入 controlled mode `` + `通用凭据槽位对话框真实内容由 Phase 3 落地` - Tabs / TabsContent / Card 等所有原有内容line 18-441逐字不变 - `npx tsc --noEmit` 不引入指向 `app/ai-model/page.tsx` 的新错误 ## Plan 级整体验证

执行完两个任务后,运行下列4 条整体校验:

1. TS 编译(不引入新错误)

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.tsapp/ai-model/page.tsx

2. RBAC 矩阵正确性5+1 角色逐一确认)

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  # 期望 0tail 跳过第一个匹配避免「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

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                  # 期望 ≥1hasPermission 调用)
grep -c "KeyRound" app/ai-model/page.tsx                                    # 期望 ≥2import + JSX
grep -c "凭据槽位" app/ai-model/page.tsx                                     # 期望 ≥2Button + DialogTitle
grep -c "通用凭据槽位" app/ai-model/page.tsx                                  # 期望 1DialogTitle
grep -c "对话框真实内容由 Phase 3 落地" app/ai-model/page.tsx                  # 期望 1
grep -c "setIsCredentialDialogOpen" app/ai-model/page.tsx                   # 期望 ≥3useState + onClick + 通过 onOpenChange 间接传引用)
grep -c "useState" app/ai-model/page.tsx                                    # 期望 ≥3import + 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. 不引入新依赖

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修改记录追加

<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>
完成后由 Plan 02-02 在收尾任务中创建 `.planning/phases/02-rbac-ai/02-01-SUMMARY.md`(与 Plan 02-02 的 SUMMARY 各自独立)。