359 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
phase: 01-credential-slot-api
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- lib/api/credential-slot.ts
- lib/api/index.ts
autonomous: true
requirements:
- CRED-FE-01
must_haves:
truths:
- "新文件 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 类型"
artifacts:
- path: "lib/api/credential-slot.ts"
provides: "凭据槽位 API client 模块(类型 + 适配器 + GET/PUT 函数)"
exports:
- "getCredentialSlot"
- "updateCredentialSlot"
- "type CredentialSlot"
- "type CredentialSlotUpdatePayload"
contains:
- "interface BackendCredentialSlot"
- "function mapBackendCredentialSlot"
- "apiClient.get('/v1/admin/credential-slot/'"
- "apiClient.put('/v1/admin/credential-slot/'"
- "response.data?.data || response.data"
- path: "lib/api/index.ts"
provides: "barrel re-export 入口,新增凭据槽位模块导出"
contains:
- "from './credential-slot'"
- "getCredentialSlot"
- "updateCredentialSlot"
- "type CredentialSlot"
- "type CredentialSlotUpdatePayload"
key_links:
- from: "lib/api/credential-slot.ts"
to: "lib/api/client.ts"
via: "import { apiClient } from './client'"
pattern: "import.*apiClient.*from.*[\"']\\./client[\"']"
- from: "lib/api/index.ts"
to: "lib/api/credential-slot.ts"
via: "具名 re-export"
pattern: "from\\s+['\"]\\./credential-slot['\"]"
- from: "getCredentialSlot / updateCredentialSlot"
to: "mapBackendCredentialSlot"
via: "返回值前先经 adapter 转换 snake -> camel"
pattern: "return\\s+mapBackendCredentialSlot\\("
---
<objective>
新建 `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 路径。
Output`lib/api/credential-slot.ts`(新建)+ `lib/api/index.ts`(末尾追加 7 行)。
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
<interfaces>
<!-- 关键接口契约 — 执行者直接照抄,不需要再去探索代码库 -->
From lib/api/client.ts (已存在,本 plan 不修改)
```typescript
// 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 完全照抄结构)
```typescript
// 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 在末尾追加导出)
```typescript
// 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) => { /* ... */ }
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="false">
<name>Task 1新建 lib/api/credential-slot.ts类型 + 适配器 + GET/PUT</name>
<read_first>
- `lib/api/ai-models.ts`L1-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` <decisions> 节 — 锁定的类型命名(`accessTokenMasked` 故意带 Masked 后缀;`accessToken` 用于明文提交载荷)
- `.planning/phases/01-credential-slot-api/01-RESEARCH.md` 「Code Examples」节 — 完整可粘贴骨架
</read_first>
<files>lib/api/credential-slot.ts</files>
<action>
`lib/api/credential-slot.ts` 创建以下**完整**文件内容(直接写入,不要省略任何行;中文 JSDoc 必须保留):
```typescript
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.data`per RESEARCH Pitfall 1拦截器**不**自动解包;不能直接 `mapBackendCredentialSlot(response.data)`
3. PUT body **不带** `updated_at`per 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.ts`CONTEXT.md 锁定)
</action>
<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>
<verify>
<automated>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'); "</automated>
</verify>
<done>
- `lib/api/credential-slot.ts` 文件存在
- 上述 acceptance_criteria 中所有 grep 检查全部满足
- `verify.automated` 命令退出码为 0 且打印 `OK`
- 文件不含 `/api/v1/admin/credential-slot` 这种重复 `/api` 前缀
- PUT body 字面量内不含 `updated_at` 键
</done>
</task>
<task type="auto" tdd="false">
<name>Task 2在 lib/api/index.ts 末尾追加具名 re-export</name>
<read_first>
- `lib/api/index.ts`(全文 197 行)— 确认当前导出风格混合(`export *` + `usersApi` / `rolesApi` 对象 + `handleApiError` 函数),定位末尾追加位置
- `.planning/phases/01-credential-slot-api/01-CONTEXT.md` <decisions> 节「`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>
<files>lib/api/index.ts</files>
<action>
在 `lib/api/index.ts` **文件末尾**(最后一行 `}` 之后,紧贴 `handleApiError` 定义的下方)追加以下 7 行(不要修改文件中任何已有内容;不要插在文件中部):
```typescript
// 凭据槽位Milestone v1.0 通用凭据槽位前端集成 — Phase 1 / CRED-FE-01
export {
getCredentialSlot,
updateCredentialSlot,
type CredentialSlot,
type CredentialSlotUpdatePayload,
} from './credential-slot'
```
**关键约束**
1. 必须用**具名 re-export**`export { fn1, fn2, type T } from './module'`**不**要用 `export * from './credential-slot'`per RESEARCH 问题 6具名导出更可控、可读性最高
2. **必须**包含 `type CredentialSlot` 与 `type CredentialSlotUpdatePayload` 两个类型导出(前置 `type` 关键字使其在 isolatedModules 模式下安全)
3. 追加位置选**文件末尾**`handleApiError` 之后),不要插在 `usersApi` / `rolesApi` 之间
4. 该模块导入路径用 `'./credential-slot'`(相对路径,与现有 `export * from './card'` 一致),不要写 `'@/lib/api/credential-slot'`
5. **不要**触碰文件中其他任何行(包括 `import * as client`、`export * from './card'`、`usersApi`、`rolesApi`、`handleApiError`
追加之前最后一行示意(确认 anchor
```typescript
// 导出错误处理函数
export const handleApiError = (error: any) => {
if (error instanceof Error) {
return error.message
}
return "发生未知错误,请重试"
}
// ← 在这行 } 之后追加上述 7 行
```
</action>
<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>
<verify>
<automated>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'); "</automated>
</verify>
<done>
- `lib/api/index.ts` 末尾包含具名 re-export 块
- 4 个符号2 函数 + 2 类型)全部导出
- 现有 `export * from "./card"` / `usersApi` / `rolesApi` / `handleApiError` 不受破坏
- `verify.automated` 命令退出码为 0 且打印 `OK`
</done>
</task>
</tasks>
<verification>
本 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 路径)就位。
</verification>
<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>
<output>
完成后创建 `.planning/phases/01-credential-slot-api/01-01-SUMMARY.md`,记录:
- 创建的 `credential-slot.ts` 完整字节大小、行数
- `index.ts` 追加前后的行数对比
- 实际命中的关键串清单GET 路径、PUT 路径、4 个导出符号、双保险解包行计数)
- 任何与 PLAN action 锁定写法的偏差及理由(理论上应为 0 偏差)
</output>