mcpField
G
geofeed-tuner

geofeed-tuner

github.com/github云与 DevOps40.5k1.3k 次安装1 天前更新
代码仓库

关于 geofeed-tuner

geofeed-tuner 是用于创建和改进公开可路由 IP 地址地理位置 CSV Feed 的技能。它检查 RFC 8805 格式和数据质量,通过 Fastah 的 MCP 工具查询调优数据,并生成 HTML 报告。

主要功能

  1. 01按 RFC 8805 检查 CSV 结构
  2. 02验证公开 IP 前缀
  3. 03标记数据质量和隐私问题
  4. 04使用 Fastah 查询数据进行调优
  5. 05生成 HTML 分析报告

适用场景

  1. 01准备待发布的地理位置 Feed
  2. 02检查现有 Feed 的格式错误
  3. 03查看 IP 地理位置改进建议

MCP 相关

该技能在调优数据查询阶段使用 Fastah 的 MCP 工具,以获取有助于提高地理位置准确性的数据。README 未说明它向 MCP 客户端提供哪些工具。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/github/awesome-copilot --skill geofeed-tuner

安装后在 Claude Code 中输入/geofeed-tuner确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/
iso3166-1.json
iso3166-2.json
small-territories.json
assets/example/
01-user-input-rfc8805-feed.csv
references/
rfc8805.txt
snippets-python3.md
scripts/templates/
index.html

详细介绍

Geofeed Tuner – 创建更好的 IP 地理定位 Feed

本技能通过以下方式帮助你创建和改进 CSV 格式的 IP 地理定位 Feed:

  • 确保你的 CSV 格式规范且一致
  • 检查是否符合 RFC 8805(行业标准)
  • 应用从真实部署中总结出的有主见的最佳实践
  • 针对准确性、完整性和隐私提出改进建议

何时使用本技能

  • 当用户请求帮助创建、改进或发布 CSV 格式的 IP 地理定位 Feed 文件时,使用本技能。
  • 用于调整和排查 CSV 地理定位 Feed 问题——发现错误、提出改进建议,并确保在实际场景中可用,而不仅仅是符合 RFC。
  • 目标受众:
    • 负责公网可路由 IP 地址空间的网络运营人员、管理员和工程师
    • ISP、移动运营商、云服务提供商、主机托管和机房托管公司、互联网交换中心运营方以及卫星互联网提供商等组织
  • 请勿将本技能用于私有或内部 IP 地址管理;它仅适用于公网可路由 IP 地址。

前置条件

  • 需要 Python 3。

目录结构与文件管理

本技能明确区分分发文件(只读)和工作文件(运行时生成)。

只读目录(请勿修改)

以下目录包含静态分发资源。请勿在这些目录中创建、修改或删除文件:

目录用途
assets/静态数据文件(ISO 代码、示例)
references/供参考的 RFC 规范和代码片段
scripts/可执行代码和用于报告的 HTML 模板文件
工作目录(生成内容)

所有生成、临时和输出文件都放在以下目录中:

目录用途
run/所有 Agent 生成内容的工作目录
run/data/从远程 URL 下载的 CSV 文件
run/report/生成的 HTML 调整报告
文件管理规则
  1. 切勿写入 assets/、references/ 或 scripts/——它们属于技能分发内容,必须保持不变。
  2. 所有下载的输入文件(来自远程 URL)必须保存到 ./run/data/。
  3. 所有生成的 HTML 报告必须保存到 ./run/report/。
  4. 所有生成的 Python 脚本必须保存到 ./run/。
  5. run/ 目录可能会在会话之间被清空;请勿在其中存储永久数据。
  6. 执行时的工作目录: ./run/ 中所有生成的脚本都必须以技能根目录(包含 SKILL.md 的目录)作为当前工作目录来执行,以便像 assets/iso3166-1.json 和 ./run/data/report-data.json 这样的相对路径能够正确解析。运行脚本前请勿 cd 到 ./run/。

处理流水线:顺序阶段执行

所有阶段必须按顺序执行,从 Phase 1 到 Phase 6。每个阶段都依赖于前一阶段的成功完成。例如,结构检查必须在质量分析运行之前完成。

各阶段概述如下。Agent 必须遵循每个阶段部分中进一步列出的详细步骤。

Phase名称描述
1理解标准审查 RFC 8805 中关于自行发布的 IP 地理位置馈送的关键要求
2收集输入从本地文件或远程 URL 收集 IP 子网数据
3检查与建议验证 CSV 结构、分析 IP 前缀并检查数据质量
4调优数据查找使用 Fastah 的 MCP 工具检索调优数据,以提高地理位置精度
5生成调优报告创建用于总结分析和建议的 HTML 报告
6最终审查验证报告数据的一致性与完整性

不要跳过任何阶段。 每个阶段都提供了后续阶段所需的关键检查或数据转换。

执行计划规则

在执行每个阶段之前,Agent 必须生成一份可见的 TODO 清单。

计划必须:

  • 出现在该阶段的最开始
  • 按顺序列出每个步骤
  • 使用复选框格式
  • 在步骤完成时实时更新
Phase 1:理解标准

本技能所执行的 RFC 8805 关键要求总结如下。将此总结作为你的工作参考。 只有遇到边缘情况、模糊情形,或用户询问此处未涵盖的标准问题时,才查阅完整的 RFC 8805 文本。

RFC 8805 关键事实

目的: 自行发布的 IP 地理位置馈送允许网络运营商以简单的 CSV 格式发布其 IP 地址空间的权威位置数据,从而使地理位置提供商能够纳入运营商提供的更正信息。

