From 6e74e74263bf68231a54b8ecc07beb441e668eb6 Mon Sep 17 00:00:00 2001
From: pmc <740076875@qq.com>
Date: Fri, 8 May 2026 11:00:35 +0800
Subject: [PATCH] =?UTF-8?q?docs(01):=20qy-lty-admin=20Phase=201=20PLAN=20?=
=?UTF-8?q?=C3=972=EF=BC=8801-01=20lib/api/credential-slot.ts=20/=2001-02?=
=?UTF-8?q?=20=E4=BF=AE=E6=94=B9=E8=AE=B0=E5=BD=95=20+=20=E5=8F=8C?=
=?UTF-8?q?=E9=87=8D=E9=AA=8C=E8=AF=81=EF=BC=89=EF=BC=8Cplan-checker=20?=
=?UTF-8?q?=E4=B8=80=E9=81=8D=E8=BF=87?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
qy-lty-admin/.planning/ROADMAP.md | 6 +-
.../01-credential-slot-api/01-01-PLAN.md | 358 ++++++++++++++++++
.../01-credential-slot-api/01-02-PLAN.md | 309 +++++++++++++++
3 files changed, 671 insertions(+), 2 deletions(-)
create mode 100644 qy-lty-admin/.planning/phases/01-credential-slot-api/01-01-PLAN.md
create mode 100644 qy-lty-admin/.planning/phases/01-credential-slot-api/01-02-PLAN.md
diff --git a/qy-lty-admin/.planning/ROADMAP.md b/qy-lty-admin/.planning/ROADMAP.md
index 4eed44f..3784b9c 100644
--- a/qy-lty-admin/.planning/ROADMAP.md
+++ b/qy-lty-admin/.planning/ROADMAP.md
@@ -33,7 +33,9 @@
2. 模块导出共享类型 `CredentialSlot { appId: string; accessTokenMasked: string; updatedAt: string }` 与提交载荷类型,前端类型为 camelCase;后端 snake_case 字段(`app_id` / `access_token` / `updated_at`)通过 `mapBackendCredentialSlot()` 适配器统一转换,沿用 `lib/api/adapters.ts` 的 `mapBackend*` 约定
3. `lib/api/index.ts` 导出新模块,`import { getCredentialSlot, updateCredentialSlot, type CredentialSlot } from '@/lib/api'` 在任一组件文件中均能解析通过 `tsc --noEmit`
4. 在浏览器开发态以 mock 后端或后端 Phase 2 联调环境调用 `getCredentialSlot()`,控制台可以看到一条带 `Authorization: Bearer ...` 的请求,且返回值字段名是前端 camelCase(说明适配器生效,未把后端原始 snake_case 直接透传)
-**Plans**: TBD
+**Plans**: 2 plans
+ - [ ] 01-01-PLAN.md — 新建 lib/api/credential-slot.ts(类型 + adapter + GET/PUT)+ lib/api/index.ts 末尾追加具名 re-export
+ - [ ] 01-02-PLAN.md — docs/修改记录.md 顶部追加 Phase 1 条目 + 跑双重验证(npm run lint + npx tsc --noEmit)+ 探针验证 barrel 入口
### Phase 2: RBAC 收敛 + AI 模型页入口
**Goal**: 通过 `lib/permissions.ts` 把"凭据槽位"声明为受控模块、仅向"超级管理员"与"AI模型管理员"开放;并在 `/ai-model` 页面渲染受权限校验收敛的入口控件,让授权用户能看到入口、未授权用户看不到入口
@@ -67,7 +69,7 @@ Phase 按数值顺序执行:1 → 2 → 3(如出现紧急插入,记为 1.1
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
-| 1. 凭据槽位 API 客户端 | 0/TBD | Not started | - |
+| 1. 凭据槽位 API 客户端 | 0/2 | Not started | - |
| 2. RBAC 收敛 + AI 模型页入口 | 0/TBD | Not started | - |
| 3. 编辑对话框 + 提交反馈 | 0/TBD | Not started | - |
diff --git a/qy-lty-admin/.planning/phases/01-credential-slot-api/01-01-PLAN.md b/qy-lty-admin/.planning/phases/01-credential-slot-api/01-01-PLAN.md
new file mode 100644
index 0000000..84fb832
--- /dev/null
+++ b/qy-lty-admin/.planning/phases/01-credential-slot-api/01-01-PLAN.md
@@ -0,0 +1,358 @@
+---
+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\\("
+---
+
+
+新建 `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 行)。
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.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 不修改):
+```typescript
+// 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 { success: boolean; code: number; message: string; data: T }
+```
+
+From lib/api/ai-models.ts (1:1 模板,本 plan 完全照抄结构):
+```typescript
+// 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 => {
+ 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): Promise => {
+ 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) => { /* ... */ }
+```
+
+
+
+
+
+
+ Task 1:新建 lib/api/credential-slot.ts(类型 + 适配器 + GET/PUT)
+
+
+ - `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」节 — 完整可粘贴骨架
+
+
+ lib/api/credential-slot.ts
+
+
+ 在 `lib/api/credential-slot.ts` 创建以下**完整**文件内容(直接写入,不要省略任何行;中文 JSDoc 必须保留):
+
+ ```typescript
+ 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 => {
+ 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 => {
+ 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 锁定)
+
+
+
+ - 文件 `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 字面量里
+
+
+
+ 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
+
+
+ - `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 *`
+
+
+ lib/api/index.ts
+
+
+ 在 `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 行
+ ```
+
+
+
+ - `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)
+
+
+
+ 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 路径)就位。
+
+
+
+- [ ] `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 干扰、无残缺字符)
+
+
+
diff --git a/qy-lty-admin/.planning/phases/01-credential-slot-api/01-02-PLAN.md b/qy-lty-admin/.planning/phases/01-credential-slot-api/01-02-PLAN.md
new file mode 100644
index 0000000..9db9bff
--- /dev/null
+++ b/qy-lty-admin/.planning/phases/01-credential-slot-api/01-02-PLAN.md
@@ -0,0 +1,309 @@
+---
+phase: 01-credential-slot-api
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 01-01
+files_modified:
+ - docs/修改记录.md
+autonomous: true
+requirements:
+ - CRED-FE-01
+
+must_haves:
+ truths:
+ - "docs/修改记录.md 顶部(『修改历史』段第一条)追加 [2026-05-08] Phase 1 条目"
+ - "条目包含『配套服务端 Phase』『覆盖前端需求』前置元数据,引用 commit 46d72b8"
+ - "条目跨项目联动字段写明:本 phase 不引入新跨项目契约,无需再次互引(后端 Phase 2 已建立互引)"
+ - "npm run lint 退出码为 0(ESLint 检查 lib/api/credential-slot.ts 与 lib/api/index.ts)"
+ - "npx tsc --noEmit 退出码为 0(项目级类型检查全部通过,含 plan 01 新增的 CredentialSlot / CredentialSlotUpdatePayload)"
+ - "外部组件文件能成功 import { getCredentialSlot, type CredentialSlot } from '@/lib/api'(通过临时 .tsx 探针验证)"
+ artifacts:
+ - path: "docs/修改记录.md"
+ provides: "本仓库变更日志,顶部新增 Phase 1 条目"
+ contains:
+ - "[2026-05-08] Phase 1"
+ - "CRED-FE-01"
+ - "lib/api/credential-slot.ts"
+ - "lib/api/index.ts"
+ - "46d72b8"
+ - "accessTokenMasked"
+ - "accessToken"
+ key_links:
+ - from: "docs/修改记录.md 顶部新条目"
+ to: "qy_lty 后端 Phase 2 commit 46d72b8 已建立的前后端互引"
+ via: "条目『服务端联动』字段中文引用"
+ pattern: "46d72b8"
+ - from: "外部消费者(Phase 2/3 组件文件)"
+ to: "lib/api 入口 barrel"
+ via: "import { getCredentialSlot, type CredentialSlot } from '@/lib/api'"
+ pattern: "from\\s+['\"]@/lib/api['\"]"
+---
+
+
+为 plan 01 落地的代码补齐两件事:
+1. 在 `docs/修改记录.md` 顶部按项目「修改格式说明」追加一条 Phase 1 条目(CLAUDE.md L70-95 强制规则)。
+2. 跑两条**独立**验证命令,确认 plan 01 的代码质量:
+ - `npm run lint` —— ESLint 检查(`next lint`)
+ - `npx tsc --noEmit` —— TypeScript 类型检查(**不**能省,per RESEARCH 问题 7:`npm run lint` 不跑 tsc)
+
+并通过一个临时探针 `.tsx` 文件验证外部消费者可以从 `@/lib/api` 入口解析新增的 4 个符号;探针验证完成后删除。
+
+Purpose:把 Phase 1 的「成功 = 代码 + 文档 + 类型可被消费」三件套全部落地,为 Phase 2/3 提供干净起点。
+
+Output:`docs/修改记录.md`(修改);plan 落地后无新代码文件残留(探针文件验证后删除)。
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/01-credential-slot-api/01-CONTEXT.md
+@.planning/phases/01-credential-slot-api/01-RESEARCH.md
+@.planning/phases/01-credential-slot-api/01-01-SUMMARY.md
+
+@CLAUDE.md
+@docs/修改记录.md
+@package.json
+@next.config.mjs
+@lib/api/credential-slot.ts
+@lib/api/index.ts
+
+
+
+
+
+ Task 1:在 docs/修改记录.md 顶部追加 Phase 1 条目
+
+
+ - `docs/修改记录.md` L1-50(特别是 L9-20 头部「修改格式说明」 + L24-47 已存在的 [2026-05-07] Phase 2 条目作为格式模板)
+ - `CLAUDE.md` L70-95「项目修改记录规则」 — 强制每次代码改动追加到顶部、跨项目独立维护
+ - `.planning/phases/01-credential-slot-api/01-CONTEXT.md` 节「修改记录」段(L152-156)— 锁定的跨项目联动文案
+ - `.planning/phases/01-credential-slot-api/01-RESEARCH.md` 「Code Examples」节内的「`docs/修改记录.md` 顶部追加条目」完整模板
+
+
+ docs/修改记录.md
+
+
+ 在 `docs/修改记录.md` 中找到这一行:
+
+ ```
+
+ ```
+
+ 在该注释行之后、紧贴现有 `### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)` 条目之前,**插入**以下完整 Markdown 块(之间留 1 个空行):
+
+ ```markdown
+
+ ### [2026-05-08] Phase 1(前端)凭据槽位 API 客户端
+
+ 配套服务端 Phase:[../qy_lty/.planning/phases/02-admin-rest/](../../qy_lty/.planning/phases/02-admin-rest/)(已落地,commit 46d72b8)
+ 覆盖前端需求:CRED-FE-01
+
+ - **文件路径**:
+ - `lib/api/credential-slot.ts`(新增)
+ - `lib/api/index.ts`(修改)
+ - **修改类型**: 新增(API 客户端层;纯逻辑,无 UI 改动)
+ - **修改内容**:
+ - 新建 `lib/api/credential-slot.ts`,封装:
+ - 类型 `CredentialSlot { appId, accessTokenMasked, updatedAt }`(脱敏掩码语义命名)
+ - 类型 `CredentialSlotUpdatePayload { appId, accessToken }`(明文语义命名)
+ - 适配器 `mapBackendCredentialSlot()`(snake_case → camelCase)
+ - API 函数 `getCredentialSlot()` / `updateCredentialSlot(payload)`,分别走 `apiClient.get` / `apiClient.put` 命中 `/v1/admin/credential-slot/`,沿用仓库统一的 `response.data?.data || response.data` 双保险解包;PUT body 仅传 `{ app_id, access_token }`,不携 `updated_at`(与 `updateAiModel` / `updateOutfit` 风格一致)
+ - `lib/api/index.ts` 末尾追加具名 re-export,让组件层可 `import { getCredentialSlot, type CredentialSlot } from '@/lib/api'`
+ - **修改原因**:
+ - 启动 Milestone v1.0「通用凭据槽位前端集成」,本 phase 为后续 Phase 2(RBAC + 入口控件)、Phase 3(编辑对话框 + Sonner 反馈)提供调用层基础
+ - `accessTokenMasked` vs `accessToken` 故意命名不同,让 TS 编译期捕捉「把脱敏字符串当真值回写 PUT」的 bug
+ - **服务端联动**: 无 — Phase 1 是纯 API client 层落地(无 UI 改动),调用的后端接口由 qy_lty 后端 Milestone v1.0 Phase 2 提供(commit `46d72b8` 已建立前后端互引修改记录);本 phase 不引入新跨项目代码契约,无需再次互引。前端 UI 集成(Phase 2 + 3)引入实质用户能力时再评估是否需要新一轮互引
+
+ ```
+
+ **关键约束**:
+ 1. 插入位置**必须**在 `` 注释**之后**、`### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)` 标题**之前**(项目约定:最新在前)
+ 2. 顶部 `### [2026-05-08]` 必须用今天日期 `2026-05-08`(与 RESEARCH 的 「Researched: 2026-05-08」一致;不是 2026-05-07)
+ 3. 必须出现关键字符串:`CRED-FE-01`、`46d72b8`、`accessTokenMasked`、`accessToken`、`lib/api/credential-slot.ts`、`lib/api/index.ts`、`/v1/admin/credential-slot/`
+ 4. 「服务端联动」字段必须明确写出「无 — ... commit 46d72b8 已建立 ... 本 phase 不引入新跨项目代码契约,无需再次互引」(per CONTEXT.md 锁定文案)
+ 5. **不要**修改 `docs/修改记录.md` 中任何已有条目;只做顶部插入
+ 6. **不要**在 qy_lty 项目侧建立新条目(CONTEXT.md 锁定:本 phase 不需要新建跨项目互引)
+
+
+
+ - `grep -nF "[2026-05-08] Phase 1(前端)凭据槽位 API 客户端" docs/修改记录.md` 命中 1 次
+ - 该新条目的行号 < 现有 `[2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约` 标题的行号(最新在前顺序正确)
+ - `grep -F "CRED-FE-01" docs/修改记录.md` 命中 ≥1 次
+ - `grep -F "46d72b8" docs/修改记录.md` 命中 ≥1 次(plan 01 之前已存在 1 次,本 plan 后应为 ≥2 次)
+ - `grep -F "accessTokenMasked" docs/修改记录.md` 命中 ≥1 次
+ - `grep -F "lib/api/credential-slot.ts" docs/修改记录.md` 命中 ≥1 次
+ - `grep -F "/v1/admin/credential-slot/" docs/修改记录.md` 命中 ≥1 次
+ - `grep -F "无需再次互引" docs/修改记录.md` 命中 ≥1 次
+ - 现有 [2026-05-07] Phase 2 条目内容**完全不变**(行级 diff 仅为顶部插入,不修改既有行)
+
+
+
+ node -e "const c = require('fs').readFileSync('docs/修改记录.md','utf8'); const newIdx = c.indexOf('[2026-05-08] Phase 1(前端)凭据槽位 API 客户端'); const oldIdx = c.indexOf('[2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约'); if (newIdx < 0) { console.error('FAIL: 新条目缺失'); process.exit(1); } if (oldIdx < 0) { console.error('FAIL: 旧条目消失'); process.exit(1); } if (newIdx >= oldIdx) { console.error('FAIL: 新条目未置顶'); process.exit(1); } const must = ['CRED-FE-01', '46d72b8', 'accessTokenMasked', 'accessToken', 'lib/api/credential-slot.ts', 'lib/api/index.ts', '/v1/admin/credential-slot/', '无需再次互引', '配套服务端 Phase', '覆盖前端需求']; for (const s of must) { if (!c.includes(s)) { console.error('FAIL: 缺失关键字 ' + s); process.exit(1); } } console.log('OK'); "
+
+
+
+ - `docs/修改记录.md` 顶部「修改历史」段第一条为 `[2026-05-08] Phase 1(前端)凭据槽位 API 客户端`
+ - 现有 [2026-05-07] Phase 2 条目位置后移、内容不变
+ - `verify.automated` 命令退出码为 0 且打印 `OK`
+ - 所有关键字段(CRED-FE-01 / 46d72b8 / accessTokenMasked / 文件路径 / 服务端联动文案)齐全
+
+
+
+
+ Task 2:跑双重验证(npm run lint + npx tsc --noEmit)+ 临时探针验证 barrel 入口可解析
+
+
+ - `package.json` L9 — 确认 `lint` 脚本是 `next lint`(per RESEARCH 问题 7:只跑 ESLint,不跑 tsc)
+ - `next.config.mjs`(L17 / L20)— 确认 `eslint.ignoreDuringBuilds` 与 `typescript.ignoreBuildErrors` 仅影响 `next build`,不影响显式 `next lint` 与 `npx tsc --noEmit`
+ - `tsconfig.json` — 确认 strict 模式开启
+ - `lib/api/credential-slot.ts`(plan 01 落地)+ `lib/api/index.ts`(plan 01 落地)— 类型与导出已就位
+ - `.planning/phases/01-credential-slot-api/01-RESEARCH.md` 「Pitfall 5: 包管理器混用导致 lockfile 漂移」节 — 验证步骤**只读不写**,不要跑 `npm install`
+
+
+ (不创建持久化新文件;仅临时探针 `lib/api/__phase1_probe__.ts`,验证后删除)
+
+
+ 按以下顺序**严格**执行,每步退出码必须为 0;任一步失败必须先排错再继续:
+
+ **步骤 1:创建临时类型探针文件 `lib/api/__phase1_probe__.ts`**(用 `.ts` 而非 `.tsx`,因不引入 React;`__` 前后缀降低被 IDE/lint 误识别为业务文件的风险)
+
+ 写入以下完整内容:
+
+ ```typescript
+ // 临时探针 — Phase 1 plan 02 验证 barrel 入口可正确解析新增符号;验证后立即删除
+ import {
+ getCredentialSlot,
+ updateCredentialSlot,
+ type CredentialSlot,
+ type CredentialSlotUpdatePayload,
+ } from "@/lib/api"
+
+ async function __probe(): Promise {
+ const slot: CredentialSlot = await getCredentialSlot()
+ const payload: CredentialSlotUpdatePayload = {
+ appId: slot.appId,
+ accessToken: "plaintext-not-masked", // 故意写明文,type 系统应允许
+ }
+ const next: CredentialSlot = await updateCredentialSlot(payload)
+ void next.accessTokenMasked // 触达字段证明类型形状
+ }
+ void __probe
+ ```
+
+ **步骤 2:跑 `npx tsc --noEmit`**
+
+ ```bash
+ npx tsc --noEmit
+ ```
+
+ 退出码必须为 0。如有错误:
+ - 若错误指向 `__phase1_probe__.ts` 的 `import "@/lib/api"` —— 说明 plan 01 的 `index.ts` re-export 有问题,回 plan 01 排错
+ - 若错误指向 `lib/api/credential-slot.ts` —— 说明 plan 01 的类型定义有问题,回 plan 01 排错
+ - 若错误指向**其他文件**(项目中的存量类型问题,与本 phase 无关)—— 记录错误清单到 SUMMARY,但本 task 仍判定**通过**,因为本 phase 的责任范围是新增文件不引入类型回归
+
+ **关键判定规则**:把 `npx tsc --noEmit` 输出存到临时文件,过滤出**仅与 `lib/api/credential-slot.ts` 或 `__phase1_probe__.ts` 或 `lib/api/index.ts` 相关**的错误行;这三处错误数必须为 0。其他文件的存量错误不算 phase 1 失败。
+
+ **步骤 3:跑 `npm run lint`**
+
+ ```bash
+ npm run lint
+ ```
+
+ 退出码必须为 0。如有警告/错误:
+ - 若错误/警告指向新增文件 `lib/api/credential-slot.ts` 或 `__phase1_probe__.ts` —— 必须修复
+ - 若错误/警告指向 `lib/api/index.ts` 但**仅限**新增的具名 re-export 块 —— 必须修复
+ - 若错误/警告指向**其他存量文件** —— 记录到 SUMMARY,本 task 仍判定通过
+
+ 注意 `next lint` 默认对项目所有 `.ts` / `.tsx` 跑;如果 ESLint 配置严格,`__phase1_probe__.ts` 中的 `void __probe` 与未使用变量可能触发 `no-unused-vars`。如真触发,添加文件级 `// eslint-disable-next-line @typescript-eslint/no-unused-vars` 注释或将 `__probe` / `__probe2` 等命名调整避免触发,但**优先**调整代码而非禁用规则。
+
+ **步骤 4:删除临时探针文件**
+
+ ```bash
+ rm lib/api/__phase1_probe__.ts
+ ```
+
+ (Windows PowerShell 等价:`Remove-Item lib/api/__phase1_probe__.ts`)
+
+ **步骤 5:再跑一次 `npx tsc --noEmit` 与 `npm run lint`**,确认删除探针后两条命令仍全部退出码 0(防止漏删导致后续 phase 把临时文件当真)
+
+ **关键约束(per RESEARCH Pitfall 5)**:
+ 1. **不**要跑 `npm install` / `pnpm install` / `yarn install`(lockfile 漂移风险,本 phase 不引入新依赖)
+ 2. **不**要修改 `package.json` / `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml`
+ 3. **不**要修改 `next.config.mjs` / `tsconfig.json` / `.eslintrc*`(CONTEXT.md / RESEARCH.md 均隐含锁定)
+ 4. 探针文件**必须**删除,不允许残留任何临时文件到 phase 末态
+ 5. 两条验证命令的退出码与「与新增文件相关的错误数」都必须为 0;不可用 `--force` / `--no-warnings` 等屏蔽手段
+
+
+
+ - 步骤 2:`npx tsc --noEmit` 在创建探针后退出码 0;如有非 0,输出中**没有**任何包含 `lib/api/credential-slot.ts` / `lib/api/__phase1_probe__.ts` / `lib/api/index.ts` 路径的错误行(执行者负责把过滤判定写入 SUMMARY 证据段)
+ - 步骤 3:`npm run lint` 退出码 0;同上规则,新增/修改的文件零错误零警告
+ - 步骤 4:`lib/api/__phase1_probe__.ts` 不存在(grep / Test-Path 验证)
+ - 步骤 5:删除探针后 `npx tsc --noEmit` 与 `npm run lint` 仍退出码 0
+ - `git status --short`(如可用)显示只有 `lib/api/credential-slot.ts`(新增)+ `lib/api/index.ts`(修改)+ `docs/修改记录.md`(修改)三个改动;不允许有 `__phase1_probe__.ts` / `pnpm-lock.yaml` / `yarn.lock` / `package-lock.json` / `package.json` 出现在 diff 中(探针残留或包管理器混用信号)
+
+
+
+ node -e "const fs = require('fs'); const cp = require('child_process'); function run(cmd){ try { cp.execSync(cmd, { stdio: 'pipe' }); return { code: 0, out: '' }; } catch (e) { return { code: e.status || 1, out: (e.stdout?.toString() || '') + (e.stderr?.toString() || '') }; } } if (fs.existsSync('lib/api/__phase1_probe__.ts')) { console.error('FAIL: 临时探针文件未删除'); process.exit(1); } const tsc = run('npx tsc --noEmit'); if (tsc.code !== 0) { const lines = tsc.out.split(/\r?\n/).filter(l => /lib\/api\/(credential-slot|__phase1_probe__|index)\.ts/.test(l)); if (lines.length > 0) { console.error('FAIL: tsc 在新增/修改文件上报错:\n' + lines.join('\n')); process.exit(1); } else { console.error('WARN: tsc 在存量文件上有错误,但与本 phase 无关:\n' + tsc.out.split(/\r?\n/).slice(0, 20).join('\n')); } } const lint = run('npm run lint'); if (lint.code !== 0) { const lines = lint.out.split(/\r?\n/).filter(l => /lib\/api\/(credential-slot|__phase1_probe__|index)\.ts/.test(l)); if (lines.length > 0) { console.error('FAIL: lint 在新增/修改文件上报错:\n' + lines.join('\n')); process.exit(1); } else { console.error('WARN: lint 在存量文件上有错误,但与本 phase 无关:\n' + lint.out.split(/\r?\n/).slice(0, 20).join('\n')); } } console.log('OK'); "
+
+
+
+ - 探针验证完整跑过:先创建 -> tsc 0 (新文件零错) -> lint 0 (新文件零错) -> 删除探针 -> 再跑 tsc 0 + lint 0
+ - 临时探针 `lib/api/__phase1_probe__.ts` 已删除,git diff 不残留它
+ - 包管理器 lockfile 状态未变(git status 不显示 `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml` / `package.json` 的改动)
+ - `verify.automated` 命令退出码为 0 且打印 `OK`
+ - SUMMARY 中记录两条验证命令的退出码、与新增文件相关的错误清单(应为空)、存量错误清单(如有,仅作信息)
+
+
+
+
+
+
+本 plan 完成意味着 Phase 1 全部交付完成。汇总验证:
+
+1. **结构性**(plan 01 + plan 02 联合):
+ - `lib/api/credential-slot.ts` 文件存在、4 个公共符号导出、路径与解包行齐全
+ - `lib/api/index.ts` 末尾具名 re-export 4 个符号
+ - `docs/修改记录.md` 顶部新增 [2026-05-08] Phase 1 条目
+
+2. **工具链**(本 plan task 2 兜底):
+ - `npx tsc --noEmit` 退出码 0(新增/修改文件零类型错误)
+ - `npm run lint` 退出码 0(新增/修改文件零 ESLint 错误)
+
+3. **可消费性**(本 plan task 2 探针验证):
+ - `import { getCredentialSlot, type CredentialSlot } from '@/lib/api'` 在临时探针文件中类型解析通过
+ - 探针文件删除后两条验证命令仍退出码 0(确认 plan 02 没引入残留文件依赖)
+
+4. **跨项目联动**:
+ - 修改记录条目明确写出「无需再次互引(后端 commit 46d72b8 已建立)」
+ - **不**修改 `qy_lty/docs/修改记录.md`(CONTEXT.md 锁定)
+
+5. **包管理器零漂移**:
+ - `git status` 不显示 `package.json` / `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml` 的任何修改
+
+
+
+- [ ] `docs/修改记录.md` 顶部第一条为 `[2026-05-08] Phase 1(前端)凭据槽位 API 客户端`
+- [ ] 该条目包含全部锁定关键字:`CRED-FE-01`、`46d72b8`、`accessTokenMasked`、`accessToken`、`lib/api/credential-slot.ts`、`lib/api/index.ts`、`/v1/admin/credential-slot/`、`无需再次互引`
+- [ ] 现有 [2026-05-07] Phase 2 条目内容不变、位置下移
+- [ ] `npx tsc --noEmit` 退出码 0;新增/修改文件零类型错误(存量错误不影响本 phase 判定)
+- [ ] `npm run lint` 退出码 0;新增/修改文件零 ESLint 错误
+- [ ] 临时探针 `lib/api/__phase1_probe__.ts` 已删除,git diff 不残留
+- [ ] `git status --short` 仅显示 `lib/api/credential-slot.ts`(新增) + `lib/api/index.ts`(修改) + `docs/修改记录.md`(修改)+ `.planning/...` 内的 PLAN/SUMMARY 文档;**不**显示 `package.json` / 任一 lockfile 改动
+
+
+