使用 AI 辅助开发一个开源 IP 信息查询工具:一
本文将分享如何借助当下流行的 AI 工具,一步步完成一个开源项目的开发。
写在前面
在写代码时,总是会遇到一些有趣的机缘巧合。前几天,我在翻看自己之前的开源项目时,又看到了 DDNS 相关的讨论。虽然在 2021 年我写过两篇相对详细的教程:《使用 Nginx 提供 DDNS 服务(前篇)》和《使用 Nginx 提供 DDNS 服务(中篇)》,但总觉得还可以做得更好。
这几天在上海出差上课,本地的网络和算力资源都比较有限。正好借这个机会,快速开发一个轻量的小工具,顺便也回应下之前有朋友问我的问题:在 AI 时代,开发一个简单应用的成本到底有多低?
去年五月份,我写过一篇文章《AI 加持的代码编写实战:快速实现 Nginx 配置格式化工具》,当时使用的是 ChatGPT,这篇文章中,我们来使用代码能力更强的 Anthropic Claude Sonnet 来完成类似的事情。在这篇文章中,我会尽可能使用对“非程序员”友好的方法,尽量避免使用复杂的 IDE。
项目已经在 Github 开源 soulteary/ip-helper,有需要可以自取,如果觉得有帮助的话,别忘了“一键三连”支持一下。
这个开源小工具的交互设计借鉴了 CIP.CC 的 IP 查询工具。
我一直觉得 CIP.CC 是个非常实用的工具。简洁明了,能快速提供有价值的参考信息。它整合了三个不同的 IP 数据源。实在要说缺点的话,如果能够公开数据库的来源和版本就更棒了。不过,在当前国内数据库和数据源要么收费要么需要申请的环境下,这类网站可能终将成为互联网的一段历史。
本文之所以能够成文,感谢好朋友高老师(IPIP)提供基础数据支持这个项目,在战胜了各种侵权事件之后,IPIP 的数据目前应该是毫无疑问的第一梯队了,恭喜!
另外,遗憾的是,目前该网站的“纯 IP 信息查询”以及“使用 Telnet、FTP 等方式查询”功能已经无法使用。
所以在这个项目中,我会根据自己的理解来实现并补充这些功能。
好了,让我们从前端到后端,来折腾出来这个小工具。
第一步:使用多模态模型创建基础 UI 界面
2024 年底,各大模型都在推出“多模态”能力,让 AI 不仅能读懂文字,还能理解图片、音视频。让我们一步步用这些能力来搭建一个实用的工具界面吧。
从界面设计开始
我们可以先让模型帮助我们生成一个简洁的 UI 模块设计图:设计一个网页工具,左右分栏布局,右侧是查询界面。
然后,把已有的界面截图(Sketch 画一个,或使用你想借鉴的产品界面)丢给模型,提出一个典型的模糊产品需求:用 HTML 和 CSS 实现一个类似的精致界面。
接下来,我们在新的对话中继续完善布局细节:“使用 CSS 和 HTML 创建一个左右分栏布局,左侧固定 30%,包含 Logo 图片。”
好了,界面的设计和代码就都有了,接下来我们需要一个吸引眼球的 Logo。
主视觉 & Logo 设计
这个环节我选择用 Midjourney 来设计:“来一只动感的大熊猫”。关于提示词,你可以自由发挥,创造更酷的版本。如果感兴趣,可以参考我在 2023 年 4 月写的文章《八十行代码实现开源的 Midjourney、Stable Diffusion “咒语”作图工具》
图片优化
生成的图片往往需要进一步调整。你可以用图片编辑软件调整内容、尺寸和格式:
如果你是 macOS 用户,只想调整图片尺寸,用命令行会更快(这里我们把宽度设为 380 像素):
sips -Z 380 /Users/soulteary/Downloads/panda.png --out small-panda.png
/Users/soulteary/Downloads/panda.png/Users/soulteary/Lab/github/ip-helper/small-panda.png
Favicon 制作
别忘了网站还需要一个 Favicon(收藏夹图标)。
我们可以让 AI 基于已有 Logo 设计一个像素化版本:“参考图片,设计一个简单的马赛克版本的 LOGO”。
完成这些设计后,我们就可以把 AI 生成的代码保存下来,准备进行下一步的整合处理了。
组装 AI 生成的界面素材
组合好的代码素材,得到的界面类似下面这张图。
对于AI 生成的界面素材,我们该如何组装成一个完整的应用界面呢?方法其实很简单。当你有了多个独立的界面组件后,可以通过以下方式将它们整合起来:
最简单的方式是创建一个新的AI对话,并提供明确的整合需求,比如:“将查询工具组件集成到左右布局面板的右侧区域"。
如果你具备前端开发经验,更推荐手动组合这些代码。这样不仅能优化性能,还能构建出更合理的代码结构,为后续功能扩展打好基础。
我们得到了界面后,接下来就可以来实现基础的后端服务啦。
第二步:完成服务端设计
后端服务的核心任务是获取和解析用户的 IP 信息,并将结果呈现给用户。
按照经典的模块化思路,我们可以把功能划分为以下几个部分:Web 界面渲染模块、IP 信息解析模块、IP 信息 API 接口模块,以及在原始工具基础上新增的多协议支持(包括 Telnet、FTP 等)。
搭建基础服务框架
接下来,我们继续让 AI 助手帮我们生成代码:使用 Gin 实现一个简单的服务,解析命令行参数和环境变量中的端口和域名信息、以及用户口令。
很快,基础框架代码就准备就绪了。这段代码为我们提供了一个运行在 8080 端口的服务器,支持通过命令行参数或环境变量来配置服务端口和域名,同时具备基于 TOKEN 的用户认证功能。
完成和模版的交互
我们先把前面的前端代码保存到项目的 public/index.template.html
文件中,同时将 Logo 等静态资源文件也放入 public
目录下。同时根据需要优化程序代码,让用户认证和代码交互体验更加自然顺畅。
另外,我们可以搭配使用我在今年年初写的文章《完善 Golang Gin 框架的静态中间件:Gin-Static》中介绍的中间件 soulteary/gin-static。这样不仅能让程序支持单文件发布,还能提升整体性能。如果你想深入了解相关原理,可以参考《深入浅出 Golang 资源嵌入方案:前篇》以及查看 Go-Embed 标签下的系列文章。
package mainimport ("embed""flag""fmt""io""log""net/http""os""github.com/gin-gonic/gin"static "github.com/soulteary/gin-static"
)type Config struct {Domain stringPort stringToken string
}// 解析配置参数
func parseConfig() *Config {config := &Config{}// 解析命令行参数flag.StringVar(&config.Port, "port", "", "服务器端口")flag.StringVar(&config.Domain, "domain", "", "服务器域名")flag.StringVar(&config.Token, "token", "", "API 访问令牌")flag.Parse()// 尝试从环境变量中获取未设置的内容if config.Port == "" {config.Port = os.Getenv("SERVER_PORT")}if config.Domain == "" {config.Domain = os.Getenv("SERVER_DOMAIN")}if config.Token == "" {config.Token = os.Getenv("TOKEN")}// 使用默认值if config.Port == "" {config.Port = "8080"}if config.Domain == "" {config.Domain = "localhost"}if config.Token == "" {config.Token = ""log.Println("提醒:为了提高安全性,可以设置 `TOKEN` 环境变量或 `token` 命令行参数")}return config
}// 验证请求中的令牌
func authMiddleware(config *Config) gin.HandlerFunc {return func(c *gin.Context) {if config.Token != "" {token := c.Query("token")if token == "" {token = c.GetHeader("X-Token")}if token != config.Token {c.JSON(401, gin.H{"error": "无效的认证令牌"})c.Abort()return}}c.Next()}
}func Get(link string) ([]byte, error) {resp, err := http.Get(link)if err != nil {return nil, err}defer resp.Body.Close()if resp.StatusCode != http.StatusOK {return nil, fmt.Errorf("服务器返回非200状态码: %d", resp.StatusCode)}body, err := io.ReadAll(resp.Body)if err != nil {return nil, fmt.Errorf("读取响应内容失败: %v", err)}return body, nil
}//go:embed public
var EmbedFS embed.FSfunc main() {config := parseConfig()r := gin.Default()r.GET("/health", func(c *gin.Context) {c.JSON(200, gin.H{"status": "ok","domain": config.Domain,})})r.Use(static.Serve("/", static.LocalFile("./public", false)))r.Use(authMiddleware(config))r.GET("/", func(c *gin.Context) {buf, err := Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))if err != nil {c.String(500, "读取模板文件失败: %v", err)return}c.Data(200, "text/html; charset=utf-8", buf)})serverAddr := fmt.Sprintf(":%s", config.Port)log.Printf("启动服务器于 %s:%s\n", "config.Domain", config.Port)if err := r.Run(serverAddr); err != nil {log.Fatalf("启动服务器失败: %v", err)}
}
IP 获取和基础分析功能的实现
在与模型的进一步对话中,我们实现更核心的功能:使用 Golang Gin 框架来获取用户访问时的 IP 信息,并判断请求是否经过了代理服务器。
获得这段代码后,我们将它与之前的功能进行整合(新增代码有注释)。现在,我们的服务不仅可以获取用户的 IP 信息,还能够基础地判断请求是否通过代理服务器转发。
除了网页展示外,我们还新增了一个 /ip
接口,让用户可以直接通过程序获取纯 IP 信息,提供了更灵活的使用方式。
// ...// IPInfo 存储 IP 相关信息
type IPInfo struct {ClientIP string `json:"client_ip"`ProxyIP string `json:"proxy_ip,omitempty"`IsProxy bool `json:"is_proxy"`ForwardedFor string `json:"forwarded_for,omitempty"`RealIP string `json:"real_ip"`
}// 获取并分析 IP 信息的中间件
func IPAnalyzer() gin.HandlerFunc {return func(c *gin.Context) {ipInfo := analyzeIP(c)// 将 IP 信息存储到上下文中c.Set("ip_info", ipInfo)c.Next()}
}// 分析 IP 信息
func analyzeIP(c *gin.Context) IPInfo {var ipInfo IPInfo// 获取客户端 IPipInfo.ClientIP = c.ClientIP()// 获取 X-Forwarded-For 头信息forwardedFor := c.GetHeader("X-Forwarded-For")if forwardedFor != "" {ipInfo.ForwardedFor = forwardedFor// X-Forwarded-For 可能包含多个 IP,第一个是原始客户端 IPips := strings.Split(forwardedFor, ",")if len(ips) > 0 {ipInfo.RealIP = strings.TrimSpace(ips[0])if len(ips) > 1 {ipInfo.IsProxy = trueipInfo.ProxyIP = strings.TrimSpace(ips[len(ips)-1])}}} else {ipInfo.RealIP = ipInfo.ClientIP}// 获取 X-Real-IP 头信息xRealIP := c.GetHeader("X-Real-IP")if xRealIP != "" && xRealIP != ipInfo.RealIP {ipInfo.IsProxy = trueipInfo.ProxyIP = ipInfo.ClientIPipInfo.RealIP = xRealIP}// 检查是否为私有 IPif isPrivateIP(ipInfo.ClientIP) {ipInfo.IsProxy = true}return ipInfo
}// 检查是否为私有 IP 地址
func isPrivateIP(ipStr string) bool {ip := net.ParseIP(ipStr)if ip == nil {return false}// 检查是否为私有 IP 范围privateIPRanges := []struct {start net.IPend net.IP}{{net.ParseIP("10.0.0.0"), net.ParseIP("10.255.255.255")},{net.ParseIP("172.16.0.0"), net.ParseIP("172.31.255.255")},{net.ParseIP("192.168.0.0"), net.ParseIP("192.168.255.255")},}for _, r := range privateIPRanges {if bytes.Compare(ip, r.start) >= 0 && bytes.Compare(ip, r.end) <= 0 {return true}}return false
}//go:embed public
var EmbedFS embed.FSfunc main() {// ...r.Use(static.Serve("/", static.LocalFile("./public", false)))r.Use(authMiddleware(config))// 使用IP分析中间件r.Use(IPAnalyzer())r.GET("/", func(c *gin.Context) {// 先获取 IP 信息ipInfo, exists := c.Get("ip_info")if !exists {c.JSON(500, gin.H{"error": "IP info not found"})return}buf, err := Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))if err != nil {c.String(500, "读取模板文件失败: %v", err)return}// TODO 将 IP 信息传递给模板fmt.Println(ipInfo)c.Data(200, "text/html; charset=utf-8", buf)})// 单独提供一个接口,来获取 IP 信息r.GET("/ip", func(c *gin.Context) {ipInfo, exists := c.Get("ip_info")if !exists {c.JSON(500, gin.H{"error": "IP info not found"})return}c.JSON(200, ipInfo)})serverAddr := fmt.Sprintf(":%s", config.Port)log.Printf("启动服务器于 %s:%s\n", "config.Domain", config.Port)if err := r.Run(serverAddr); err != nil {log.Fatalf("启动服务器失败: %v", err)}r.Run(":8080")
}
启动程序后,我们可以通过命令行或者直接在浏览器中访问 http://localhost:8080/ip
来测试功能。比如使用 curl 命令:
# curl 127.0.0.1:8080/ip{"client_ip":"127.0.0.1","is_proxy":false,"real_ip":"127.0.0.1"}
看到这个返回结果,说明我们的基础功能已经正常运行了。
接下来,我们先不着急处理模板渲染的部分,而是把注意力放在 IP 信息和数据库对接这个核心模块上。
完成 IP 数据库查询功能
在2020年时,因业务需求我曾使用过高老师的 IP 库(通过阿里云购买),并写过两篇关于如何处理本地数据的高性能方案文章:《阿里云 IP 地理位置库(淘宝IP库)实践(前篇)》和《阿里云 IP 地理位置库(淘宝IP库)实践(后篇)》。
这次为了开发这个小工具,我向高老师获取了精简版数据和解析文档。由于我只需要像文章开头提到的那样解析基础地理信息,所以我选择 fork 了一个 Go SDK 并进行了简化处理。
这次为了完成这个小工具,和高老师要来了精简版的数据,以及解析文档,因为我只想和文章开头一样,解析出基础的地理信息,所以我 fork 了一个 Go SDK 版本,并做了 “青春版化” 处理。
首先,在项目目录中执行以下命令来下载简化版 SDK:
go get github.com/soulteary/ipdb-go
接下来,我们将在之前的代码基础上添加查询功能,并新增一个 /ip/:ip
路由,让用户可以查询指定 IP 的数据。
// ...// 帮助我们对数据库中的内容进行去重
// eg: ["CLOUDFLARE.COM","CLOUDFLARE.COM",""] => ["CLOUDFLARE.COM",""]func removeDuplicates(strSlice []string) []string {// 创建一个 map 用于存储唯一的字符串encountered := make(map[string]bool)result := []string{}// 遍历切片,将未出现过的字符串添加到结果中for _, str := range strSlice {if !encountered[str] {encountered[str] = trueresult = append(result, str)}}return result
}//go:embed public
var EmbedFS embed.FSfunc main() {config := parseConfig()// 初始化 IP 数据库db, err := ipdb.NewCity("./data/ipipfree.ipdb")if err != nil {log.Fatal(err)}// 更新 ipdb 文件后可调用 Reload 方法重新加载内容// db.Reload("./data/ipipfree.ipdb")r := gin.Default()r.GET("/health", func(c *gin.Context) {c.JSON(200, gin.H{"status": "ok","domain": config.Domain,})})r.Use(static.Serve("/", static.LocalFile("./public", false)))r.Use(authMiddleware(config))r.Use(IPAnalyzer())r.GET("/", func(c *gin.Context) {ipInfo, exists := c.Get("ip_info")if !exists {c.JSON(500, gin.H{"error": "IP info not found"})return}buf, err := Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))if err != nil {c.String(500, "读取模板文件失败: %v", err)return}// TODO 将 IP 信息传递给模板fmt.Println(ipInfo)c.Data(200, "text/html; charset=utf-8", buf)})// 获取当前请求方的 IP 地址信息r.GET("/ip", func(c *gin.Context) {ipInfo, exists := c.Get("ip_info")if !exists {c.JSON(500, gin.H{"error": "IP info not found"})return}c.JSON(200, ipInfo)})// 获取指定 IP 地址信息r.GET("/ip/:ip", func(c *gin.Context) {// 获取 URL 中的 IP 地址ipaddr := c.Param("ip")fmt.Println("ip", ipaddr)if ipaddr == "" {ipInfo, exists := c.Get("ip_info")if !exists {c.JSON(500, gin.H{"error": "IP info not found"})return}ipaddr = ipInfo.(IPInfo).RealIP}dbInfo, err := db.Find(ipaddr, "CN")if err != nil {dbInfo = []string{"未找到 IP 地址信息"}}dbInfo = removeDuplicates(dbInfo)c.JSON(200, map[string]any{"ip": ipaddr, "info": dbInfo})})serverAddr := fmt.Sprintf(":%s", config.Port)log.Printf("启动服务器于 %s:%s\n", "config.Domain", config.Port)if err := r.Run(serverAddr); err != nil {log.Fatalf("启动服务器失败: %v", err)}r.Run(":8080")
}
让我们通过命令行或浏览器来验证服务是否正常运行。我们可以测试几个不同的 IP 地址:
首先测试获取当前请求来源的 IP 信息。
# curl 127.0.0.1:8080/ip{"client_ip":"127.0.0.1","is_proxy":false,"real_ip":"127.0.0.1"}
然后测试查询特定IP地址。
# curl 127.0.0.1:8080/ip/123.123.123.123{"info":["中国","北京"],"ip":"123.123.123.123"}
最后测试一个 CloudFlare 的 IP:
# curl 127.0.0.1:8080/ip/1.1.1.1{"info":["CLOUDFLARE.COM",""],"ip":"1.1.1.1"}
第三步:从静态页面到动态网站,数据与界面的整合
我们已经完成了基础架构的搭建工作,现在要进入最后也是最关键的阶段:将数据层和展示层打通,让整个系统真正运转起来。让我们一步步来实现这个目标。
模版和服务数据联动
第一步,我们需要改造之前的静态模板。我们要把原本写死的数据替换成程序可以动态填充的占位符:
接下来,我们先实现一个基础版本的IP信息查询功能:当用户访问网站首页时,系统会自动获取访问者的IP地址,并展示相关的IP信息。
// ...
func main() {// ...r.GET("/", func(c *gin.Context) {ipInfo, exists := c.Get("ip_info")if !exists {c.JSON(500, gin.H{"error": "IP info not found"})return}// 查询 IP 地址具体信息dbInfo, err := db.Find(ipInfo.(IPInfo).RealIP, "CN")if err != nil {dbInfo = []string{"未找到 IP 地址信息"}}// 读取默认模版template, err := Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))if err != nil {c.String(500, "读取模板文件失败: %v", err)return}// 更新模版中的 IP 地址template = bytes.ReplaceAll(template, []byte("%IP_ADDR%"), []byte(ipInfo.(IPInfo).ClientIP))// 更新模版中的域名template = bytes.ReplaceAll(template, []byte("%DOMAIN%"), []byte(config.Domain))// 更新模版中的 IP 地址信息template = bytes.ReplaceAll(template, []byte("%DATA_1_INFO%"), []byte(strings.Join(removeDuplicates(dbInfo), " ")))c.Data(200, "text/html; charset=utf-8", template)})
// ...
}
完成模板更新后,我们需要启动服务来验证功能。使用以下命令启动:
SERVER_DOMAIN=localhost:8080 go run main.go
启动服务后,打开浏览器访问 localhost:8080
,我们就可以看到如下界面:
从界面可以看到,页面的数据联动功能已经正常工作。不过目前使用的数据库还不支持 IPv6 地址的查询(需要使用商业版本或增加其他数据库),导致部分信息展示不符合预期。没关系,接下来我们就来实现按指定 IP 查询的功能。
后端处理前端用户输入
为了让用户能够与我们的应用进行交互,现在让我们对之前的静态 HTML 模板做一些优化。我们将添加一个表单来处理用户输入的 IP 地址。
首先,在 HTML 模版中添加数据表单:
<div class="search-container"><form action="/" method="post"><input type="text" name="ip" class="search-input" placeholder="请输入要查询的 IP 地址" value="%IP_ADDR%" /><button class="search-button" type="submit">查询</button> </form>
</div>
在这段代码中:
- 使用
form
标签创建表单,设置action="/"
将数据提交到根路径 method="post"
指定使用POST
方法提交数据- 输入框中的
value="%IP_ADDR%"
用于回显用户之前输入的 IP 地址
接下来,我们需要在后端添加相应的处理逻辑:
// ...
// 使用 net 包验证 IP 地址
func isValidIPAddress(ip string) bool {if parsedIP := net.ParseIP(ip); parsedIP != nil {return true}return false
}// IPForm 定义表单结构
type IPForm struct {IP string `form:"ip" binding:"required"`
}func main() {// ...// 处理 POST 请求,解析表单数据r.POST("/", func(c *gin.Context) {// 获取请求中的 IP 地址信息ipInfo, exists := c.Get("ip_info")if !exists {c.JSON(500, gin.H{"error": "IP info not found"})return}// 默认 IP 地址为空ip := ""var form IPForm// 使用 ShouldBind 绑定表单数据if err := c.ShouldBind(&form); err != nil {// 如果绑定失败,使用请求中的 IP 地址ip = ipInfo.(IPInfo).RealIP} else {// 获取到 IP 地址后的处理逻辑ip = form.IP// 如果 IP 地址不合法,使用请求中的 IP 地址if !isValidIPAddress(ip) {ip = ipInfo.(IPInfo).RealIP}}c.Redirect(302, fmt.Sprintf("/ip/%s", ip))})
// ...
}
程序首先会记录发起请求的客户端 IP。然后检查用户通过表单提交的 IP 地址是否正确。如果 IP 地址正确,会自动跳转到类似 /ip/123.123.123.123
这样的地址来展示 IP 详细信息。如果提交的 IP 地址无效,则会使用客户端的实际 IP 地址进行跳转。
打造统一的接口,适配多种场景
细心的朋友可能注意到了,前面提到的 /ip/:ip
接口原本是为命令行工具设计的,默认返回 JSON 格式数据,而不是网页界面。在 CIP 网站的设计中,浏览器访问和命令行调用使用了不同的接口地址。不过通过一些技巧,我们完全可以让同一个接口同时支持这两种使用场景。
先来将 IP 获取和信息查询,以及渲染部分分别抽象为独立的模块:
// ...
func main() {//...// 获取客户端 IP 信息getClientIPInfo := func(c *gin.Context, ipaddr string) (resultIP string, resultDBInfo []string, err error) {// 判断是否有传入 IP 地址if ipaddr == "" {// 如果没有有效 IP,默认使用发起请求的客户端 IP 信息ipInfo, exists := c.Get("ip_info")if !exists {return resultIP, resultDBInfo, fmt.Errorf("IP info not found")}ipaddr = ipInfo.(IPInfo).RealIP}dbInfo, err := db.Find(ipaddr, "CN")if err != nil {dbInfo = []string{"未找到 IP 地址信息"}}dbInfo = removeDuplicates(dbInfo)return ipaddr, dbInfo, nil}// 渲染模板renderTemplate := func(globalTemplate []byte, ipaddr string, dbInfo []string) []byte {template := bytes.ReplaceAll(globalTemplate, []byte("%IP_ADDR%"), []byte(ipaddr))template = bytes.ReplaceAll(template, []byte("%DOMAIN%"), []byte(config.Domain))template = bytes.ReplaceAll(template, []byte("%DATA_1_INFO%"), []byte(strings.Join(removeDuplicates(dbInfo), " ")))return template}// 渲染 JSONrenderJSON := func(ipaddr string, dbInfo []string) map[string]any {return map[string]any{"ip": ipaddr, "info": dbInfo}}globalTemplate := []byte{}r.GET("/", func(c *gin.Context) {// 预缓存模板文件if len(globalTemplate) == 0 {globalTemplate, err = Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))if err != nil {log.Fatalf("读取模板文件失败: %v\n", err)return}}// 获取客户端 IP 信息,首页不需要传入 IP 地址ipAddr, dbInfo, err := getClientIPInfo(c, "")if err != nil {c.JSON(500, gin.H{"error": err.Error()})return}// 返回渲染后的 HTML 内容c.Data(200, "text/html; charset=utf-8", renderTemplate(globalTemplate, ipAddr, dbInfo))})r.GET("/ip/:ip", func(c *gin.Context) {ip := c.Param("ip")// 获取指定 IP 地址的信息ipAddr, dbInfo, err := getClientIPInfo(c, ip)if err != nil {c.JSON(500, gin.H{"error": err.Error()})return}c.JSON(200, renderJSON(ipAddr, dbInfo))})// ...
}
接下来,我们要实现一个功能:自动识别访问请求是来自类似 curl
的命令行工具,还是来自浏览器。
// ...
// 判断请求发起方是否为“下载工具”
func IsDownloadTool(userAgent string) bool {// 转换为小写以便不区分大小写比较ua := strings.ToLower(userAgent)// 常见下载工具的特征字符串downloadTools := []string{"curl","wget","aria2","python-requests","axios","got","postman",}for _, tool := range downloadTools {if strings.Contains(ua, tool) {return true}}return false
}func main() {// ...r.GET("/", func(c *gin.Context) {if len(globalTemplate) == 0 {globalTemplate, err = Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))if err != nil {log.Fatalf("读取模板文件失败: %v\n", err)return}}ipAddr, dbInfo, err := getClientIPInfo(c, "")if err != nil {c.JSON(500, gin.H{"error": err.Error()})return}// 获取请求头中的 User-Agent 信息userAgent := c.GetHeader("User-Agent")// 使用下载工具访问时返回 JSON 格式if IsDownloadTool(userAgent) {c.JSON(200, renderJSON(ipAddr, dbInfo))} else {c.Data(200, "text/html; charset=utf-8", renderTemplate(globalTemplate, ipAddr, dbInfo))}})r.GET("/ip/:ip", func(c *gin.Context) {ip := c.Param("ip")ipAddr, dbInfo, err := getClientIPInfo(c, ip)if err != nil {c.JSON(500, gin.H{"error": err.Error()})return}// 获取请求头中的 User-Agent 信息userAgent := c.GetHeader("User-Agent")// 使用下载工具访问时返回 JSON 格式if IsDownloadTool(userAgent) {c.JSON(200, renderJSON(ipAddr, dbInfo))} else {c.Data(200, "text/html; charset=utf-8", renderTemplate(globalTemplate, ipAddr, dbInfo))}})// ...
}
经过上面的改进,不管是访问根路径 /
还是 /ip/:ip
接口,程序都能根据访问方式自动返回合适的格式。浏览器访问会看到格式化的页面,命令行工具访问则获得纯文本结果。这样一来,我们其实可以考虑是否要保留之前专门为命令行工具设计的 /ip
接口,因为现在 /
已经能够处理这两种场景了。当然,如果特别在意性能,保留专门的接口也是一种选择。
和之前一样,重启程序后,我们可以打开浏览器做个简单测试。随便输入一个 IP 地址进行查询,你会发现一切都在按照预期正常运行。
最后
到这里,我们已经实现了这个应用的核心功能。在下一篇文章中,我们将继续探讨本文中的一些遗留问题,看看如何借助 AI 的力量来帮助我们更快地完成应用开发。
–EOF
我们有一个小小的折腾群,里面聚集了一些喜欢折腾、彼此坦诚相待的小伙伴。
我们在里面会一起聊聊软硬件、HomeLab、编程上、生活里以及职场中的一些问题,偶尔也在群里不定期的分享一些技术资料。
关于交友的标准,请参考下面的文章:
致新朋友:为生活投票,不断寻找更好的朋友
当然,通过下面这篇文章添加好友时,请备注实名和公司或学校、注明来源和目的,珍惜彼此的时间 😄
关于折腾群入群的那些事
如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。
如果你想更快的看到后续内容的更新,请戳 “点赞”、“分享”、“在看” ,这些免费的鼓励将会影响后续有关内容的更新速度。
本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)
本文作者: 苏洋
创建时间: 2024年12月21日
统计字数: 17445字
阅读时间: 35分钟阅读
本文链接: https://soulteary.com/2024/12/21/use-ai-to-assist-in-developing-an-open-source-ip-information-tool-part-1.html
相关文章:
使用 AI 辅助开发一个开源 IP 信息查询工具:一
本文将分享如何借助当下流行的 AI 工具,一步步完成一个开源项目的开发。 写在前面 在写代码时,总是会遇到一些有趣的机缘巧合。前几天,我在翻看自己之前的开源项目时,又看到了 DDNS 相关的讨论。虽然在 2021 年我写过两篇相对详细的教程&am…...
『 Linux 』高级IO (一)
文章目录 内容回顾及铺垫五种IO模型不同类型IO的区别非阻塞IOfcntl( ) 多路转接 - select( )select( ) 的基本使用 - SelectServer服务器 内容回顾及铺垫 在博客『 Linux 』基础IO/文件IO (万字)中介绍了对IO的认识; IO实际上为Input/Output,输入输出; 以网络协议栈的视角来看,…...
Cisco WebEx 数据平台:统一 Trino、Pinot、Iceberg 及 Kyuubi,探索 Apache Doris 在 Cisco 的改造实践
导读:Cisco WebEx 早期数据平台采用了多系统架构(包括 Trino、Pinot、Iceberg 、 Kyuubi 等),面临架构复杂、数据冗余存储、运维困难、资源利用率低、数据时效性差等问题。因此,引入 Apache Doris 替换了 Trino、Pinot…...
Java - 日志体系_Apache Commons Logging(JCL)日志接口库
文章目录 官网1. 什么是JCL?2. JCL的主要特点3. JCL的核心组件4. JCL的实现机制5. SimpleLog 简介6. CodeExample 1 : 默认日志实现 (JCL 1.3.2版本)Example 2 : JCL (1.2版本) Log4J 【安全风险高,请勿使用】 7. 使用…...
Linux驱动开发 IIC I2C驱动 编写APP访问EEPROM AT24C02
在嵌入式开发中,I2C(Inter-Integrated Circuit)是一种常用的串行通信协议,广泛应用于与外设(如 EEPROM、传感器、显示屏等)进行数据交换。AT24C02 是一种常见的 I2C EEPROM 存储器,它提供 2Kbit…...
盒子模型(外边距的设置)
用于页面中元素的合理布局所有的元素都可以有宽高所有元素都是一个矩形所有元素都可以看成一个盒子盒子包括 外边距边框内边距元素内容 外边距设置 外边距的要素:top、bottom、left、right外边距的尺寸:合法的尺寸单位外边距语法:marign-方…...
买卖股票的最佳时机 IV - 困难
************* C topic:188. 买卖股票的最佳时机 IV - 力扣(LeetCode) ************* Stock angin: Still stocks. Intuitively, it feels hard. For once: class Solution { public:int maxProfit(vector<int>& prices) {in…...
ElasticSearch - 深入解析 Elasticsearch Composite Aggregation 的分页与去重机制
文章目录 Pre概述什么是 composite aggregation?基本结构after 参数的作用问题背景:传统分页的重复问题after 的设计理念响应示例 after 如何确保数据不重复核心机制Example步骤 1: 创建测试数据创建索引插入测试数据 步骤 2: 查询第一页结果查询第一页返…...
HTML5 Web IndexedDB 数据库
IndexedDB 是一种基于浏览器的 NoSQL 数据库,用于在客户端持久化存储大量结构化数据。 IndexedDB 允许通过键值对存储复杂的数据对象(如对象、数组、文件等),并支持事务、索引、版本控制和复杂查询操作。 IndexedDB 是异步的&am…...
苏黎世联邦理工学院与加州大学伯克利分校推出MaxInfoRL:平衡内在与外在探索的全新强化学习框架
每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…...
读书笔记~管理修炼-缄默效应
缄默效应:学会正确批评下属 员工明明犯了错误,却不及时告知你,总是拖到最后一刻无法弥补时才不得不承认出了问题——你遇到过这样的问题吗? 这其实是缄默效应在发挥作用。 在职场中,即使再扁平化的环境&…...
LabVIEW声音信号处理系统
开发了一种基于LabVIEW的声音信号处理系统,通过集成的信号采集与分析一体化解决方案,提升电子信息领域教学与研究的质量。系统利用LabVIEW图形化编程环境和硬件如USB数据采集卡及声音传感器,实现了从声音信号的采集到频谱分析的全过程。 项目…...
2024.12.10——攻防世界Web_php_include
知识点:代码审计 文件包含 伪协议 伪协议知识点补充: 在PHP中,伪协议(Pseudo Protocols)也被称为流包装器,这些伪协议以 php://开头,后面跟着一些参数,用于指定要执行的操作或需要…...
Linux shell脚本用于常见图片png、jpg、jpeg、webp、tiff格式批量转PDF文件
Linux Debian12基于ImageMagick图像处理工具编写shell脚本用于常见图片png、jpg、jpeg、webp、tiff格式批量转PDF文件,”多个图片分开生成多个PDF文件“或者“多个图片合并生成一个PDF文件” 在Linux系统中,使用ImageMagick可以图片格式转换,…...
有没有检测吸烟的软件 ai视频检测分析厂区抽烟报警#Python
在现代厂区管理中,安全与规范是重中之重,而吸烟行为的管控则是其中关键一环。传统的禁烟管理方式往往依赖人工巡逻,效率低且存在监管死角,难以满足当下复杂多变的厂区环境需求。此时,AI视频检测技术应运而生࿰…...
LeetCode 每日一题 2024/12/16-2024/12/22
记录了初步解题思路 以及本地实现代码;并不一定为最优 也希望大家能一起探讨 一起进步 目录 12/16 1847. 最近的房间12/17 3291. 形成目标字符串需要的最少字符串数 I12/18 3292. 形成目标字符串需要的最少字符串数 II12/19 3285. 找到稳定山的下标12/20 3138. 同位…...
gitlab代码推送
点击这个√ 修改的文件全部选上 填好提交的名称 点击commit 选取提交的 gitlab 库 点击Push...
小红书飞书素材库 | AI改写 | 无水印下载 | 多维表格 | 采集同步 | 影刀RPA
小红书飞书素材库 | AI改写 | 无水印下载 | 多维表格 | 采集同步 | 影刀RPA 模板准备 进入【小红书】素材采集库_荷逸模板,点击使用模板 创建文档应用 在开发者后台 - 飞书开放平台创建 企业自建应用 (需要账号有相应的权限, 如果没有权限向管理员申请) 获取 Ap…...
【计算机视觉基础CV-图像分类】02-入门详解图像分类、经典数据集、比赛与冠军图像模型演进史
前言 图像分类(Image Classification)是计算机视觉(Computer Vision)中一项基础且核心的任务。简单来说,就是让计算机从给定的类别集合中,为一张输入图片分配一个正确的类别标签。这个过程听起来直观&…...
【机器学习】探索机器学习与人工智能:驱动未来创新的关键技术
探索机器学习与人工智能:驱动未来创新的关键技术 前言:人工智能的核心技术深度学习:自然语言处理(NLP):计算机视觉: 机器学习与人工智能的驱动创新医疗健康领域金融行业智能制造与工业互联网智慧…...
DS二叉树--基于数组存储的构建
题目描述 任意二叉树可以根据完全二叉树性质保存在一个数组中。已知二叉树的数组存储,用程序构建该二叉树。 提示:用递归方法或非递归都可以 输入 第一行输入一个整数t,表示有t个测试数据 第二行起输入二叉树的数组存储结果,空…...
入侵他人电脑,实现远程控制(待补充)
待补充 在获取他人无线网网络密码后,进一步的操作是实现入侵他人电脑,这一步需要获取对方的IP地址并需要制作自己的代码工具自动化的开启或者打开对方的远程访问权限。 1、获取IP地址(通过伪造的网页、伪造的Windows窗口、hook,信…...
STM32基于标准库如何查看时钟主频,100%简单
基于原有的工程写入两行代码,见下图 RCC_ClocksTypeDef get_rcc_clock; RCC_GetClocksFreq(&get_rcc_clock); 进入我们的仿真加入断点,然后在watch1观察变量值数据,然后在计算器计算就能得出,如上图。 但是这样看的PAB1上…...
HarmonyOS NEXT 技术实践-基于意图框架服务实现智能分发
在智能设备的交互中,如何准确理解并及时响应用户需求,成为提升用户体验的关键。HarmonyOS Next 的意图框架服务(Intents Kit)为这一目标提供了强大的技术支持。本文将通过一个项目实现的示例,展示如何使用意图框架服务…...
[原创](Modern C++)现代C++的第三方库的导入方式: 例如Visual Studio 2022导入GSL 4.1.0
[简介] 常用网名: 猪头三 出生日期: 1981.XX.XX 企鹅交流: 643439947 个人网站: 80x86汇编小站 编程生涯: 2001年~至今[共23年] 职业生涯: 21年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、Delphi、XCode、Eclipse…...
【jenkins插件】
1) 2) 3) 4) 5) 6) 参考: 知识库/运维/Jenkins/01-安装/13-插件.md zfoo/java-developer-document - 码云 - 开源中国...
springboot472基于web网上村委会业务办理系统(论文+源码)_kaic
摘 要 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本网上村委会业务办理系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数…...
七层网络笔记
首先,用户感知到的只是最上面一层应用层,自上而下每层都依赖于下一层,所以我们从最下一层开始切入,比较好理解 每层都运行特定的协议,越往上越靠近用户,越往下越靠近硬件 物理层由来:上面提到&a…...
PostgreSQL编译安装教程
下载安装 1.在家目录创建一个文件夹放下载安装包 mkdir softwarecd software 2.下载文件压缩包 wget https://ftp.postgresql.org/pub/source/v16.0/postgresql-16.0.tar.gz 3.解压 tar -xzvf postgresql-16.0.tar.gz 4.编译 在software/postgresql-16.0下 cd software…...
音视频学习(二十四):hls协议
基本原理 HLS协议通过将视频文件切分成多个小的媒体段(通常是10秒左右的.ts文件),并通过HTTP传输给客户端。视频播放过程中,客户端按顺序请求这些小段文件来逐步播放整个视频流。HLS还支持多种码率,以便适应不同网络条…...
pytorch MoE(专家混合网络)的简单实现。
专家混合(Mixture of Experts, MoE)是一种深度学习模型架构,通常用于处理大规模数据和复杂任务。它通过将输入分配给多个专家网络(即子模型),然后根据门控网络(gating network)的输出…...
代码随想录day24 | leetcode 93.复原IP地址 90.子集 90.子集II
93.复原IP地址 Java class Solution {List<String> result new ArrayList<String>();StringBuilder stringBuilder new StringBuilder();public List<String> restoreIpAddresses(String s) {backtracking(s, 0, 0);return result;}// number表示stringb…...
Scala项目(图书管理系统)
3、service BookService package org.app package serviceimport org.app.dao.{BookDAO, BorrowRecordDAO} import org.app.models.{BookModel, BorrowRecordModel}import java.time.LocalDateTime import scala.collection.mutable.ListBuffer// 图书业务逻辑层 class BookS…...
Deepin/Linux clash TUN模式不起作用,因网关导致的问题的解决方案。
网关导致的问题的解决方案 查看路由 ip route寻找默认路由 默认路由应当为Mihomo default dev Mihomo scope link 如果不是,则 sudo ip route add default dev Mihomo在clash TUN开关状态发生变化时,Mihomo网卡会消失,所以提示找不到网卡…...
【macos java反编译工具Java Decompiler】
mac上能用的反编译工具 https://java-decompiler.github.io/...
VLAN数据格式
概念:VLAN叫做虚拟局域网,指在物理网络中根据用于、工作组、应用等进行逻辑划分的局域网,是一个广播域,与用户的物理位置没有关系。 作用:VLAN可以灵活的进行分段和组织,能够实现更好的安全性,…...
Error response from daemon: Get “https://registry-1.docker.io/v2/“ 问题解决
文章目录 一、参考资料二、修改nameserver三、修改daemon.json文件 一、参考资料 问题的根源在于系统每次都会自动修改DNS,所以需要永久修改 linux永久修改dns 二、修改nameserver sudo nano /etc/resolvconf/resolv.conf.d/head 没有该文件的话需要安装 sudo a…...
每天40分玩转Django:Django部署
Django部署 一、今日学习内容概述 学习模块重要程度主要内容生产环境配置⭐⭐⭐⭐⭐settings配置、环境变量WSGI服务器⭐⭐⭐⭐⭐Gunicorn配置、性能优化Nginx配置⭐⭐⭐⭐反向代理、静态文件安全设置⭐⭐⭐⭐⭐SSL证书、安全选项 二、生产环境配置 2.1 项目结构调整 mypr…...
学习threejs,scene.overrideMaterial全局材质效果
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.2 ☘️THREE.Scene 场景1.2 ☘️…...
新版Android Studio 2024.1.2版本,如何通过无线wifi连接手机实现交互
1、首先,先确定手机是否启动了开发者选项 在我的设备 -> 全部参数 -> MIUI版本点击6下 (有的手机是 关于手机 -> 查看手机版本 ) 2、在设置中搜索 开启开发者选项 3、进入开发者选项后,在 调试 中选择 无线调试并选择…...
PTA 时间几何
作者 Happyer 单位 湖北文理学院 乘火车或飞机常有由始发时间历经时间计算终到时间的事儿。我们通过三个 函数来完成,当然,为了存储几点几分这个时间,我们专门定义了一个结构体Time_gxx,你要完成的是写二个函数:1)st…...
事务、管道
目录 事务 相关命令 悲观锁 乐观锁 管道 实例 Pipeline与原生批量命令对比 Pipeline与事物对比 使用Pipeline注意事项 事务 相关命令 命令描述discard取消事务,放弃执行事务块内的所有命令exec执行所有事务块内的事务(所有命令依次执行&#x…...
深入了解蓝牙Profile类型与设备的对应关系
在现代技术中,蓝牙作为一种无线通信技术,广泛应用于各种设备之间的短距离通信。不同的设备在连接时使用不同的蓝牙Profile(配置文件),每种Profile都为特定的设备功能提供支持,例如音频流传输、语音通话、文件传输等。在本文中,我们将详细介绍蓝牙Profile的常见类型及其对…...
net_device结构
net_device是Linux内核中用于表示网络设备的数据结构,它存储了特定网络设备的所有信息,无论是真实设备(如Ethernet)还是虚拟设备(如Bonding或VLAN)。以下是对net_device的详细解析: 一、定义与位置 net_device数据结构定义在Linux内核的include/linux/netdevice.h文件中…...
DuetWebControl 开源项目常见问题解决方案
DuetWebControl 开源项目常见问题解决方案 DuetWebControl A completely new web interface for the Duet electronics [这里是图片001] 项目地址: https://gitcode.com/gh_mirrors/du/DuetWebControl 一、项目基础介绍 DuetWebControl 是一个为 RepRapFirmware 设计的完全响…...
c# 不同数据类型转换
namespace Systempublic static class ConvertExtension {public static byte[] ToBinaryByteArray(this byte[] bytes){// 每个字节有 8 位,所以总位数为 bytes.Length * 8byte[] binaryArray new byte[bytes.Length * 8];int index 0;// 遍历每个字节foreach (b…...
2025.01.15python商业数据分析top2
一、 导入项目 导入项目、准备项目数据 import pandas as pd# 文件路径为python文件位置下的相对路径dwxpd.read_excel("电蚊香套装市场近三年交易额.xlsx") fmfzpd.read_excel("防霉防蛀片市场近三年交易额.xlsx") msmcpd.read_excel("灭鼠杀虫剂市…...
软件信息化平台项目投标技术方案中如何进行项目实施方案以及安全质量方案培训售后方案应急预案的编写?
在软件平台投标技术方案中,项目实施方案、质量管理、安全管理、培训方案、售后服务方案和应急预案等章节至关重要,它们分别从不同角度确保项目的顺利实施、高质量交付、安全稳定运行、用户有效使用、持续服务保障以及应对突发情况的能力。各章节编制要点相互关联、协同作用,…...
目标检测-R-CNN
R-CNN在2014年被提出,算法流程可以概括如下: 候选区域生成:利用选择性搜索(selective search)方法找出图片中可能存在目标的候选区域(region proposal) CNN网络提取特征:对候选区域进行特征提取(可以使用AlexNet、VGG等网络) 目…...
计算机毕业设计PySpark+PyFlink+Hive地震预测系统 地震数据分析可视化 地震爬虫 大数据毕业设计 Hadoop 机器学习 深度学习
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...