CSV 列顺序(第 2.1.1.1–2.1.1.5 节):

列字段必需备注
1ip_prefix是CIDR 表示法;IPv4 或 IPv6;必须是网络地址
2alpha2code否ISO 3166-1 alpha-2 国家代码;为空或 "ZZ" = 不进行地理位置定位
3region否ISO 3166-2 行政区划代码(例如 US-CA)
4city否自由文本城市名称;无权威验证集
5postal_code否已弃用 — 必须留空或省略

结构规则:

  • 文件可以包含以 # 开头的注释行(包括表头,如果存在)。
  • 表头行是可选的;如果存在,且以 # 开头,则将其视为注释。
  • 文件必须以 UTF-8 编码。
  • 不得设置子网主机位(即 192.168.1.1/24 无效;请使用 192.168.1.0/24)。
  • 仅适用于全局可路由的单播地址 — 不适用于私有、环回、链路本地或组播地址空间。

不进行地理位置定位: alpha2code 为空或不区分大小写的 ZZ 的条目(无论 region/city 的值如何)是明确的信号,表明运营商不希望对该前缀应用地理位置定位。

邮政编码已弃用(第 2.1.1.5 节): 第五列不得包含邮政编码或 ZIP 码。它们对于 IP 范围映射而言过于精细,并会引发隐私问题。

阶段 2:收集输入
  • 如果用户尚未提供 IP 子网或范围列表(有时称为 inetnum 或 inet6num),请提示他们提供。接受的输入格式:

    • 粘贴到聊天中的文本
    • 本地 CSV 文件
    • 指向 CSV 文件的远程 URL
  • 如果输入是远程 URL:

    • 在处理之前,尝试将 CSV 文件下载到 ./run/data/。
    • 遇到 HTTP 错误(4xx、5xx、超时或重定向循环)时,立即停止并向用户报告: Feed URL is not reachable: HTTP {status_code}. Please verify the URL is publicly accessible.
    • 不要使用不完整或空的下载内容继续进入阶段 3。
  • 如果输入是本地文件,直接处理,无需下载。

  • 编码检测与规范化:

    1. 首先尝试以 UTF-8 读取文件。
    2. 如果引发 UnicodeDecodeError,尝试 utf-8-sig(带 BOM 的 UTF-8),然后尝试 latin-1。
    3. 成功解码后,将工作副本重新编码并写为 UTF-8。
    4. 如果没有任何编码成功,停止并报告:Unable to decode input file. Please save it as UTF-8 and try again.
阶段 3:检查与建议
执行规则
  • 为此阶段生成一个脚本。
  • 不要将此阶段与其他阶段合并。
  • 不要预计算未来阶段的数据。
  • 将输出存储为 JSON 文件,路径为:./run/data/report-data.json
Schema 定义

下面的 JSON 结构在 Phase 3 期间是不可变的。Phase 4 之后会向 Entries 中的每个对象添加一个 TunedEntry 对象——这是唯一允许的 schema 扩展,并且会在单独的阶段进行。

JSON 键直接映射到模板占位符,如 {{.CountryCode}}、{{.HasError}} 等。

{
  "InputFile": "",
  "Timestamp": 0,

  "TotalEntries": 0,
  "IpV4Entries": 0,
  "IpV6Entries": 0,
  "InvalidEntries": 0,

  "Errors": 0,
  "Warnings": 0,
  "OK": 0,
  "Suggestions": 0,

  "CityLevelAccuracy": 0,
  "RegionLevelAccuracy": 0,
  "CountryLevelAccuracy": 0,
  "DoNotGeolocate": 0,

  "Entries": [
    {
      "Line": 0,
      "IPPrefix": "",
      "CountryCode": "",
      "RegionCode": "",
      "City": "",

      "Status": "",
      "IPVersion": "",

      "Messages": [
        {
          "ID": "",
          "Type": "",
          "Text": "",
          "Checked": false
        }
      ],

      "HasError": false,
      "HasWarning": false,
      "HasSuggestion": false,
      "DoNotGeolocate": false,
      "GeocodingHint": "",
      "Tunable": false
    }
  ]
}

字段定义:

顶层元数据:

  • InputFile:原始输入源,可以是本地文件名或远程 URL。
  • Timestamp:执行调优时的 Unix 纪元毫秒数。
  • TotalEntries:处理的数据行总数(不包括注释行和空行)。
  • IpV4Entries:属于 IPv4 子网的条目数。
  • IpV6Entries:属于 IPv6 子网的条目数。
  • InvalidEntries:IP 前缀解析和 CSV 解析失败的条目数。
  • Errors:Status 为 ERROR 的条目总数。
  • Warnings:Status 为 WARNING 的条目总数。
  • OK:Status 为 OK 的条目总数。
  • Suggestions:Status 为 SUGGESTION 的条目总数。
  • CityLevelAccuracy:City 非空的有效条目数。
  • RegionLevelAccuracy:RegionCode 非空且 City 为空的有效条目数。
  • CountryLevelAccuracy:CountryCode 非空、RegionCode 为空且 City 为空的有效条目数。
  • DoNotGeolocate(元数据):CountryCode、RegionCode 和 City 均为空的有效条目数。

