> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/zh/architecture/ip-data-sources.md).

# IP 数据源

驱动 MyIP 的主要有两个问题，它们由不同的机制回答：

* **“我的 IP 是什么？”** —— 浏览器直接向多个第三方回显端点发起请求。后端不参与。
* **“这个 IP 在哪里？”** —— 浏览器会请求 *我们的* 后端，它会查询一个地理位置提供商（或本地数据库）并规范化结果。

将它们分开是刻意的：回显端点必须看到访客自己的连接，因此不能经过代理。

```mermaid
flowchart TD
    subgraph 浏览器
      G["utils/getips/ — 每个来源一个函数"]
      T["utils/transform-ip-data.js"]
      C["IP 卡片"]
    end
    subgraph 后端
      H["api/ 中的地理位置处理器<br/>ipinfo-io, ipapi-com, ipapi-is,<br/>ip2location-io, ip-sb, ipcheck-ing, maxmind"]
      M["common/maxmind-service.js — 本地 mmdb"]
    end
    P["第三方回显端点<br/>Cloudflare, IPCheck.ing, IPIP.net, ..."]
    U["地理位置提供商"]

    G -->|"直接抓取"| P
    G -->|"该 IP"| H
    H --> U
    H --> M
    H -->|"规范化 JSON"| T --> C
```

## 步骤 1 —— 解析你自己的 IP

`frontend/utils/getips/` 每个来源都对应一个小模块。每个模块导出一个 `async` 返回以下内容的函数 `{ ip, source }`，并使用 `isValidIP()` 来自 `common/valid-ip.js`，并经过 `fetchWithTimeout` （浏览器默认 5 秒）。

`frontend/components/IpInfos.vue` 最多渲染六张卡片，并按索引为每张卡片分配一个来源函数：

| 卡片 | 主要来源               | 端点                                     | 回退到                                |
| -- | ------------------ | -------------------------------------- | ---------------------------------- |
| 0  | IPCheck.ing IPv4   | `4.ipcheck.ing`                        | IPify IPv4（`api4.ipify.org`)       |
| 1  | IPCheck.ing IPv6   | `6.ipcheck.ing`                        | IPify IPv6（`api6.ipify.org`)       |
| 2  | Cloudflare IPv4    | `1.0.0.1/cdn-cgi/trace`                | MyExternalIP IPv4                  |
| 3  | Cloudflare IPv6    | `[2606:4700:4700::1111]/cdn-cgi/trace` | MyExternalIP IPv6                  |
| 4  | IPIP.net           | `myip.ipip.net/json`                   | Upai（`pubstatic.b0.upaiyun.com`)   |
| 5  | IPCheck.ing IPv6/4 | `64.ipcheck.ing`                       | ——（JSON，然后是同一主机的 `/cdn-cgi/trace`) |

{% hint style="info" %}
这三个 IPCheck.ing 来源都接受一个 `originalSite` 参数。在规范部署中，它们读取 JSON 端点；在其他环境中，它们读取同一主机的 Cloudflare 风格 `/cdn-cgi/trace` 文本。如果 JSON 调用失败，它们会先通过 trace 重试，然后才放弃。
{% endhint %}

故障处理是分层的：

1. 抛出异常或返回无效 IP 的来源会记录一条 `console.warn` 并返回其声明的回退值，或者 `{ ip: null, source }`.
2. 每张卡片都运行自己的“先解析再获取详情”流水线，而且六张卡片都在 `Promise.allSettled`之下运行，因此某个失效来源不会拖垮整个批次。卡片会在结果返回时独立渲染。
3. 当某张卡片的整条链路都失败时，SPA 会发出一个 `ip-source:exhausted` 事件到应用总线上。Sentry（如已配置）只有在同一 IP 版本的另一张卡片成功解析时才会捕获它——否则“我们的链路失败”与访客没有 IPv6 的情况无法区分，这属于常规噪音。

单个来源的失败是 `console.warn` 刻意如此，因此它们永远不会进入错误监控；每张卡片的耗尽事件才是健康信号。

## 步骤 2 —— 为 IP 定位地理位置

一旦卡片拿到 IP，它就会调用后端。 `frontend/data/ip-databases.js` 是可选来源的注册表：

| id | 名称             | 端点                                        | 需要密钥                          |
| -- | -------------- | ----------------------------------------- | ----------------------------- |
| 0  | IPCheck.ing    | `/api/ipchecking?ip={{ip}}&lang={{lang}}` | `IPCHECKING_API_KEY` （私有 API） |
| 1  | IPinfo.io      | `/api/ipinfo?ip={{ip}}`                   | 可选（`IPINFO_API_KEY`)          |
| 2  | IP-API.com     | `/api/ipapicom?ip={{ip}}&lang={{lang}}`   | 否                             |
| 3  | IPAPI.is       | `/api/ipapiis?ip={{ip}}`                  | `IPAPIIS_API_KEY`             |
| 4  | IP2Location.io | `/api/ip2location?ip={{ip}}`              | `IP2LOCATION_API_KEY`         |
| 5  | IP.sb          | `/api/ipsb?ip={{ip}}`                     | 否                             |
| 6  | MaxMind        | `/api/maxmind?ip={{ip}}&lang={{lang}}`    | 本地数据库，请求时无需密钥                 |

