> ## Documentation Index
> Fetch the complete documentation index at: https://docs.magic-resume.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP 客户端

> 单一网关源、两个仅用于调用语义的 Axios 实例、共享鉴权拦截器，以及集中存放的路由。

整个前端只跟**一个配置好的地址**通信——网关。`apps/web/src/lib/api/httpClient.ts` 暴露两个 Axios 实例，它们都指向这同一个源、共享同一个鉴权拦截器。所有网络调用都走它们——不要直接 `fetch`，不要为某个功能再 `axios.create()` 一个。

## 单一源

```ts theme={null}
// apps/web/src/lib/api/routes.ts
export const API_ORIGIN = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3110'
```

`NEXT_PUBLIC_API_URL` 是**唯一**的后端变量，浏览器端和服务端用同一套解析——没有按服务拆分的历史回退。它指向**网关**（`apps/gateway`，dev 里 `:3110`），网关按路径前缀转发到对应上游（`platform-api` 还是 `agent-service`）。前端从不直接寻址某个具体服务。

<Note>
  `API_ORIGIN` 放在无依赖的 `routes.ts`（而非 `httpClient.ts`），这样 client 和 server 都能 import 它、又不会顺带把 Axios 拉进来。
</Note>

## 两个实例，同一个源

```ts theme={null}
import { httpClient } from '@/lib/api/httpClient'

httpClient.api      // CRUD 类调用（简历、用户、通知……）
httpClient.agent    // AI 类调用（面试等）
```

| 实例                 | 语义                        |
| ------------------ | ------------------------- |
| `httpClient.api`   | 产品 CRUD——简历、版本历史、分享、用户、通知 |
| `httpClient.agent` | AI / 长任务——面试及其它 agent 调用  |

两者都是 `createClient(API_ORIGIN)`——**同一个 base URL**。拆成两个纯粹是为了**调用侧语义**（CRUD 和 AI 分开读起来更清楚），**并不是两个后端**。每条 `/api/...` 具体去哪，由网关决定。

## 鉴权拦截器

`configureHttpClient(getter)` 在两个客户端上注册同一个请求拦截器：

```ts theme={null}
configureHttpClient(async () => clerk.session?.getToken())
```

拦截器调用 `getAuthToken()`，拿到 token 就设 `Authorization: Bearer <jwt>`（除非调用方已自带）。`getAuthToken()` 同时缓存最近一次有效 token；`getCachedAuthToken()` 把它同步暴露出来，供退出时的 keepalive 请求用（见 `resumeApi.syncResumeKeepalive`）——那种场景页面随时会死，等不起异步取 token。

<Warning>
  功能代码里不要手动传 token。如果你在写 `httpClient.api.get(url, { headers: { Authorization: ... } })`，说明拦截器在这个上下文没接好——去修接线，别绕过它。
</Warning>

## 集中存路由

所有路径放在 `src/lib/api/routes.ts`，且都在 `/api/*` 前缀下（网关的命名空间）：

```ts theme={null}
export const API_ROUTES = {
  resumes: {
    list:   '/api/resumes/mine',
    create: '/api/resumes',
    byId:   (id: string) => `/api/resumes/${id}`,
    versions: (id: string) => `/api/resumes/${id}/versions`,
    // …分享、评论、回复
  },
  users:         { pats: '/api/users/me/personal-access-tokens', /* … */ },
  notifications: { list: '/api/notifications', /* … */ },
  knowledge:     { timelines: '/api/knowledge/timelines' },
}

export const AGENT_ROUTES = {
  interview: { start: '/api/interview/start', chat: '/api/interview/chat', /* … */ },
}

// AI Lab 服务层调用的 Next.js 路由处理器（app/api/chat-agent/*）
export const WEB_AGENT_ROUTES = { chat: '/api/chat-agent', chatApprove: '/api/chat-agent/approve', /* … */ }
```

规则：

1. **不允许内联路径字符串**——每个后端路径都进这几张表。
2. `API_ROUTES` / `AGENT_ROUTES` 打到网关；`WEB_AGENT_ROUTES` 是 web 应用**自己的** Next.js 路由处理器（`app/api/chat-agent/*`），再由它代理到 agent。

这让"后端改路由"从"全仓 grep + 祈祷"变成一份单文件改动。

## 加一个新接口

1. 把路径加到 `routes.ts` 里对应的表。
2. 在 `src/lib/api/<feature>.ts` 里加一个带类型的封装，返回类型化响应。
3. 用 `httpClient.api` 或 `httpClient.agent`——不要再 `axios.create()` 第三个客户端。
4. 如果是云端专属功能，在调用侧用 `APP_MODE === 'cloud'` 做开关。
