---
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>
