634 lines
28 KiB
Markdown
634 lines
28 KiB
Markdown
---
|
||
phase: 03-dialog-feedback
|
||
plan: 02
|
||
type: execute
|
||
wave: 2
|
||
depends_on:
|
||
- "03-01"
|
||
files_modified:
|
||
- components/ai-model/credential-slot-dialog.tsx
|
||
- app/ai-model/page.tsx
|
||
autonomous: true
|
||
requirements:
|
||
- CRED-FE-04
|
||
- CRED-FE-05
|
||
must_haves:
|
||
truths:
|
||
- "授权运营点击「凭据槽位」按钮能打开真实编辑对话框(不再是占位空 Dialog)"
|
||
- "对话框打开时通过 getCredentialSlot() 拉取后端数据,appId 字段以明文预填、accessToken 字段为空、placeholder 显示脱敏掩码"
|
||
- "updated_at 字段以中文格式只读显示(toLocaleString('zh-CN'))"
|
||
- "用户必须重新输入 access_token 才能提交(强制输入语义;防止把脱敏掩码当真值回写)"
|
||
- "提交成功 → Sonner toast.success(\"凭据槽位已更新\") + 自动关闭对话框"
|
||
- "提交失败 → 经 handleApiError 映射 + Sonner toast.error 提示,对话框保持打开 + 表单字段不丢"
|
||
- "失败重试时再次打开对话框会重新调 getCredentialSlot 拉最新数据"
|
||
artifacts:
|
||
- path: "components/ai-model/credential-slot-dialog.tsx"
|
||
provides: "CredentialSlotDialog 组件 (RHF + Zod + Sonner + handleApiError)"
|
||
exports: ["CredentialSlotDialog"]
|
||
contains: "useForm"
|
||
min_lines: 130
|
||
- path: "app/ai-model/page.tsx"
|
||
provides: "import 新组件 + 删除 Phase 2 占位 Dialog (L473-485) + 删除不再使用的 Dialog 系列 import (L9-15)"
|
||
contains: "CredentialSlotDialog"
|
||
key_links:
|
||
- from: "components/ai-model/credential-slot-dialog.tsx"
|
||
to: "lib/api/credential-slot.ts"
|
||
via: "import { getCredentialSlot, updateCredentialSlot, type CredentialSlot }"
|
||
pattern: "from \"@/lib/api/credential-slot\""
|
||
- from: "components/ai-model/credential-slot-dialog.tsx"
|
||
to: "lib/api/error-handler.ts"
|
||
via: "import { handleApiError }"
|
||
pattern: "from \"@/lib/api/error-handler\""
|
||
- from: "components/ai-model/credential-slot-dialog.tsx"
|
||
to: "sonner npm package"
|
||
via: "import { toast } from \"sonner\""
|
||
pattern: "from \"sonner\""
|
||
- from: "app/ai-model/page.tsx"
|
||
to: "components/ai-model/credential-slot-dialog.tsx"
|
||
via: "import { CredentialSlotDialog }"
|
||
pattern: "from \"@/components/ai-model/credential-slot-dialog\""
|
||
---
|
||
|
||
<objective>
|
||
落地 Phase 3 核心实现:
|
||
|
||
1. **新建** `components/ai-model/credential-slot-dialog.tsx`(kebab-case 命名,与仓库 9 个现有业务对话框一致),导出 `CredentialSlotDialog` 组件,基于 React Hook Form + Zod + shadcn Form wrapper + Sonner
|
||
2. **修改** `app/ai-model/page.tsx`:删 L9-15 不再用的 Dialog 系列 import、新增 `CredentialSlotDialog` import、删 L473-485 占位 Dialog、替换为 `<CredentialSlotDialog open onOpenChange />`
|
||
|
||
**Purpose**:让授权运营能查看脱敏的当前凭据、安全提交新值,且成功 / 失败两条路径都有清晰的中文 toast 反馈;表单语义按 CONTEXT D-提交逻辑 锁定为「access_token 强制输入」(每次保存都要重输;不实现「留空保留旧值」,避免回写脱敏掩码 —— 该语义需后端配合识别脱敏掩码格式,已记入候选下一周期 milestone)。
|
||
|
||
**Output**:1 个新组件文件 ~150 行 + page.tsx 局部 4 处改动。
|
||
</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/03-dialog-feedback/03-CONTEXT.md
|
||
@.planning/phases/03-dialog-feedback/03-RESEARCH.md
|
||
@.planning/phases/03-dialog-feedback/03-01-SUMMARY.md
|
||
@CLAUDE.md
|
||
@components/users/user-form-dialog.tsx
|
||
@components/ui/dialog.tsx
|
||
@components/ui/form.tsx
|
||
@components/ui/sonner.tsx
|
||
@lib/api/credential-slot.ts
|
||
@lib/api/error-handler.ts
|
||
@app/ai-model/page.tsx
|
||
|
||
<interfaces>
|
||
<!-- Phase 1 已落地的 API client 类型 + 函数(直接消费) -->
|
||
|
||
From lib/api/credential-slot.ts:
|
||
```typescript
|
||
export interface CredentialSlot {
|
||
appId: string
|
||
accessTokenMasked: string // 后端返回的脱敏字符串(前端绝不能当真值回写)
|
||
updatedAt: string // ISO 8601
|
||
}
|
||
|
||
export interface CredentialSlotUpdatePayload {
|
||
appId: string
|
||
accessToken: string // 明文(提交后将完整覆写后端记录)
|
||
}
|
||
|
||
export const getCredentialSlot: () => Promise<CredentialSlot>
|
||
export const updateCredentialSlot: (payload: CredentialSlotUpdatePayload) => Promise<CredentialSlot>
|
||
```
|
||
|
||
From lib/api/error-handler.ts (line 38):
|
||
```typescript
|
||
export const handleApiError = (error: unknown): string
|
||
// 实现:error instanceof Error → error.message;否则 → "发生未知错误,请重试"
|
||
```
|
||
**注意**:lib/api/index.ts:191 也有同名 dead-code 重复定义;本 phase **必须**显式 `from "@/lib/api/error-handler"`,**不要**走 barrel。
|
||
|
||
From sonner npm package(已在 deps `^1.7.1`):
|
||
```typescript
|
||
import { toast } from "sonner"
|
||
toast.success(title: string, options?: { description?: string }): void
|
||
toast.error(title: string, options?: { description?: string }): void
|
||
```
|
||
**注意**:仓库 `hooks/use-toast.ts` 是 Radix Toast 实现,与 Sonner 不通;**不要**走 useToast hook。
|
||
|
||
From components/ui/dialog.tsx:
|
||
```typescript
|
||
export const Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter
|
||
```
|
||
|
||
From components/ui/form.tsx:
|
||
```typescript
|
||
export const Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage
|
||
```
|
||
|
||
From react-hook-form + @hookform/resolvers/zod + zod(已在 deps):
|
||
```typescript
|
||
import { useForm } from "react-hook-form"
|
||
import { zodResolver } from "@hookform/resolvers/zod"
|
||
import * as z from "zod"
|
||
```
|
||
|
||
Phase 2 已落地的 page.tsx 上下文(行号引用):
|
||
- L1:`"use client"`(保留)
|
||
- L9-15:`Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle` 命名导入(**本 plan 删除**,因占位 Dialog 删后 page 不再直接用 Dialog primitive)
|
||
- L16:`KeyRound` 等 lucide-react 图标 import(保留)
|
||
- L17:`hasPermission` import(保留)
|
||
- L20-25:mounted state + isCredentialDialogOpen state + useEffect mounted 守卫(保留)
|
||
- L35-43:「凭据槽位」Button 入口(保留)
|
||
- L473-485:占位 Dialog(**本 plan 删除并替换**)
|
||
</interfaces>
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto">
|
||
<name>Task 1:新建 components/ai-model/credential-slot-dialog.tsx(CRED-FE-04 + CRED-FE-05 主体)</name>
|
||
<files>components/ai-model/credential-slot-dialog.tsx</files>
|
||
<read_first>
|
||
必读 4 个 canonical references(已在 context @ 块声明,再次明确):
|
||
|
||
1. `components/users/user-form-dialog.tsx` L1-289 —— RHF + Zod + shadcn Form 1:1 模板(**确认 import 块结构 + useForm + handleSubmit + form.reset 关闭模式**)
|
||
2. `lib/api/credential-slot.ts` 全文 —— 确认 `getCredentialSlot` 返回 `Promise<CredentialSlot>`、`updateCredentialSlot` 接受 `CredentialSlotUpdatePayload`、字段名 `appId` / `accessTokenMasked` / `updatedAt`
|
||
3. `lib/api/error-handler.ts` L38-43 —— 确认 `handleApiError(error: unknown): string` 签名
|
||
4. `components/ui/form.tsx` L169-178 —— 确认 Form / FormField / FormItem / FormLabel / FormControl / FormDescription / FormMessage 都已导出
|
||
|
||
并确认 `components/ai-model/` 目录不存在,**创建文件时需要 mkdir**(Write 工具会自动创建父目录)。
|
||
</read_first>
|
||
<action>
|
||
**先 mkdir** `components/ai-model/`(PowerShell:`New-Item -ItemType Directory -Force -Path 'components/ai-model'`)—— 如果用 Write tool 创建文件,父目录通常自动创建,但本仓在 Windows / git bash 混用环境下若不行需显式 mkdir。
|
||
|
||
**新建文件 `components/ai-model/credential-slot-dialog.tsx`,逐字写入以下完整内容**(~150 行;改写自 `components/users/user-form-dialog.tsx` 模板,已对齐 CONTEXT 锁定决策):
|
||
|
||
```tsx
|
||
"use client"
|
||
|
||
import { useEffect, useState } from "react"
|
||
import { useForm } from "react-hook-form"
|
||
import { zodResolver } from "@hookform/resolvers/zod"
|
||
import * as z from "zod"
|
||
import { toast } from "sonner"
|
||
import { Loader2 } from "lucide-react"
|
||
|
||
import { Button } from "@/components/ui/button"
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog"
|
||
import {
|
||
Form,
|
||
FormControl,
|
||
FormDescription,
|
||
FormField,
|
||
FormItem,
|
||
FormLabel,
|
||
FormMessage,
|
||
} from "@/components/ui/form"
|
||
import { Input } from "@/components/ui/input"
|
||
import {
|
||
getCredentialSlot,
|
||
updateCredentialSlot,
|
||
type CredentialSlot,
|
||
} from "@/lib/api/credential-slot"
|
||
import { handleApiError } from "@/lib/api/error-handler"
|
||
|
||
// ───── Zod schema ──────────────────────────────────────────────────────
|
||
// access_token 强制输入(CONTEXT D-提交逻辑 锁定):
|
||
// - 后端 PUT 是全字段覆写语义;前端无法识别脱敏掩码格式
|
||
// - 「留空保留旧值」需后端配合识别掩码格式,已记入候选下一周期 milestone
|
||
// - 本 phase 退化为「每次保存都要重输 access_token」—— UX 略差但语义正确
|
||
const formSchema = z.object({
|
||
appId: z.string().min(1, { message: "App ID 不能为空" }),
|
||
accessToken: z.string().min(1, { message: "请输入 Access Token" }),
|
||
})
|
||
|
||
type FormValues = z.infer<typeof formSchema>
|
||
|
||
interface CredentialSlotDialogProps {
|
||
open: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
}
|
||
|
||
export function CredentialSlotDialog({ open, onOpenChange }: CredentialSlotDialogProps) {
|
||
const [slot, setSlot] = useState<CredentialSlot | null>(null)
|
||
const [isLoading, setIsLoading] = useState(false)
|
||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||
|
||
const form = useForm<FormValues>({
|
||
resolver: zodResolver(formSchema),
|
||
defaultValues: { appId: "", accessToken: "" },
|
||
})
|
||
|
||
// open=true 时拉数据 + reset 表单(accessToken 永远默认空串,绝不回填脱敏掩码)
|
||
useEffect(() => {
|
||
if (!open) return
|
||
let cancelled = false
|
||
setIsLoading(true)
|
||
getCredentialSlot()
|
||
.then((data) => {
|
||
if (cancelled) return
|
||
setSlot(data)
|
||
form.reset({ appId: data.appId, accessToken: "" })
|
||
})
|
||
.catch((e) => {
|
||
if (cancelled) return
|
||
toast.error("加载失败", { description: handleApiError(e) })
|
||
})
|
||
.finally(() => {
|
||
if (!cancelled) setIsLoading(false)
|
||
})
|
||
return () => {
|
||
cancelled = true
|
||
}
|
||
}, [open, form])
|
||
|
||
const handleOpenChange = (next: boolean) => {
|
||
onOpenChange(next)
|
||
// 关闭时清表单 + 清 slot,避免下次打开残留上次输入
|
||
if (!next) {
|
||
form.reset({ appId: "", accessToken: "" })
|
||
setSlot(null)
|
||
}
|
||
}
|
||
|
||
const handleSubmit = async (values: FormValues) => {
|
||
setIsSubmitting(true)
|
||
try {
|
||
await updateCredentialSlot({
|
||
appId: values.appId,
|
||
accessToken: values.accessToken,
|
||
})
|
||
toast.success("凭据槽位已更新", { description: "配置已生效" })
|
||
handleOpenChange(false)
|
||
} catch (e) {
|
||
// 失败时不关闭对话框、不清空表单值(CONTEXT D-错误处理 锁定)
|
||
toast.error("保存失败", { description: handleApiError(e) })
|
||
} finally {
|
||
setIsSubmitting(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||
<DialogContent className="sm:max-w-[500px]">
|
||
<DialogHeader>
|
||
<DialogTitle>通用凭据槽位</DialogTitle>
|
||
<DialogDescription>
|
||
管理 APP ID 与 Access Token;提交将全字段覆写后端记录。
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
{isLoading ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||
</div>
|
||
) : (
|
||
<Form {...form}>
|
||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 py-2">
|
||
<FormField
|
||
control={form.control}
|
||
name="appId"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>APP ID</FormLabel>
|
||
<FormControl>
|
||
<Input placeholder="输入 APP ID" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="accessToken"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Access Token</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
placeholder={slot?.accessTokenMasked ?? "输入 Access Token"}
|
||
{...field}
|
||
/>
|
||
</FormControl>
|
||
<FormDescription>
|
||
每次保存都需要重新输入 Access Token(不会显示原值,避免回写脱敏掩码)
|
||
</FormDescription>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
{slot && (
|
||
<p className="text-sm text-muted-foreground">
|
||
最后更新:{new Date(slot.updatedAt).toLocaleString("zh-CN")}
|
||
</p>
|
||
)}
|
||
<DialogFooter>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => handleOpenChange(false)}
|
||
disabled={isSubmitting}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button type="submit" disabled={isSubmitting}>
|
||
{isSubmitting ? (
|
||
<>
|
||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
保存中...
|
||
</>
|
||
) : (
|
||
"保存"
|
||
)}
|
||
</Button>
|
||
</DialogFooter>
|
||
</form>
|
||
</Form>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
}
|
||
```
|
||
|
||
**严格约束(绝不偏离)**:
|
||
- 文件命名 **kebab-case** `credential-slot-dialog.tsx`(与 `user-form-dialog.tsx` / `add-song-dialog.tsx` 等 9 个对齐;CONTEXT D-命名 + RESEARCH 决议)
|
||
- 组件导出名 **PascalCase** `CredentialSlotDialog`(具名导出,沿用 `user-form-dialog.tsx:57` 的 `export function UserFormDialog` 写法)
|
||
- 顶部首行 **必须** `"use client"`(含 `useState` / `useEffect` / RHF)
|
||
- toast 走 **`import { toast } from "sonner"`** —— **不要** import `useToast` from `@/hooks/use-toast`(Radix Toast,与 Sonner 不通)
|
||
- handleApiError 走 **`from "@/lib/api/error-handler"`** —— **不要** from `@/lib/api`(barrel 里有 dead-code 同名重复定义)
|
||
- `defaultValues.accessToken` **必须为空字符串 `""`** —— **不要**写 `slot?.accessTokenMasked`(会回写脱敏掩码,违反 D-提交逻辑)
|
||
- `placeholder` 用 `slot?.accessTokenMasked ?? "输入 Access Token"`(仅作视觉提示)
|
||
- 失败路径 **不**调用 `handleOpenChange(false)`、**不**调 `form.reset()` —— 保持对话框打开 + 表单值不丢
|
||
- 不引入新依赖(sonner / lucide-react / @hookform/resolvers / zod / react-hook-form / shadcn UI 全部已在 deps)
|
||
- 不修改 shadcn 原子组件(Dialog / Form / Input / Button 不动)
|
||
</action>
|
||
<verify>
|
||
<automated>
|
||
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
|
||
|
||
# A 段:tsc 反向断言(必须 0 条指向新文件)
|
||
npx tsc --noEmit 2>&1 | Select-String -Pattern 'components/ai-model/credential-slot-dialog\.tsx|components\\ai-model\\credential-slot-dialog\.tsx'
|
||
# 期望:无任何输出
|
||
|
||
# B 段:grep 13 条 specifics(CONTEXT.md L253-268 表)—— 全部命中
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'export function CredentialSlotDialog' # spec #1
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'useForm' # spec #2a
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'zodResolver' # spec #2b
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'z\.object' # spec #2c
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'useEffect' # spec #3a
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'getCredentialSlot' # spec #3b
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'placeholder.*accessTokenMasked' # spec #5
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern '每次保存都需要重新输入' # spec #6
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'slot\.updatedAt' # spec #7
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'updateCredentialSlot' # spec #8
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'toast\.success.*凭据槽位已更新' # spec #9
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'handleApiError' # spec #10
|
||
|
||
# C 段:反向断言(绝不能命中 —— 防回归)
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'defaultValues.*accessTokenMasked'
|
||
# 期望:0 行(绝不能把脱敏掩码当默认值)
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'from\s+"@/hooks/use-toast"|from\s+''@/hooks/use-toast'''
|
||
# 期望:0 行(绝不走 Radix Toast hook)
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern 'from\s+"@/lib/api"\s*$'
|
||
# 期望:0 行(必须显式 from "@/lib/api/error-handler",不走 barrel)
|
||
Select-String -Path 'components/ai-model/credential-slot-dialog.tsx' -Pattern '^"use client"'
|
||
# 期望:1 行(首行必须 "use client")
|
||
|
||
# D 段:lockfile 未动
|
||
git diff --stat HEAD -- package.json yarn.lock package-lock.json pnpm-lock.yaml
|
||
# 期望:0 行
|
||
</automated>
|
||
</verify>
|
||
<done>
|
||
- `components/ai-model/credential-slot-dialog.tsx` 存在且 ≥130 行
|
||
- 12 条 grep specifics(spec #1-3, #5-10)全部 ≥1 行命中
|
||
- 4 条反向断言(accessTokenMasked 不在 defaultValues / 不 import use-toast / 不走 barrel handleApiError / 首行 "use client")全部满足
|
||
- `npx tsc --noEmit` 过滤后 0 条新错误指向本文件
|
||
- lockfile 0 行 diff
|
||
</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 2:改 app/ai-model/page.tsx 删占位 Dialog 并接入 CredentialSlotDialog</name>
|
||
<files>app/ai-model/page.tsx</files>
|
||
<read_first>
|
||
必读 `app/ai-model/page.tsx` 全文确认改动定位。当前结构关键行号:
|
||
|
||
- L1:`"use client"`(保留)
|
||
- L9-15:Dialog 系列命名导入(**本 task 删除整段**):
|
||
```tsx
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog"
|
||
```
|
||
- L16:lucide-react 图标 import(保留 `KeyRound` 等)
|
||
- L17:`hasPermission` import(保留)
|
||
- L20-25:mounted state + isCredentialDialogOpen state + useEffect(保留)
|
||
- L35-43:「凭据槽位」Button 入口(保留)
|
||
- L473-485:占位 Dialog(**本 task 删除整段并替换**):
|
||
```tsx
|
||
<Dialog
|
||
open={isCredentialDialogOpen}
|
||
onOpenChange={setIsCredentialDialogOpen}
|
||
>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>通用凭据槽位</DialogTitle>
|
||
<DialogDescription>
|
||
对话框真实内容由 Phase 3 落地
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
</DialogContent>
|
||
</Dialog>
|
||
```
|
||
|
||
Tabs / TabsContent / Card 等其余内容(L18 / L26-46 间未列段 / L46-471)**逐字不动**。
|
||
</read_first>
|
||
<action>
|
||
**精确改动 4 处**:
|
||
|
||
**改动 1:删除 L9-15** Dialog 系列命名导入整段(占位 Dialog 删后 page 不再直接使用 Dialog primitive)
|
||
|
||
删前(当前):
|
||
```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"
|
||
```
|
||
|
||
删后:
|
||
```tsx
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { Brain, Mic, Database, Plus, Sparkles, Edit, Play, Sliders, User, KeyRound } from "lucide-react"
|
||
```
|
||
|
||
**改动 2:在 lucide-react import 之后追加 1 行新 import**:
|
||
|
||
```tsx
|
||
import { CredentialSlotDialog } from "@/components/ai-model/credential-slot-dialog"
|
||
```
|
||
|
||
放在 `import { Brain, ... } from "lucide-react"` 之后、`import { hasPermission } from "@/lib/permissions"` 之前 —— 与同目录的业务组件 import 紧邻,逻辑成组。
|
||
|
||
最终该段(删除 + 新增之后)应为:
|
||
```tsx
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { Brain, Mic, Database, Plus, Sparkles, Edit, Play, Sliders, User, KeyRound } from "lucide-react"
|
||
import { CredentialSlotDialog } from "@/components/ai-model/credential-slot-dialog"
|
||
import { hasPermission } from "@/lib/permissions"
|
||
```
|
||
|
||
**改动 3:删除 L473-485(占位 Dialog 整 13 行)+ 替换为 1 行 `<CredentialSlotDialog />`**
|
||
|
||
删前(当前):
|
||
```tsx
|
||
<Dialog
|
||
open={isCredentialDialogOpen}
|
||
onOpenChange={setIsCredentialDialogOpen}
|
||
>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>通用凭据槽位</DialogTitle>
|
||
<DialogDescription>
|
||
对话框真实内容由 Phase 3 落地
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</DashboardShell>
|
||
```
|
||
|
||
删后:
|
||
```tsx
|
||
<CredentialSlotDialog
|
||
open={isCredentialDialogOpen}
|
||
onOpenChange={setIsCredentialDialogOpen}
|
||
/>
|
||
</DashboardShell>
|
||
```
|
||
|
||
**改动 4:保留所有其他内容**(line 1 `"use client"` / line 17 `hasPermission` import / L19-25 默认导出函数 + state + mounted useEffect / L26-46 DashboardHeader 含 Button 入口 / L47-471 Tabs 内容)—— **逐字不动**。
|
||
|
||
**严格约束**:
|
||
- **不**改 mounted 守卫的形态(沿用 Phase 2 已落地的 `mounted && hasPermission("credential-slot")`)
|
||
- **不**给 page.tsx 加 Loader2 import(Loader2 仅在新组件 `credential-slot-dialog.tsx` 内用)
|
||
- **不**改 Button 入口文案 / 图标 / variant
|
||
- **不**改 isCredentialDialogOpen state 名称
|
||
- 替换占位 Dialog 时**保留**前后的缩进与换行,确保 JSX 树形对齐 `</DashboardShell>`
|
||
</action>
|
||
<verify>
|
||
<automated>
|
||
cd C:\Users\admin\Desktop\Lila-Server\qy-lty-admin
|
||
|
||
# A 段:tsc 反向断言(必须 0 条指向 page.tsx)
|
||
npx tsc --noEmit 2>&1 | Select-String -Pattern 'app/ai-model/page\.tsx|app\\ai-model\\page\.tsx'
|
||
# 期望:无任何输出
|
||
|
||
# B 段:正向 grep —— 改动应该都在
|
||
Select-String -Path 'app/ai-model/page.tsx' -Pattern 'import \{ CredentialSlotDialog \} from "@/components/ai-model/credential-slot-dialog"' # spec #11a
|
||
# 期望:1 行命中
|
||
Select-String -Path 'app/ai-model/page.tsx' -Pattern '<CredentialSlotDialog' # spec #11b
|
||
# 期望:1 行命中
|
||
Select-String -Path 'app/ai-model/page.tsx' -Pattern '"use client"'
|
||
# 期望:1 行(首行)
|
||
Select-String -Path 'app/ai-model/page.tsx' -Pattern 'hasPermission\("credential-slot"\)'
|
||
# 期望:1 行(Phase 2 已落地,本 plan 不应删)
|
||
Select-String -Path 'app/ai-model/page.tsx' -Pattern '凭据槽位'
|
||
# 期望:≥1 行(Button 文案 + DialogTitle 现已搬到子组件,page 仍保留 Button 文案)
|
||
|
||
# C 段:反向断言(绝不能命中 —— 旧占位 Dialog 已删干净)
|
||
Select-String -Path 'app/ai-model/page.tsx' -Pattern '对话框真实内容由 Phase 3 落地' # spec #11c
|
||
# 期望:0 行
|
||
Select-String -Path 'app/ai-model/page.tsx' -Pattern 'from "@/components/ui/dialog"'
|
||
# 期望:0 行(Dialog 系列 import 已整段删除)
|
||
Select-String -Path 'app/ai-model/page.tsx' -Pattern '<Dialog\s'
|
||
# 期望:0 行(page 内不再直接使用 Dialog primitive)
|
||
|
||
# D 段:lockfile 未动
|
||
git diff --stat HEAD -- package.json yarn.lock package-lock.json pnpm-lock.yaml
|
||
# 期望:0 行
|
||
</automated>
|
||
</verify>
|
||
<done>
|
||
- `app/ai-model/page.tsx` 含 1 行 `import { CredentialSlotDialog } from "@/components/ai-model/credential-slot-dialog"`
|
||
- `app/ai-model/page.tsx` 含 1 处 `<CredentialSlotDialog open={isCredentialDialogOpen} onOpenChange={setIsCredentialDialogOpen} />`
|
||
- 旧占位 Dialog(含「对话框真实内容由 Phase 3 落地」字面量)已 0 行命中
|
||
- Dialog 系列命名导入已 0 行命中
|
||
- `mounted && hasPermission("credential-slot")` 守卫保留(Phase 2 不被破坏)
|
||
- `npx tsc --noEmit` 过滤后 0 条新错误指向本文件
|
||
- lockfile 0 行 diff
|
||
</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<verification>
|
||
**Plan 03-02 整体串联验证**(Plan 内已涵盖,此处汇总):
|
||
|
||
1. **类型检查**:`npx tsc --noEmit` exit 非 0(67 条存量错误),但 `Select-String` 过滤 `credential-slot-dialog.tsx` + `app/ai-model/page.tsx` 命中 **0 条**新错误
|
||
2. **13 条 grep specifics**(CONTEXT.md L253-268 表)全部命中:
|
||
- #1 文件存在 + 导出 ✓(Task 1)
|
||
- #2 RHF + Zod ✓(Task 1)
|
||
- #3 useEffect + getCredentialSlot ✓(Task 1)
|
||
- #4 反向:defaultValues 不含 accessTokenMasked ✓(Task 1)
|
||
- #5 placeholder 用 accessTokenMasked ✓(Task 1)
|
||
- #6 「如需更新...」中文提示 ✓(Task 1,等价表述「每次保存都需要重新输入...」)
|
||
- #7 updatedAt 只读显示 ✓(Task 1)
|
||
- #8 updateCredentialSlot 调用 ✓(Task 1)
|
||
- #9 toast.success 中文 ✓(Task 1)
|
||
- #10 handleApiError ✓(Task 1)
|
||
- #11 page.tsx 占位删除 + import 新组件 ✓(Task 2)
|
||
- #12 tsc 过滤 0 条 ✓(Task 1+2)
|
||
- #13 修改记录条目(推迟到 Plan 03-03)
|
||
3. **不引入新依赖**:4 个 lockfile 0 行 diff
|
||
4. **lint 跳过**:项目无 ESLint infra,沿用 Phase 1+2 判定
|
||
|
||
**Phase 3 success criteria 对应**(ROADMAP.md L58-63):
|
||
- #1 打开自动 GET 拉取 + appId 明文预填 + accessToken placeholder 掩码 + updatedAt 只读 ✓ Task 1
|
||
- #2 RHF + Zod ✓ + 强制输入 access_token(替代「留空保留旧值」语义;Plan 03-03 修改记录写权衡说明)✓ Task 1
|
||
- #3 提交成功 → toast.success + 关闭 ✓ Task 1(重新打开时 useEffect 自动 reload,无需主动刷新)
|
||
- #4 提交失败 → handleApiError + toast.error + 不关闭对话框 + 表单值不丢 ✓ Task 1
|
||
- #5 端到端串联依赖后端 Phase 2 落地 —— 程序化验证(tsc + grep)通过即可,浏览器 E2E 推迟(无 E2E 框架)
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
- [ ] `components/ai-model/credential-slot-dialog.tsx` 存在 ≥130 行
|
||
- [ ] `components/ai-model/` 目录已创建
|
||
- [ ] 12 条正向 grep(Task 1 specifics #1-10)全部命中
|
||
- [ ] 4 条反向断言(Task 1)全部满足
|
||
- [ ] `app/ai-model/page.tsx` 含 `CredentialSlotDialog` import + JSX 元素
|
||
- [ ] `app/ai-model/page.tsx` 不再含旧占位 Dialog 字面量「对话框真实内容由 Phase 3 落地」
|
||
- [ ] `app/ai-model/page.tsx` 不再含 `from "@/components/ui/dialog"` import
|
||
- [ ] `mounted && hasPermission("credential-slot")` 守卫保留(Phase 2 不破坏)
|
||
- [ ] `npx tsc --noEmit` 过滤后 0 条新错误指向 2 个改动文件
|
||
- [ ] 4 个 lockfile 0 行 diff
|
||
</success_criteria>
|
||
|
||
<output>
|
||
完成后创建 `.planning/phases/03-dialog-feedback/03-02-SUMMARY.md`,按 `$HOME/.claude/get-shit-done/templates/summary.md` 格式记录:
|
||
- 改动文件清单(2 个:新建 + 修改)
|
||
- 12 条正向 grep + 4 条反向断言全表结果
|
||
- tsc 过滤结果(0 条新错误)
|
||
- lockfile diff(0 行)
|
||
- Phase 3 ROADMAP success criteria 对应表(#1-#5 状态:本 plan 完成 #1-#4 + #5 程序化验证;浏览器端到端推迟到后端 Phase 2 联调)
|
||
- 下一步:执行 Plan 03-03(修改记录追加 + plan 级双重验证)
|
||
</output>
|