19 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-credential-slot-api | 01 | execute | 1 |
|
true |
|
|
Purpose:为 Milestone v1.0「通用凭据槽位前端集成」搭建纯逻辑层调用基础。本 plan 是 Phase 1 的代码落地,无 UI 依赖;TS 类型层面通过 accessTokenMasked vs accessToken 命名差异在编译期切断「把脱敏字符串当真值回写 PUT」这条 bug 路径。
Output:lib/api/credential-slot.ts(新建)+ lib/api/index.ts(末尾追加 7 行)。
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/REQUIREMENTS.md @.planning/phases/01-credential-slot-api/01-CONTEXT.md @.planning/phases/01-credential-slot-api/01-RESEARCH.md@CLAUDE.md @lib/api/client.ts @lib/api/ai-models.ts @lib/api/index.ts
From lib/api/client.ts (已存在,本 plan 不修改):
// L9:baseURL 已包含 /api,业务路径不要重复写 /api
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api"
// L12-17:单例 axios,请求拦截器自动注入 Authorization: Bearer,响应拦截器只透传不解包
export const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json' } })
// L91-96:标准响应壳层(仅类型,本 plan 不需 import)
export interface ApiResponse<T> { success: boolean; code: number; message: string; data: T }
From lib/api/ai-models.ts (1:1 模板,本 plan 完全照抄结构):
// L1-3:import 风格
import type { AiModel } from "./types"
import { apiClient } from "./client"
// L7-20:mapBackend* 函数风格(snake -> camel + 字段默认值)
function mapBackendBot(b: any): AiModel { return { id: String(b.id), name: b.name, /* ... */ } }
// L46-50:单资源 GET 形态(与本 plan getCredentialSlot 完全一致)
export const getAiModel = async (id: string): Promise<AiModel> => {
const response = await apiClient.get(`/ai/bots/${id}/`)
const data = response.data?.data || response.data // ← 双保险解包
return mapBackendBot(data)
}
// L65-73:写入 body 仅含业务字段,不带 updated_at
export const updateAiModel = async (id: string, modelData: Partial<AiModel>): Promise<AiModel> => {
const payload: any = {}
if (modelData.name !== undefined) payload.name = modelData.name
// 注意:updated_at 不在 payload 中
const response = await apiClient.patch(`/ai/bots/${id}/`, payload)
const data = response.data?.data || response.data
return mapBackendBot(data)
}
From lib/api/index.ts (本 plan 在末尾追加导出):
// L1-6:当前文件头部 — 现有 export * 风格,与新增的具名 re-export 在同文件混用合法
import * as client from "./client"
export * from "./card"
export * from "./upload"
export * from "./food"
// L190-196:当前文件末尾 — 在 handleApiError 之前或之后追加新模块导出(推荐文件末尾)
export const handleApiError = (error: any) => { /* ... */ }
Task 1:新建 lib/api/credential-slot.ts(类型 + 适配器 + GET/PUT)
<read_first>
- lib/api/ai-models.ts(L1-50 + L65-73,1:1 模板)— 确认 import 风格、mapBackend* 函数形态、response.data?.data || response.data 双保险解包、PUT body 不带 updated_at
- lib/api/client.ts L9-17 — 确认 apiClient 由该文件导出,baseURL 已含 /api,业务路径不要重复写
- .planning/phases/01-credential-slot-api/01-CONTEXT.md 节 — 锁定的类型命名(accessTokenMasked 故意带 Masked 后缀;accessToken 用于明文提交载荷)
- .planning/phases/01-credential-slot-api/01-RESEARCH.md 「Code Examples」节 — 完整可粘贴骨架
</read_first>
lib/api/credential-slot.ts
在 `lib/api/credential-slot.ts` 创建以下**完整**文件内容(直接写入,不要省略任何行;中文 JSDoc 必须保留):import { apiClient } from "./client"
// ───── 后端响应原始 dict(snake_case,仅 adapter 内部用,不导出)────────────
interface BackendCredentialSlot {
app_id: string
access_token: string
updated_at: string
}
// ───── 前端响应类型(camelCase,导出给 UI 层 import)──────────────────────
/**
* 凭据槽位(后端响应)。
* 注意:access_token 已是脱敏掩码(末 4 位明文),不要把它当明文回写。
*/
export interface CredentialSlot {
appId: string
accessTokenMasked: string // 后端返回的脱敏字符串
updatedAt: string // ISO 8601
}
// ───── 提交载荷类型(camelCase 明文)──────────────────────────────────────
/**
* 凭据槽位更新载荷。
* 注意:accessToken 是明文,提交后将完整覆写后端记录。
*/
export interface CredentialSlotUpdatePayload {
appId: string
accessToken: string // 明文
}
// ───── adapter(后端 → 前端)─────────────────────────────────────────────
function mapBackendCredentialSlot(raw: BackendCredentialSlot): CredentialSlot {
return {
appId: raw.app_id,
accessTokenMasked: raw.access_token,
updatedAt: raw.updated_at,
}
}
// ───── API 函数 ──────────────────────────────────────────────────────────
/**
* 读取当前凭据槽位(access_token 字段为脱敏掩码)。
*/
export const getCredentialSlot = async (): Promise<CredentialSlot> => {
const response = await apiClient.get('/v1/admin/credential-slot/')
const data = response.data?.data || response.data // 仓库统一双保险解包
return mapBackendCredentialSlot(data)
}
/**
* 全字段覆写凭据槽位(access_token 必须为明文;响应里返回的同样是脱敏掩码)。
*/
export const updateCredentialSlot = async (
payload: CredentialSlotUpdatePayload
): Promise<CredentialSlot> => {
const body = {
app_id: payload.appId,
access_token: payload.accessToken,
// 不带 updated_at —— 后端 auto_now 维护,与 updateOutfit / updateAiModel 风格一致
}
const response = await apiClient.put('/v1/admin/credential-slot/', body)
const data = response.data?.data || response.data
return mapBackendCredentialSlot(data)
}
关键约束(不可偏离,均来自 RESEARCH.md 实证):
- 路径必须写
/v1/admin/credential-slot/,不要写/api/v1/admin/credential-slot/(baseURL 已含/api,重复会变/api/api/v1/...导致 404) - 解包行必须写
const data = response.data?.data || response.data(per RESEARCH Pitfall 1,拦截器不自动解包;不能直接mapBackendCredentialSlot(response.data)) - PUT body 不带
updated_at(per RESEARCH 问题 5,与现有updateAiModel/updateOutfit一致) BackendCredentialSlot是内部接口(无export),仅 adapter 入参类型用;不要把它 exportmapBackendCredentialSlot是模块级私有函数(无export),与现有mapBackendBot/mapBackendOutfit约定一致- 函数风格选
export const fn = async () => {}(与ai-models.ts一致;不要写export async function) - 不要 import
lib/api/types.ts(业务专属类型在本文件内定义,per RESEARCH 问题 4) - 不要修改
lib/api/types.ts/lib/api/adapters.ts/lib/api/client.ts(CONTEXT.md 锁定)
<acceptance_criteria>
- 文件
lib/api/credential-slot.ts存在且非空 grep -E "^import \{ apiClient \} from \"\./client\"" lib/api/credential-slot.ts命中 1 次grep -E "^export interface CredentialSlot[^U]" lib/api/credential-slot.ts命中 1 次(公共响应类型,名字后不接 U,避免误命中CredentialSlotUpdatePayload)grep -E "^export interface CredentialSlotUpdatePayload" lib/api/credential-slot.ts命中 1 次grep -E "^export const getCredentialSlot = async" lib/api/credential-slot.ts命中 1 次grep -E "^export const updateCredentialSlot = async" lib/api/credential-slot.ts命中 1 次grep -E "function mapBackendCredentialSlot" lib/api/credential-slot.ts命中 1 次grep -E "interface BackendCredentialSlot" lib/api/credential-slot.ts命中 1 次(不带 export)grep -F "apiClient.get('/v1/admin/credential-slot/')" lib/api/credential-slot.ts命中 1 次grep -F "apiClient.put('/v1/admin/credential-slot/'" lib/api/credential-slot.ts命中 1 次grep -F "response.data?.data || response.data" lib/api/credential-slot.ts命中 2 次(GET / PUT 各一次)grep -E "/api/v1/admin/credential-slot" lib/api/credential-slot.ts不命中(确保路径没多写/api)grep -E "updated_at" lib/api/credential-slot.ts仅命中BackendCredentialSlot.updated_at与mapBackendCredentialSlot内raw.updated_at与一行注释,不出现在 PUT body 字面量里 </acceptance_criteria>
<read_first>
- lib/api/index.ts(全文 197 行)— 确认当前导出风格混合(export * + usersApi / rolesApi 对象 + handleApiError 函数),定位末尾追加位置
- .planning/phases/01-credential-slot-api/01-CONTEXT.md 节「lib/api/index.ts 导出」段(L130-141)— 锁定的具名 re-export 写法
- .planning/phases/01-credential-slot-api/01-RESEARCH.md 「6. lib/api/index.ts 导出风格」节 — 解释为何用具名 re-export 而非 export *
</read_first>
lib/api/index.ts
在 `lib/api/index.ts` **文件末尾**(最后一行 `}` 之后,紧贴 `handleApiError` 定义的下方)追加以下 7 行(不要修改文件中任何已有内容;不要插在文件中部):
// 凭据槽位(Milestone v1.0 通用凭据槽位前端集成 — Phase 1 / CRED-FE-01)
export {
getCredentialSlot,
updateCredentialSlot,
type CredentialSlot,
type CredentialSlotUpdatePayload,
} from './credential-slot'
关键约束:
- 必须用具名 re-export(
export { fn1, fn2, type T } from './module'),不要用export * from './credential-slot'(per RESEARCH 问题 6:具名导出更可控、可读性最高) - 必须包含
type CredentialSlot与type CredentialSlotUpdatePayload两个类型导出(前置type关键字使其在 isolatedModules 模式下安全) - 追加位置选文件末尾(
handleApiError之后),不要插在usersApi/rolesApi之间 - 该模块导入路径用
'./credential-slot'(相对路径,与现有export * from './card'一致),不要写'@/lib/api/credential-slot' - 不要触碰文件中其他任何行(包括
import * as client、export * from './card'、usersApi、rolesApi、handleApiError)
追加之前最后一行示意(确认 anchor):
// 导出错误处理函数
export const handleApiError = (error: any) => {
if (error instanceof Error) {
return error.message
}
return "发生未知错误,请重试"
}
// ← 在这行 } 之后追加上述 7 行
<acceptance_criteria>
grep -F "from './credential-slot'" lib/api/index.ts命中 1 次grep -F "getCredentialSlot," lib/api/index.ts命中 1 次grep -F "updateCredentialSlot," lib/api/index.ts命中 1 次grep -F "type CredentialSlot," lib/api/index.ts命中 1 次grep -F "type CredentialSlotUpdatePayload," lib/api/index.ts命中 1 次grep -F "export * from './credential-slot'" lib/api/index.ts不命中(确保用具名而非 barrel)- 文件原有 197 行业务代码(usersApi / rolesApi / handleApiError / 顶部 4 行 export * /import)保留不变
- 追加后整个文件经
node -e "require('fs').readFileSync(...)"不报错(合法 UTF-8) </acceptance_criteria>
唯一例外:每个 task 的 verify.automated 已嵌入文本级正则检查,确保关键串(路径、解包行、类型名、re-export 路径)就位。
<success_criteria>
lib/api/credential-slot.ts文件存在- 文件导出
CredentialSlot类型 +CredentialSlotUpdatePayload类型 +getCredentialSlot函数 +updateCredentialSlot函数共 4 个公共符号 mapBackendCredentialSlot函数已定义(私有,未导出)- GET / PUT 路径精确为
/v1/admin/credential-slot/(不含重复/api) - GET 与 PUT 函数体内各含 1 次
response.data?.data || response.data双保险解包 - PUT body 字面量不含
updated_at lib/api/index.ts末尾通过具名 re-export 暴露 4 个符号,路径./credential-slotlib/api/index.ts中现有export * from "./card" / "./upload" / "./food"与usersApi/rolesApi/handleApiError完全不变- 两个文件均为合法 UTF-8(无 BOM 干扰、无残缺字符) </success_criteria>