28 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
03-dialog-feedback 02 execute 2
03-01
components/ai-model/credential-slot-dialog.tsx
app/ai-model/page.tsx
true
CRED-FE-04
CRED-FE-05
truths artifacts key_links
授权运营点击「凭据槽位」按钮能打开真实编辑对话框(不再是占位空 Dialog
对话框打开时通过 getCredentialSlot() 拉取后端数据appId 字段以明文预填、accessToken 字段为空、placeholder 显示脱敏掩码
updated_at 字段以中文格式只读显示toLocaleString('zh-CN')
用户必须重新输入 access_token 才能提交(强制输入语义;防止把脱敏掩码当真值回写)
提交成功 → Sonner toast.success("凭据槽位已更新") + 自动关闭对话框
提交失败 → 经 handleApiError 映射 + Sonner toast.error 提示,对话框保持打开 + 表单字段不丢
失败重试时再次打开对话框会重新调 getCredentialSlot 拉最新数据
path provides exports contains min_lines
components/ai-model/credential-slot-dialog.tsx CredentialSlotDialog 组件 (RHF + Zod + Sonner + handleApiError)
CredentialSlotDialog
useForm 130
path provides contains
app/ai-model/page.tsx import 新组件 + 删除 Phase 2 占位 Dialog (L473-485) + 删除不再使用的 Dialog 系列 import (L9-15) CredentialSlotDialog
from to via pattern
components/ai-model/credential-slot-dialog.tsx lib/api/credential-slot.ts import { getCredentialSlot, updateCredentialSlot, type CredentialSlot } from "@/lib/api/credential-slot"
from to via pattern
components/ai-model/credential-slot-dialog.tsx lib/api/error-handler.ts import { handleApiError } from "@/lib/api/error-handler"
from to via pattern
components/ai-model/credential-slot-dialog.tsx sonner npm package import { toast } from "sonner" from "sonner"
from to via pattern
app/ai-model/page.tsx components/ai-model/credential-slot-dialog.tsx import { CredentialSlotDialog } from "@/components/ai-model/credential-slot-dialog"
落地 Phase 3 核心实现:
  1. 新建 components/ai-model/credential-slot-dialog.tsxkebab-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

Output1 个新组件文件 ~150 行 + page.tsx 局部 4 处改动。

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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

From lib/api/credential-slot.ts:

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)

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

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

export const Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter

From components/ui/form.tsx

export const Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage

From react-hook-form + @hookform/resolvers/zod + zod已在 deps

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-15Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle 命名导入(本 plan 删除,因占位 Dialog 删后 page 不再直接用 Dialog primitive
  • L16KeyRound 等 lucide-react 图标 import保留
  • L17hasPermission import保留
  • L20-25mounted state + isCredentialDialogOpen state + useEffect mounted 守卫(保留)
  • L35-43「凭据槽位」Button 入口(保留)
  • L473-485占位 Dialog本 plan 删除并替换
Task 1新建 components/ai-model/credential-slot-dialog.tsxCRED-FE-04 + CRED-FE-05 主体) components/ai-model/credential-slot-dialog.tsx 必读 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> 先 mkdir components/ai-model/PowerShellNew-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 锁定决策):

"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:57export function UserFormDialog 写法)

  • 顶部首行 必须 "use client"(含 useState / useEffect / RHF

  • toast 走 import { toast } from "sonner" —— 不要 import useToast from @/hooks/use-toastRadix Toast与 Sonner 不通)

  • handleApiError 走 from "@/lib/api/error-handler" —— 不要 from @/lib/apibarrel 里有 dead-code 同名重复定义)

  • defaultValues.accessToken 必须为空字符串 "" —— 不要slot?.accessTokenMasked(会回写脱敏掩码,违反 D-提交逻辑)

  • placeholderslot?.accessTokenMasked ?? "输入 Access Token"(仅作视觉提示)

  • 失败路径 调用 handleOpenChange(false)form.reset() —— 保持对话框打开 + 表单值不丢

  • 不引入新依赖sonner / lucide-react / @hookform/resolvers / zod / react-hook-form / shadcn UI 全部已在 deps

  • 不修改 shadcn 原子组件Dialog / Form / Input / Button 不动) 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 行
    
    - `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
Task 2改 app/ai-model/page.tsx 删占位 Dialog 并接入 CredentialSlotDialog app/ai-model/page.tsx 必读 `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> 精确改动 4 处

改动 1删除 L9-15 Dialog 系列命名导入整段(占位 Dialog 删后 page 不再直接使用 Dialog primitive

删前(当前):

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

import { CredentialSlotDialog } from "@/components/ai-model/credential-slot-dialog"

放在 import { Brain, ... } from "lucide-react" 之后、import { hasPermission } from "@/lib/permissions" 之前 —— 与同目录的业务组件 import 紧邻,逻辑成组。

最终该段(删除 + 新增之后)应为:

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

删前(当前):

      <Dialog
        open={isCredentialDialogOpen}
        onOpenChange={setIsCredentialDialogOpen}
      >
        <DialogContent>
          <DialogHeader>
            <DialogTitle>通用凭据槽位</DialogTitle>
            <DialogDescription>
              对话框真实内容由 Phase 3 落地
            </DialogDescription>
          </DialogHeader>
        </DialogContent>
      </Dialog>
    </DashboardShell>

删后:

      <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> 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 行
    
    - `app/ai-model/page.tsx` 含 1 行 `import { CredentialSlotDialog } from "@/components/ai-model/credential-slot-dialog"` - `app/ai-model/page.tsx` 含 1 处 `` - 旧占位 Dialog含「对话框真实内容由 Phase 3 落地」字面量)已 0 行命中 - Dialog 系列命名导入已 0 行命中 - `mounted && hasPermission("credential-slot")` 守卫保留Phase 2 不被破坏) - `npx tsc --noEmit` 过滤后 0 条新错误指向本文件 - lockfile 0 行 diff
**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 specificsCONTEXT.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 框架)

<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.tsxCredentialSlotDialog 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>
完成后创建 `.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 级双重验证)