条目字段:

  • Entries:对象数组,每个数据行对应一个对象,每个条目包含以下字段:
    • Line:原始 CSV 中的行号,从 1 开始计数(包括注释行和空行在内的所有行)。
    • IPPrefix:使用 CIDR 斜杠表示法的规范化 IP 前缀。
    • CountryCode:ISO 3166-1 alpha-2 国家/地区代码,或空字符串。
    • RegionCode:ISO 3166-2 区域代码(例如 US-CA),或空字符串。
    • City:城市名称,或空字符串。
    • Status:分配的最高严重级别:ERROR > WARNING > SUGGESTION > OK。
    • IPVersion:根据解析出的 IP 前缀,为 "IPv4" 或 "IPv6"。
    • Messages:消息对象数组,每个对象包含:
      • ID:来自下方 Validation Rules Reference 表的字符串标识符(例如 "1101"、"3301")。
      • Type:严重级别类型:"ERROR"、"WARNING" 或 "SUGGESTION"。
      • Text:人类可读的验证消息字符串。
      • Checked:如果验证规则可自动调优(参考表中 Tunable: true),则为 true,否则为 false。用于控制报告中的复选框是 checked 还是 disabled。
    • HasError:如果任意消息的 Type 为 "ERROR",则为 true。
    • HasWarning:如果任意消息的 Type 为 "WARNING",则为 true。
    • HasSuggestion:如果任意消息的 Type 为 "SUGGESTION",则为 true。
    • DoNotGeolocate(条目):如果 CountryCode 为空或为 "ZZ",则为 true——该条目是明确的禁止地理定位信号。
    • GeocodingHint:在 Phase 3 中始终为空字符串 ""。保留供将来使用。
    • Tunable:如果条目中任意消息的 Checked: true,则为 true。按所有消息 Checked 值的逻辑 OR 计算得出。此标志决定报告中 "Tune" 按钮的可见性。
校验规则参考

向条目添加消息时,请使用下表中的 ID、Type、Text 和 Checked 值。

IDTypeTextCheckedCondition Reference
1101ERRORIP 前缀为空falseIP Prefix Analysis: empty
1102ERROR无效的 IP 前缀:无法解析为 IPv4 或 IPv6 网络falseIP Prefix Analysis: invalid syntax
1103ERRORRFC 8805 feed 中不允许使用非公网 IP 范围falseIP Prefix Analysis: non-public
3101SUGGESTIONIPv4 前缀异常大,可能表示存在笔误falseIP Prefix Analysis: IPv4 < /22
3102SUGGESTIONIPv6 前缀异常大,可能表示存在笔误falseIP Prefix Analysis: IPv6 < /64
1201ERROR无效的国家/地区代码:不是有效的 ISO 3166-1 alpha-2 值trueCountry Code Analysis: invalid
1301ERROR无效的地区格式;应为 COUNTRY-SUBDIVISION(例如 US-CA)trueRegion Code Analysis: bad format
1302ERROR无效的地区代码:不是有效的 ISO 3166-2 行政区划trueRegion Code Analysis: unknown code
1303ERROR地区代码与指定的国家/地区代码不匹配trueRegion Code Analysis: mismatch
1401ERROR无效的城市名称:不允许使用占位符值falseCity Name Analysis: placeholder
1402ERROR无效的城市名称:检测到缩写或基于代码的值trueCity Name Analysis: abbreviation
2401WARNING城市名称格式不一致;建议规范化该值trueCity Name Analysis: formatting
1501ERROR邮政编码已被 RFC 8805 弃用,出于隐私原因必须移除truePostal Code Check
3301SUGGESTION对于小地区,地区通常是不必要的;建议移除地区值trueTuning: small territory region
3402SUGGESTION对于小地区,城市级粒度通常是不必要的;建议移除城市值trueTuning: small territory city
3303SUGGESTION指定城市时建议填写地区代码;请从下拉列表中选择一个地区trueTuning: missing region with city
3104SUGGESTION确认此子网是有意标记为不进行地理位置定位,还是缺少位置数据trueTuning: unspecified geolocation
填充消息

当校验检查匹配时,使用参考表中的值向条目的 Messages 数组添加一条消息:

entry["Messages"].append({
    "ID": "1201",      # From the table
    "Type": "ERROR",   # From the table
    "Text": "Invalid country code: not a valid ISO 3166-1 alpha-2 value",  # From the table
    "Checked": True    # From the table (True = tunable)
})

为条目填充完所有消息后,推导出条目级标志:

entry["HasError"] = any(m["Type"] == "ERROR" for m in entry["Messages"])
entry["HasWarning"] = any(m["Type"] == "WARNING" for m in entry["Messages"])
entry["HasSuggestion"] = any(m["Type"] == "SUGGESTION" for m in entry["Messages"])
entry["Tunable"] = any(m["Checked"] for m in entry["Messages"])
准确度级别统计规则

准确度级别互斥。根据最细粒度的非空地理字段,将每个有效条目(非 ERROR、非无效)恰好分配到一个分桶中:

条件分桶
City 非空CityLevelAccuracy
RegionCode 非空且 City 为空RegionLevelAccuracy
CountryCode 非空,RegionCode 和 City 为空CountryLevelAccuracy
DoNotGeolocate(条目)为 trueDoNotGeolocate(元数据)

不要统计 HasError: true 的条目或 InvalidEntries 中的条目到任何准确度分桶中。

Agent 不得:

  • 重命名字段
  • 添加或删除字段
  • 更改数据类型
  • 重新排序键
  • 改变嵌套结构
  • 包装对象
  • 拆分为多个文件

如果某个值未知,将其留空——切勿捏造数据。

结构与格式检查

此阶段验证你的 feed 是否格式良好且可解析。关键结构错误必须先解决,调优器才能分析地理定位质量。

