11 KiB
Phase 1:凭据槽位 API 客户端 - Context
Gathered: 2026-05-08
Status: Ready for planning
Source: 用户在 /gsd-plan-phase 1 调用时提供的内联约束(PRD 快速通道)
本 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-slotkey)+/ai-model页面入口控件 - Phase 3:编辑对话框组件 + Sonner toast 反馈
联动:
- 消费 qy_lty 后端 Milestone v1.0 已锁定的 API 契约(Phase 2 落地,commit
46d72b8起前后端互引修改记录已建立) - 当前 phase 是纯 API client 层;执行测试可走 mock,不需要后端 dev server 真实运行
接口契约(从后端 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):
// 后端响应中的"凭据槽位",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 响应反向):
function mapBackendCredentialSlot(raw: BackendCredentialSlot): CredentialSlot {
return {
appId: raw.app_id,
accessTokenMasked: raw.access_token,
updatedAt: raw.updated_at,
}
}
前端→后端(PUT 请求体正向):
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/
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 导出
追加:
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 注释(中文)— 推荐加
<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>
## 具体要点(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 客户端 字样 |
/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 集成后端到端再做
Phase: 01-credential-slot-api Context gathered: 2026-05-08 via inline PRD(用户在 /gsd-plan-phase 1 调用时提供完整约束)