`buildDbUrl(db, ip, lang)` 替换 `{{ip}}` 和 `{{lang}}` 占位符。每个来源的 `enabled` 标志源自 `/api/configs` 在加载时读取特性标志（`applyConfigAvailability`）——受密钥限制的来源遵循其标志，免密钥的来源始终可用——而运行时抓取失败不会影响它。用户在“偏好设置”中切换来源；如果某个已存储的选择指向一个不再配置的来源，它会迁移到最近可用的那个（`nearestEnabledId`，按 id 顺序向前查找）并弹出提示——这是唯一会重写已存储偏好的情况。密钥配置见 [可选 API 密钥](/developer/zh/configuration/optional-api-keys.md).

### 客户端回退链

`IpInfos.vue` 不会只是查询首选来源后就放弃。在 `fetchIPDetails()`:

* 所请求的来源会在 **enabled** 列表中查找；如果它不在其中（其配置标志已关闭，或者配置尚未加载），则从第一个已启用的来源开始遍历。
* 一旦出错，它会记录日志，前进到下一个已启用的来源并重试——直到所有已启用的来源都尝试过为止。
* 如果最终落到与请求不同的来源上，会显示一次性提示，并漂移该会话的运行时来源，这样后续查询会从可工作的来源开始——已存储的偏好永远不会被重写。
* 结果按 IP 缓存，进行中的请求也按 IP 去重，因此显示同一地址的六张卡片只会产生一个请求。

## 规范响应结构

无论上游是什么样，每个地理位置处理器都会返回相同的 JSON。来自 `api/ipinfo-io.js`:

```js
{
    ip,
    city,
    region,
    country,        // ISO 3166-1 alpha-2
    country_name,
    country_code,   // 与 `country` 相同的代码
    latitude,
    longitude,
    asn,            // "AS13335" — 带 AS 前缀
    org
}
```

| 字段                         | 备注                                   |
| -------------------------- | ------------------------------------ |
| `ip`                       | 由上游回显返回                              |
| `city` / `region`          | 自由格式字符串； `'无'` 当上游没有内容时              |
| `country` / `country_code` | 两者都携带 ISO alpha-2 代码                 |
| `country_name`             | 上游自身的名称——前端通常会替换它                    |
| `latitude` / `longitude`   | 数字                                   |
| `asn`                      | 规范化为 `AS<number>` 形式；返回纯数字的来源会自动补上前缀 |
| `org`                      | 组织或 ISP 名称                           |

有两个来源对其进行了扩展。 `api/ipapi-is.js` 添加 `isHosting` 和 `isProxy` 布尔值。 `api/ipcheck-ing.js` 是私有 IPCheck.ing API 的直通代理，并原样返回该 API 的负载，包括一个 `advancedData` 对象，前端会将其拆解为 proxy、IP-type、native-IP、quality-score、protocol 和 provider 字段。

<details>

<summary>前端如何处理该负载</summary>

`frontend/utils/transform-ip-data.js` 将规范响应转换为卡片数据：

* `country_name` 会根据 country **代码** 在本地重新推导，因此每个来源都会显示相同的 UI 语言名称；上游字符串只是回退。
* `country_code` 的 `'无'` 会变成空字符串。
* `org` 变为 `isp`，并且一个 `asn` 以 `AS` 会获得一个 `asnlink` 到 bgp.tools 的链接。
* 坐标四舍五入到一位小数，用于 `mapUrl` / `mapUrl_dark`，它们指向 `/api/map`。在地图缩放级别下，约 0.176° 相当于一个像素，因此 0.1° 低于像素级——标记看起来完全一样，而同一网格单元中的每个 IP 都会归并到同一个边缘缓存键。完整精度的坐标会保留用于展示。
* 对于来源 `0` （IPCheck.ing），它还会提取 `advancedData` 字段。

</details>

添加一个来源意味着编写一个生成这种结构的处理器，并向 `data/ip-databases.js`。 `makeGeoHandler({ name, buildUrl, normalize })` 工厂函数，位于 `common/geo-handler.js`，它负责共享外壳：读取已经验证过的 `?ip`，通过 `fetchUpstream`，在非 2xx 状态时抛出异常（故障页面会以 HTML 返回，否则会在 `JSON.parse`）中导致崩溃，随后规范化、响应，并在失败时记录日志并返回 500。

## 本地数据集

有两个数据集保存在磁盘上的 `common/` 中，并在请求时同步读取。两者都有一个在同一进程中运行的自动更新器。

### MaxMind GeoLite2

