docs(02): qy-lty-admin Phase 2 PLAN ×2(02-01 RBAC + 入口控件 / 02-02 修改记录 + 类型检查),plan-checker 一遍过

This commit is contained in:
pmc 2026-05-08 11:41:45 +08:00
parent d396249aef
commit d4a404eb1b
3 changed files with 1089 additions and 2 deletions

View File

@ -46,7 +46,9 @@
2. `getModuleFromPath('/ai-model')` 行为不变(凭据槽位是 `/ai-model` 内嵌子能力,不占独立路由),不引入侧边栏新菜单项
3. 以"AI模型管理员"角色登录访问 `/ai-model`,页面工具栏 / Header 区域可见明确的"凭据槽位"入口控件(按钮或卡片,文案明确);以"内容管理员"或"查看者"角色登录访问同一页面入口控件不渲染DOM 中不存在,而非仅隐藏)
4. 入口控件的可见性判断走 `hasPermission('credential-slot')`,不直接读 `localStorage.user_role` 字符串比较点击入口控件触发对话框打开行为Phase 3 落地后端到端可用,本 phase 至少打开一个空对话框占位以验证联动点存在)
**Plans**: TBD
**Plans**: 2 plans
- [ ] 02-01-PLAN.md — 扩展 lib/permissions.ts RBACPermissionModule union +1 / 矩阵 +2 角色)+ app/ai-model/page.tsx 加 "use client"、入口 Button、占位 Dialog
- [ ] 02-02-PLAN.md — docs/修改记录.md 顶部追加 Phase 2 条目 + plan 级双重验证npx tsc --noEmit 反向断言 + grep 11 条 specifics + 不引入新依赖)
**UI hint**: yes
### Phase 3: 编辑对话框 + 提交反馈
@ -70,7 +72,7 @@ Phase 按数值顺序执行1 → 2 → 3如出现紧急插入记为 1.1
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. 凭据槽位 API 客户端 | 2/2 | ✅ Complete | 2026-05-08 |
| 2. RBAC 收敛 + AI 模型页入口 | 0/TBD | Not started | - |
| 2. RBAC 收敛 + AI 模型页入口 | 0/2 | Not started | - |
| 3. 编辑对话框 + 提交反馈 | 0/TBD | Not started | - |
---

View File

@ -0,0 +1,667 @@
---
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>

View File

