19 KiB
Raw Permalink Blame History

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
lib/api/credential-slot.ts
lib/api/index.ts
true
CRED-FE-01
truths artifacts key_links
新文件 lib/api/credential-slot.ts 存在并导出两个 API 函数 + 两个公开类型
getCredentialSlot() 走 apiClient.get 命中 /v1/admin/credential-slot/(无 /api 前缀)
updateCredentialSlot() 走 apiClient.put 命中 /v1/admin/credential-slot/body 仅包含 { app_id, access_token } 不含 updated_at
mapBackendCredentialSlot 把 { app_id, access_token, updated_at } 映射为 { appId, accessTokenMasked, updatedAt }
lib/api/index.ts 末尾通过具名 re-export 导出新模块的 2 函数 + 2 类型
path provides exports contains
lib/api/credential-slot.ts 凭据槽位 API client 模块(类型 + 适配器 + GET/PUT 函数)
getCredentialSlot
updateCredentialSlot
type CredentialSlot
type CredentialSlotUpdatePayload
interface BackendCredentialSlot
function mapBackendCredentialSlot
apiClient.get('/v1/admin/credential-slot/'
apiClient.put('/v1/admin/credential-slot/'
response.data?.data || response.data
path provides contains
lib/api/index.ts barrel re-export 入口,新增凭据槽位模块导出
from './credential-slot'
getCredentialSlot
updateCredentialSlot
type CredentialSlot
type CredentialSlotUpdatePayload
from to via pattern
lib/api/credential-slot.ts lib/api/client.ts import { apiClient } from './client' import.*apiClient.*from.*["']./client["']
from to via pattern
lib/api/index.ts lib/api/credential-slot.ts 具名 re-export from\s+['"]./credential-slot['"]
from to via pattern
getCredentialSlot / updateCredentialSlot mapBackendCredentialSlot 返回值前先经 adapter 转换 snake -> camel return\s+mapBackendCredentialSlot(
新建 `lib/api/credential-slot.ts`,按 1:1 模板(`lib/api/ai-models.ts`)封装凭据槽位 GET / PUT 两个调用 + camelCase 类型 + `mapBackendCredentialSlot` 适配器;并在 `lib/api/index.ts` 末尾追加具名 re-export让 UI 层Phase 2/3可以 `import { getCredentialSlot, type CredentialSlot } from '@/lib/api'`。

Purpose为 Milestone v1.0「通用凭据槽位前端集成」搭建纯逻辑层调用基础。本 plan 是 Phase 1 的代码落地,无 UI 依赖TS 类型层面通过 accessTokenMasked vs accessToken 命名差异在编译期切断「把脱敏字符串当真值回写 PUT」这条 bug 路径。

Outputlib/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 不修改)

// L9baseURL 已包含 /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-3import 风格
import type { AiModel } from "./types"
import { apiClient } from "./client"

// L7-20mapBackend* 函数风格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.tsL1-50 + L65-731: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"

// ───── 后端响应原始 dictsnake_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 实证)

  1. 路径必须/v1/admin/credential-slot/不要/api/v1/admin/credential-slot/baseURL 已含 /api,重复会变 /api/api/v1/... 导致 404
  2. 解包行必须写 const data = response.data?.data || response.dataper RESEARCH Pitfall 1拦截器自动解包;不能直接 mapBackendCredentialSlot(response.data)
  3. PUT body 不带 updated_atper RESEARCH 问题 5与现有 updateAiModel / updateOutfit 一致)
  4. BackendCredentialSlot内部接口(无 export),仅 adapter 入参类型用;不要把它 export
  5. mapBackendCredentialSlot模块级私有函数(无 export),与现有 mapBackendBot / mapBackendOutfit 约定一致
  6. 函数风格选 export const fn = async () => {}(与 ai-models.ts 一致;不要写 export async function
  7. 不要 import lib/api/types.ts业务专属类型在本文件内定义per RESEARCH 问题 4
  8. 不要修改 lib/api/types.ts / lib/api/adapters.ts / lib/api/client.tsCONTEXT.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_atmapBackendCredentialSlotraw.updated_at 与一行注释,出现在 PUT body 字面量里 </acceptance_criteria>
node -e "const c = require('fs').readFileSync('lib/api/credential-slot.ts','utf8'); const checks = [/^import \{ apiClient \} from \"\.\/client\"/m, /^export interface CredentialSlot\b/m, /^export interface CredentialSlotUpdatePayload\b/m, /^export const getCredentialSlot = async/m, /^export const updateCredentialSlot = async/m, /function mapBackendCredentialSlot/, /interface BackendCredentialSlot/, /apiClient\.get\('\/v1\/admin\/credential-slot\/'\)/, /apiClient\.put\('\/v1\/admin\/credential-slot\/'/]; const fails = checks.filter(r => !r.test(c)); if (fails.length) { console.error('FAIL:', fails.map(r=>r.toString()).join('\n')); process.exit(1); } if (/\/api\/v1\/admin\/credential-slot/.test(c)) { console.error('FAIL: 路径错误,含 /api 前缀'); process.exit(1); } const occCount = (c.match(/response\.data\?\.data \|\| response\.data/g) || []).length; if (occCount !== 2) { console.error('FAIL: 双保险解包行命中次数 =', occCount, '应为 2'); process.exit(1); } console.log('OK'); " - `lib/api/credential-slot.ts` 文件存在 - 上述 acceptance_criteria 中所有 grep 检查全部满足 - `verify.automated` 命令退出码为 0 且打印 `OK` - 文件不含 `/api/v1/admin/credential-slot` 这种重复 `/api` 前缀 - PUT body 字面量内不含 `updated_at` 键 Task 2在 lib/api/index.ts 末尾追加具名 re-export

<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'

关键约束

  1. 必须用具名 re-exportexport { fn1, fn2, type T } from './module'要用 export * from './credential-slot'per RESEARCH 问题 6具名导出更可控、可读性最高
  2. 必须包含 type CredentialSlottype CredentialSlotUpdatePayload 两个类型导出(前置 type 关键字使其在 isolatedModules 模式下安全)
  3. 追加位置选文件末尾handleApiError 之后),不要插在 usersApi / rolesApi 之间
  4. 该模块导入路径用 './credential-slot'(相对路径,与现有 export * from './card' 一致),不要写 '@/lib/api/credential-slot'
  5. 不要触碰文件中其他任何行(包括 import * as clientexport * from './card'usersApirolesApihandleApiError

追加之前最后一行示意(确认 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>
node -e "const c = require('fs').readFileSync('lib/api/index.ts','utf8'); const checks = /from\s+'\.\/credential-slot'/, 1], [/getCredentialSlot,/, 1], [/updateCredentialSlot,/, 1], [/type CredentialSlot,/, 1], [/type CredentialSlotUpdatePayload,/, 1; for (const [r, expected] of checks) { const n = (c.match(new RegExp(r.source, 'g')) || []).length; if (n !== expected) { console.error('FAIL:', r.toString(), '命中', n, '期望', expected); process.exit(1); } } if (/export \* from '\.\/credential-slot'/.test(c)) { console.error('FAIL: 错误使用 export * 风格'); process.exit(1); } if (!/export \* from \"\.\/card\"/.test(c) && !/export \* from '\.\/card'/.test(c)) { console.error('FAIL: 现有 card 导出被破坏'); process.exit(1); } if (!/export const handleApiError/.test(c)) { console.error('FAIL: 现有 handleApiError 被破坏'); process.exit(1); } console.log('OK'); " - `lib/api/index.ts` 末尾包含具名 re-export 块 - 4 个符号2 函数 + 2 类型)全部导出 - 现有 `export * from "./card"` / `usersApi` / `rolesApi` / `handleApiError` 不受破坏 - `verify.automated` 命令退出码为 0 且打印 `OK` 本 plan 完成后,**仅**做 grep / 文件存在性级别的结构性验证acceptance_criteria。完整的工具链验证`npm run lint` + `npx tsc --noEmit`)放在 plan 02因为 plan 02 也要编辑 `docs/修改记录.md`,把所有 lint/type-check 集中在 plan 02 末尾一次跑完,避免重复运行类型检查。

唯一例外:每个 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-slot
  • lib/api/index.ts 中现有 export * from "./card" / "./upload" / "./food"usersApi / rolesApi / handleApiError 完全不变
  • 两个文件均为合法 UTF-8无 BOM 干扰、无残缺字符) </success_criteria>
完成后创建 `.planning/phases/01-credential-slot-api/01-01-SUMMARY.md`,记录: - 创建的 `credential-slot.ts` 完整字节大小、行数 - `index.ts` 追加前后的行数对比 - 实际命中的关键串清单GET 路径、PUT 路径、4 个导出符号、双保险解包行计数) - 任何与 PLAN action 锁定写法的偏差及理由(理论上应为 0 偏差)