668 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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') 收敛的「凭据槽位」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 模式)"
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-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 行)
```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-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.tsx`VERIFIED 全文 20 行)
```tsx
// 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
```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` 关键 variantsVERIFIED 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 RBACPermissionModule 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` 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
- 不要重排数组顺序(仅在数组**末尾**追加)
</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
# 预期0lib/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>任务 2app/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-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>
<action>
对 `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+ 即存在)
</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` 命中 ≥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>
<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
# 预期≥10KeyRound×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 # 期望 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
```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 # 期望 ≥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. 不引入新依赖
```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>