feat: metona-search-proxy v2 — CLI命令/端口7899/百度引擎/详细README
This commit is contained in:
+184
-27
@@ -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 # 今日日志
|
||||||
|
```
|
||||||
|
|||||||
+46
-22
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,17 +99,11 @@ type SearchResp struct {
|
|||||||
|
|
||||||
// ── 全局配置 ──
|
// ── 全局配置 ──
|
||||||
var (
|
var (
|
||||||
httpClient = &http.Client{
|
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"
|
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"
|
acceptLg = "zh-CN,zh;q=0.9,en;q=0.8"
|
||||||
limiter = newRateLimiter(30, 60) // 30 req/s, burst 60
|
limiter *RateLimiter
|
||||||
|
cacheTTL int64
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user