# Docker 镜像加速站使用说明

> 本文档供 AI 助手（大模型）阅读。用户会给你这个网址，让你帮忙下载 Docker 镜像。
> 阅读完本文档后，你就能通过调用 API 帮用户完成镜像下载。

## 服务简介

这是一个 **Docker 镜像加速站**（https://docker.hsfp.cn）。

**解决的问题**：国内服务器无法直接访问 Docker Hub（被墙/限速），拉镜像极慢或失败。

**原理**：你提交镜像名 → 服务通过 GitHub Actions（境外服务器）拉取 Docker Hub 镜像 → 推送到阿里云 ACR（国内 CDN）→ 用户 `docker pull` 秒级下载。

**你的角色**：用户告诉你"帮我拉 xxx 镜像"，你调用下方 API 提交镜像，等待完成后告诉用户拉取命令。

## API 端点

### 1. 查缓存清单（**建议第一步就查**，毫秒级、不打 ACR）

```
GET /api/cached              # 读本地库（毫秒级，不打 ACR）—— 日常用这个
GET /api/cached?verify=1     # 顺便跟 ACR 对账（1-2 秒，别频繁调）
```

返回本站缓存的镜像和版本，用来判断“这个镜像是不是已经在缓存里”：

```json
{
  "ok": true,
  "source": "db",
  "counts": { "images": 26, "tags": 49 },
  "images": [
    { "image": "mysql",
      "pull_base": "registry.cn-beijing.aliyuncs.com/dockerhub_happen/mysql",
      "tags": ["5.7", "8.0", "8.1", "8.2.0", "8.3.0", "latest"],
      "latest_cached": true, "copies": 7, "source": "db" }
  ],
  "not_cached": ["zefhemely/silverbullet"],
  "unverified": []
}
```

字段说明：

- `source`（顶层）：`db` = 默认模式，读本地记录（快，不保证 ACR 上一定还在）；`acr` = 加了 `?verify=1`，每个仓库都问过 ACR
- `image`：官方镜像名（可直接拿去提交给 `/api/pull`）
- `tags`：已缓存、可直接拉取的版本；拉取命令就是 `docker pull {pull_base}:{tag}`
- `latest_cached`：缓存的版本里有没有 `latest`
- `copies`：被复制使用过的次数（越大说明越常用，清单按它降序、其余按名字）
- `not_cached`：没有可用缓存（db 模式 = 本地库没有成功记录；acr 模式 = ACR 上已不存在）
- `unverified`：仅 `?verify=1` 时可能出现，ACR 查询失败、无法确认的仓库

**怎么用（重点）**：用户要跑的服务如果清单里已有同名镜像，**直接从 `tags` 里挑一个已缓存的版本**告诉用户即可（`docker pull {pull_base}:{tag}`），既不用等 1-2 分钟，也不会把服务换到没用过的新版本上；只有清单里没有、或确实需要新版本时，才走下面的 `/api/pull`。

注意：默认模式只覆盖**本站经手过的**镜像（阿里云个人版不允许匿名枚举仓库，`/v2/_catalog` 返回 401），没出现在清单里不等于一定没缓存；要确认就用 `?verify=1`。

### 2. 提交镜像（核心）

```
POST /api/pull
Content-Type: application/json
```

请求体（提交镜像名，**不带 tag 会自动补 `:latest`**）：

```json
{"image": "nginx:alpine"}
```

可选参数：`force`（`true` 时强制重新拉取，绕过缓存，默认 `false`）。

**响应 A —— 缓存命中**（ACR 上已经有这个 tag，无需重新拉取）：

```json
{
  "ok": true,
  "status": "hit",
  "cached": true,
  "image": "nginx:alpine",
  "aliyun_url": "registry.cn-beijing.aliyuncs.com/dockerhub_happen/nginx:alpine",
  "verified_by": "acr"
}
```

缓存判定的依据是 **阿里云 ACR 上实际有没有这个 tag**（服务会直接查一遍 ACR）：

- **非 `latest` 的 tag**：只要 ACR 上已有（包括以前拉过、甚至没经过本服务的），就直接命中，**不会触发 GitHub Actions**。
- **`latest` 的 tag**：总是重新拉取一遍以更新（想跳过就用固定 tag）。
- `verified_by` 为 `acr` 表示刚查过 ACR；为 `db` 表示 ACR 查询失败、按本地记录返回（两者都算命中）。

→ 直接给用户拉取命令（见下方“重命名提示”）。

**响应 B —— 已触发拉取**（首次拉取 / latest 更新）：

```json
{
  "ok": true,
  "status": "pending",
  "mode": "pull",
  "image": "nginx:alpine",
  "aliyun_url": "registry.cn-beijing.aliyuncs.com/dockerhub_happen/nginx:alpine"
}
```

→ 告诉用户"正在拉取，约 1-2 分钟"，然后等待后查询状态。

### 3. 查询状态 / 镜像列表

```
GET /api/status
GET /api/status?keyword=nginx        # 按关键词过滤
```

