From b8266fd495ee24205b579d7f58af9de7203de233 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 10 Jun 2026 17:21:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20metona-search-proxy=20v2=20=E2=80=94=20?= =?UTF-8?q?CLI=E5=91=BD=E4=BB=A4/=E7=AB=AF=E5=8F=A37899/=E7=99=BE=E5=BA=A6?= =?UTF-8?q?=E5=BC=95=E6=93=8E/=E8=AF=A6=E7=BB=86README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- search-proxy/README.md | 211 ++++++++++++++++++++--- search-proxy/main.go | 86 +++++---- search-proxy/metona-search-proxy.service | 13 ++ search-proxy/search-proxy.service | 13 -- 4 files changed, 252 insertions(+), 71 deletions(-) create mode 100644 search-proxy/metona-search-proxy.service delete mode 100644 search-proxy/search-proxy.service diff --git a/search-proxy/README.md b/search-proxy/README.md index 0e3b323..1f1cc38 100644 --- a/search-proxy/README.md +++ b/search-proxy/README.md @@ -1,46 +1,203 @@ -# search-proxy +# metona-search-proxy -部署在可直连 Google/Bing/DuckDuckGo 的云服务器上,通过 HTTP API 提供搜索引擎代理。 +部署在可直连 Google/Bing/DuckDuckGo/Baidu 的云服务器上,通过 HTTP API 提供搜索引擎代理。 -## 部署 +``` +本应用(web_search) → HTTP → metona-search-proxy(:7899) → Google/Bing/DDG/Baidu → JSON +``` + +--- + +## 前置要求 + +| 依赖 | 说明 | +|------|------| +| **Go** | 1.21+(编译用,运行不需要) | +| **服务器** | 能直连 Google/Bing/DDG(海外云服务器/VPS) | +| **系统** | Linux (Ubuntu/Debian/CentOS) / macOS | + +### 安装 Go(服务器上没有的话) ```bash -# 1. 上传到服务器 -scp search-proxy user@your-server:/opt/search-proxy/ +# Ubuntu/Debian +sudo apt update && sudo apt install -y golang-go + +# CentOS/RHEL +sudo yum install -y golang + +# 或手动安装最新版 +wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz +sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz +echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && source ~/.bashrc +``` + +--- + +## 安装与编译 + +```bash +# 1. 克隆仓库 +cd /opt +git clone https://gitee.com/thzxx/metona-ollama-desktop.git +cd metona-ollama-desktop/search-proxy # 2. 编译 -cd /opt/search-proxy && go build -o search-proxy . +go build -o metona-search-proxy . -# 本地交叉编译: -# GOOS=linux GOARCH=amd64 go build -o search-proxy . +# 3. 验证 +./metona-search-proxy --version +# metona-search-proxy v2.0.0 go/go1.22.0 +``` -# 3. 启动(默认端口 18080) -PORT=18080 ./search-proxy & +### 本地交叉编译(在本地编译好上传) -# 4. 测试 -curl "http://localhost:18080/health" -curl "http://localhost:18080/search?q=hello&n=5" +```bash +# Windows 上编译 Linux 版本 +GOOS=linux GOARCH=amd64 go build -o metona-search-proxy . -# 5. 安装 systemd 服务 -sudo cp search-proxy.service /etc/systemd/system/ +# macOS 上编译 Linux 版本 +GOOS=linux GOARCH=amd64 go build -o metona-search-proxy . + +# 上传到服务器 +scp metona-search-proxy user@your-server:/opt/metona-search-proxy/ +``` + +--- + +## CLI 命令 + +```bash +./metona-search-proxy [选项] + +选项: + -port int 监听端口 (默认 7899) + -cache int 缓存时间/分钟 (默认 5) + -rate int 限流速率 req/s (默认 30) + -burst int 限流突发容量 (默认 60) + -timeout int 搜索引擎超时/秒 (默认 12) + -version 显示版本号 + +示例: + ./metona-search-proxy # 默认配置 + ./metona-search-proxy -port 7899 -cache 10 # 自定义端口和缓存 + ./metona-search-proxy -port 7899 -rate 50 -burst 100 # 高并发配置 + PORT=7899 ./metona-search-proxy # 也可用环境变量 +``` + +--- + +## 测试 + +```bash +# 启动 +./metona-search-proxy & + +# 健康检查 +curl http://localhost:7899/health + +# 单引擎搜索 +curl "http://localhost:7899/search?q=hello+world&engine=google&n=5" + +# 四引擎并行 +curl "http://localhost:7899/search?q=今天天气&engine=all&n=10" + +# 百度搜索 +curl "http://localhost:7899/search?q=春节&engine=baidu&n=5" +``` + +### 响应格式 + +```json +{ + "success": true, + "query": "hello world", + "engine": "google", + "total": 5, + "results": [ + { + "title": "...", + "url": "https://...", + "snippet": "...", + "engine": "google" + } + ] +} +``` + +--- + +## 安装为系统服务(开机自启) + +### 1. 拷贝二进制文件 + +```bash +sudo mkdir -p /opt/metona-search-proxy +sudo cp metona-search-proxy /opt/metona-search-proxy/ +sudo chmod +x /opt/metona-search-proxy/metona-search-proxy +``` + +### 2. 创建 systemd 服务文件 + +```bash +sudo tee /etc/systemd/system/metona-search-proxy.service << 'EOF' +[Unit] +Description=Metona Search Proxy - Google/Bing/DDG/Baidu API +After=network.target + +[Service] +Type=simple +ExecStart=/opt/metona-search-proxy/metona-search-proxy +Restart=always +RestartSec=5 +Environment=PORT=7899 + +[Install] +WantedBy=multi-user.target +EOF +``` + +### 3. 启动服务 + +```bash sudo systemctl daemon-reload -sudo systemctl enable --now search-proxy +sudo systemctl enable metona-search-proxy # 开机自启 +sudo systemctl start metona-search-proxy # 立即启动 + +# 查看状态 +sudo systemctl status metona-search-proxy + +# 查看日志 +sudo journalctl -u metona-search-proxy -f ``` -## API +### 4. 验证 +```bash +curl http://localhost:7899/health +# {"engines":["google","bing","ddg","baidu","all"],"status":"ok","time":"...","version":"2.0.0"} ``` -GET /search?q=关键词&engine=google&n=10 -engine: google | bing | ddg | baidu | all (默认) -n: 1-20,默认10 -缓存: 5分钟 -限流: 30 req/s - -GET /health -→ {"status":"ok","engines":["google","bing","ddg","baidu","all"],"time":"..."} -``` +--- ## 本应用配置 -设置面板 → 代理设置 → 填入 `http://服务器IP:18080` → 重启 +Metona Ollama Desktop 设置面板 → 代理设置 → 填入: + +``` +http://你的服务器IP:7899 +``` + +重启应用即可。web_search 的 Google/DDG/Bing/百度 四引擎请求会自动路由到这台服务器。 + +--- + +## 常用管理命令 + +```bash +sudo systemctl start metona-search-proxy # 启动 +sudo systemctl stop metona-search-proxy # 停止 +sudo systemctl restart metona-search-proxy # 重启 +sudo systemctl status metona-search-proxy # 状态 +sudo journalctl -u metona-search-proxy -f # 实时日志 +sudo journalctl -u metona-search-proxy --since today # 今日日志 +``` diff --git a/search-proxy/main.go b/search-proxy/main.go index 26ee11c..6aec6e9 100644 --- a/search-proxy/main.go +++ b/search-proxy/main.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "flag" "fmt" "io" "log" @@ -11,6 +12,7 @@ import ( "os" "os/signal" "regexp" + "runtime" "strings" "sync" "sync/atomic" @@ -18,11 +20,23 @@ import ( "time" ) +const version = "2.0.0" + +// ── CLI ── +var ( + cliPort = flag.Int("port", 7899, "监听端口") + cliCache = flag.Int("cache", 5, "缓存时间(分钟)") + cliRate = flag.Int("rate", 30, "限流速率(req/s)") + cliBurst = flag.Int("burst", 60, "限流突发容量") + cliTimeout = flag.Int("timeout", 12, "搜索引擎请求超时(秒)") + cliVersion = flag.Bool("version", false, "显示版本") +) + // ── 限流器 ── type RateLimiter struct { tokens atomic.Int64 capacity int64 - rate int64 // tokens/sec + rate int64 lastFill atomic.Int64 } @@ -74,28 +88,22 @@ type Result struct { } type SearchResp struct { - Success bool `json:"success"` - Query string `json:"query"` - Total int `json:"total"` - Results []Result `json:"results"` - Engine string `json:"engine,omitempty"` - Error string `json:"error,omitempty"` - FromCache bool `json:"from_cache,omitempty"` + Success bool `json:"success"` + Query string `json:"query"` + Total int `json:"total"` + Results []Result `json:"results"` + Engine string `json:"engine,omitempty"` + Error string `json:"error,omitempty"` + FromCache bool `json:"from_cache,omitempty"` } // ── 全局配置 ── var ( - httpClient = &http.Client{ - Timeout: 12 * time.Second, - Transport: &http.Transport{ - MaxIdleConns: 100, - MaxIdleConnsPerHost: 20, - IdleConnTimeout: 90 * time.Second, - }, - } - ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" - acceptLg = "zh-CN,zh;q=0.9,en;q=0.8" - limiter = newRateLimiter(30, 60) // 30 req/s, burst 60 + httpClient *http.Client + ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" + acceptLg = "zh-CN,zh;q=0.9,en;q=0.8" + limiter *RateLimiter + cacheTTL int64 ) // ── recovery middleware ── @@ -282,10 +290,7 @@ func decodeEntities(s string) string { } func parallelSearch(q string, n int) []Result { - type engResult struct { - results []Result - } - ch := make(chan engResult, 4) + ch := make(chan []Result, 4) engines := []func(string, int) []Result{searchGoogle, searchBing, searchBaidu, searchDDG} for _, fn := range engines { go func(f func(string, int) []Result) { @@ -293,13 +298,13 @@ func parallelSearch(q string, n int) []Result { if r == nil { r = []Result{} } - ch <- engResult{r} + ch <- r }(fn) } seen := map[string]bool{} var out []Result for range engines { - for _, r := range (<-ch).results { + for _, r := range <-ch { key := strings.TrimRight(r.URL, "/") if !seen[key] { seen[key] = true @@ -341,7 +346,6 @@ func handleSearch(w http.ResponseWriter, r *http.Request) { n = 20 } - // 缓存 ck := fmt.Sprintf("%s|%s|%d", q, engine, n) if v, ok := cache.Load(ck); ok { ce := v.(cacheEntry) @@ -368,11 +372,11 @@ func handleSearch(w http.ResponseWriter, r *http.Request) { case "all": results = parallelSearch(q, n) default: - writeJSON(w, http.StatusBadRequest, SearchResp{Error: "unknown engine: " + engine + " (use google/bing/ddg/baidu/all)"}) + writeJSON(w, http.StatusBadRequest, SearchResp{Error: "unknown engine: " + engine}) return } - cache.Store(ck, cacheEntry{Results: results, ExpiresAt: time.Now().Add(5 * time.Minute).Unix()}) + cache.Store(ck, cacheEntry{Results: results, ExpiresAt: time.Now().Add(time.Duration(cacheTTL) * time.Minute).Unix()}) writeJSON(w, http.StatusOK, SearchResp{ Success: true, Query: q, Engine: engine, Total: len(results), Results: results, @@ -382,6 +386,7 @@ func handleSearch(w http.ResponseWriter, r *http.Request) { func handleHealth(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]interface{}{ "status": "ok", + "version": version, "engines": []string{"google", "bing", "ddg", "baidu", "all"}, "time": time.Now().UTC().Format(time.RFC3339), }) @@ -395,11 +400,30 @@ func writeJSON(w http.ResponseWriter, status int, v interface{}) { } func main() { + flag.Parse() + + if *cliVersion { + fmt.Printf("metona-search-proxy v%s go/%s\n", version, runtime.Version()) + return + } + port := os.Getenv("PORT") if port == "" { - port = "18080" + port = fmt.Sprintf("%d", *cliPort) } + // 初始化 + httpClient = &http.Client{ + Timeout: time.Duration(*cliTimeout) * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 20, + IdleConnTimeout: 90 * time.Second, + }, + } + limiter = newRateLimiter(int64(*cliRate), int64(*cliBurst)) + cacheTTL = int64(*cliCache) + mux := http.NewServeMux() mux.HandleFunc("/search", recoverWrap(handleSearch)) mux.HandleFunc("/health", recoverWrap(handleHealth)) @@ -412,7 +436,6 @@ func main() { IdleTimeout: 60 * time.Second, } - // 优雅关闭 go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) @@ -423,7 +446,8 @@ func main() { srv.Shutdown(ctx) }() - log.Printf("search-proxy :%s engines: google/bing/ddg/baidu/all cache:5min rate:%d/s", port, 30) + log.Printf("metona-search-proxy v%s :%s engines: google/bing/ddg/baidu/all cache: %dm rate: %d/s", + version, port, *cliCache, *cliRate) if err := srv.ListenAndServe(); err != http.ErrServerClosed { log.Fatal(err) } diff --git a/search-proxy/metona-search-proxy.service b/search-proxy/metona-search-proxy.service new file mode 100644 index 0000000..e85895d --- /dev/null +++ b/search-proxy/metona-search-proxy.service @@ -0,0 +1,13 @@ +[Unit] +Description=Metona Search Proxy - Google/Bing/DDG/Baidu API +After=network.target + +[Service] +Type=simple +ExecStart=/opt/metona-search-proxy/metona-search-proxy +Restart=always +RestartSec=5 +Environment=PORT=7899 + +[Install] +WantedBy=multi-user.target diff --git a/search-proxy/search-proxy.service b/search-proxy/search-proxy.service deleted file mode 100644 index 2f3e953..0000000 --- a/search-proxy/search-proxy.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Search Proxy - Google/Bing/DDG/Baidu API -After=network.target - -[Service] -Type=simple -ExecStart=/opt/search-proxy/search-proxy -Restart=always -RestartSec=5 -Environment=PORT=18080 - -[Install] -WantedBy=multi-user.target