# 管理后台前端代码修改记录

本文档记录每次对管理后台前端（**qy-lty-admin**，Next.js + React）代码的修改，方便追踪变更历史。

> **范围说明**：本仓库与服务端 [qy_lty](../../qy_lty/) 是独立项目，各自维护独立的修改记录。**仅记录本仓库（管理后台前端）改动**；跨项目联动改动需在 [qy_lty/docs/修改记录.md](../../qy_lty/docs/修改记录.md) 同期写一条相互引用的条目。

---

## 修改格式说明

每次修改按以下格式记录：

```
### [日期] 修改简述

- **文件路径**: 相对于项目根目录的文件路径
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug
- **修改内容**: 具体修改了什么
- **修改原因**: 为什么要做这个修改
```

---

## 修改历史

<!-- 新的修改记录添加在此处下方，最新的在最前面 -->

### [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`：
    - `PermissionModule` union 末尾追加 `"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.tsx` mounted 守卫模式避免 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）逐字不变
- **修改原因**:
  - 推进 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/](../../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 反馈）提供调用层基础
  - `accessTokenMasked` vs `accessToken` 故意命名不同，让 TS 编译期捕捉「把脱敏字符串当真值回写 PUT」的 bug
- **跨项目联动**: 无 — 后端 commit 46d72b8 已建立互引；Phase 1 是纯 API client 层落地（无 UI 改动），调用的后端接口由 qy_lty 后端 Milestone v1.0 Phase 2 提供（commit `46d72b8` 已建立前后端互引修改记录）；本 phase 不引入新跨项目代码契约，无需再次互引。前端 UI 集成（Phase 2 + 3）引入实质用户能力时再评估是否需要新一轮互引
- **服务端联动**: 同上「跨项目联动」字段；后端 commit `46d72b8` 已建立互引闭环，本 phase 无需再次互引

### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约（消费方文档化）

配套服务端 Phase：[../qy_lty/.planning/phases/02-admin-rest/](../../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（参数无效）
- **修改原因**:
  - 服务端首次为本管理后台暴露受控的凭据读写接口；本仓库即将启动 CRED-FE-01（API client） + CRED-FE-02（表单录入页面）等 phase，先把后端契约固化进本仓库修改记录便于反查
  - 文档化"GET 与 PUT 响应均脱敏 access_token"避免前端工程师误以为可以从响应回填明文表单（实际明文仅存于 DB；任何回填只能保留掩码或要求运营重新输入）
- **服务端联动**: 后端联动条目 [../qy_lty/docs/修改记录.md](../../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.yaml`
  - `k8s/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` 在容器运行时设了变量——所以打包出的镜像里硬编码的是默认 fallback `http://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。本次修复一并纠正。
- **服务端联动**: 本次修复仅涉及前端构建链与部署配置，未改动 `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](../../qy_lty/docs/修改记录.md) 2026-04-30 "CLAUDE.md 新增项目修改记录规则段落"
