feat: 站点访问统计与可配置侧边栏组件

- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面
- 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件
- 新增 widget 嵌入代码生成(widgetCode)与 widgetApi
- 遥测逻辑迁移至 handler 层,删除 service/telemetry
- env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
2026-09-24 18:53:45 +08:00
parent 3e55b5d230
commit 7b327bc8cc
60 changed files with 6539 additions and 572 deletions

View File

@@ -0,0 +1,69 @@
package service
import "strings"
// botSignatures 爬虫 UA 特征(小写子串 → 展示名),覆盖搜索引擎 / SEO 工具 / AI 爬虫 / 命令行客户端
var botSignatures = []struct {
token string
name string
}{
{"googlebot", "Googlebot"},
{"google-inspectiontool", "Google Inspection Tool"},
{"apis-google", "Googlebot"},
{"bingbot", "bingbot"},
{"baiduspider", "Baiduspider"},
{"sogou", "Sogou spider"},
{"yisou", "YisouSpider"},
{"360spider", "360Spider"},
{"haosouspider", "HaosouSpider"},
{"bytespider", "Bytespider"},
{"duckduckbot", "DuckDuckBot"},
{"applebot", "Applebot"},
{"petalbot", "PetalBot"},
{"yandexbot", "YandexBot"},
{"semrushbot", "SemrushBot"},
{"ahrefsbot", "AhrefsBot"},
{"mj12bot", "MJ12bot"},
{"dotbot", "DotBot"},
{"gptbot", "GPTBot"},
{"claudebot", "ClaudeBot"},
{"claude-web", "ClaudeBot"},
{"ccbot", "CCBot"},
{"perplexitybot", "PerplexityBot"},
{"amazonbot", "Amazonbot"},
{"facebookexternalhit", "facebookexternalhit"},
{"twitterbot", "Twitterbot"},
{"slackbot", "Slackbot"},
{"discordbot", "Discordbot"},
{"linkedinbot", "LinkedInBot"},
{"curl/", "curl"},
{"wget", "Wget"},
{"python-requests", "python-requests"},
{"python-urllib", "python-urllib"},
{"httpclient", "HTTP Client"},
{"go-http-client", "Go-http-client"},
{"java/", "Java Client"},
{"okhttp", "OkHttp"},
{"scrapy", "Scrapy"},
{"headlesschrome", "HeadlessChrome"},
{"puppeteer", "Puppeteer"},
{"phantomjs", "PhantomJS"},
{"bot;", "Bot"},
{"spider", "Spider"},
{"crawler", "Crawler"},
}
// ClassifyBot 根据 User-Agent 判定爬虫;命中返回爬虫展示名,否则返回空串。
// 纯小写包含匹配,便于单测。
func ClassifyBot(ua string) string {
low := strings.ToLower(strings.TrimSpace(ua))
if low == "" {
return ""
}
for _, sig := range botSignatures {
if strings.Contains(low, sig.token) {
return sig.name
}
}
return ""
}