226 lines
11 KiB
Markdown
Raw Permalink 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 1凭据槽位 API 客户端 - Context
**Gathered**: 2026-05-08
**Status**: Ready for planning
**Source**: 用户在 `/gsd-plan-phase 1` 调用时提供的内联约束PRD 快速通道)
<domain>
## Phase 边界
本 phase 是 Milestone v1.0 前端集成的**起点**,纯逻辑层(无 UI
- 新建 `lib/api/credential-slot.ts`:封装 GET / PUT 两个调用 + 类型 + 后端→前端适配器
-`lib/api/index.ts` 导出新模块
- `tsc --noEmit` 通过(项目用 `npm run lint` 跑)
**不负责**(留给后续 phase
- Phase 2RBAC 模块声明(`lib/permissions.ts``credential-slot` key+ `/ai-model` 页面入口控件
- Phase 3编辑对话框组件 + Sonner toast 反馈
**联动**
- 消费 qy_lty 后端 Milestone v1.0 已锁定的 API 契约Phase 2 落地commit `46d72b8` 起前后端互引修改记录已建立)
- 当前 phase 是纯 API client 层;执行测试可走 mock**不需要**后端 dev server 真实运行
</domain>
<decisions>
## 实现决策(锁定)
### 接口契约(从后端 milestone v1.0 镜像,**不变**
**GET `/api/v1/admin/credential-slot/`** — admin token 鉴权
- 响应壳层(经过 `StandardResponseMiddleware``{ success, code, message, data }`
- `data` 字段:`{ app_id: string, access_token: string, updated_at: string }`
- 关键:**`access_token` 在响应中已是脱敏掩码**(末 4 位明文,前面 `*`
**PUT `/api/v1/admin/credential-slot/`** — admin token 鉴权
- 请求体:`{ app_id: string, access_token: string }` —— 全字段覆写,**明文**提交
- 响应壳层与 GET 同;`data.access_token` 也是脱敏后返回(不会回显运营提交的明文)
**错误响应**(同样标准壳层):
- 401无 Authorization 头 / 无效 token
- 403携普通 user token非 admin
### 文件路径与命名
- **新建**`lib/api/credential-slot.ts`(单文件,沿用其他 lib/api/ 模块的扁平结构)
- **修改**`lib/api/index.ts`(增加导出)
- **不动**`lib/api/client.ts`(拦截器已就位)、`lib/api/adapters.ts`mapBackend* 约定参考)、`lib/api/types.ts`(共享类型集合)
### 类型定义
**前端类型camelCase导出供页面/组件 import**
```typescript
// 后端响应中的"凭据槽位"access_token 已是脱敏掩码
export interface CredentialSlot {
appId: string
accessTokenMasked: string // 后端返回的脱敏字符串,前端命名上明确"已脱敏"
updatedAt: string // ISO 8601 时间字符串
}
// PUT 请求体access_token 是明文(运营录入态)
export interface CredentialSlotUpdatePayload {
appId: string
accessToken: string // 明文,提交后后端覆写
}
// 后端响应原始 dictsnake_case仅 adapter 内部用)
interface BackendCredentialSlot {
app_id: string
access_token: string
updated_at: string
}
```
**关键决策**
- 前端类型用 `accessTokenMasked` 名字(不是 `accessToken`),明确语义"这是脱敏后的字符串",让后续 phase 写对话框时不会误把它当真值回写 PUT
- 提交载荷类型用 `accessToken`(无 Masked 后缀),明确"这是明文,将覆写后端"
- 这两个类型故意命名不同,让 TypeScript 编译期就能捕捉"把脱敏字符串当真值回写"的 bug
### 适配器(沿用 mapBackend* 约定)
**后端→前端**GET 响应反向):
```typescript
function mapBackendCredentialSlot(raw: BackendCredentialSlot): CredentialSlot {
return {
appId: raw.app_id,
accessTokenMasked: raw.access_token,
updatedAt: raw.updated_at,
}
}
```
**前端→后端**PUT 请求体正向):
```typescript
function toBackendUpdatePayload(payload: CredentialSlotUpdatePayload): { app_id: string; access_token: string } {
return {
app_id: payload.appId,
access_token: payload.accessToken,
// 不带 updated_at —— 仓库现有约定researcher 实测 ai-models.ts/outfits.ts 全部仅传业务字段)
// 后端 auto_now=True 自动维护
}
}
```
### API 函数签名
**关键修正researcher 实测)**
- `apiClient` 响应拦截器**不解包**(仅 `console.log` 后透传 response
- 仓库现有模块(`ai-models.ts` / `outfits.ts` 等)统一约定:调用方手写 `const data = response.data?.data || response.data` 兼容"标准壳层"与"裸响应"两种形态
- 路径**不含 `/api` 前缀**`API_BASE_URL` 已吃掉 `/api`),写 `/v1/admin/credential-slot/`
```typescript
export async function getCredentialSlot(): Promise<CredentialSlot> {
const response = await apiClient.get('/v1/admin/credential-slot/')
const raw = response.data?.data || response.data // 兼容标准壳层与裸响应
return mapBackendCredentialSlot(raw)
}
export async function updateCredentialSlot(payload: CredentialSlotUpdatePayload): Promise<CredentialSlot> {
const body = toBackendUpdatePayload(payload)
const response = await apiClient.put('/v1/admin/credential-slot/', body)
const raw = response.data?.data || response.data
return mapBackendCredentialSlot(raw)
}
```
**1:1 模板**`lib/api/ai-models.ts` L1-85`getAiModel(id)` 单资源 GET 形态最贴近、L65-73`updateAiModel` PATCH body 构造,仅传业务字段不带 `updated_at`。Planner 应把这两段作为照抄起点。
### `lib/api/index.ts` 导出
追加:
```typescript
export {
getCredentialSlot,
updateCredentialSlot,
type CredentialSlot,
type CredentialSlotUpdatePayload,
} from './credential-slot'
```
`index.ts` 不存在或导出风格不同按仓库现有约定researcher 必须看 `lib/api/index.ts` 当前内容)。
### Lint + 类型检查researcher 修正:必须两条独立命令)
- `npm run lint` 实际**只跑 `next lint`ESLint****不跑 tsc**researcher 实测)
- `next.config.mjs``typescript.ignoreBuildErrors: true` 仅影响 `next build`**不**影响独立 `tsc`
- Phase 1 完成态需**两条**独立命令都退出码 0
- `npm run lint` —— ESLint 检查
- `npx tsc --noEmit` —— TypeScript 类型检查(独立运行,**不**省略)
### 修改记录
`qy-lty-admin/docs/修改记录.md` 顶部追加一条 Phase 1 条目:
- 文件路径:`lib/api/credential-slot.ts``lib/api/index.ts`
- 修改类型:新增
- 跨项目联动:「无 — Phase 1 是纯 API client 层落地,调用的后端接口由 qy_lty 后端 Milestone v1.0 Phase 2 提供commit `46d72b8` 已建立前后端互引Phase 1 不引入新代码契约,无需再次互引。前端 UI 集成Phase 2 + 3完成前不会真正调到后端。」
### Claude's Discretion
- PUT body 是否携带 `updated_at` 字段 — 看 axios 拦截器是否对 PUT body 做过滤
- TypeScript 类型放在 `lib/api/credential-slot.ts` 内部还是抽到 `lib/api/types.ts` — 看现有约定
- 是否给 `getCredentialSlot()` / `updateCredentialSlot()` 加 JSDoc 注释(中文)— 推荐加
</decisions>
<canonical_refs>
## Canonical References
**下游 agent 必读**
### 项目宪法
- `qy-lty-admin/CLAUDE.md` — 沟通语言(中文)+ 修改记录强制规则 + 跨项目联动 + 包管理器警告(不混用 npm/pnpm/yarn
- `qy-lty-admin/.planning/PROJECT.md` — Milestone v1.0「本期 Milestone」段、关键约束特别是「留空保留旧值」语义会影响 Phase 3但 Phase 1 仅 API client 层不涉及)
- `qy-lty-admin/.planning/REQUIREMENTS.md` — Active 段 CRED-FE-01 完整描述
- `qy-lty-admin/.planning/ROADMAP.md` — Phase 1 详情段Goal、Success Criteria 4 条)
### lib/api/ 现有模块必读1:1 模板候选)
- `qy-lty-admin/lib/api/client.ts` — Axios 实例 + 请求/响应拦截器(**关键**:必须确认 response 拦截器是否已经解包 `data.data`
- `qy-lty-admin/lib/api/types.ts` — 共享类型集合(`ApiResponse<T>` 形态)
- `qy-lty-admin/lib/api/adapters.ts``mapBackend*` 工具函数集合
- `qy-lty-admin/lib/api/outfits.ts` — 业务模块的 CRUD 范式(候选模板)
- `qy-lty-admin/lib/api/ai-models.ts` — AI 模型管理 API与本 phase 语义最贴近Phase 2 入口控件就在 `/ai-model` 页面)
- `qy-lty-admin/lib/api/songs.ts` — 另一个 CRUD 模块参考
- `qy-lty-admin/lib/api/index.ts` — 模块导出汇总点
### 后端契约
- `../qy_lty/aiapp/views.py``CredentialSlotAdminView`GET 响应实际形状 + PUT 请求体格式)
- `../qy_lty/aiapp/serializers.py``CredentialSlotSerializer`(字段集 / read_only_fields
- `../qy_lty/.planning/phases/02-admin-rest/02-VERIFICATION.md` — 后端 Phase 2 端到端验收记录(含真实响应样本)
### 修改记录
- `qy-lty-admin/docs/修改记录.md` — 头部「修改格式说明」+ 已存在的 Phase 2 互引条目作模板
</canonical_refs>
<specifics>
## 具体要点Success Criteria 显式化)
| # | 验证点 | 检查方式 |
|---|--------|----------|
| 1 | `lib/api/credential-slot.ts` 导出 2 函数 + 2 类型 | `grep "export.*getCredentialSlot\|updateCredentialSlot\|CredentialSlot\|CredentialSlotUpdatePayload" lib/api/credential-slot.ts` 命中 ≥4 次 |
| 2 | 两函数走 `apiClient` + 路径 `/v1/admin/credential-slot/` | grep `apiClient.get.*/v1/admin/credential-slot/` + `apiClient.put.*/v1/admin/credential-slot/` 各 1 命中 |
| 3 | adapter `mapBackendCredentialSlot` 把 snake → camel | 单元测试式 import 调用:`mapBackendCredentialSlot({app_id:'a', access_token:'b', updated_at:'c'})` 返回 `{appId:'a', accessTokenMasked:'b', updatedAt:'c'}` |
| 4 | `lib/api/index.ts` 导出新模块 | grep `export.*credential-slot` 命中 |
| 5 | `npm run lint` / `tsc --noEmit` 退出码 0 | shell 退出码检查(注:项目 next.config.mjs 有 `typescript.ignoreBuildErrors: true`**这是仅构建期忽略**`tsc --noEmit` 仍会真实报错) |
| 6 | 类型契约:从 component 文件 import 应能解析 | 写一个临时 tsx 文件 import 这些符号,跑 tsc --noEmit 不报错 |
| 7 | 修改记录顶部追加 Phase 1 条目 | grep `qy-lty-admin/docs/修改记录.md` 顶部出现 `[2026-05-08] Phase 1 (前端) 凭据槽位 API 客户端` 字样 |
</specifics>
<deferred>
## 推迟事项(不在 Phase 1 范围)
- **`/ai-model` 页面入口控件 / RBAC 模块声明** — Phase 2
- **编辑对话框组件 + Sonner toast 反馈** — Phase 3
- **跨项目互引修改记录** — 后端 Phase 2 已建立互引commit `46d72b8`),前端 Phase 1 不需要再次互引Phase 2 + 3 引入实质 UI 集成时再考虑(如确实需要)
- **Cypress / Playwright E2E 测试** — 项目当前无 E2E 框架Phase 1 只验证 tsc + adapter 单元行为
- **真实后端 dev server 联调** — Phase 1 完成不要求Phase 2/3 集成后端到端再做
</deferred>
---
*Phase: 01-credential-slot-api*
*Context gathered: 2026-05-08 via inline PRD用户在 /gsd-plan-phase 1 调用时提供完整约束)*