CSV 结构

本小节定义了用于 IP 地理定位 feed 的 CSV 格式输入文件的规则。 目标是确保文件能够被可靠地解析,并规范化为一致的内部表示。

  • CSV 结构检查

    • 如果 pandas 可用,使用它进行 CSV 解析。

    • 否则,回退到 Python 内置的 csv 模块。

    • 确保 CSV 包含恰好 4 或 5 个逻辑列。

    • 允许注释行。

    • 表头行可能存在,也可能不存在。

    • 如果不存在表头行,假定隐式列顺序为:

      ip_prefix, alpha2code, region, city, postal code (deprecated)
      
    • 参考示例输入文件: assets/example/01-user-input-rfc8805-feed.csv

  • CSV 清理与规范化

    • 使用等效于以下操作的 Python 逻辑清理并规范化 CSV:

      • 仅选择前五列,丢弃第五列之后的任何列。
      • 使用 UTF-8 BOM 写入输出文件。
    • 注释

      • 删除第一列以 # 开头的注释行。
      • 如果表头行以 # 开头,这也会将其删除。
      • 创建一个注释映射,以从 1 开始的行号为键,完整的原始行为值。同时存储空行。
      • 将此映射存储到 JSON 文件中,路径为:./run/data/comments.json
      • 示例:{ "4": "# It's OK for small city states to leave state ISO2 code unspecified" }
  • 说明

    • 两种实现路径(pandas 和内置 csv)都必须使用 utf-8-sig 编码写入输出,以确保存在 UTF-8 BOM。
IP 前缀分析
  • 检查每个条目中的 IPPrefix 字段是否存在且非空。

  • 检查各条目之间是否存在重复的 IPPrefix 值。

  • 如果发现重复,停止该技能并向用户报告,消息为:Duplicate IP prefix detected: {ip_prefix_value} appears on lines {line_numbers}

  • 如果未发现重复,继续进行分析。

  • 检查

    • 每个子网必须使用 references/ 文件夹中的代码片段,能够被正确解析为 IPv4 或 IPv6 网络。
    • 子网必须规范化并以 CIDR 斜杠表示法 显示。
      • 单主机 IPv4 子网必须表示为 /32。
      • 单主机 IPv6 子网必须表示为 /128。
  • ERROR

    • 将以下情况报告为 ERROR:

    • 无效的子网语法

      • 消息 ID:1102
    • 非公网地址空间

      • 适用于 私有、环回、链路本地、组播或其他非公网 的子网
        • 在 Python 中,使用 ./references 中所示的 is_private 及相关地址属性来检测非公网范围。
      • 消息 ID:1103
  • SUGGESTION

    • 将以下情况报告为 SUGGESTION:

    • 过大的 IPv6 子网

      • 前缀短于 /64
      • 消息 ID:3102
    • 过大的 IPv4 子网

      • 前缀短于 /22
      • 消息 ID:3101
地理位置质量检查

分析地理位置数据的 准确性和一致性:

  • 国家/地区代码
  • 区域代码
  • 城市名称
  • 已弃用字段

此阶段在结构检查通过后运行。

国家/地区代码分析
  • 使用本地可用的数据表 ISO3166-1 进行检查。

    • 包含带 ISO 代码的国家和地区的 JSON 数组
    • 每个对象包含:
      • alpha_2:两字母国家/地区代码
      • name:国家/地区短名称
      • flag:旗帜 emoji
    • 此文件代表 RFC 8805 CSV 的 有效 CountryCode 值的超集。
  • 将条目的 CountryCode(RFC 8805 第 2.1.1.2 节,列 alpha2code)与 alpha_2 属性进行比对。

  • 示例代码可在 references/ 目录中找到。

  • 如果在 assets/small-territories.json 中找到某个国家/地区,则在内部将该条目标记为小型属地。此标志用于后续检查和建议,但 不会存储在输出的 JSON 中(它是临时的验证状态)。

  • 注意: small-territories.json 包含一些历史/有争议的代码(AN、CS、XK),这些代码不存在于 iso3166-1.json 中。使用其中某个代码作为 CountryCode 的条目将无法通过国家/地区代码验证(ERROR),即使它作为小型属地匹配成功。国家/地区代码 ERROR 优先——不要因为小型属地标志而抑制它。

  • ERROR

    • 将以下情况报告为 ERROR:
    • 无效的国家/地区代码
      • 条件:CountryCode 存在,但未在 alpha_2 集合中找到
      • 消息 ID:1201
  • SUGGESTION

    • 将以下情况报告为 SUGGESTION:

    • 子网未指定地理位置

      • 条件:子网的所有地理字段(CountryCode、RegionCode、City)均为空。
      • 操作:
        • 将该条目的 DoNotGeolocate 设置为 true。
        • 将该条目的 CountryCode 设置为 ZZ。
      • 消息 ID:3104
