feat: metona-search-proxy v2 — CLI命令/端口7899/百度引擎/详细README

This commit is contained in:
thzxx
2026-06-10 17:21:19 +08:00
parent 87f80dd11e
commit b8266fd495
4 changed files with 252 additions and 71 deletions
+184 -27
View File
@@ -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 ```bash
# 1. 上传到服务器 # Ubuntu/Debian
scp search-proxy user@your-server:/opt/search-proxy/ 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. 编译 # 2. 编译
cd /opt/search-proxy && go build -o search-proxy . go build -o metona-search-proxy .
# 本地交叉编译: # 3. 验证
# GOOS=linux GOARCH=amd64 go build -o search-proxy . ./metona-search-proxy --version
# metona-search-proxy v2.0.0 go/go1.22.0
```
# 3. 启动(默认端口 18080 ### 本地交叉编译(在本地编译好上传)
PORT=18080 ./search-proxy &
# 4. 测试 ```bash
curl "http://localhost:18080/health" # Windows 上编译 Linux 版本
curl "http://localhost:18080/search?q=hello&n=5" GOOS=linux GOARCH=amd64 go build -o metona-search-proxy .
# 5. 安装 systemd 服务 # macOS 上编译 Linux 版本
sudo cp search-proxy.service /etc/systemd/system/ 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 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 # 今日日志
```
+55 -31
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"flag"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -11,6 +12,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"regexp" "regexp"
"runtime"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -18,11 +20,23 @@ import (
"time" "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 { type RateLimiter struct {
tokens atomic.Int64 tokens atomic.Int64
capacity int64 capacity int64
rate int64 // tokens/sec rate int64
lastFill atomic.Int64 lastFill atomic.Int64
} }
@@ -74,28 +88,22 @@ type Result struct {
} }
type SearchResp struct { type SearchResp struct {
Success bool `json:"success"` Success bool `json:"success"`
Query string `json:"query"` Query string `json:"query"`
Total int `json:"total"` Total int `json:"total"`
Results []Result `json:"results"` Results []Result `json:"results"`
Engine string `json:"engine,omitempty"` Engine string `json:"engine,omitempty"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
FromCache bool `json:"from_cache,omitempty"` FromCache bool `json:"from_cache,omitempty"`
} }
// ── 全局配置 ── // ── 全局配置 ──
var ( var (
httpClient = &http.Client{ httpClient *http.Client
Timeout: 12 * 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"
Transport: &http.Transport{ acceptLg = "zh-CN,zh;q=0.9,en;q=0.8"
MaxIdleConns: 100, limiter *RateLimiter
MaxIdleConnsPerHost: 20, cacheTTL int64
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
) )
// ── recovery middleware ── // ── recovery middleware ──
@@ -282,10 +290,7 @@ func decodeEntities(s string) string {
} }
func parallelSearch(q string, n int) []Result { func parallelSearch(q string, n int) []Result {
type engResult struct { ch := make(chan []Result, 4)
results []Result
}
ch := make(chan engResult, 4)
engines := []func(string, int) []Result{searchGoogle, searchBing, searchBaidu, searchDDG} engines := []func(string, int) []Result{searchGoogle, searchBing, searchBaidu, searchDDG}
for _, fn := range engines { for _, fn := range engines {
go func(f func(string, int) []Result) { go func(f func(string, int) []Result) {
@@ -293,13 +298,13 @@ func parallelSearch(q string, n int) []Result {
if r == nil { if r == nil {
r = []Result{} r = []Result{}
} }
ch <- engResult{r} ch <- r
}(fn) }(fn)
} }
seen := map[string]bool{} seen := map[string]bool{}
var out []Result var out []Result
for range engines { for range engines {
for _, r := range (<-ch).results { for _, r := range <-ch {
key := strings.TrimRight(r.URL, "/") key := strings.TrimRight(r.URL, "/")
if !seen[key] { if !seen[key] {
seen[key] = true seen[key] = true
@@ -341,7 +346,6 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
n = 20 n = 20
} }
// 缓存
ck := fmt.Sprintf("%s|%s|%d", q, engine, n) ck := fmt.Sprintf("%s|%s|%d", q, engine, n)
if v, ok := cache.Load(ck); ok { if v, ok := cache.Load(ck); ok {
ce := v.(cacheEntry) ce := v.(cacheEntry)
@@ -368,11 +372,11 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
case "all": case "all":
results = parallelSearch(q, n) results = parallelSearch(q, n)
default: 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 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{ writeJSON(w, http.StatusOK, SearchResp{
Success: true, Query: q, Engine: engine, Success: true, Query: q, Engine: engine,
Total: len(results), Results: results, 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) { func handleHealth(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]interface{}{ writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "ok", "status": "ok",
"version": version,
"engines": []string{"google", "bing", "ddg", "baidu", "all"}, "engines": []string{"google", "bing", "ddg", "baidu", "all"},
"time": time.Now().UTC().Format(time.RFC3339), "time": time.Now().UTC().Format(time.RFC3339),
}) })
@@ -395,11 +400,30 @@ func writeJSON(w http.ResponseWriter, status int, v interface{}) {
} }
func main() { func main() {
flag.Parse()
if *cliVersion {
fmt.Printf("metona-search-proxy v%s go/%s\n", version, runtime.Version())
return
}
port := os.Getenv("PORT") port := os.Getenv("PORT")
if 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 := http.NewServeMux()
mux.HandleFunc("/search", recoverWrap(handleSearch)) mux.HandleFunc("/search", recoverWrap(handleSearch))
mux.HandleFunc("/health", recoverWrap(handleHealth)) mux.HandleFunc("/health", recoverWrap(handleHealth))
@@ -412,7 +436,6 @@ func main() {
IdleTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second,
} }
// 优雅关闭
go func() { go func() {
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
@@ -423,7 +446,8 @@ func main() {
srv.Shutdown(ctx) 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 { if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err) log.Fatal(err)
} }
+13
View File
@@ -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
-13
View File
@@ -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