`common/maxmind-service.js` 打开 `GeoLite2-City.mmdb` 和 `GeoLite2-ASN.mmdb` 来自 `common/maxmind-db/` 并将两个读取器都保存在内存中。 `lookupMaxMind(ip, lang)` 将 City 和 ASN 记录合并为规范结构，并通过英语然后 `'无'` 回退来解析本地化名称。如果任一读取器缺失，它会抛出， `statusCode` 503 `/api/maxmind` 返回 503——API 会降级，但服务器仍然运行。

更新由 `common/maxmind-updater.js`:

| 行为     | 值                                                                          |
| ------ | -------------------------------------------------------------------------- |
| 启动时引导  | 仅在文件缺失时下载，最长 5 分钟；无论 `MAXMIND_AUTO_UPDATE`                                 |
| 首次计划检查 | 启动后 60 秒                                                                   |
| 重复间隔   | 每 24 小时                                                                    |
| 调度器开关  | `MAXMIND_AUTO_UPDATE=true` 加上 `MAXMIND_ACCOUNT_ID` 和 `MAXMIND_LICENSE_KEY` |
| 并发     | 一个锁文件，2 小时后视为陈旧                                                            |

下载会先暂存，再以原子方式发布。另一个文件监视器（`startMaxMindFileWatcher()`）每 5 秒轮询这两个文件；当另一个进程替换它们时会重新加载读取器，并以 1 秒去抖，因此 City 和 ASN 会作为一次重载一起生效。如果新文件无效，现有读取器会保持不变。设置说明见 [MaxMind 设置](/developer/zh/getting-started/maxmind-setup.md).

### CAIDA as2org 和 AS 关系

`common/caida-updater.js` 使用相同的锁 / 状态 / 原子发布 / 验证 / 重载机制管理两个数据集：

| 数据集      | 文件                                 | 来源                                                                                  | 用于          |
| -------- | ---------------------------------- | ----------------------------------------------------------------------------------- | ----------- |
| `as2org` | `common/as-org-db/as-org2info.txt` | `publicdata.caida.org/datasets/as-organizations/latest.as-org2info.txt.gz`          | AS → 组织名称查询 |
| `as-rel` | `common/as-rel-db/as-rel2.txt`     | 最新 `*.as-rel2.txt.bz2` 在 `publicdata.caida.org/datasets/as-relationships/serial-2/` | AS 连通性图     |

`as2org` 有一个稳定的 `latest` 上游符号链接，因此一个 `HEAD` 加上 `Last-Modified` 就足以检测到新的快照。 `as-rel` 没有，所以更新器会抓取目录列表，并取字典序最新的 `YYYYMMDD` 文件名。两者都会在传输过程中解压，并在发布前进行验证。

调度与 MaxMind 相同：如果缺失则在启动时引导（上限 2 分钟），首次检查在启动后 60 秒，然后每 24 小时一次，周期调度器由 `CAIDA_AUTO_UPDATE=true`控制。引导始终会运行，因此新检出的代码也能正常工作。

`common/as-rel-db.js` 解析管道分隔的行，并只保留提供者到客户（`-1`) `/api/asn-connectivity` 然后从起始 AS 向 Tier 1 执行完全本地、同步的 BFS。

`common/as-org-db.js` 解析 CAIDA 的管道分隔 TXT（约 12 MB），而不是等价的 JSONL（约 28 MB）——内容相同，而且 `split('|')` 优于 `JSON.parse` 按行处理时大约快 40%。两个模块都会按修改时间选择最新匹配的文件，因此手动下载、文件名不同的快照也能正常工作。

## 当某些东西失败时会发生什么

| 故障                  | 结果                                                       |
| ------------------- | -------------------------------------------------------- |
| 一个 echo 端点不可用       | 它的模块会回退到第二个端点；如果那也失败，卡片将不显示任何内容并发出 `ip-source:exhausted` |
| 一个地理位置提供方出错         | 客户端会回退到下一个已启用的数据源并显示一个提示；后续查询从可用的数据源开始                   |
| 某个 IP 的所有地理位置数据源都失败 | 卡片的详细字段保持为空；错误会在客户端记录                                    |
| 上游挂起                | `fetchUpstream` 在 8 秒后中止，处理程序返回 `500 { error }`          |
| MaxMind 数据库缺失或无效    | `/api/maxmind` 返回 503；其余 API 不受影响                        |
| CAIDA 快照缺失          | 连通性图会为空返回，组织名称查询会回退到 RIPEstat 的 `概览`                     |
| 上游返回一个 HTML 故障页面    | `makeGeoHandler` 在解析前会对非 2xx 状态抛出异常                      |

## 相关页面

* [后端](/developer/zh/architecture/backend.md) — 保护、缓存层和启动序列
* [可选 API 密钥](/developer/zh/configuration/optional-api-keys.md) — 需要哪些数据源凭据
* [MaxMind 设置](/developer/zh/getting-started/maxmind-setup.md) — 获取和刷新 GeoLite2
* [API 端点](/developer/zh/reference/api-endpoints.md) — 完整的请求/响应协议


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/zh/architecture/ip-data-sources.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