区域代码分析
  • 使用本地可用的数据表 ISO3166-2 进行检查。

    • 包含 ISO 分配代码的国家行政区划的 JSON 数组
    • 每个对象包含:
      • code:带有国家代码前缀的行政区划代码(例如 US-CA)
      • name:行政区划简称
    • 此文件代表 RFC 8805 CSV 的 有效 RegionCode 值的超集。
  • 如果提供了 RegionCode 值(RFC 8805 第 2.1.1.3 节):

    • 检查格式是否匹配 {COUNTRY}-{SUBDIVISION}(例如 US-CA、AU-NSW)。
    • 对照 code 属性(已带有国家代码前缀)检查该值。
  • 小地区例外: 如果条目是小地区 且 RegionCode 值等于该条目的 CountryCode(例如新加坡的 SG 同时作为国家和区域),则将该区域视为可接受 — 跳过此条目的所有区域验证检查。小地区实际上是没有有意义的 ISO 3166-2 行政区划的城邦。

  • ERROR

    • 将以下情况报告为 ERROR:
    • 无效的区域格式
      • 条件:RegionCode 不匹配 {COUNTRY}-{SUBDIVISION} 且 小地区例外不适用
      • 消息 ID:1301
    • 未知的区域代码
      • 条件:RegionCode 值未在 code 集合中找到 且 小地区例外不适用
      • 消息 ID:1302
    • 国家–区域不匹配
      • 条件:RegionCode 的国家部分与 CountryCode 不匹配
      • 消息 ID:1303
城市名称分析
  • 城市名称仅使用 启发式检查 进行验证。

  • 目前 没有权威数据集 可用于验证城市名称。

  • ERROR

    • 将以下情况报告为 ERROR:

    • 占位符或无意义的值

      • 条件:占位符或无意义的值,包括但不限于:
        • undefined
        • Please select
        • null
        • N/A
        • TBD
        • unknown
      • 消息 ID:1401
    • 截断的名称、缩写或机场代码

      • 条件:不代表有效城市名称的截断名称、缩写或机场代码:
        • LA
        • Frft
        • sin01
        • LHR
        • SIN
        • MAA
      • 消息 ID:1402
  • WARNING

    • 将以下情况报告为 WARNING:
    • 大小写或格式不一致
      • 条件:大小写、间距或格式不一致,可能降低数据质量的城市名称,例如:
        • HongKong 与 Hong Kong
        • 大小写混用或意外的文字系统使用
      • 消息 ID:2401
邮政编码检查
  • RFC 8805 第 2.1.1.5 节明确 弃用邮政或 ZIP 代码。

  • 邮政编码可能代表非常小的人口,并且 不被认为对映射 IP 地址范围是隐私安全的,因为 IP 地址范围本质上是统计性的。

  • ERROR

    • 将以下情况报告为 ERROR:
    • 存在邮政编码
      • 条件:邮政/ZIP 代码字段中存在非空值。
      • 消息 ID:1501
调优与建议

此阶段应用 有倾向性的建议,超越 RFC 8805,源自真实世界的 geofeed 部署经验,可提高准确性和可用性。

  • SUGGESTION
    • 将以下情况报告为 SUGGESTION:

    • 为小地区指定了区域或城市

      • 条件:
        • 条目是小地区
        • RegionCode 非空 或
        • City 非空。
      • 消息 ID:3301(针对区域)、3402(针对城市)
    • 指定了城市但缺少区域代码

      • 条件:
        • City 非空
        • RegionCode 为空
        • 条目 不是 小地区
      • 消息 ID:3303
阶段 4:调优数据查找
目标

使用 Fastah 的 rfc8805-row-place-search 工具查找所有 Entries。

执行规则
  • 仅为生成载荷而生成新脚本(读取数据集并写入一个或多个载荷 JSON 文件;不要在此脚本中调用 MCP)。
  • 服务每次请求仅接受 1000 条条目,因此如果条目超过 1000 条,请拆分为多个请求。
  • Agent 必须读取生成的载荷文件,依据它们构建请求,并将这些请求以每批最多 1000 条的方式发送到 MCP 服务。
  • MCP 失败时: 如果 MCP 服务不可达、返回错误,或任何批次均无结果,请记录警告并继续执行阶段 5。对所有受影响的条目设置 TunedEntry: {}。不要阻塞报告生成。清晰通知用户:Tuning data lookup unavailable; the report will show validation results only.
  • 建议仅供参考——切勿自动填充。
步骤 1:构建带去重的查找载荷

从以下位置加载数据集:./run/data/report-data.json

  • 读取 Entries 数组。每个条目将用于构建 MCP 查找载荷。

通过去重相同条目来减少服务请求:

  • 对 Entries 中的每个条目,计算内容哈希(CountryCode + RegionCode + City 的哈希)。
  • 创建去重映射:{ contentHash -> { rowKey, payload, entryIndices: [] } }。rowKey 是一个 UUID,将发送到 MCP 服务以匹配响应。
  • 如果某条目的哈希已存在,则将其在 Entries 中的 基于 0 的数组索引追加到该去重条目的 entryIndices 数组。
  • 如果哈希是新的,则生成一个 **UUID(rowKey)**并创建一个新的去重条目。

构建请求批次:

  • 从映射中提取唯一的去重条目,并保持去重顺序。
  • 构建每批最多 1000 项的请求批次。
  • 对每个批次,保留一个内存结构,例如 [{ rowKey, payload, entryIndices }, ...],以便按 rowKey 匹配响应。
  • 写入 MCP 载荷文件时,在每个载荷对象中包含 rowKey 字段:
[
    {"rowKey": "550e8400-e29b-41d4-a716-446655440000", "countryCode":"CA","regionCode":"CA-ON","cityName":"Toronto"},
    {"rowKey": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "countryCode":"IN","regionCode":"IN-KA","cityName":"Bangalore"},
    {"rowKey": "6ba7b811-9dad-11d1-80b4-00c04fd430c8", "countryCode":"IN","regionCode":"IN-KA"}
]
  • 读取响应时,将每个响应的 rowKey 字段匹配到相应的去重条目,以检索所有关联的 entryIndices。