@ -0,0 +1,418 @@
---
phase: 02-rbac-ai
plan: 02
type: execute
wave: 2
depends_on:
- "02-01"
files_modified:
- docs/修改记录.md
autonomous: true
requirements:
- CRED-FE-02
- CRED-FE-03
must_haves:
truths:
- "docs/修改记录.md 顶部含一条 [2026-05-08] Phase 2 条目最新在最前紧跟「修改历史」标记之后、Phase 1 [2026-05-08] 条目之前)"
- "Phase 2 条目含完整 5 字段:日期、文件路径、修改类型、修改内容、修改原因,外加跨项目联动 + 服务端联动两个项目惯用扩展字段"
- "Phase 2 条目「文件路径」字段列出本期实际改动的 2 个文件lib/permissions.ts、app/ai-model/page.tsx"
- "「跨项目联动」字段写「无 — Phase 2 是纯前端 RBAC + UI 入口落地,不引入新跨项目契约;后端 commit 46d72b8 已建立的互引仍有效Phase 3 引入实质 PUT 调用时若涉及新契约再评估」CONTEXT.md D-XX 锁定文案)"
- "Phase 2 条目覆盖前端需求CRED-FE-02 + CRED-FE-03在条目元信息行明确列出"
- "整体类型检查 npx tsc --noEmit 整体退出码可能为 2存量错误但 grep 过滤后 0 条指向本 phase 改动的 lib/permissions.ts 与 app/ai-model/page.tsxPhase 1 已建立的判定模式)"
- "package.json / yarn.lock / package-lock.json / pnpm-lock.yaml 4 个 manifest/lockfile 均未改动(不引入新依赖)"
artifacts:
- path: "docs/修改记录.md"
provides: "顶部 Phase 2 修改记录条目"
contains: "[2026-05-08] Phase 2"
min_lines: 100
key_links:
- from: "docs/修改记录.md Phase 2 条目"
to: "Plan 02-01 改动的 lib/permissions.ts + app/ai-model/page.tsx"
via: "「文件路径」字段精确列出"
pattern: "lib/permissions\\.ts|app/ai-model/page\\.tsx"
- from: "docs/修改记录.md Phase 2 条目「服务端联动」字段"
to: "qy_lty/.planning/phases/02-admin-rest commit 46d72b8"
via: "文本引用 commit hash"
pattern: "46d72b8"
---
<objective>
完成 Phase 2 收尾:在 `docs/修改记录.md` 顶部追加 Phase 2 条目CLAUDE.md 强制要求),并执行 plan 级整体验证(双重类型检查 + grep 11 条 specifics 全命中 + 不引入新依赖)。
Purpose满足 CLAUDE.md 项目宪法「修改记录强制」规则;按 Phase 1 已建立的双重验证模式tsc + grep封盘 Phase 2让 STATE.md 可推进到 Phase 3 待启动状态。
Output`docs/修改记录.md`(修改,仅顶部追加;不动其他历史条目)。
</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/phases/02-rbac-ai/02-CONTEXT.md
@.planning/phases/02-rbac-ai/02-RESEARCH.md
@.planning/phases/02-rbac-ai/02-01-PLAN.md
@CLAUDE.md
@docs/修改记录.md
@lib/permissions.ts
@app/ai-model/page.tsx
</context>
<interfaces>
<!-- 修改记录头部「修改格式说明」VERIFIED docs/修改记录.md line 9-20 -->
```
### [日期] 修改简述
- **文件路径**: 相对于项目根目录的文件路径
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug
- **修改内容**: 具体修改了什么
- **修改原因**: 为什么要做这个修改
```
<!-- Phase 1 条目VERIFIED docs/修改记录.md line 28-48作为本期模板关键扩展字段 -->
Phase 1 条目结构(**直接抄此 7 字段结构**
1. **元信息行**(标题下方紧跟):「配套服务端 Phase: ...」+「覆盖前端需求: CRED-FE-XX」
2. **文件路径**:列出实际改动的所有文件相对路径
3. **修改类型**:新增 / 修改 / 删除 / 重构 / 修复Bug
4. **修改内容**:分点 bullet 描述每个文件做了什么
5. **修改原因**:解释为什么做、对后续 phase 的支撑作用
6. **跨项目联动**项目惯用扩展字段CONTEXT.md 已锁定本期文案)
7. **服务端联动**(项目惯用扩展字段,可与「跨项目联动」复用文案或简短交叉引用)
<!-- 顶部插入位置 -->
`docs/修改记录.md` line 26 是注释 `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->`line 28 是当前最顶 Phase 1 条目 `### [2026-05-08] Phase 1前端凭据槽位 API 客户端`。**新条目必须插入到 line 26 的注释**之后**、line 28 的 Phase 1 条目**之前**。
</interfaces>
<tasks>
<task type="auto" tdd="false">
<name>任务 1docs/修改记录.md 顶部追加 Phase 2 条目</name>
<files>docs/修改记录.md</files>
<read_first>
1. **必读**`docs/修改记录.md` 完整 line 1-50确认头部「修改格式说明」+ line 26 锚点注释 + line 28-48 Phase 1 条目作为模板)
2. **必读**`.planning/phases/02-rbac-ai/02-CONTEXT.md` 「修改记录」段CONTEXT D-XX 锁定的「跨项目联动」字段精确文案)
3. **必读**`.planning/phases/02-rbac-ai/02-01-PLAN.md`(确认 Phase 2 实际改动文件 = `lib/permissions.ts` + `app/ai-model/page.tsx`
4. **必读**`CLAUDE.md` 「项目修改记录规则(重要 — 自动执行)」段 + 「`qy-lty-admin``qy_lty` 是独立项目,各自维护」段
</read_first>
<action>
用 Edit 工具对 `docs/修改记录.md` 做**1 处**精确插入:在 line 26 的锚点注释 `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->` 之后、line 28 的 `### [2026-05-08] Phase 1前端凭据槽位 API 客户端` 之前,插入完整 Phase 2 条目。
**old_string**(精确匹配 line 26-28含中间空行
```
<!-- 新的修改记录添加在此处下方,最新的在最前面 -->
### [2026-05-08] Phase 1前端凭据槽位 API 客户端
```
**new_string**(在锚点与 Phase 1 之间插入完整 Phase 2 条目Phase 2 条目结尾 → 1 个空行 → Phase 1 条目原行):
```
<!-- 新的修改记录添加在此处下方,最新的在最前面 -->
### [2026-05-08] Phase 2前端RBAC 收敛 + AI 模型页凭据槽位入口
配套服务端 Phase本 phase **不**触达服务端;与服务端 v1.0 Phase 2「管理端读写接口」commit `46d72b8` 既有契约保持兼容(不引入新契约)
覆盖前端需求CRED-FE-02、CRED-FE-03
- **文件路径**
- `lib/permissions.ts`(修改)
- `app/ai-model/page.tsx`(修改)
- **修改类型**: 修改(前端 RBAC 矩阵扩展 + 页面入口控件 + 占位 Dialog纯前端无新依赖、不动 lockfile
- **修改内容**:
- `lib/permissions.ts`
- `PermissionModule` union 末尾追加 `"credential-slot"`,扩为 14 项
- `PERMISSION_MATRIX["超级管理员"]` 数组末尾追加 `"credential-slot"`
- `PERMISSION_MATRIX["AI模型管理员"]` 数组末尾追加 `"credential-slot"`
- 其他 4 个角色(内容管理员 / 卡牌管理员 / 查看者 / 管理员)数组**逐字不变**
- `getModuleFromPath` 函数体完全不动(凭据槽位是 `/ai-model` 子能力,不占独立路由)
- 顶部「权限矩阵对照表」注释新增一行「凭据槽位」与代码同步
- `app/ai-model/page.tsx`
- 文件 line 1 顶部新增 `"use client"` 指令,从 Server Component 转为 Client Component
- 新增 import`useState` / `useEffect`react+ `Dialog` / `DialogContent` / `DialogDescription` / `DialogHeader` / `DialogTitle`@/components/ui/dialog+ `KeyRound`lucide-react+ `hasPermission`@/lib/permissions
- 函数体顶部新增 `mounted` + `isCredentialDialogOpen` 两个 `useState` + 1 个 `useEffect``mounted` 为 true复用 `components/sidebar.tsx` mounted 守卫模式避免 SSR 水合不匹配)
- `DashboardHeader` 内部用 `<div className="flex items-center gap-2">` 包两个 Button保留原有「添加新模型」+ 新增 `{mounted && hasPermission("credential-slot") && <Button variant="outline" onClick={() => setIsCredentialDialogOpen(true)}><KeyRound /> 凭据槽位</Button>}`
- `</Tabs>` 之后、`</DashboardShell>` 之前新增 controlled mode `<Dialog open={isCredentialDialogOpen} onOpenChange={setIsCredentialDialogOpen}>`,内含 `DialogTitle`「通用凭据槽位」+ `DialogDescription`「对话框真实内容由 Phase 3 落地」(占位,无表单)
- Tabs / TabsContent / Card / 卡片内的现有按钮等所有内容line 18-441逐字不变
- **修改原因**:
- 推进 Milestone v1.0「通用凭据槽位前端集成」第二步:让授权运营立即看到入口(已就位的 UX 收敛未授权角色彻底看不到DOM 中完全不存在的安全前置)
- 沿用 RBAC 单一来源原则(`lib/permissions.ts:hasPermission`+ shadcn Dialog primitive不重复造轮子
- 为 Phase 3 真实表单CRED-FE-04 + CRED-FE-05预留 Dialog 挂载点Dialog 用 controlled mode 让 Phase 3 可在打开瞬间触发 `getCredentialSlot()`
- 注意:前端 RBAC 仅是 UI 礼貌,最终安全闭环依赖后端 `/v1/admin/credential-slot/` 的 admin 鉴权PERM-06 / `qy_lty` 后端);本 phase 不消化该闭环
- **跨项目联动**: 无 — Phase 2 是纯前端 RBAC + UI 入口落地,不引入新跨项目契约;后端 commit 46d72b8 已建立的互引仍有效Phase 3 引入实质 PUT 调用时若涉及新契约再评估
- **服务端联动**: 同上「跨项目联动」字段;后端 commit `46d72b8` 已建立互引闭环,本 phase 无需再次互引
### [2026-05-08] Phase 1前端凭据槽位 API 客户端
```
**明确不要做的事**
- 不要动 line 1-26 的头部说明 / 修改格式说明 / 修改历史标记
- 不要动 line 28 之后任何已有条目Phase 1 / 2026-05-07 的两个条目 / 2026-04-30 初始化条目)
- 不要把「跨项目联动」字段文案缩短或重写CONTEXT.md 已逐字锁定)
- 不要漏掉 Phase 2 条目的「覆盖前端需求」元信息行(必须含 CRED-FE-02 + CRED-FE-03
- 不要在「文件路径」中列入 docs/修改记录.md 自身CLAUDE.md 适用范围说「单纯的 typo 修复 / 注释微调可省略」,但本条目作为 phase 收尾骨干条目,遵循 Phase 1 模板的写法 = 仅列被修改的代码文件)
</action>
<acceptance_criteria>
- `grep -n "\\[2026-05-08\\] Phase 2" docs/修改记录.md` 命中 1 行(标题行)
- `grep -n "\\[2026-05-08\\] Phase 1" docs/修改记录.md` 仍命中 1 行Phase 1 条目未被破坏)
- `head -n 30 docs/修改记录.md` 输出含「修改格式说明」+「修改历史」+ `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->` 锚点line 1-26 完全不变)
- `awk '/### \\[2026-05-08\\]/{count++} count==1 && /Phase 2/{print "PASS"; exit} count==1 && /Phase 1/{print "FAIL: Phase 1 在最前面"; exit}' docs/修改记录.md` 输出 `PASS`(确保 Phase 2 在 Phase 1 之上)
- `grep -nE "CRED-FE-(02|03)" docs/修改记录.md | head -n 5` 命中至少 1 行包含 `CRED-FE-02``CRED-FE-03` 的元信息行(在 Phase 2 条目之内)
- `grep -n "credential-slot" docs/修改记录.md` 命中 ≥1 行Phase 2 条目里描述 PermissionModule 字面量)
- `grep -n "46d72b8" docs/修改记录.md` 命中 ≥2 行Phase 1 条目已有的 + Phase 2 条目新增的,证明跨项目联动文案已嵌入)
- `grep -n "凭据槽位" docs/修改记录.md` 命中 ≥1 行Phase 2 条目内容描述)
- `grep -n "通用凭据槽位" docs/修改记录.md` 命中 ≥1 行Phase 2 条目 DialogTitle 描述)
- `git diff --stat docs/修改记录.md` 显示仅 +N 行(无 -行;纯追加)
</acceptance_criteria>
<verify>
<automated>
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -c "\[2026-05-08\] Phase 2" docs/修改记录.md
# 预期1
</automated>
<automated>
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -c "\[2026-05-08\] Phase 1" docs/修改记录.md
# 预期1Phase 1 条目仍存在)
</automated>
<automated>
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && awk '/### \[2026-05-08\] Phase 2/{p2=NR} /### \[2026-05-08\] Phase 1/{p1=NR} END{ if(p2>0 && p1>0 && p2<p1) print "PASS"; else print "FAIL p2="p2" p1="p1 }' docs/修改记录.md
# 预期PASS
</automated>
<automated>
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -c "CRED-FE-02" docs/修改记录.md
# 预期≥1
</automated>
</verify>
<done>
- `docs/修改记录.md` 顶部新增 Phase 2 条目(最新在最前)
- 条目含 5 个标准字段(文件路径 / 修改类型 / 修改内容 / 修改原因)+ 2 个项目惯用扩展字段(跨项目联动 / 服务端联动)+ 元信息行(覆盖前端需求 + 配套服务端 Phase
- 「跨项目联动」字段文案逐字与 CONTEXT.md 锁定一致
- Phase 1 条目以及更早的所有历史条目逐字不变
</done>
</task>
<task type="auto" tdd="false">
<name>任务 2plan 级整体双重验证tsc + grep + 不引入新依赖)</name>
<files></files>
<read_first>
1. **必读**`.planning/phases/02-rbac-ai/02-01-PLAN.md``<verification>`4 条整体校验列表)
2. **必读**`.planning/phases/02-rbac-ai/02-CONTEXT.md` 「specifics」表11 条验证点)
3. **必读**`.planning/STATE.md` Phase 1 收尾段line 80-81确认 Phase 1 已建立的 tsc 双重验证模式(整体退出码 2 但过滤后零指向新文件)
4. **必读**:本 plan 任务 1 落地后的 `docs/修改记录.md` 顶部(确认 Phase 2 条目结构正确)
</read_first>
<action>
本任务**不修改任何代码或配置文件**,仅执行验证命令并把结果汇总写入 `02-02-SUMMARY.md`(在 Plan 02-02 收尾时由 execute-plan 流程统一写)。
按以下顺序执行**4 大类**验证命令,逐条记录退出码与命中数:
### 验证 ATypeScript 编译(双重判定,与 Phase 1 一致)
```bash
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
npx tsc --noEmit > /tmp/tsc-phase2.log 2>&1
echo "tsc exit code: $?"
# 整体错误数(预期与 Phase 1 的 67 条相近 ± 微小波动;不应大幅增加)
grep -cE "error TS" /tmp/tsc-phase2.log
# 反向断言:本 phase 改动文件零类型错误
grep -E "(lib/permissions\.ts|app/ai-model/page\.tsx)" /tmp/tsc-phase2.log
# 预期0 行输出grep 退出码 1 也算 PASS
```
**判定**
- tsc 整体退出码可能为 2存量错误允许
- 反向 grep 必须 0 行输出(无任何错误指向 `lib/permissions.ts``app/ai-model/page.tsx`
- 整体错误数与 Phase 1 收尾时67 条)相比浮动 ≤ 3 条,否则人工核查是否有新引入
### 验证 Bspecifics 11 条 grep 全命中CONTEXT.md 锁定)
```bash
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
# specifics 1PermissionModule union 含 credential-slot
grep -cE "['\"]credential-slot['\"]" lib/permissions.ts # 期望 ≥3
# specifics 2-3超管 + AI模型管理员含其他 4 角色不含
awk '/超级管理员: \[/,/\],/' lib/permissions.ts | grep -c "credential-slot" # 期望 1
awk '/AI模型管理员: \[/,/\],/' lib/permissions.ts | grep -c "credential-slot" # 期望 1
awk '/内容管理员: \[/,/\],/' lib/permissions.ts | grep -c "credential-slot" # 期望 0
awk '/卡牌管理员: \[/,/\],/' lib/permissions.ts | grep -c "credential-slot" # 期望 0
awk '/查看者: \[/,/\],/' lib/permissions.ts | grep -c "credential-slot" # 期望 0
# 「管理员」最末位匹配避免被「AI模型管理员」前缀干扰用末尾段
awk '/^ 管理员: \[/,/\],/' lib/permissions.ts | grep -c "credential-slot" # 期望 0
# specifics 4getModuleFromPath 行为不变
grep -c '"ai-model": "ai-model"' lib/permissions.ts # 期望 1
# specifics 5app/ai-model/page.tsx 第 1 行是 "use client"
head -n 1 app/ai-model/page.tsx | grep -c '"use client"' # 期望 1
# specifics 6含「凭据槽位」文案
grep -c "凭据槽位" app/ai-model/page.tsx # 期望 ≥2Button + DialogTitle
# specifics 7含 KeyRound
grep -c "KeyRound" app/ai-model/page.tsx # 期望 ≥2
# specifics 8含 hasPermission("credential-slot")
grep -cE "hasPermission\\([\"']credential-slot[\"']\\)" app/ai-model/page.tsx # 期望 ≥1
# specifics 9含 useState + onClick + Dialog 三连
grep -c "useState" app/ai-model/page.tsx # 期望 ≥3import + 2 调用)
grep -c "setIsCredentialDialogOpen" app/ai-model/page.tsx # 期望 ≥3
# specifics 10占位 Dialog 含「通用凭据槽位」DialogTitle
grep -c "通用凭据槽位" app/ai-model/page.tsx # 期望 1
# specifics 11修改记录顶部含 Phase 2 条目
grep -c "\[2026-05-08\] Phase 2" docs/修改记录.md # 期望 1
```
**判定**:以上 14 条 grep 全部满足预期值 → 11 条 specifics 全命中。
### 验证 C不引入新依赖
```bash
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
git diff --name-only -- package.json yarn.lock package-lock.json pnpm-lock.yaml 2>/dev/null
# 期望0 行输出4 个文件均未改动)
git diff --name-only HEAD~1 -- package.json yarn.lock package-lock.json pnpm-lock.yaml 2>/dev/null
# 期望0 行输出(与上一 commit 比较仍无 manifest 改动)
```
**判定**4 个文件均未出现在 diff → 不引入新依赖。
### 验证 D跳过next lint
`npm run lint`(即 `next lint`)在 Phase 1 已确认会进入「ESLint 未 bootstrap → 交互式 prompt」状态`.eslintrc*` / `eslint-config-next`),按 STATE.md line 81「ESLint 基础设施补齐留给 PERM-06 候选 #3」的判定 → **本 phase 跳过 lint**,与 Phase 1 一致。
在 SUMMARY 中显式记录「next lint 因项目未 bootstrap ESLint 跳过;判定与 Phase 1 一致;不指向本 phase 改动文件」。
### 失败处理
任一条验证失败 → **不要继续**,立即返回 STATE 报告问题,不可静默继续 SUMMARY 写入;按以下优先级处理:
1. tsc 出现指向 `lib/permissions.ts``app/ai-model/page.tsx` 的新错误 → 回到 Plan 02-01 任务 1 或 2 修补
2. 反向 grep 在「内容管理员 / 卡牌管理员 / 查看者 / 管理员」数组中命中 `credential-slot` → 回到 Plan 02-01 任务 1 重新核对
3. specifics grep 数量不足 → 回到 Plan 02-01 任务 2 检查 import / JSX
4. package.json / lockfile 出现 diff → 回滚 lockfile`git checkout HEAD -- package.json yarn.lock package-lock.json pnpm-lock.yaml`
</action>
<acceptance_criteria>
- 验证 Atsc 整体退出码 2 容许;过滤后 0 行指向 `lib/permissions.ts` / `app/ai-model/page.tsx`
- 验证 B14 条 grep 全部满足预期值specifics 11 条 + 反向断言 3 条)
- 验证 C`git diff` 对 4 个 manifest/lockfile 文件输出 0 行
- 验证 D在 SUMMARY 中显式记录「next lint 跳过原因」
</acceptance_criteria>
<verify>
<automated>
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && npx tsc --noEmit 2>&1 | grep -cE "(lib/permissions\.ts|app/ai-model/page\.tsx)"
# 预期0
</automated>
<automated>
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && (awk '/超级管理员: \[/,/\],/' lib/permissions.ts | grep -c "credential-slot") && (awk '/AI模型管理员: \[/,/\],/' lib/permissions.ts | grep -c "credential-slot")
# 预期:两条命令各输出 1
</automated>
<automated>
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && for ROLE in 内容管理员 卡牌管理员 查看者; do echo "$ROLE: $(awk -v role="$ROLE" '$0 ~ role"\\: \\[" {flag=1} flag {print} /\],/ {flag=0}' lib/permissions.ts | grep -c "credential-slot")"; done
# 预期3 行各为 X: 0
</automated>
<automated>
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && git diff --name-only -- package.json yarn.lock package-lock.json pnpm-lock.yaml | wc -l
# 预期0
</automated>
<automated>
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && head -n 1 app/ai-model/page.tsx
# 预期:含 "use client"
</automated>
<automated>
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -c "\[2026-05-08\] Phase 2" docs/修改记录.md
# 预期1
</automated>
</verify>
<done>
- 4 大类验证全部通过A / B / C / D
- 11 条 specifics 全命中,反向断言(其他 4 角色数组不含 `credential-slot`)通过
- tsc 不引入指向本 phase 改动文件的新错误
- 不引入新依赖4 个 manifest/lockfile 均未改动)
- SUMMARY 中明确记录 next lint 跳过原因(与 Phase 1 一致)
</done>
</task>
</tasks>
<verification>
## Plan 级整体验证(覆盖整个 Phase 2
执行完两个任务后,确认以下**3 条**整体校验通过:
### 1. 修改记录顶部条目格式
```bash
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
sed -n '/<!-- 新的修改记录添加在此处下方/,/### \[2026-05-08\] Phase 1/p' docs/修改记录.md | head -n 60
```
**判定**:输出含完整 Phase 2 条目结构(标题 / 元信息行 / 文件路径 / 修改类型 / 修改内容 / 修改原因 / 跨项目联动 / 服务端联动)+ 紧跟 Phase 1 标题行作为下一条。
### 2. RBAC 矩阵 5+1 角色逐一确认
```bash
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
echo "应含 credential-slot 的 2 角色:"
awk '/超级管理员: \[/,/\],/' lib/permissions.ts | grep "credential-slot"
awk '/AI模型管理员: \[/,/\],/' lib/permissions.ts | grep "credential-slot"
echo "应不含 credential-slot 的 4 角色:"
for ROLE in 内容管理员 卡牌管理员 查看者; do
HIT=$(awk -v role="$ROLE" '$0 ~ role"\\: \\[" {flag=1} flag {print} /\],/ {flag=0}' lib/permissions.ts | grep -c "credential-slot")
echo "$ROLE: $HIT 次(期望 0"
done
# 「管理员」单独取末尾段避免「AI模型管理员」前缀污染
awk '/^ 管理员: \[/,/\],/' lib/permissions.ts | grep -c "credential-slot"
```
**判定**:超管 + AI模型管理员各命中 1 次;其他 4 角色各 0 次。
### 3. Phase 2 入口控件 + 占位 Dialog 端到端就位
```bash
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
# 整段验证1) "use client" 顶置2) 入口 Button 受 mounted + hasPermission 守卫3) Dialog 在 DashboardShell 末尾
echo "=== line 1 ==="
head -n 1 app/ai-model/page.tsx
echo "=== mounted 守卫 + hasPermission(credential-slot) ==="
grep -nE "mounted &&" app/ai-model/page.tsx
grep -nE "hasPermission\\([\"']credential-slot" app/ai-model/page.tsx
echo "=== Dialog 占位 ==="
grep -nE "<DialogTitle>通用凭据槽位</DialogTitle>" app/ai-model/page.tsx
grep -nE "对话框真实内容由 Phase 3 落地" app/ai-model/page.tsx
```
**判定**:所有 5 段输出非空且行号合理("use client" 在 line 1mounted + hasPermission 守卫在 DashboardHeader 区域Dialog 在文件末尾区域)。
**整体判定**3 条全部通过 → Phase 2 全部交付,可推进 STATE.md → Phase 3 待启动。
</verification>
<success_criteria>
**ROADMAP.md Phase 2 Success Criteria 4 条最终确认(前 3 条由 Plan 02-01 主体落地、本 Plan 02-02 验证;第 4 条由 Plan 02-01 落地、本 Plan 02-02 验证CLAUDE.md 修改记录强制由本 Plan 02-02 落地):**
1. ✅ 验证:`hasPermission('credential-slot')` 对超管 + AI模型管理员返回 true、其他角色返回 false通过本 plan 验证 B 段反向断言确认)
2. ✅ 验证:`getModuleFromPath('/ai-model')` 行为不变(通过 grep `'ai-model': 'ai-model'` 仍命中 1 行确认)
3. ✅ 验证:`/ai-model` 页面 DashboardHeader 含「凭据槽位」入口控件,未授权角色 DOM 中不存在(通过 mounted + hasPermission 守卫的 grep 命中确认)
4. ✅ 验证:可见性走 `hasPermission('credential-slot')`、点击触发占位 Dialog 打开(通过 grep `setIsCredentialDialogOpen(true)` + `<Dialog open=` 命中确认)
5. ✅ CLAUDE.md 修改记录强制:`docs/修改记录.md` 顶部含 Phase 2 条目跨项目联动字段写「无」CONTEXT.md 锁定文案)
</success_criteria>
<output>
完成后由 execute-plan 自动创建:
- `.planning/phases/02-rbac-ai/02-01-SUMMARY.md`Plan 02-01 收尾摘要RBAC 矩阵 + 入口控件 + Dialog 落地)
- `.planning/phases/02-rbac-ai/02-02-SUMMARY.md`Plan 02-02 收尾摘要:修改记录追加 + 双重验证结果,含 next lint 跳过说明)
并由后续 `/gsd-verify-phase 2`(如启用)或人工触发把 STATE.md 推进到 Phase 3 待启动状态。
</output>