docs(01): qy-lty-admin Phase 1 CONTEXT.md(API 客户端 PRD 快速通道)
This commit is contained in:
parent
9965d0bcf0
commit
9aa29877e9
@ -0,0 +1,222 @@
|
||||
# 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 2:RBAC 模块声明(`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 // 明文,提交后后端覆写
|
||||
}
|
||||
|
||||
// 后端响应原始 dict(snake_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): BackendCredentialSlot {
|
||||
return {
|
||||
app_id: payload.appId,
|
||||
access_token: payload.accessToken,
|
||||
updated_at: '', // 后端 auto_now=True 自动维护,前端传空字符串占位即可
|
||||
}
|
||||
// 备选:直接 { app_id, access_token } 不带 updated_at;planner 在 read_first 阶段确认现有 mapBackend* 约定
|
||||
}
|
||||
```
|
||||
|
||||
**Planner 决定**:是否在 PUT 适配器中携带 `updated_at: ''`,还是只传两个真实字段(让 axios 自动序列化为 JSON)。仓库现有 lib/api/*.ts 哪种风格更主流,照抄。
|
||||
|
||||
### API 函数签名
|
||||
|
||||
**注意**:此处**不**使用 `apiClient.get('/v1/admin/credential-slot/')` 的相对路径硬编码 —— 沿用 `lib/api/` 现有模块的写法(researcher 必须 read_first 看 outfits.ts / songs.ts 等已有模块的路径风格):
|
||||
|
||||
```typescript
|
||||
export async function getCredentialSlot(): Promise<CredentialSlot> {
|
||||
const response = await apiClient.get<ApiResponse<BackendCredentialSlot>>('/v1/admin/credential-slot/')
|
||||
// 拦截器已解包 success/code/message/data,到这里 response.data 就是 BackendCredentialSlot
|
||||
// 但 axios 默认行为是把 HTTP body 整体放到 response.data —— 必须 read_first 确认 client.ts 的拦截器是否已经做了 .data 提取
|
||||
// 若拦截器已提取,则这里 response.data 就是 BackendCredentialSlot;否则需要 response.data.data
|
||||
return mapBackendCredentialSlot(response.data) // 或 response.data.data,依拦截器行为而定
|
||||
}
|
||||
|
||||
export async function updateCredentialSlot(payload: CredentialSlotUpdatePayload): Promise<CredentialSlot> {
|
||||
const body = toBackendUpdatePayload(payload)
|
||||
const response = await apiClient.put<ApiResponse<BackendCredentialSlot>>('/v1/admin/credential-slot/', body)
|
||||
return mapBackendCredentialSlot(response.data)
|
||||
}
|
||||
```
|
||||
|
||||
**Planner 必须在 read_first 阶段确认 `apiClient` 拦截器的具体行为**(特别是 `client.ts` 的 response interceptor 是否已经把 `data.data` 提取出来),然后给出与现有模块 100% 一致的写法。
|
||||
|
||||
### `lib/api/index.ts` 导出
|
||||
|
||||
追加:
|
||||
```typescript
|
||||
export {
|
||||
getCredentialSlot,
|
||||
updateCredentialSlot,
|
||||
type CredentialSlot,
|
||||
type CredentialSlotUpdatePayload,
|
||||
} from './credential-slot'
|
||||
```
|
||||
|
||||
如 `index.ts` 不存在或导出风格不同,按仓库现有约定(researcher 必须看 `lib/api/index.ts` 当前内容)。
|
||||
|
||||
### `tsc --noEmit` 通过
|
||||
|
||||
- 项目脚本是 `npm run lint`(实际跑 `next lint` + 某种 tsc 检查;planner 验证 `package.json` 的 `lint` script 实际行为)
|
||||
- Phase 1 完成态:`npm run lint` 退出码 0,无 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 调用时提供完整约束)*
|
||||
Loading…
x
Reference in New Issue
Block a user