- 在 L26 注释之后、L28 Phase 2 条目之前插入 [2026-05-08] Phase 3 完整条目 - 条目按 CLAUDE.md L72-82 4 字段格式(文件路径 / 修改类型 / 修改内容 / 修改原因)+ 跨项目联动 + 服务端联动 - 列出 3 个改动文件(app/layout.tsx + components/ai-model/credential-slot-dialog.tsx + app/ai-model/page.tsx) - 修改原因段显式说明 access_token 强制输入语义的业务权衡 + 候选下一周期 milestone 锚点(识别脱敏掩码保留旧值) - 跨项目联动 + 服务端联动字段使用 CONTEXT.md / 上下文锁定的中文文本 - 覆盖 CRED-FE-04 + CRED-FE-05 - CLAUDE.md L70-94 修改记录强制规则闭环
19 KiB
19 KiB
管理后台前端代码修改记录
本文档记录每次对管理后台前端(qy-lty-admin,Next.js + React)代码的修改,方便追踪变更历史。
范围说明:本仓库与服务端 qy_lty 是独立项目,各自维护独立的修改记录。仅记录本仓库(管理后台前端)改动;跨项目联动改动需在 qy_lty/docs/修改记录.md 同期写一条相互引用的条目。
修改格式说明
每次修改按以下格式记录:
### [日期] 修改简述
- **文件路径**: 相对于项目根目录的文件路径
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug
- **修改内容**: 具体修改了什么
- **修改原因**: 为什么要做这个修改
修改历史
[2026-05-08] Phase 3(前端)凭据槽位编辑对话框 + 提交反馈
配套服务端 Phase:本 phase 不触达服务端;与服务端 v1.0 Phase 2「管理端读写接口」commit 46d72b8 既有契约保持兼容(GET 脱敏掩码 + PUT 全字段覆写语义不变)
覆盖前端需求:CRED-FE-04、CRED-FE-05
- 文件路径:
app/layout.tsx(修改)components/ai-model/credential-slot-dialog.tsx(新增)app/ai-model/page.tsx(修改)
- 修改类型: 修改 + 新增(前端 UI 收尾;纯前端,无新依赖、不动 lockfile、不触达服务端)
- 修改内容:
app/layout.tsx(修复仓库 pre-existing 死代码 bug):- 顶部新增
import { Toaster } from "@/components/ui/sonner" <body>{children}</body>内{children}之后追加<Toaster />- 修复仓库内 9 处
toast(...)调用全部静默的 dead-code 状态(components/ui/sonner.tsx早就存在 Toaster 包装但从未在 RootLayout 挂载)
- 顶部新增
components/ai-model/credential-slot-dialog.tsx(新建 ~150 行):- 顶部
"use client"指令;具名导出CredentialSlotDialog - 文件命名 kebab-case(与仓库 9 个现有业务对话框
user-form-dialog.tsx/add-song-dialog.tsx等对齐) - 表单技术栈:React Hook Form + Zod + shadcn Form wrapper(1:1 模板自
components/users/user-form-dialog.tsx) - Zod schema:
appId+accessToken都min(1)(access_token 强制输入,见「修改原因」段权衡说明) - 受控接口:
{ open: boolean; onOpenChange: (open: boolean) => void } - 打开时
useEffect调getCredentialSlot()拉取 →form.reset({ appId: data.appId, accessToken: "" })—— accessToken 永远默认空串,绝不回填脱敏掩码 <Input placeholder={slot?.accessTokenMasked} />—— 仅作视觉提示<FormDescription>每次保存都需要重新输入 Access Token(不会显示原值,避免回写脱敏掩码)</FormDescription>updatedAt以new Date(slot.updatedAt).toLocaleString("zh-CN")中文只读显示- 提交成功:
toast.success("凭据槽位已更新", { description: "配置已生效" })+handleOpenChange(false) - 提交失败:
toast.error("保存失败", { description: handleApiError(e) })+ 对话框保持打开 + 表单值不丢 - 关闭时
form.reset({ appId: "", accessToken: "" })+setSlot(null)—— 避免下次打开残留上次输入 - useEffect cleanup
cancelledflag 防止快速开关导致的 race condition - 关键 import 决策(避免仓库内同名 dead code):
import { toast } from "sonner"—— 不走@/hooks/use-toast(Radix Toast 实现,与 Sonner 不通信)import { handleApiError } from "@/lib/api/error-handler"—— 不走 barrel@/lib/api(barrel 内有同名重复定义)
- 顶部
app/ai-model/page.tsx:- 删除 L9-15 Dialog 系列命名导入整段(
Dialog / DialogContent / DialogDescription / DialogHeader / DialogTitle)—— 占位 Dialog 删除后 page 不再直接使用 Dialog primitive - 在 lucide-react import 之后新增
import { CredentialSlotDialog } from "@/components/ai-model/credential-slot-dialog" - 删除 L473-485 占位 Dialog(含
<DialogTitle>通用凭据槽位</DialogTitle>+<DialogDescription>对话框真实内容由 Phase 3 落地</DialogDescription>) - 替换为
<CredentialSlotDialog open={isCredentialDialogOpen} onOpenChange={setIsCredentialDialogOpen} /> - 保留 L1
"use client"/ L20-25 mounted state + isCredentialDialogOpen state + useEffect 守卫 / L35-43「凭据槽位」Button 入口(含mounted && hasPermission("credential-slot")守卫) - Tabs / TabsContent / Card 等其余内容(L18-471)逐字不动
- 删除 L9-15 Dialog 系列命名导入整段(
- 修改原因:
- 收尾 Milestone v1.0「通用凭据槽位前端集成」:让授权运营能查看脱敏的当前凭据、安全提交新值,且成功 / 失败两条路径都有清晰的中文 toast 反馈
- 修复
app/layout.tsx的 pre-existing dead code:仓库components/ui/sonner.tsx早已存在 Sonner Toaster 包装但从未挂载到 RootLayout,导致仓库内 9 处既有toast(...)调用全部静默;本 phase 顺手修复(否则 Phase 3 反馈不可见) - 拆出独立组件
credential-slot-dialog.tsx而非把表单内联进 page.tsx:(a) 与仓库 9 个现有业务对话框抽离风格一致;(b) 让 page.tsx 保持简洁,不掺业务表单状态;(c) 关闭对话框时组件级 form.reset 隔离干净 - 业务语义权衡(重要 — 候选下一周期 milestone 锚点):本 phase Zod schema 把
accessToken: z.string().min(1)—— 强制每次重输 access_token,不实现 ROADMAP success criteria #2 中提到的「留空保留旧值」语义。原因:- 后端 PUT 是全字段覆写语义(qy_lty 后端 v1.0 Phase 2 已锁定 commit
46d72b8) - 后端 GET 返回的 access_token 字段是脱敏掩码(末 4 位明文 + 前缀
*),前端永远拿不到真值 - 「留空保留旧值」需后端配合识别 PUT body 中 access_token 的脱敏掩码格式并保留旧值(后端逻辑:
if access_token == mask_token(current.access_token): preserve old) - 该后端识别逻辑不在 Milestone v1.0 范畴,已记入候选下一周期 milestone(参见 PROJECT.md / REQUIREMENTS.md 候选清单 + STATE.md 风险段)
- 当前实现退化为「每次保存都要重输 access_token」—— UX 略差但语义正确(永远不会回写脱敏掩码导致后端清空真实凭据)
- 后端 PUT 是全字段覆写语义(qy_lty 后端 v1.0 Phase 2 已锁定 commit
- 沿用 Phase 1 已建立的「
accessTokenMaskedvsaccessToken类型层屏障」(前者是脱敏字符串、后者是明文)—— TS 编译期会拦截「把脱敏字符串赋给 accessToken 字段」的 bug 路径 - Sonner(
components/ui/sonner.tsx)+lib/api/error-handler.ts:handleApiError是 CONTEXT D-Toast / D-错误处理 锁定的双依赖;不引入新依赖、不混合 Radix Toast hook、不走 barrel 同名重复定义,避免仓库内 dead code 串扰
- 跨项目联动: 无 — Phase 3 是前端 UI 收尾,access_token 强制输入语义为 Phase 1+2 已建立的前后端互引(commit
46d72b8)的延续;'留空保留旧值' 语义需后端识别脱敏掩码格式 + 保留旧值,已记入候选下一周期 milestone(不属于 v1.0 范畴) - 服务端联动: 同上「跨项目联动」字段;后端 commit
46d72b8已建立互引闭环,本 phase 无需再次互引;未来若启动「识别脱敏掩码保留旧值」的后端 patch milestone,届时双端各写新一轮互引条目
[2026-05-08] Phase 2(前端)RBAC 收敛 + AI 模型页凭据槽位入口
配套服务端 Phase:本 phase 不触达服务端;与服务端 v1.0 Phase 2「管理端读写接口」commit 46d72b8 既有契约保持兼容(不引入新契约)
覆盖前端需求:CRED-FE-02、CRED-FE-03
- 文件路径:
lib/permissions.ts(修改)app/ai-model/page.tsx(修改)
- 修改类型: 修改(前端 RBAC 矩阵扩展 + 页面入口控件 + 占位 Dialog;纯前端,无新依赖、不动 lockfile)
- 修改内容:
lib/permissions.ts:PermissionModuleunion 末尾追加"credential-slot",扩为 14 项PERMISSION_MATRIX["超级管理员"]数组末尾追加"credential-slot"PERMISSION_MATRIX["AI模型管理员"]数组末尾追加"credential-slot"- 其他 4 个角色(内容管理员 / 卡牌管理员 / 查看者 / 管理员)数组逐字不变
getModuleFromPath函数体完全不动(凭据槽位是/ai-model子能力,不占独立路由)- 顶部「权限矩阵对照表」注释新增一行「凭据槽位」与代码同步
app/ai-model/page.tsx:- 文件 line 1 顶部新增
"use client"指令,从 Server Component 转为 Client Component - 新增 import:
useState/useEffect(react)+Dialog/DialogContent/DialogDescription/DialogHeader/DialogTitle(@/components/ui/dialog)+KeyRound(lucide-react)+hasPermission(@/lib/permissions) - 函数体顶部新增
mounted+isCredentialDialogOpen两个useState+ 1 个useEffect设mounted为 true(复用components/sidebar.tsxmounted 守卫模式避免 SSR 水合不匹配) DashboardHeader内部用<div className="flex items-center gap-2">包两个 Button:保留原有「添加新模型」+ 新增{mounted && hasPermission("credential-slot") && <Button variant="outline" onClick={() => setIsCredentialDialogOpen(true)}><KeyRound /> 凭据槽位</Button>}</Tabs>之后、</DashboardShell>之前新增 controlled mode<Dialog open={isCredentialDialogOpen} onOpenChange={setIsCredentialDialogOpen}>,内含DialogTitle「通用凭据槽位」+DialogDescription「对话框真实内容由 Phase 3 落地」(占位,无表单)- Tabs / TabsContent / Card / 卡片内的现有按钮等所有内容(line 18-441)逐字不变
- 文件 line 1 顶部新增
- 修改原因:
- 推进 Milestone v1.0「通用凭据槽位前端集成」第二步:让授权运营立即看到入口(已就位的 UX 收敛),未授权角色彻底看不到(DOM 中完全不存在的安全前置)
- 沿用 RBAC 单一来源原则(
lib/permissions.ts:hasPermission)+ shadcn Dialog primitive,不重复造轮子 - 为 Phase 3 真实表单(CRED-FE-04 + CRED-FE-05)预留 Dialog 挂载点;Dialog 用 controlled mode 让 Phase 3 可在打开瞬间触发
getCredentialSlot() - 注意:前端 RBAC 仅是 UI 礼貌,最终安全闭环依赖后端
/v1/admin/credential-slot/的 admin 鉴权(PERM-06 /qy_lty后端);本 phase 不消化该闭环
- 跨项目联动: 无 — Phase 2 是纯前端 RBAC + UI 入口落地,不引入新跨项目契约;后端 commit
46d72b8已建立的互引仍有效;Phase 3 引入实质 PUT 调用时若涉及新契约再评估 - 服务端联动: 同上「跨项目联动」字段;后端 commit
46d72b8已建立互引闭环,本 phase 无需再次互引
[2026-05-08] Phase 1(前端)凭据槽位 API 客户端
配套服务端 Phase:../qy_lty/.planning/phases/02-admin-rest/(已落地,commit 46d72b8) 覆盖前端需求:CRED-FE-01
- 文件路径:
lib/api/credential-slot.ts(新增)lib/api/index.ts(修改)
- 修改类型: 新增(API 客户端层;纯逻辑,无 UI 改动)
- 修改内容:
- 新建
lib/api/credential-slot.ts,封装:- 类型
CredentialSlot { appId, accessTokenMasked, updatedAt }(脱敏掩码语义命名) - 类型
CredentialSlotUpdatePayload { appId, accessToken }(明文语义命名) - 适配器
mapBackendCredentialSlot()(snake_case → camelCase) - API 函数
getCredentialSlot()/updateCredentialSlot(payload),分别走apiClient.get/apiClient.put命中/v1/admin/credential-slot/,沿用仓库统一的response.data?.data || response.data双保险解包;PUT body 仅传{ app_id, access_token },不携updated_at(与updateAiModel/updateOutfit风格一致)
- 类型
lib/api/index.ts末尾追加具名 re-export,让组件层可import { getCredentialSlot, type CredentialSlot } from '@/lib/api'
- 新建
- 修改原因:
- 启动 Milestone v1.0「通用凭据槽位前端集成」,本 phase 为后续 Phase 2(RBAC + 入口控件)、Phase 3(编辑对话框 + Sonner 反馈)提供调用层基础
accessTokenMaskedvsaccessToken故意命名不同,让 TS 编译期捕捉「把脱敏字符串当真值回写 PUT」的 bug
- 跨项目联动: 无 — 后端 commit
46d72b8已建立互引;Phase 1 是纯 API client 层落地(无 UI 改动),调用的后端接口由 qy_lty 后端 Milestone v1.0 Phase 2 提供(commit46d72b8已建立前后端互引修改记录);本 phase 不引入新跨项目代码契约,无需再次互引。前端 UI 集成(Phase 2 + 3)引入实质用户能力时再评估是否需要新一轮互引 - 服务端联动: 同上「跨项目联动」字段;后端 commit
46d72b8已建立互引闭环,本 phase 无需再次互引
[2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)
配套服务端 Phase:../qy_lty/.planning/phases/02-admin-rest/ 覆盖服务端需求:CRED-03 + CRED-04(本仓库消费方)
- 文件路径:
docs/修改记录.md(仅文档新增条目;本仓库代码未改) - 修改类型: 新增(文档)
- 修改内容:
- 文档化服务端在本日落地的
/api/v1/admin/credential-slot/REST 接口契约(GET 脱敏读取 + PUT 全字段覆写 + admin token 鉴权),为后续 Web 管理后台前端(本仓库)的 CRED-FE-* phase 写 API client 与表单 UI 留下契约锚点 - 接口契约要点(消费方视角):
- URL:
{NEXT_PUBLIC_API_BASE_URL}/v1/admin/credential-slot/ - 鉴权:
Authorization: Bearer <admin_token>(来源于/api/v1/admin/login/的现有 admin 登录返回值) - GET 响应:
{ success: boolean, code: number, message: string, data: { app_id: string, access_token: string /* 末 4 位脱敏掩码,前缀 * */, updated_at: string /* ISO 8601 */ } } - PUT 请求体:
{ app_id?: string, access_token?: string }(任一字段缺省时由后端兜底保留原值;写入是全字段覆写语义,建议前端 UI 始终提交两字段全集以避免歧义) - PUT 响应同 GET 形态(access_token 同样脱敏返回,前端不应用响应值回填明文输入框)
- 错误矩阵:401(无 token / token 失效)、403(持非 admin token,message 含"需要管理员权限")、400(参数无效)
- URL:
- 文档化服务端在本日落地的
- 修改原因:
- 服务端首次为本管理后台暴露受控的凭据读写接口;本仓库即将启动 CRED-FE-01(API client) + CRED-FE-02(表单录入页面)等 phase,先把后端契约固化进本仓库修改记录便于反查
- 文档化"GET 与 PUT 响应均脱敏 access_token"避免前端工程师误以为可以从响应回填明文表单(实际明文仅存于 DB;任何回填只能保留掩码或要求运营重新输入)
- 服务端联动: 后端联动条目 ../qy_lty/docs/修改记录.md 同期
[2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口(GET 脱敏 / PUT 覆写)。本仓库代码未改,仅文档侧做契约固化;待本仓库 CRED-FE-01 phase 启动落地 API client + Hook 时再补一条独立条目并互引
[2026-05-07] 修复 NEXT_PUBLIC_API_BASE_URL 注入时机错误(线上登录 Network Error)
- 文件路径:
qy-lty-admin/Dockerfile.gitea/workflows/deploy.yaml(仓库根目录,与本前端构建/部署链直接相关)k8s/admin-deployment-prod.yaml(仓库根目录,与本前端构建/部署链直接相关)
- 修改类型: 修复Bug
- 修改内容:
qy-lty-admin/Dockerfile:在COPY . .之后、RUN yarn build之前新增ARG NEXT_PUBLIC_API_BASE_URL与ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL},让该值在 build 期可被注入.gitea/workflows/deploy.yaml:在 admin 镜像docker build命令上加--build-arg NEXT_PUBLIC_API_BASE_URL=https://${DOMAIN_API}/api(test 环境拼成https://qy-lty.test.airlabs.art/api,prod 环境拼成https://qy-lty.airlabs.art/api);同时删除Replace domain placeholders by environment段中已失效的sed -i "s|https://qy-lty.airlabs.art|https://${DOMAIN_API}|g" k8s/admin-deployment-prod.yamlk8s/admin-deployment-prod.yaml:删除运行时无效的env: NEXT_PUBLIC_API_BASE_URL=https://qy-lty.airlabs.art,改为注释说明该变量必须在 docker build 时通过--build-arg注入
- 修改原因:
- 线上 https://qy-lty-admin.test.airlabs.art/login 点登录弹 "Network Error"。DevTools 抓到 Request URL 是
http://localhost:8000/api/v1/admin/login/,对应lib/api/client.ts中process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api"的 fallback 值。 - 根因:Next.js 的
NEXT_PUBLIC_*变量在next build时被静态编译进客户端 JS 包,运行时再设置容器环境变量已经无效。原 Dockerfile 没有ARG/ENV,原 deploy.yaml 没有--build-arg,只有k8s/admin-deployment-prod.yaml在容器运行时设了变量——所以打包出的镜像里硬编码的是默认 fallbackhttp://localhost:8000/api,前端 HTTPS 页面去打本机的 8000 端口,浏览器报ERR_CONNECTION_REFUSED,axios 包装为 "Network Error"。 - 修复后构建期会把正确的
https://qy-lty.test.airlabs.art/api/https://qy-lty.airlabs.art/api编译进 JS 包,登录请求会正确打到后端。 - 备注:原
k8s/admin-deployment-prod.yaml写的是https://qy-lty.airlabs.art(缺少/api后缀),即便注入时机正确,路径也会拼错(/v1/admin/login/而非/api/v1/admin/login/),双重 bug。本次修复一并纠正。
- 线上 https://qy-lty-admin.test.airlabs.art/login 点登录弹 "Network Error"。DevTools 抓到 Request URL 是
- 服务端联动: 本次修复仅涉及前端构建链与部署配置,未改动
qy_lty后端代码,无需在服务端写联动条目。
[2026-04-30] 初始化 CLAUDE.md 与 docs/修改记录.md 骨架
- 文件路径:
CLAUDE.md、docs/修改记录.md - 修改类型: 新增
- 修改内容:
- 新建
CLAUDE.md:写明项目身份(Next.js 15 App Router + React 19 后台)、技术栈、路由分组、鉴权与权限模型、与qy_lty后端的依赖关系,并嵌入"项目修改记录规则(重要 — 自动执行)"段落 - 新建本文件
docs/修改记录.md:以qy_lty/docs/修改记录.md同名文件为骨架,划清"仅记录管理后台前端改动"的边界
- 新建
- 修改原因:
- 此前本仓库无 CLAUDE.md 和修改记录文档,新会话进入工作时缺乏上下文锚点;与服务端
qy_lty项目的改动追踪互不可见 - 配套服务端
qy_lty/CLAUDE.md同日新增的"项目修改记录规则"段落,要求两端各自独立维护修改记录、跨项目联动改动相互引用,从源头避免漏记和混记 - 服务端同期条目:qy_lty/docs/修改记录.md 2026-04-30 "CLAUDE.md 新增项目修改记录规则段落"
- 此前本仓库无 CLAUDE.md 和修改记录文档,新会话进入工作时缺乏上下文锚点;与服务端