规则:

  • 将载荷写入:./run/data/mcp-server-payload.json
  • 写入载荷后退出脚本。
步骤 2:调用 Fastah MCP 工具
  • Fastah MCP 服务的 mcp.json 风格配置示例如下:
    "fastah-ip-geofeed": {
      "type": "http",
      "url": "https://mcp.fastah.ai/mcp"
    }
  • 服务:https://mcp.fastah.ai/mcp

  • 工具及其 Schema:在首次 tools/call 之前,Agent 必须发送 tools/list 请求,以读取 rfc8805-row-place-search 的输入和输出 schema。 将发现的 schema 作为字段名称、类型和约束的权威来源。

  • 以下仅为说明性示例;始终以 tools/list 返回的 schema 为准:

    [
        {"rowKey": "550e8400-...", "countryCode":"CA", ...},
        {"rowKey": "690e9301-...", "countryCode":"ZZ", ...}
    ]
    
  • Open ./run/data/mcp-server-payload.json and send all deduplicated entries with their rowKeys.

  • If there are more than 1000 deduplicated entries after deduplication, split into multiple requests of 1000 entries each.

  • The server will respond with the same rowKey field in each response for mapping back.

  • Do NOT use local data.

Step 3: Attach Tuned Data to Entries
  • Generate a new script for attaching tuned data.
  • Load both ./run/data/report-data.json and the deduplication map (held in memory from Step 1, or re-derived from the payload file).
  • For each response from the MCP server:
    • Extract the rowKey from the response.
    • Look up the entryIndices array associated with that rowKey from the deduplication map.
    • For each index in entryIndices, attach the best match to Entries[index].
  • Use the first (best) match from the response when available.

Create the field on each affected entry if it does not exist. Remap the MCP API response keys to Go struct field names:

"TunedEntry": {
  "Name": "",
  "CountryCode": "",
  "RegionCode": "",
  "PlaceType": "",
  "H3Cells": [],
  "BoundingBox": []
}

TunedEntry 字段是单个对象(不是数组)。它保存来自 MCP 服务的最佳匹配。

MCP 响应键 → JSON 键映射:

MCP API 响应键JSON 键
placeNameName
countryCodeCountryCode
stateCodeRegionCode
placeTypePlaceType
h3CellsH3Cells
boundingBoxBoundingBox

没有 UUID 匹配的条目(即 MCP 服务未针对其 UUID 返回响应)必须接收空的 TunedEntry: {} 对象——绝不要使该字段缺失。

  • 将数据集写回:./run/data/report-data.json
  • 规则:
    • 保留所有现有的验证标志。
    • 不要创建额外的中间文件。
阶段 5:生成调优报告

通过渲染 ./scripts/templates/index.html 处的模板,并使用来自 ./run/data/report-data.json 和 ./run/data/comments.json 的数据,生成一份自包含的 HTML 报告。

将完成的报告写入 ./run/report/geofeed-report.html。生成后,尝试在系统的默认浏览器中打开它(例如 webbrowser.open())。如果在无头环境、CI 流水线或远程容器中运行且没有可用浏览器,则跳过浏览器步骤,改为向用户展示文件路径,以便他们打开或下载它。

该模板使用 Go 的 html/template 语法({{.Field}}、{{range}}、{{if eq}} 等)。编写一个 Python 脚本,读取模板,从 JSON 数据文件构建渲染上下文,并处理模板占位符以生成最终 HTML。不要修改模板文件本身——所有处理都在渲染时于 Python 脚本中进行。

步骤 1:替换元数据占位符

将模板中的每个 {{.Metadata.X}} 占位符替换为 report-data.json 中对应的值。由于 JSON 键与模板占位符匹配,因此映射是直接的——{{.Metadata.InputFile}} 映射到 InputFile JSON 键,依此类推。

模板占位符JSON 键(report-data.json)
{{.Metadata.InputFile}}InputFile
{{.Metadata.Timestamp}}Timestamp
{{.Metadata.TotalEntries}}TotalEntries
{{.Metadata.IpV4Entries}}IpV4Entries
{{.Metadata.IpV6Entries}}IpV6Entries
{{.Metadata.InvalidEntries}}InvalidEntries
{{.Metadata.Errors}}Errors
{{.Metadata.Warnings}}Warnings
{{.Metadata.Suggestions}}Suggestions
{{.Metadata.OK}}OK
{{.Metadata.CityLevelAccuracy}}CityLevelAccuracy
{{.Metadata.RegionLevelAccuracy}}RegionLevelAccuracy
{{.Metadata.CountryLevelAccuracy}}CountryLevelAccuracy
{{.Metadata.DoNotGeolocate}}DoNotGeolocate(元数据)

关于 {{.Metadata.Timestamp}} 的说明: 此占位符出现在 JavaScript 的 new Date(...) 调用内部。将其替换为原始整数值(对于 <script> 内的数字字面量,无需进行 HTML 转义)。由于所有其他元数据值都出现在 HTML 元素文本内部,因此应对它们进行 HTML 转义。

步骤 2:替换评论映射占位符

在模板中找到以下模式:

const commentMap = {{.Comments}};

将 {{.Comments}} 替换为来自 ./run/data/comments.json 的序列化 JSON 对象。该 JSON 直接作为 JavaScript 对象字面量嵌入(而非在字符串内部),因此无需额外转义:

comments_json = json.dumps(comments)
template = template.replace("{{.Comments}}", comments_json)
第 3 步:展开 Entries 范围块

