11 KiB
Raw Blame History

Phase 1凭据槽位 API 客户端 - Context

Gathered: 2026-05-08 Status: Ready for planning Source: 用户在 /gsd-plan-phase 1 调用时提供的内联约束PRD 快速通道)

## 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.tscredential-slot key+ /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.tsmapBackend* 约定参考)、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  // 明文,提交后后端覆写
}

// 后端响应原始 dictsnake_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-85getAiModel(id) 单资源 GET 形态最贴近、L65-73updateAiModel 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 lintESLint不跑 tscresearcher 实测)
  • next.config.mjstypescript.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.tslib/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.tsmapBackend* 工具函数集合
  • 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.pyCredentialSlotAdminViewGET 响应实际形状 + PUT 请求体格式)
  • ../qy_lty/aiapp/serializers.pyCredentialSlotSerializer(字段集 / 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 客户端 字样
## 推迟事项(不在 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 集成后端到端再做

Phase: 01-credential-slot-api Context gathered: 2026-05-08 via inline PRD用户在 /gsd-plan-phase 1 调用时提供完整约束)