docs(02): qy-lty-admin Phase 2 PLAN ×2(02-01 RBAC + 入口控件 / 02-02 修改记录 + 类型检查),plan-checker 一遍过
This commit is contained in:
parent
d396249aef
commit
d4a404eb1b
@ -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 RBAC(PermissionModule 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 | - |
|
||||
|
||||
---
|
||||
|
||||
667
qy-lty-admin/.planning/phases/02-rbac-ai/02-01-PLAN.md
Normal file
667
qy-lty-admin/.planning/phases/02-rbac-ai/02-01-PLAN.md
Normal 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') 收敛的「凭据槽位」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>
|
||||
418
qy-lty-admin/.planning/phases/02-rbac-ai/02-02-PLAN.md
Normal file
418
qy-lty-admin/.planning/phases/02-rbac-ai/02-02-PLAN.md
Normal 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.tsx(Phase 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>任务 1:docs/修改记录.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
|
||||
# 预期:1(Phase 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>任务 2:plan 级整体双重验证(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 大类**验证命令,逐条记录退出码与命中数:
|
||||
|
||||
### 验证 A:TypeScript 编译(双重判定,与 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 条,否则人工核查是否有新引入
|
||||
|
||||
### 验证 B:specifics 11 条 grep 全命中(CONTEXT.md 锁定)
|
||||
|
||||
```bash
|
||||
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
|
||||
|
||||
# specifics 1:PermissionModule 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 4:getModuleFromPath 行为不变
|
||||
grep -c '"ai-model": "ai-model"' lib/permissions.ts # 期望 1
|
||||
|
||||
# specifics 5:app/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 # 期望 ≥2(Button + 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 # 期望 ≥3(import + 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>
|
||||
- 验证 A:tsc 整体退出码 2 容许;过滤后 0 行指向 `lib/permissions.ts` / `app/ai-model/page.tsx`
|
||||
- 验证 B:14 条 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 1;mounted + 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>
|
||||
Loading…
x
Reference in New Issue
Block a user