模板在 <tbody id="entriesTableBody"> 内包含一个 {{range .Entries}}...{{end}} 块。按以下方式处理它:

  1. 提取范围块的主体,使用正则表达式。关键:该块包含嵌套的 {{end}} 标签(来自 {{if eq .Status ...}}、{{if .Checked}} 和 {{range .Messages}})。像 \{\{range \.Entries\}\}(.*?)\{\{end\}\} 这样的简单非贪婪匹配会匹配到第一个内部的 {{end}},从而截断该块。相反,应将外层的 {{end}} 锚定到紧随其后的 </tbody>:
    m = re.search(
        r'\{\{range \.Entries\}\}(.*?)\{\{end\}\}\s*</tbody>',
        template,
        re.DOTALL,
    )
    entry_body = m.group(1)  # template text for one entry iteration
    
    这样可以确保捕获完整块主体,包括全部三个 <tr> 行以及嵌套的 {{range .Messages}}...{{end}}。
  2. 遍历 report-data.json 的 Entries 数组中的每个条目。
  3. 按照下面的处理顺序,为每个条目展开块主体。
  4. 替换整个匹配内容(从 {{range .Entries}} 到 </tbody>)为拼接后的展开 HTML,后跟 </tbody>。

每个条目的处理顺序(最内层结构优先,以避免 {{end}} 混淆):

  1. 求值 {{if eq .Status ...}}...{{end}} 条件(状态徽章类和图标)。
  2. 求值 {{if .Checked}}...{{end}} 条件(消息复选框)。
  3. 展开 {{range .Messages}}...{{end}} 内部范围。
  4. 替换简单的 {{.Field}} 占位符。
条目字段映射

在范围块主体内,为每个条目替换这些占位符。由于 JSON 键与模板占位符匹配,模板占位符 {{.X}} 直接映射到 JSON 键 X:

模板占位符JSON 键(Entries[])备注
{{.Line}}Line直接为整数值
{{.IPPrefix}}IPPrefix经 HTML 转义
{{.CountryCode}}CountryCode经 HTML 转义
{{.RegionCode}}RegionCode经 HTML 转义
{{.City}}City经 HTML 转义
{{.Status}}Status经 HTML 转义
{{.HasError}}HasError小写字符串:"true" 或 "false"
{{.HasWarning}}HasWarning小写字符串:"true" 或 "false"
{{.HasSuggestion}}HasSuggestion小写字符串:"true" 或 "false"
{{.GeocodingHint}}GeocodingHint空字符串 ""
{{.DoNotGeolocate}}DoNotGeolocate"true" 或 "false"
{{.Tunable}}Tunable"true" 或 "false"
{{.TunedEntry.CountryCode}}TunedEntry.CountryCode若 TunedEntry 为空 {},则为 ""
{{.TunedEntry.RegionCode}}TunedEntry.RegionCode若 TunedEntry 为空 {},则为 ""
{{.TunedEntry.Name}}TunedEntry.Name若 TunedEntry 为空 {},则为 ""
{{.TunedEntry.H3Cells}}TunedEntry.H3Cells方括号包裹、空格分隔;若为空则为 "[]"(见下方格式)
{{.TunedEntry.BoundingBox}}TunedEntry.BoundingBox方括号包裹、空格分隔;若为空则为 "[]"(见下方格式)

data-h3-cells 和 data-bounding-box 格式: 这些不是 JSON 数组。它们是方括号包裹、空格分隔的值。不要使用 JSON 序列化(字符串元素不加引号,数字之间不加逗号)。示例:

  • [836752fffffffff 836755fffffffff] — 正确
  • ["836752fffffffff","836755fffffffff"] — 错误,引号会破坏解析
  • [-71.70 10.73 -71.52 10.55] — 正确
  • [] — 空值时的正确写法
评估状态条件

在替换简单的 {{.Field}} 占位符之前先处理这些内容 —— 否则 {{end}} 标记会被消耗掉,正则将无法匹配。

该模板使用 {{if eq .Status "..."}} 条件语句来处理状态徽章的 CSS 类和图标。通过检查条目的 status 值并仅保留匹配的分支文本来评估它们。

状态徽章行在单个行上包含两个 {{if eq .Status ...}}...{{end}} 块 —— 一个用于 CSS 类,一个用于图标。使用带回调的 re.sub 来解析所有出现:

STATUS_CSS = {"ERROR": "error", "WARNING": "warning", "SUGGESTION": "suggestion", "OK": "ok"}
STATUS_ICON = {
    "ERROR": "bi-x-circle-fill",
    "WARNING": "bi-exclamation-triangle-fill",
    "SUGGESTION": "bi-lightbulb-fill",
    "OK": "bi-check-circle-fill",
}

def resolve_status_if(match_obj, status):
    """Pick the branch matching `status` from a {{if eq .Status ...}}...{{end}} block."""
    block = match_obj.group(0)
    # Try each branch: {{if eq .Status "X"}}val{{else if ...}}val{{else}}val{{end}}
    for st, val in [("ERROR",), ("WARNING",), ("SUGGESTION",)]:
        # not needed to parse generically — just map from the known patterns
    ...

更简单的方法:由于只有两个已知模式,将它们作为字面字符串替换:

