634 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

---
phase: 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-25mounted 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.tsxCRED-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 条 specificsCONTEXT.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 specificsspec #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-15Dialog 系列命名导入(**本 task 删除整段**
```tsx
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
```
- L16lucide-react 图标 import保留 `KeyRound` 等)
- L17`hasPermission` import保留
- L20-25mounted 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 importLoader2 仅在新组件 `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 非 067 条存量错误),但 `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 条正向 grepTask 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 diff0 行)
- Phase 3 ROADMAP success criteria 对应表(#1-#5 状态:本 plan 完成 #1-#4 + #5 程序化验证;浏览器端到端推迟到后端 Phase 2 联调)
- 下一步:执行 Plan 03-03修改记录追加 + plan 级双重验证)
</output>