diff --git a/qy-lty-admin/.planning/ROADMAP.md b/qy-lty-admin/.planning/ROADMAP.md index f92db4e..ee98cfd 100644 --- a/qy-lty-admin/.planning/ROADMAP.md +++ b/qy-lty-admin/.planning/ROADMAP.md @@ -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 | - | --- diff --git a/qy-lty-admin/.planning/phases/02-rbac-ai/02-01-PLAN.md b/qy-lty-admin/.planning/phases/02-rbac-ai/02-01-PLAN.md new file mode 100644 index 0000000..e8b4d2d --- /dev/null +++ b/qy-lty-admin/.planning/phases/02-rbac-ai/02-01-PLAN.md @@ -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 在 之后、 之前,controlled mode(open + onOpenChange),DialogTitle 含中文「通用凭据槽位」+ DialogDescription 含「对话框真实内容由 Phase 3 落地」" + - "useState(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 setIsCredentialDialogOpen" + pattern: "setIsCredentialDialogOpen\\(true\\)" + - from: "lib/permissions.ts PermissionModule union" + to: "PERMISSION_MATRIX 角色数组" + via: "TS literal 校验 + Record" + pattern: "[\"']credential-slot[\"']" +--- + + +落地 Phase 2 的两条核心需求: +1. **CRED-FE-02**:扩展 `lib/permissions.ts` 的 RBAC,让 `'credential-slot'` 模块仅对「超级管理员」与「AI模型管理员」开放 +2. **CRED-FE-03**:在 `/ai-model` 页面渲染受 `hasPermission('credential-slot')` 收敛的「凭据槽位」入口 Button,点击触发占位 Dialog 打开 + +Purpose:让授权运营立即能在大模型管理页看到入口控件,未授权角色彻底看不到(DOM 中完全不存在);为 Phase 3 的真实表单落地预留 Dialog 挂载点。 +Output:`lib/permissions.ts`(修改)+ `app/ai-model/page.tsx`(修改)。无新依赖、不动 lockfile。 + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/REQUIREMENTS.md +@.planning/phases/02-rbac-ai/02-CONTEXT.md +@.planning/phases/02-rbac-ai/02-RESEARCH.md +@CLAUDE.md +@lib/permissions.ts +@app/ai-model/page.tsx +@components/dashboard-header.tsx +@components/ui/button.tsx +@components/ui/dialog.tsx +@components/sidebar.tsx + + + + + +### `lib/permissions.ts` 当前结构(VERIFIED 全文 123 行) + +```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 = { + 超级管理员: [ + "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 = { + "": "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 ( + + + + + + + ... + + // 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 ( +
+
+

{heading}

+ {text &&
{text}
} +
+ {children} {/* ← 直接渲染 children,本身不是 flex 容器 */} +
+ ) +} +``` + +**关键判断**:外层是 `flex items-center justify-between`,左侧是 heading 容器、右侧 children 直接渲染。当 children 是**多个** Button 时,多个 Button 会被同等参与 flex 横排,但**之间无 gap**。**结论**:在 page.tsx 把两个 Button 用 `
` 包起来,作为 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 用法**:``,内部用 `` 包 `{ + }`,本 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(...) && } 包裹任何依赖 localStorage 的 UI +``` + +**为什么必须 mounted 守卫**:`getUserRole()` 在 SSR 阶段(typeof window === "undefined")一律 fallback「查看者」;hydration 后才能读到真实 role。不加守卫会出现「超管刷新页面瞬间按钮闪一下消失再出现」的水合警告。 + + + + + + 任务 1:扩展 lib/permissions.ts RBAC(PermissionModule union +1 / 矩阵 +2 角色) + lib/permissions.ts + + + 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) + + + + 用 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 + - 不要重排数组顺序(仅在数组**末尾**追加) + + + + - `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) + + + + + 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 的错误) + + + cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -cE "['\"]credential-slot['\"]" lib/permissions.ts + # 预期:3 + + + + + - `lib/permissions.ts` PermissionModule union 含 14 项(最后一项是 `"credential-slot"`) + - 「超级管理员」数组末尾含 `"credential-slot"`,「AI模型管理员」数组末尾含 `"credential-slot"` + - 其他 4 角色数组逐字不变 + - `getModuleFromPath` 函数体完全不变 + - `npx tsc --noEmit` 不引入指向 `lib/permissions.ts` 的新错误 + + + + + 任务 2:app/ai-model/page.tsx 加 "use client"、入口 Button、占位 Dialog + app/ai-model/page.tsx + + + 1. **必读**:`app/ai-model/page.tsx` 完整 1-446 行(确认 line 1-7 import 块、line 8-16 DashboardHeader 段、line 442 ``、line 443 ``) + 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 报错 + + + + 对 `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 ( + + + ``` + + new_string: + ```tsx + export default function AIModelPage() { + const [mounted, setMounted] = useState(false) + const [isCredentialDialogOpen, setIsCredentialDialogOpen] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + return ( + + + ``` + + **改动 4:在 DashboardHeader 内部、line 16 `` 之前用 flex 容器把现有「添加新模型」Button 与新增的「凭据槽位」Button 包起来(dashboard-header 的 children 是单 slot,多 Button 需自行加 gap)** + + old_string(line 12-16 完整照搬): + ```tsx + + + + ``` + + new_string: + ```tsx + +
+ + {mounted && hasPermission("credential-slot") && ( + + )} +
+
+ ``` + + **改动 5:在 ``(line 442)之后、`
`(line 443)之前插入占位 Dialog** + + old_string(line 442-444 完整照搬,含末尾 `}`): + ```tsx + +
+ ) + ``` + + new_string: + ```tsx + + + + + + 通用凭据槽位 + + 对话框真实内容由 Phase 3 落地 + + + + + + ) + ``` + + **明确不要做的事**: + - 不要新建 `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+ 即存在) +
+ + + - `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 内) + + + + + cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && npx tsc --noEmit 2>&1 | grep -E "app/ai-model/page\.tsx" | wc -l + # 预期:0(新文件零类型错误;沿用 Phase 1 已建立的判定模式:tsc 整体退出码 2,但 grep 过滤后不指向 app/ai-model/page.tsx) + + + cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && head -n 1 app/ai-model/page.tsx + # 预期:包含 "use client"(精确字符串:"use client") + + + cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -cE "(KeyRound|凭据槽位|通用凭据槽位|hasPermission\\([\"']credential-slot|setIsCredentialDialogOpen|对话框真实内容由 Phase 3 落地)" app/ai-model/page.tsx + # 预期:≥10(KeyRound×2 + 凭据槽位×2 + 通用凭据槽位×1 + hasPermission(credential-slot)×1 + setIsCredentialDialogOpen×3 + 对话框真实内容由 Phase 3 落地×1) + + + + + - 文件 line 1 是 `"use client"` + - 新增 5 个 import:`useState`、`useEffect`(react)+ Dialog 子组件 5 个(@/components/ui/dialog)+ `KeyRound`(lucide-react)+ `hasPermission`(@/lib/permissions) + - 函数体顶部含 `mounted` + `isCredentialDialogOpen` 两个 useState + 1 个 useEffect 设 mounted + - DashboardHeader children 改为 `
` 包两个 Button:保留原「添加新模型」Button + 新增 `{mounted && hasPermission("credential-slot") && }` + - `` 之后、`` 之前插入 controlled mode `` + `通用凭据槽位对话框真实内容由 Phase 3 落地` + - Tabs / TabsContent / Card 等所有原有内容(line 18-441)逐字不变 + - `npx tsc --noEmit` 不引入指向 `app/ai-model/page.tsx` 的新错误 + + + + + + +## 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(修改记录追加)。 + + + +**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 落地」) + + + +完成后由 Plan 02-02 在收尾任务中创建 `.planning/phases/02-rbac-ai/02-01-SUMMARY.md`(与 Plan 02-02 的 SUMMARY 各自独立)。 + diff --git a/qy-lty-admin/.planning/phases/02-rbac-ai/02-02-PLAN.md b/qy-lty-admin/.planning/phases/02-rbac-ai/02-02-PLAN.md new file mode 100644 index 0000000..9e7a6a5 --- /dev/null +++ b/qy-lty-admin/.planning/phases/02-rbac-ai/02-02-PLAN.md @@ -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" +--- + + +完成 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`(修改,仅顶部追加;不动其他历史条目)。 + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + + +``` +### [日期] 修改简述 + +- **文件路径**: 相对于项目根目录的文件路径 +- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug +- **修改内容**: 具体修改了什么 +- **修改原因**: 为什么要做这个修改 +``` + + + +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 条目**之前**。 + + + + + + 任务 1:docs/修改记录.md 顶部追加 Phase 2 条目 + docs/修改记录.md + + + 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` 是独立项目,各自维护」段 + + + + 用 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` 内部用 `
` 包两个 Button:保留原有「添加新模型」+ 新增 `{mounted && hasPermission("credential-slot") && }` + - `` 之后、`` 之前新增 controlled mode ``,内含 `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 模板的写法 = 仅列被修改的代码文件) + + + + - `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 行(无 -行;纯追加) + + + + + cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -c "\[2026-05-08\] Phase 2" docs/修改记录.md + # 预期:1 + + + cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -c "\[2026-05-08\] Phase 1" docs/修改记录.md + # 预期:1(Phase 1 条目仍存在) + + + 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 + + + + - `docs/修改记录.md` 顶部新增 Phase 2 条目(最新在最前) + - 条目含 5 个标准字段(文件路径 / 修改类型 / 修改内容 / 修改原因)+ 2 个项目惯用扩展字段(跨项目联动 / 服务端联动)+ 元信息行(覆盖前端需求 + 配套服务端 Phase) + - 「跨项目联动」字段文案逐字与 CONTEXT.md 锁定一致 + - Phase 1 条目以及更早的所有历史条目逐字不变 + + + + + 任务 2:plan 级整体双重验证(tsc + grep + 不引入新依赖) + + + + 1. **必读**:`.planning/phases/02-rbac-ai/02-01-PLAN.md` 的 `` 段(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 条目结构正确) + + + + 本任务**不修改任何代码或配置文件**,仅执行验证命令并把结果汇总写入 `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`) + + + + - 验证 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 跳过原因」 + + + + + 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 + + + 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 + + + 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 + + + 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 + + + cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && head -n 1 app/ai-model/page.tsx + # 预期:含 "use client" + + + cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin && grep -c "\[2026-05-08\] Phase 2" docs/修改记录.md + # 预期:1 + + + + + - 4 大类验证全部通过(A / B / C / D) + - 11 条 specifics 全命中,反向断言(其他 4 角色数组不含 `credential-slot`)通过 + - tsc 不引入指向本 phase 改动文件的新错误 + - 不引入新依赖(4 个 manifest/lockfile 均未改动) + - SUMMARY 中明确记录 next lint 跳过原因(与 Phase 1 一致) + + + + + + +## Plan 级整体验证(覆盖整个 Phase 2) + +执行完两个任务后,确认以下**3 条**整体校验通过: + +### 1. 修改记录顶部条目格式 +```bash +cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin +sed -n '/