css_class = STATUS_CSS.get(status, "ok")
icon_class = STATUS_ICON.get(status, "bi-check-circle-fill")
body = body.replace(
    '{{if eq .Status "ERROR"}}error{{else if eq .Status "WARNING"}}warning{{else if eq .Status "SUGGESTION"}}suggestion{{else}}ok{{end}}',
    css_class,
)
body = body.replace(
    '{{if eq .Status "ERROR"}}bi-x-circle-fill{{else if eq .Status "WARNING"}}bi-exclamation-triangle-fill{{else if eq .Status "SUGGESTION"}}bi-lightbulb-fill{{else}}bi-check-circle-fill{{end}}',
    icon_class,
)

这完全避免了正则,且是安全的,因为这些确切的字符串在模板中逐字出现。

第 4 步:展开嵌套的 Messages 范围

{{range .Messages}}...{{end}} 块包含一个嵌套的 {{if .Checked}} checked{{else}} disabled{{end}} 条件,因此其内部的 {{end}} 会导致简单的非贪婪正则过早匹配。将正则锚定到 </td>(紧跟在 messages 范围结束 {{end}} 之后的标签)以捕获完整的块主体:

msg_match = re.search(
    r'\{\{range \.Messages\}\}(.*?)\{\{end\}\}\s*(?=</td>)',
    body, re.DOTALL
)

前瞻断言 (?=</td>) 确保正则跳过复选框条件的 {{end}}(其后跟的是 >,而非 </td>),只匹配范围结束的 {{end}}(其后跟的是空白字符然后是 </td>)。

对于条目的 Messages 数组中的每条消息,克隆捕获的块主体并展开它:

  1. 解析每条消息的复选框条件(必须在简单占位符替换之前执行,以移除嵌套的 {{end}}):

    if msg.get("Checked"):
        msg_body = msg_body.replace(
            '{{if .Checked}} checked{{else}} disabled{{end}}', ' checked'
        )
    else:
        msg_body = msg_body.replace(
            '{{if .Checked}} checked{{else}} disabled{{end}}', ' disabled'
        )
    
  2. 替换消息字段占位符:

    模板占位符来源备注
    {{.ID}}Messages[i].ID来自 JSON 的直接字符串值
    {{.Text}}Messages[i].Text已进行 HTML 转义
  3. 拼接所有展开的消息块,并用结果替换原始的 {{range .Messages}}...{{end}} 匹配(msg_match.group(0)):

    body = body[:msg_match.start()] + "".join(expanded_msgs) + body[msg_match.end():]
    

如果 Messages 为空,则将整个匹配区域替换为空字符串(没有消息 div——只保留 issues 头部)。

输出保证
  • 报告必须能在任何现代浏览器中读取,除模板中已有的 CDN 链接(leaflet、h3-js、bootstrap-icons、Raleway 字体)外不需要额外的网络依赖。
  • 所有嵌入 HTML 的值必须进行 HTML 转义(<、>、&、")以防止渲染问题。
  • commentMap 作为直接的 JavaScript 对象字面量嵌入(不在字符串内部),因此不需要 JS 字符串转义——只需输出有效的 JSON。
  • 所有值必须仅来源于分析输出,不得通过启发式方法重新计算。
阶段 6:最终审查

在向用户展示结果之前,使用具体、可检查的断言执行一次最终验证。

检查 1 — 条目数完整性

  • 统计原始输入 CSV 中非注释、非空的数据行。
  • 断言:len(entries) in report-data.json == data_row_count
  • 失败时:Row count mismatch: input has {N} data rows but report contains {M} entries.

检查 2 — 摘要计数器完整性

  • 这些计数器基于布尔标志采用互斥方式,与最高严重级别的 Status 字段保持一致。同时具有 HasError: true 和 HasWarning: true 的条目只计入 Errors,绝不计入 Warnings。这等同于按条目的 Status 字段计数。
  • 断言以下所有条件;在生成报告之前修正任何未通过的条件:
    • Errors == sum(1 for e in Entries if e['HasError'])
    • Warnings == sum(1 for e in Entries if e['HasWarning'] and not e['HasError'])
    • Suggestions == sum(1 for e in Entries if e['HasSuggestion'] and not e['HasError'] and not e['HasWarning'])
    • OK == sum(1 for e in Entries if not e['HasError'] and not e['HasWarning'] and not e['HasSuggestion'])
    • Errors + Warnings + Suggestions + OK == TotalEntries - InvalidEntries

检查 3 — 准确度分桶完整性

  • 断言:CityLevelAccuracy + RegionLevelAccuracy + CountryLevelAccuracy + DoNotGeolocate == TotalEntries - InvalidEntries
  • 注意: 阶段 3 中定义的准确度分桶说明为“不要计入 HasError: true 的条目”,但上述检查 3 的公式使用 TotalEntries - InvalidEntries(其中仍包含 ERROR 条目)。这意味着 ERROR 条目(那些成功解析为有效 IP 但未通过验证的条目)会根据其地理字段的存在情况计入准确度分桶。只有 InvalidEntries(无法解析的 IP 前缀)被排除。以检查 3 的公式为权威规则。
  • 失败时,在继续之前追踪并修复分桶逻辑。

检查 4 — 无重复行号

  • 断言:Entries 中的所有 Line 值均唯一。
  • 失败时,向用户报告重复的行号。

检查 5 — TunedEntry 完整性

  • 断言:Entries 中的每个对象都具有 TunedEntry 键(即使其值为 {})。
  • 失败时,为任何缺少该键的条目添加 "TunedEntry": {},然后重新保存 report-data.json。

检查 6 — 报告文件存在且非空

  • 确认 ./run/report/geofeed-report.html 已写入且文件大小大于零字节。
  • 失败时,在向用户展示之前重新生成报告。

评论

支持 Markdown