重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
geofeed-tuner by github/awesome-copilot
npx skills add https://github.com/github/awesome-copilot --skill geofeed-tuner此技能通过以下方式帮助您创建和改进 CSV 格式的 IP 地理位置数据源:
此技能在分发文件(只读)和工作文件(运行时生成)之间采用清晰的分离。
以下目录包含静态分发资源。请勿在这些目录中创建、修改或删除文件:
| 目录 | 用途 |
|---|
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
assets/ | 静态数据文件(ISO 代码、示例) |
references/ | 参考用的 RFC 规范和代码片段 |
scripts/ | 可执行代码和报告的 HTML 模板文件 |
所有生成的文件、临时文件和输出文件都放在这些目录中:
| 目录 | 用途 |
|---|---|
run/ | 所有代理生成内容的工作目录 |
run/data/ | 从远程 URL 下载的 CSV 文件 |
run/report/ | 生成的 HTML 调整报告 |
assets/、references/ 或 scripts/ —— 这些是技能分发的一部分,必须保持不变。./run/data/。./run/report/。./run/。run/ 目录可能在会话之间被清空;请勿在其中存储永久数据。./run/ 中生成的脚本必须以技能根目录(包含 SKILL.md 的目录)作为当前工作目录执行,以便像 assets/iso3166-1.json 和 ./run/data/report-data.json 这样的相对路径能够正确解析。在运行脚本之前,请勿 cd 进入 ./run/。所有阶段必须按顺序执行,从阶段 1 到阶段 6。每个阶段都依赖于前一阶段的成功完成。例如,结构检查必须在质量分析运行之前完成。
各阶段总结如下。代理必须遵循每个阶段部分中概述的详细步骤。
| 阶段 | 名称 | 描述 |
|---|---|---|
| 1 | 理解标准 | 回顾 RFC 8805 对自发布 IP 地理位置数据源的关键要求 |
| 2 | 收集输入 | 从本地文件或远程 URL 收集 IP 子网数据 |
| 3 | 检查与建议 | 验证 CSV 结构、分析 IP 前缀并检查数据质量 |
| 4 | 调整数据查询 | 使用 Fastah 的 MCP 工具检索用于改进地理位置准确性的调整数据 |
| 5 | 生成调整报告 | 创建总结分析和建议的 HTML 报告 |
| 6 | 最终审查 | 验证报告数据的一致性和完整性 |
请勿跳过任何阶段。 每个阶段都提供了后续阶段所需的关键检查或数据转换。
在执行每个阶段之前,代理必须生成一个可见的待办事项清单。
计划必须:
RFC 8805 中此技能强制执行的关键要求总结如下。请将此摘要作为您的工作参考。 仅在处理边缘情况、模糊情况或用户提出此处未涵盖的标准问题时,才查阅完整的 RFC 8805 文本。
目的: 自发布的 IP 地理位置数据源允许网络运营商以简单的 CSV 格式发布其 IP 地址空间的权威位置数据,使地理位置提供商能够整合运营商提供的修正。
CSV 列顺序(第 2.1.1.1–2.1.1.5 节):
| 列 | 字段 | 必需 | 备注 |
|---|---|---|---|
| 1 | ip_prefix | 是 | CIDR 表示法;IPv4 或 IPv6;必须是网络地址 |
| 2 | alpha2code | 否 | ISO 3166-1 alpha-2 国家代码;空或 "ZZ" = 请勿定位 |
| 3 | region | 否 | ISO 3166-2 细分代码(例如 US-CA) |
| 4 | city | 否 | 自由文本城市名称;无权威验证集 |
| 5 | postal_code | 否 | 已弃用 —— 必须留空或省略 |
结构规则:
# 开头的注释行(包括标题行,如果存在)。# 开头,则被视为注释。192.168.1.1/24 无效;应使用 192.168.1.0/24)。请勿定位: 如果条目的 alpha2code 为空或不区分大小写的 ZZ(无论 region/city 的值如何),则明确表示运营商不希望对该前缀应用地理位置定位。
邮政编码已弃用(第 2.1.1.5 节): 第五列不得包含邮政编码或 ZIP 码。它们对于 IP 范围映射来说过于精细,并引发隐私担忧。
inetnum 或 inet6num),请提示他们提供。接受的输入格式:
./run/data/。数据源 URL 无法访问:HTTP {status_code}。请确认该 URL 是公开可访问的。UnicodeDecodeError,则尝试 utf-8-sig(带 BOM 的 UTF-8),然后是 latin-1。无法解码输入文件。请将其保存为 UTF-8 格式后重试。./run/data/report-data.json以下 JSON 结构在阶段 3 期间是不可变的。阶段 4 稍后将在 Entries 中的每个对象上添加一个 TunedEntry 对象——这是唯一允许的模式扩展,并且发生在单独的阶段。
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:来自下方验证规则参考表的字符串标识符(例如 "1101"、"3301")。Type:严重性类型:"ERROR"、"WARNING" 或 "SUGGESTION"。Text:人类可读的验证消息字符串。Checked:如果验证规则可自动调整(参考表中 Tunable: true),则为 ,否则为 。控制报告中复选框是 还是 。HasError:如果任何消息的 Type 为 "ERROR",则为 true。HasWarning:如果任何消息的 Type 为 "WARNING",则为 true。HasSuggestion:如果任何消息的 Type 为 "SUGGESTION",则为 true。DoNotGeolocate(条目):如果 CountryCode 为空或为 "ZZ",则为 true——该条目是明确的请勿定位信号。GeocodingHint:在阶段 3 中始终为空字符串 ""。保留供将来使用。Tunable:如果条目中任何消息的 Checked: true,则为 true。计算为所有消息 Checked 值的逻辑或。此标志驱动报告中“调整”按钮的可见性。向条目添加消息时,请使用此表中的 ID、Type、Text 和 Checked 值。
| ID | 类型 | 文本 | Checked | 条件参考 |
|---|---|---|---|---|
1101 | ERROR | IP 前缀为空 | false | IP 前缀分析:空 |
1102 | ERROR | 无效的 IP 前缀:无法解析为 IPv4 或 IPv6 网络 | false | IP 前缀分析:语法无效 |
1103 | ERROR | RFC 8805 数据源中不允许非公共 IP 范围 | false | IP 前缀分析:非公共 |
3101 | SUGGESTION | IPv4 前缀异常大,可能表示输入错误 | false | IP 前缀分析:IPv4 < /22 |
3102 | SUGGESTION | IPv6 前缀异常大,可能表示输入错误 | false | IP 前缀分析:IPv6 < /64 |
1201 | ERROR | 无效的国家代码:不是有效的 ISO 3166-1 alpha-2 值 | true | 国家代码分析:无效 |
1301 | ERROR | 无效的区域格式;应为 国家-细分(例如 US-CA) | true | 区域代码分析:格式错误 |
1302 | ERROR | 无效的区域代码:不是有效的 ISO 3166-2 细分 | true | 区域代码分析:未知代码 |
1303 | ERROR | 区域代码与指定的国家代码不匹配 | true | 区域代码分析:不匹配 |
1401 | ERROR | 无效的城市名称:不允许使用占位符值 | false | 城市名称分析:占位符 |
1402 | ERROR | 无效的城市名称:检测到缩写或基于代码的值 | true | 城市名称分析:缩写 |
2401 | WARNING | 城市名称格式不一致;请考虑规范化该值 | true | 城市名称分析:格式 |
1501 | ERROR | 邮政编码已被 RFC 8805 弃用,出于隐私原因必须移除 | true | 邮政编码检查 |
3301 | SUGGESTION | 对于小领土,区域通常是不必要的;请考虑移除区域值 | true | 调整:小领土区域 |
3402 | SUGGESTION | 对于小领土,城市级别的粒度通常是不必要的;请考虑移除城市值 | true | 调整:小领土城市 |
3303 | SUGGESTION | 指定城市时建议提供区域代码;请从下拉列表中选择一个区域 | true | 调整:有城市但缺少区域 |
3104 | SUGGESTION | 请确认此子网是否被有意标记为请勿定位或缺少位置数据 | true | 调整:未指定的地理位置 |
当验证检查匹配时,使用参考表中的值向条目的 Messages 数组添加一条消息:
entry["Messages"].append({
"ID": "1201", # 来自表格
"Type": "ERROR", # 来自表格
"Text": "Invalid country code: not a valid ISO 3166-1 alpha-2 value", # 来自表格
"Checked": True # 来自表格 (True = 可调整)
})
填充条目的所有消息后,推导条目级别的标志:
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(条目)为 true | DoNotGeolocate(元数据) |
请勿将 HasError: true 的条目或 InvalidEntries 中的条目计入任何准确性桶。
代理不得:
如果某个值未知,请留空——切勿编造数据。
此阶段验证您的数据源格式是否正确且可解析。关键的结构性错误必须在调整器能够分析地理位置质量之前解决。
本小节定义了用于 IP 地理位置数据源的 CSV 格式输入文件的规则。目标是确保文件能够被可靠地解析并规范化为一致的内部表示。
pandas 可用,请使用它进行 CSV 解析。csv 模块。ip_prefix, alpha2code, region, city, postal code (deprecated)
assets/example/01-user-input-rfc8805-feed.csv# 开头的注释行。# 开头,也会被移除。./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。IPPrefix 字段是否存在且非空。IPPrefix 值。检测到重复的 IP 前缀:{ip_prefix_value} 出现在第 {line_numbers} 行references/ 文件夹中的代码片段干净地解析为 IPv4 或 IPv6 网络。/32。/128。1102is_private 和相关地址属性检测非公共范围,如 ./references 中所示。1103/643102/223101分析地理位置数据的准确性和一致性:
此阶段在结构检查通过后运行。
ISO3166-1 进行检查。
alpha_2:两位字母国家代码name:简短国家名称flag:旗帜表情符号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 优先——请勿基于小领土标志抑制它。CountryCode 存在但未在 alpha_2 集合中找到1201CountryCode、RegionCode、City)均为空。DoNotGeolocate 设置为 true。CountryCode 设置为 ZZ。3104ISO3166-2 进行检查。
code:以国家代码为前缀的细分代码(例如 US-CA)name:简短的细分名称RegionCode 值的超集。RegionCode 值(RFC 8805 第 2.1.1.3 节):
{COUNTRY}-{SUBDIVISION}(例如 US-CA、AU-NSW)。code 属性(已带有国家代码前缀)检查该值。RegionCode 值等于条目的 CountryCode(例如,新加坡的 SG 同时作为国家和区域),则将区域视为可接受的——跳过此条目的所有区域验证检查。小领土实际上是城邦,没有有意义的 ISO 3166-2 行政细分。RegionCode 不匹配 {COUNTRY}-{SUBDIVISION} 并且小领土例外不适用1301RegionCode 值未在 code 集合中找到 并且小领土例外不适用1302RegionCode 的国家部分与 CountryCode 不匹配undefinedPlease selectnullN/ATBDunknown1401LAFrftsin01LHRSINMAA1402HongKong 与 Hong Kong24011501此阶段应用超越 RFC 8805 的建议性推荐,这些推荐来自实际的地理数据源部署经验,旨在提高准确性和可用性。
RegionCode 非空 或City 非空。3301(针对区域),3402(针对城市)City 非空RegionCode 为空3303使用 Fastah 的 rfc8805-row-place-search 工具查询所有 Entries。
TunedEntry: {}。请勿阻止报告生成。明确通知用户:调整数据查询不可用;报告将仅显示验证结果。从以下位置加载数据集:./run/data/report-data.json
Entries 数组。每个条目将用于构建 MCP 查询有效负载。通过去重相同的条目来减少服务器请求:
Entries 中的每个条目,计算内容哈希(CountryCode + RegionCode + City 的哈希值)。{ contentHash -> { rowKey, payload, entryIndices: [] } }。rowKey 是发送到 MCP 服务器以匹配响应的 UUID。Entries 中的0 起始数组索引附加到该去重条目的 entryIndices 数组中。构建请求批次:
[{ rowKey, payload, entryIndices }, ...] 的结构,以便通过 rowKey 将响应匹配回来。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。规则:
mcp.json 样式配置如下: "fastah-ip-geofeed": {
"type": "http",
"url": "https://mcp.fastah.ai/mcp"
}
https://mcp.fastah.ai/mcptools/call 之前,代理必须发送一个 tools/list 请求来读取 rfc8805-row-place-search 的输入和输出模式。使用发现的模式作为字段名、类型和约束的权威来源。tools/list 返回的模式:
[
{"rowKey": "550e8400-...", "countryCode":"CA", ...},
{"rowKey": "690e9301-...", "countryCode":"ZZ", ...}
]
rowKey 字段,用于映射回来。rowKey。rowKey 关联的 entryIndices 数组。entryIndices 中的每个索引,将最佳匹配附加到 Entries[index]。如果受影响条目上不存在该字段,则创建该字段。将 MCP API 响应键重新映射到 Go 结构体字段名:
"TunedEntry": {
"Name": "",
"CountryCode": "",
"RegionCode": "",
"PlaceType": "",
"H3Cells": [],
"BoundingBox": []
}
TunedEntry 字段
This skill helps you create and improve IP geolocation feeds in CSV format by:
This skill uses a clear separation between distribution files (read-only) and working files (generated at runtime).
The following directories contain static distribution assets. Do not create, modify, or delete files in these directories:
| Directory | Purpose |
|---|---|
assets/ | Static data files (ISO codes, examples) |
references/ | RFC specifications and code snippets for reference |
scripts/ | Executable code and HTML template files for reports |
All generated, temporary, and output files go in these directories:
| Directory | Purpose |
|---|---|
run/ | Working directory for all agent-generated content |
run/data/ | Downloaded CSV files from remote URLs |
run/report/ | Generated HTML tuning reports |
assets/, references/, or scripts/ — these are part of the skill distribution and must remain unchanged../run/data/../run/report/../run/.run/ directory may be cleared between sessions; do not store permanent data there../run/ must be executed with the skill root directory (the directory containing ) as the current working directory, so that relative paths like and resolve correctly. Do not into before running scripts.All phases must be executed in order , from Phase 1 through Phase 6. Each phase depends on the successful completion of the previous phase. For example, structure checks must complete before quality analysis can run.
The phases are summarized below. The agent must follow the detailed steps outlined further in each phase section.
| Phase | Name | Description |
|---|---|---|
| 1 | Understand the Standard | Review the key requirements of RFC 8805 for self-published IP geolocation feeds |
| 2 | Gather Input | Collect IP subnet data from local files or remote URLs |
| 3 | Checks & Suggestions | Validate CSV structure, analyze IP prefixes, and check data quality |
| 4 | Tuning Data Lookup | Use Fastah's MCP tool to retrieve tuning data for improving geolocation accuracy |
| 5 | Generate Tuning Report | Create an HTML report summarizing the analysis and suggestions |
| 6 | Final Review | Verify consistency and completeness of the report data |
Do not skip phases. Each phase provides critical checks or data transformations required by subsequent stages.
Before executing each phase, the agent MUST generate a visible TODO checklist.
The plan MUST:
The key requirements from RFC 8805 that this skill enforces are summarized below. Use this summary as your working reference. Only consult the full RFC 8805 text for edge cases, ambiguous situations, or when the user asks a standards question not covered here.
Purpose: A self-published IP geolocation feed lets network operators publish authoritative location data for their IP address space in a simple CSV format, allowing geolocation providers to incorporate operator-supplied corrections.
CSV Column Order (Sections 2.1.1.1–2.1.1.5):
| Column | Field | Required | Notes |
|---|---|---|---|
| 1 | ip_prefix | Yes | CIDR notation; IPv4 or IPv6; must be a network address |
| 2 | alpha2code | No | ISO 3166-1 alpha-2 country code; empty or "ZZ" = do-not-geolocate |
| 3 | region | No | ISO 3166-2 subdivision code (e.g., US-CA) |
| 4 | city |
Structural rules:
# (including the header, if present).#.192.168.1.1/24 is invalid; use 192.168.1.0/24).Do-not-geolocate: An entry with an empty alpha2code or case-insensitive ZZ (irrespective of values of region/city) is an explicit signal that the operator does not want geolocation applied to that prefix.
Postal codes deprecated (Section 2.1.1.5): The fifth column must not contain postal or ZIP codes. They are too fine-grained for IP-range mapping and raise privacy concerns.
If the user has not already provided a list of IP subnets or ranges (sometimes referred to as inetnum or inet6num), prompt them to supply it. Accepted input formats:
If the input is a remote URL :
./run/data/ before processing.Feed URL is not reachable: HTTP {status_code}. Please verify the URL is publicly accessible.If the input is a local file , process it directly without downloading.
Encoding detection and normalization:
UnicodeDecodeError is raised, try (UTF-8 with BOM), then ../run/data/report-data.jsonThe JSON structure below is IMMUTABLE during Phase 3. Phase 4 will later add a TunedEntry object to each object in Entries — this is the only permitted schema extension and happens in a separate phase.
JSON keys map directly to template placeholders like {{.CountryCode}}, {{.HasError}}, etc.
{
"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
}
]
}
Field definitions:
Top-level metadata:
InputFile: The original input source, either a local filename or a remote URL.Timestamp: Milliseconds since Unix epoch when the tuning was performed.TotalEntries: Total number of data rows processed (excluding comment and blank lines).IpV4Entries: Count of entries that are IPv4 subnets.IpV6Entries: Count of entries that are IPv6 subnets.InvalidEntries: Count of entries that failed IP prefix parsing and CSV parsing.Errors: Total entries whose Status is ERROR.Warnings: Total entries whose is .Entry fields:
Entries: Array of objects, one per data row, with the following per-entry fields:
Line: 1-based line number in the original CSV (counting all lines including comments and blanks).IPPrefix: The normalized IP prefix in CIDR slash notation.CountryCode: The ISO 3166-1 alpha-2 country code, or empty string.RegionCode: The ISO 3166-2 region code (e.g., US-CA), or empty string.City: The city name, or empty string.Status: Highest severity assigned: ERROR > > > .When adding messages to an entry, use the ID, Type, Text, and Checked values from this table.
| ID | Type | Text | Checked | Condition Reference |
|---|---|---|---|---|
1101 | ERROR | IP prefix is empty | false | IP Prefix Analysis: empty |
1102 | ERROR | Invalid IP prefix: unable to parse as IPv4 or IPv6 network | false | IP Prefix Analysis: invalid syntax |
When a validation check matches, add a message to the entry's Messages array using the values from the reference table:
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)
})
After populating all messages for an entry, derive the entry-level flags:
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"])
Accuracy levels are mutually exclusive. Assign each valid (non-ERROR, non-invalid) entry to exactly one bucket based on the most granular non-empty geo field:
| Condition | Bucket |
|---|---|
City is non-empty | CityLevelAccuracy |
RegionCode non-empty AND City is empty | RegionLevelAccuracy |
CountryCode non-empty, RegionCode and City empty | CountryLevelAccuracy |
Do not count entries with HasError: true or entries in InvalidEntries in any accuracy bucket.
The agent MUST NOT:
If a value is unknown, leave it empty — never invent data.
This phase verifies that your feed is well-formed and parseable. Critical structural errors must be resolved before the tuner can analyze geolocation quality.
This subsection defines rules for CSV-formatted input files used for IP geolocation feeds. The goal is to ensure the file can be parsed reliably and normalized into a consistent internal representation.
CSV Structure Checks
If pandas is available, use it for CSV parsing.
Otherwise, fall back to Python's built-in csv module.
Ensure the CSV contains exactly 4 or 5 logical columns.
Comment lines are allowed.
A header row may or may not be present.
If no header row exists, assume the implicit column order:
ip_prefix, alpha2code, region, city, postal code (deprecated)
Refer to the example input file: assets/example/01-user-input-rfc8805-feed.csv
CSV Cleansing and Normalization
Check that the IPPrefix field is present and non-empty for each entry.
Check for duplicate IPPrefix values across entries.
If duplicates are found, stop the skill and report to the user with the message: Duplicate IP prefix detected: {ip_prefix_value} appears on lines {line_numbers}
If no duplicates are found, continue with the analysis.
Checks
references/ folder./32./128.Analyze the accuracy and consistency of geolocation data:
This phase runs after structural checks pass.
Use the locally available data table ISO3166-1 for checking.
alpha_2: two-letter country codename: short country nameflag: flag emojiCountryCode values for an RFC 8805 CSV.Check the entry's CountryCode (RFC 8805 Section 2.1.1.2, column alpha2code) against the alpha_2 attribute.
Use the locally available data table ISO3166-2 for checking.
code: subdivision code prefixed with country code (e.g., US-CA)name: short subdivision nameRegionCode values for an RFC 8805 CSV.If a RegionCode value is provided (RFC 8805 Section 2.1.1.3):
{COUNTRY}-{SUBDIVISION} (e.g., , ).City names are validated using heuristic checks only.
There is currently no authoritative dataset available for validating city names.
ERROR
Report the following conditions as ERROR :
Placeholder or non-meaningful values
undefinedPlease selectnullN/ATBDunknownRFC 8805 Section 2.1.1.5 explicitly deprecates postal or ZIP codes.
Postal codes can represent very small populations and are not considered privacy-safe for mapping IP address ranges, which are statistical in nature.
ERROR
1501This phase applies opinionated recommendations beyond RFC 8805, learned from real-world geofeed deployments, that improve accuracy and usability.
Report the following conditions as SUGGESTION :
Region or city specified for small territory
RegionCode is non-empty ORCity is non-empty.3301 (for region), 3402 (for city)Missing region code when city is specified
City is non-emptyRegionCode is emptyLookup all the Entries using Fastah's rfc8805-row-place-search tool.
TunedEntry: {} for all affected entries. Do not block report generation. Notify the user clearly: Tuning data lookup unavailable; the report will show validation results only.Load the dataset from: ./run/data/report-data.json
Entries array. Each entry will be used to build the MCP lookup payload.Reduce server requests by deduplicating identical entries:
Entries, compute a content hash (hash of CountryCode + RegionCode + City).{ contentHash -> { rowKey, payload, entryIndices: [] } }. rowKey is a UUID that will be sent to the MCP server for matching responses.Entries to that deduplication entry's entryIndices array.Build request batches:
Extract unique deduplicated entries from the map, keeping them in deduplication order.
Build request batches of up to 1000 items each.
For each batch, keep an in-memory structure like [{ rowKey, payload, entryIndices }, ...] to match responses back by rowKey.
When writing the MCP payload file, include the rowKey field with each payload object:
[ {"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"} ]
When reading responses, match each response rowKey field to the corresponding deduplication entry to retrieve all associated entryIndices.
Rules:
An example mcp.json style configuration of Fastah MCP server is as follows:
"fastah-ip-geofeed": {
"type": "http",
"url": "https://mcp.fastah.ai/mcp"
}
Server: https://mcp.fastah.ai/mcp
Tool and its Schema: before the first tools/call, the agent MUST send a tools/list request to read the input and output schema for rfc8805-row-place-search. Use the discovered schema as the authoritative source for field names, types, and constraints.
The following is an illustrative example only; always defer to the schema returned by tools/list:
[
{"rowKey": "550e8400-...", "countryCode":"CA", ...},
{"rowKey": "690e9301-...", "countryCode":"ZZ", ...}
]
rowKey from the response.entryIndices array associated with that rowKey from the deduplication map.entryIndices, attach the best match to Entries[index].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": []
}
The TunedEntry field is a single object (not an array). It holds the best match from the MCP server.
MCP response key → JSON key mapping :
| MCP API response key | JSON key |
|---|---|
placeName | Name |
countryCode | CountryCode |
stateCode | RegionCode |
placeType | PlaceType |
Entries with no UUID match (i.e. the MCP server returned no response for their UUID) must receive an empty TunedEntry: {} object — never leave the field absent.
Generate a self-contained HTML report by rendering the template at ./scripts/templates/index.html with data from ./run/data/report-data.json and ./run/data/comments.json.
Write the completed report to ./run/report/geofeed-report.html. After generating, attempt to open it in the system's default browser (e.g., webbrowser.open()). If running in a headless environment, CI pipeline, or remote container where no browser is available, skip the browser step and instead present the file path to the user so they can open or download it.
The template uses Gohtml/template syntax ({{.Field}}, {{range}}, {{if eq}}, etc.). Write a Python script that reads the template, builds a rendering context from the JSON data files, and processes the template placeholders to produce final HTML. Do not modify the template file itself — all processing happens in the Python script at render time.
Replace each {{.Metadata.X}} placeholder in the template with the corresponding value from report-data.json. Since JSON keys match the template placeholder, the mapping is direct — {{.Metadata.InputFile}} maps to the InputFile JSON key, etc.
| Template placeholder | JSON key (report-data.json) |
|---|---|
{{.Metadata.InputFile}} | InputFile |
{{.Metadata.Timestamp}} | Timestamp |
{{.Metadata.TotalEntries}} | TotalEntries |
{{.Metadata.IpV4Entries}} |
Note on{{.Metadata.Timestamp}}: This placeholder appears inside a JavaScript new Date(...) call. Replace it with the raw integer value (no HTML escaping needed for a numeric literal inside <script>). All other metadata values should be HTML-escaped since they appear inside HTML element text.
Locate this pattern in the template:
const commentMap = {{.Comments}};
Replace {{.Comments}} with the serialized JSON object from ./run/data/comments.json. The JSON is embedded directly as a JavaScript object literal (not inside a string), so no extra escaping is needed:
comments_json = json.dumps(comments)
template = template.replace("{{.Comments}}", comments_json)
The template contains a {{range .Entries}}...{{end}} block inside <tbody id="entriesTableBody">. Process it as follows:
Extract the range block body using regex. Critical: The block contains nested {{end}} tags (from {{if eq .Status ...}}, {{if .Checked}}, and {{range .Messages}}). A naive non-greedy match like \{\{range \.Entries\}\}(.*?)\{\{end\}\} will match the first inner {{end}}, truncating the block. Instead, anchor the outer {{end}} to the </tbody> that follows it:
m = re.search(
r'\{\{range \.Entries\}\}(.*?)\{\{end\}\}\s*</tbody>',
template,
re.DOTALL,
)
entry_body = m.group(1) # template text for one entry iteration
This ensures you capture the full block body including all three <tr> rows and the nested {{range .Messages}}...{{end}}.
2. Iterate over each entry in report-data.json's Entries array.
3. Expand the block body for each entry using the processing order below.
4. Replace the entire match (from {{range .Entries}} through </tbody>) with the concatenated expanded HTML followed by </tbody>.
Processing order for each entry (innermost constructs first to avoid {{end}} confusion):
{{if eq .Status ...}}...{{end}} conditionals (status badge class and icon).{{if .Checked}}...{{end}} conditional (message checkbox).{{range .Messages}}...{{end}} inner range.{{.Field}} placeholders.Within the range block body, replace these placeholders for each entry. Since JSON keys match the template placeholder, the template placeholder {{.X}} maps directly to JSON key X:
| Template placeholder | JSON key (Entries[]) | Notes |
|---|---|---|
{{.Line}} | Line | Direct integer value |
{{.IPPrefix}} | IPPrefix | HTML-escaped |
{{.CountryCode}} | CountryCode | HTML-escaped |
data-h3-cells and data-bounding-box format: These are NOT JSON arrays. They are bracket-wrapped, space-separated values. Do not use JSON serialization (no quotes around string elements, no commas between numbers). Examples:
[836752fffffffff 836755fffffffff] — correct["836752fffffffff","836755fffffffff"] — WRONG , quotes will break parsing[-71.70 10.73 -71.52 10.55] — correct[] — correct for emptyProcess these BEFORE replacing simple{{.Field}} placeholders — otherwise the {{end}} markers get consumed and the regex won't match.
The template uses {{if eq .Status "..."}} conditionals for the status badge CSS class and icon. Evaluate these by checking the entry's status value and keeping only the matching branch text.
The status badge line contains two {{if eq .Status ...}}...{{end}} blocks on a single line — one for the CSS class, one for the icon. Use re.sub with a callback to resolve all occurrences:
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
...
A simpler approach: since there are exactly two known patterns, replace them as literal strings:
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,
)
This avoids regex entirely and is safe because these exact strings appear verbatim in the template.
The {{range .Messages}}...{{end}} block contains a nested {{if .Checked}} checked{{else}} disabled{{end}} conditional, so its inner {{end}} would cause a simple non-greedy regex to match too early. Anchor the regex to </td> (the tag immediately after the messages range closing {{end}}) to capture the full block body:
msg_match = re.search(
r'\{\{range \.Messages\}\}(.*?)\{\{end\}\}\s*(?=</td>)',
body, re.DOTALL
)
The lookahead (?=</td>) ensures the regex skips past the checkbox conditional's {{end}} (which is followed by >, not </td>) and matches only the range-closing {{end}} (which is followed by whitespace then </td>).
For each message in the entry's Messages array, clone the captured block body and expand it:
Resolve the checkbox conditional per message (must happen before simple placeholder replacement to remove the nested {{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'
)
Replace message field placeholders :
| Template placeholder | Source | Notes |
|---|---|---|
{{.ID}} | Messages[i].ID | Direct string value from JSON |
{{.Text}} | Messages[i].Text | HTML-escaped |
Concatenate all expanded message blocks and replace the original {{range .Messages}}...{{end}} match (msg_match.group(0)) with the result:
body = body[:msg_match.start()] + "".join(expanded_msgs) + body[msg_match.end():]
If Messages is empty, replace the entire matched region with an empty string (no message divs — only the issues header remains).
leaflet, h3-js, bootstrap-icons, Raleway font).<, >, &, ") to prevent rendering issues.commentMap is embedded as a direct JavaScript object literal (not inside a string), so no JS string escaping is needed — just emit valid JSON.Perform a final verification pass using concrete, checkable assertions before presenting results to the user.
Check 1 — Entry count integrity
len(entries) in report-data.json == data_row_countRow count mismatch: input has {N} data rows but report contains {M} entries.Check 2 — Summary counter integrity
Status field. An entry with both HasError: true and HasWarning: true is counted only in Errors, never in Warnings. This is equivalent to counting by the entry's Status field.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'])Check 3 — Accuracy bucket integrity
CityLevelAccuracy + RegionLevelAccuracy + CountryLevelAccuracy + DoNotGeolocate == TotalEntries - InvalidEntriesHasError: true", but the Check 3 formula above uses TotalEntries - InvalidEntries (which still includes ERROR entries). This means ERROR entries (those that parsed as valid IPs but failed validation) are counted in accuracy buckets by their geo-field presence. Only InvalidEntries (unparsable IP prefixes) are excluded. Follow the Check 3 formula as the authoritative rule.Check 4 — No duplicate line numbers
Line values in Entries are unique.Check 5 — TunedEntry completeness
Entries has a TunedEntry key (even if its value is {})."TunedEntry": {} to any entry missing the key, then re-save report-data.json.Check 6 — Report file is present and non-empty
./run/report/geofeed-report.html was written and has a file size greater than zero bytes.Weekly Installs
53
Repository
GitHub Stars
27.0K
First Seen
2 days ago
Security Audits
Gen Agent Trust HubWarnSocketPassSnykWarn
Installed on
codex51
claude-code50
gemini-cli50
opencode50
warp49
amp49
Azure 升级评估与自动化工具 - 轻松迁移 Functions 计划、托管层级和 SKU
124,500 周安装
Next.js App Router 基础教程:从 Pages Router 迁移到现代路由架构
1,900 周安装
文本优化器:基于41条规则减少提示词和文档令牌数量20-40%,降低LLM API成本
1,900 周安装
FastAPI 生产级模板:Pydantic v2 + SQLAlchemy 2.0 异步 + JWT 认证实战
2,000 周安装
AI代码审查专家 | 自动检测安全漏洞、性能问题与代码质量
2,100 周安装
数据分析师技能:精通SQL、Python pandas和统计分析的AI助手
2,100 周安装
Agent Email CLI - 智能体邮件命令行工具,安全操作邮箱工作流
2,100 周安装
truefalsecheckeddisabled1303SKILL.mdassets/iso3166-1.json./run/data/report-data.jsoncd./run/| No |
| Free-text city name; no authoritative validation set |
| 5 | postal_code | No | Deprecated — must be left empty or absent |
utf-8-siglatin-1Unable to decode input file. Please save it as UTF-8 and try again.StatusWARNINGOK: Total entries whose Status is OK.Suggestions: Total entries whose Status is SUGGESTION.CityLevelAccuracy: Count of valid entries where City is non-empty.RegionLevelAccuracy: Count of valid entries where RegionCode is non-empty and City is empty.CountryLevelAccuracy: Count of valid entries where CountryCode is non-empty, RegionCode is empty, and City is empty.DoNotGeolocate (metadata): Count of valid entries where CountryCode, RegionCode, and City are all empty.WARNINGSUGGESTIONOKIPVersion: "IPv4" or "IPv6" based on the parsed IP prefix.Messages: Array of message objects, each with:
ID: String identifier from the Validation Rules Reference table below (e.g., "1101", "3301").Type: The severity type: "ERROR", "WARNING", or "SUGGESTION".Text: The human-readable validation message string.Checked: true if the validation rule is auto-tunable (Tunable: true in the reference table), false otherwise. Controls whether the checkbox in the report is checked or disabled.HasError: true if any message has Type "ERROR".HasWarning: true if any message has Type "WARNING".HasSuggestion: true if any message has Type "SUGGESTION".DoNotGeolocate (entry): true if CountryCode is empty or "ZZ" — the entry is an explicit do-not-geolocate signal.GeocodingHint: Always empty string "" in Phase 3. Reserved for future use.Tunable: true if any message in the entry has Checked: true. Computed as logical OR across all messages' Checked values. This flag drives the "Tune" button visibility in the report.1103 | ERROR | Non-public IP range is not allowed in an RFC 8805 feed | false | IP Prefix Analysis: non-public |
3101 | SUGGESTION | IPv4 prefix is unusually large and may indicate a typo | false | IP Prefix Analysis: IPv4 < /22 |
3102 | SUGGESTION | IPv6 prefix is unusually large and may indicate a typo | false | IP Prefix Analysis: IPv6 < /64 |
1201 | ERROR | Invalid country code: not a valid ISO 3166-1 alpha-2 value | true | Country Code Analysis: invalid |
1301 | ERROR | Invalid region format; expected COUNTRY-SUBDIVISION (e.g., US-CA) | true | Region Code Analysis: bad format |
1302 | ERROR | Invalid region code: not a valid ISO 3166-2 subdivision | true | Region Code Analysis: unknown code |
1303 | ERROR | Region code does not match the specified country code | true | Region Code Analysis: mismatch |
1401 | ERROR | Invalid city name: placeholder value is not allowed | false | City Name Analysis: placeholder |
1402 | ERROR | Invalid city name: abbreviated or code-based value detected | true | City Name Analysis: abbreviation |
2401 | WARNING | City name formatting is inconsistent; consider normalizing the value | true | City Name Analysis: formatting |
1501 | ERROR | Postal codes are deprecated by RFC 8805 and must be removed for privacy reasons | true | Postal Code Check |
3301 | SUGGESTION | Region is usually unnecessary for small territories; consider removing the region value | true | Tuning: small territory region |
3402 | SUGGESTION | City-level granularity is usually unnecessary for small territories; consider removing the city value | true | Tuning: small territory city |
3303 | SUGGESTION | Region code is recommended when a city is specified; choose a region from the dropdown | true | Tuning: missing region with city |
3104 | SUGGESTION | Confirm whether this subnet is intentionally marked as do-not-geolocate or missing location data | true | Tuning: unspecified geolocation |
DoNotGeolocate (entry) is true | DoNotGeolocate (metadata) |
Clean and normalize the CSV using Python logic equivalent to the following operations:
Comments
#.#../run/data/comments.json{ "4": "# It's OK for small city states to leave state ISO2 code unspecified" }Notes
pandas and built-in csv) must write output using the utf-8-sig encoding to ensure a UTF-8 BOM is present.ERROR
Report the following conditions as ERROR :
Invalid subnet syntax
1102Non-public address space
is_private and related address properties as shown in ./references.1103SUGGESTION
Report the following conditions as SUGGESTION :
Overly large IPv6 subnets
/643102Overly large IPv4 subnets
/223101Sample code is available in the references/ directory.
If a country is found in assets/small-territories.json, mark the entry internally as a small territory. This flag is used in later checks and suggestions but is not stored in the output JSON (it is transient validation state).
Note: small-territories.json contains some historic/disputed codes (AN, CS, XK) that are not present in iso3166-1.json. An entry using one of these as its CountryCode will fail the country code validation (ERROR) even though it matches as a small territory. The country code ERROR takes precedence — do not suppress it based on the small-territory flag.
ERROR
CountryCode is present but not found in the alpha_2 set1201SUGGESTION
Report the following conditions as SUGGESTION :
Unspecified geolocation for subnet
CountryCode, RegionCode, City) are empty for a subnet.DoNotGeolocate = true for the entry.CountryCode to ZZ for the entry.3104US-CAAU-NSWcode attribute (already prefixed with the country code).Small-territory exception: If the entry is a small territory and the RegionCode value equals the entry's CountryCode (e.g., SG as both country and region for Singapore), treat the region as acceptable — skip all region validation checks for this entry. Small territories are effectively city-states with no meaningful ISO 3166-2 administrative subdivisions.
ERROR
RegionCode does not match {COUNTRY}-{SUBDIVISION} and the small-territory exception does not apply1301RegionCode value is not found in the code set and the small-territory exception does not apply1302RegionCode does not match CountryCode13031401Truncated names, abbreviations, or airport codes
LAFrftsin01LHRSINMAA1402WARNING
HongKong vs Hong Kong24013303Open ./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.
h3Cells | H3Cells |
boundingBox | BoundingBox |
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) |
{{.RegionCode}} | RegionCode | HTML-escaped |
{{.City}} | City | HTML-escaped |
{{.Status}} | Status | HTML-escaped |
{{.HasError}} | HasError | Lowercase string: "true" or "false" |
{{.HasWarning}} | HasWarning | Lowercase string: "true" or "false" |
{{.HasSuggestion}} | HasSuggestion | Lowercase string: "true" or "false" |
{{.GeocodingHint}} | GeocodingHint | Empty string "" |
{{.DoNotGeolocate}} | DoNotGeolocate | "true" or "false" |
{{.Tunable}} | Tunable | "true" or "false" |
{{.TunedEntry.CountryCode}} | TunedEntry.CountryCode | "" if TunedEntry is empty {} |
{{.TunedEntry.RegionCode}} | TunedEntry.RegionCode | "" if TunedEntry is empty {} |
{{.TunedEntry.Name}} | TunedEntry.Name | "" if TunedEntry is empty {} |
{{.TunedEntry.H3Cells}} | TunedEntry.H3Cells | Bracket-wrapped space-separated; "[]" if empty (see format below) |
{{.TunedEntry.BoundingBox}} | TunedEntry.BoundingBox | Bracket-wrapped space-separated; "[]" if empty (see format below) |
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