响应：

```json
{
  "ok": true,
  "runs": [
    {
      "id": 5,
      "image": "nginx:alpine",
      "aliyun_url": "registry.cn-beijing.aliyuncs.com/dockerhub_happen/nginx:alpine",
      "status": "success",
      "error": null
    }
  ]
}
```

`status` 取值：`pending`（进行中）/ `success`（完成）/ `failure`（失败，看 `error` 字段）。

### 4. 删除记录

```
DELETE /api/images/{id}
```

拉取失败或想重来时可删除对应记录（id 从状态接口拿）。**只删本地记录，不会删 ACR 上的镜像**；删了以后同一个固定 tag 再提交会当作新拉取（相当于清缓存）。

## 镜像名规则（重要）

| 输入 | 处理 | 阿里云地址 |
|:---|:---|:---|
| `nginx` | 自动补 tag | `.../dockerhub_happen/nginx:latest` |
| `nginx:alpine` | 原样 | `.../dockerhub_happen/nginx:alpine` |
| `traefik/whoami` | 命名空间压平（`/`→`_`） | `.../dockerhub_happen/traefik_whoami:latest` |
| `node:22-alpine` | 原样 | `.../dockerhub_happen/node:22-alpine` |

**关键**：带命名空间的镜像 `aa/bb:tag` 在阿里云上会压平成 `aa_bb:tag`（ACR 只支持两级路径）。

## 重命名提示（务必告知用户）

拉取完成后，**建议同时给用户一条 `docker tag` 命令，把镜像重命名回原始名称**，这样用户直接用原镜像名即可（`docker run nginx:alpine`），不用记阿里云的地址：

```bash
docker pull registry.cn-beijing.aliyuncs.com/dockerhub_happen/nginx:alpine
docker tag registry.cn-beijing.aliyuncs.com/dockerhub_happen/nginx:alpine nginx:alpine
```

## 完整操作流程（给 AI 的执行指南）

1. 用户说"帮我拉 xxx 镜像"（可能是自然语言，如"拉一个 node 22 的 alpine"）
2. **先查缓存清单**（能直接命中就别触发拉取）：

   ```bash
   curl -s https://docker.hsfp.cn/api/cached
   ```

   - 清单里有这个镜像 → 从 `tags` 里挑一个已缓存的版本，直接给用户 `docker pull {pull_base}:{tag}`，结束
   - 清单里没有 / 需要新版本 → 继续下一步
3. 把它转成标准镜像名，调用：

   ```bash
   curl -s -X POST https://docker.hsfp.cn/api/pull \
     -H "Content-Type: application/json" \
     -d '{"image":"node:22-alpine"}'
   ```

4. **看响应**：
   - `status: "hit"` → 直接给用户拉取 + 重命名命令
   - `status: "pending"` → 告诉用户"正在拉取，约 1-2 分钟"，等待后查询：

     ```bash
     curl -s "https://docker.hsfp.cn/api/status?keyword=node"
     ```

   - `status: "success"` → 给用户两条命令：
     ```bash
     docker pull {aliyun_url}
     docker tag {aliyun_url} {原始镜像名}
     ```
   - `status: "failure"` → 告诉用户失败原因（`error` 字段），检查镜像名/tag 是否正确后重试
5. 用户拉取完成后，任务结束。

## 常用镜像示例

**别背这份清单，先查 `/api/cached`**（ACR 上实际有什么，以它为准）。下面是本站历史上常用的几个：

- `node:22-alpine` / `node:24-alpine` / `node:lts-alpine`（Node.js）
- `nginx:alpine`、`redis:7-alpine`、`mysql:8.0`、`traefik/whoami:latest`（连通性测试小镜像）
- `ghcr.io/ylianst/meshcentral:latest`（GitHub 上的镜像同样能中转）

尚未缓存、提交后要等 1-2 分钟的例子：`postgres:16-alpine`、`python:3.12-alpine`
（`python:3.12` 是已缓存的）。

## 注意事项

- 拉取前**先查 `/api/cached`**：已缓存的版本直接给命令，不用等
- 拉取需要 **1-2 分钟**（GitHub Actions 执行时间），期间状态是 `pending`；提交一次后轮询
  `/api/status` 即可，**别重复提交**（重复提交会重复触发流水线），超过 10 分钟仍 `pending`
  就让用户去仓库的 Actions 页面看，或重新提交一次
- 提交的镜像必须存在于 Docker Hub，**tag 拼错会失败**（如 `node:22-alipne`）
- 提交不带 tag 会自动按 `latest` 处理，`latest` 每次都会重新拉取更新
- 非 `latest` 的 tag 会先查 ACR：ACR 上已有就直接返回，不再触发流水线
- 提交/查询/删除接口无需认证，直接可调；`POST /api/sync`（把 ACR 存量补进列表）需要
  网页登录后的会话 cookie，AI 调不动，也无此必要
- 如需强制重拉已缓存镜像，加 `"force": true`
