記事一覧
AI 執筆

Hermes の検索が不安定:SearXNG のアウトバウンドを Mihomo に任せる

Hermes を SearXNG に接続すると、表面上は検索の問題を本機内に回収したように見えます:SEARXNG_URL=http://localhost:8888。しかし、国内サーバー(中華圏のサーバー)で実際に詰まりやすいのは Hermes から SearXNG への通信ではなく、SearXNG が独自に Google、DuckDuckGo、Brave、Startpage といった検索ソースへアクセスする部分です。

この記事の目次

最終的に、私はリンクを3層に分割しました。Hermes はローカルの SearXNG にのみアクセスし、SearXNG は設定内で HTTP/HTTPS のアウトバウンドリクエストを明示的に Mihomo に委ね、Mihomo が単独でサブスクリプション、ヘルスチェック、ノードの自動選択を担当します。この方法のメリットは「設定がかっこいい」ことではなく、問題発生時に階層ごとに確認できる点です。Hermes がローカル検索にリクエストを送ったか、SearXNG が JSON を返したか、Mihomo に利用可能なノードがあるか、そしてサブスクリプション自体の解析に失敗していないか。

全体のフローは次のとおりです。

Hermes Agent
  -> http://localhost:8888
  -> SearXNG
  -> http://mihomo:7890
  -> Mihomo が利用可能なノードを自動選択
  -> 海外の検索エンジン

ホストマシンでローカルポートのみを公開する:

127.0.0.1:8888  -> SearXNG
127.0.0.1:7897  -> Mihomo 混合プロキシ、デバッグ用
127.0.0.1:9097  -> Mihomo コントローラー、ローカル管理用

SearXNG と Mihomo は同じ Docker ネットワーク内にいるため、SearXNG はホストマシンの 127.0.0.1:7897 にアクセスせず、コンテナ名でアクセスします。

http://mihomo:7890

SearXNG で最も見落とされやすい 2 箇所

Hermes が SearXNG を呼び出す際に JSON をリクエストします。SearXNG 公式 Search API ドキュメントにも明記されている通り、format パラメータを使用できるかどうかは settings.ymlsearch.formats で有効化されているかどうかに依存します。有効化されていないフォーマットをリクエストすると 403 が返されます。したがって、デフォルトの HTML だけを残しておくわけにはいきません。

search:
  safe_search: 0
  autocomplete: ""
  formats:
    - html
    - json

2つ目はプロキシです。Docker の環境変数やシステムレベルの http_proxy がアプリケーション層で安定的に継承されると過信しないでください。SearXNG のリクエストは独自に行われるため、最も直接的な方法は outgoing.proxies に明示的に記述することです。

outgoing:
  request_timeout: 15.0
  max_request_timeout: 40.0
  extra_proxy_timeout: 10
  proxies:
    "http://": "http://mihomo:7890"
    "https://": "http://mihomo:7890"

SearXNG のデフォルト設定例では all://: という書き方が見られるため、自然に間違った設定項目ではないことが分かります。ただし、私が今回使用した環境では、all://: は期待通りにマッチせず、最終的に落ち着いたのは http://https:// を分けて記述する方法でした。記事内でこの境界を保持しているのは、ある一度の実測結果をすべてのバージョンにおける確定的な結論のように書いてしまわないためです。

完全な SearXNG の主要な設定は以下のように圧縮できます。

use_default_settings: true

general:
  instance_name: "Hermes SearXNG"

search:
  safe_search: 0
  autocomplete: ""
  formats:
    - html
    - json

server:
  secret_key: "请替换成随机密钥"
  limiter: false
  image_proxy: true
  bind_address: "0.0.0.0"

valkey:
  url: valkey://valkey:6379/0

outgoing:
  request_timeout: 15.0
  max_request_timeout: 40.0
  extra_proxy_timeout: 10
  proxies:
    "http://": "http://mihomo:7890"
    "https://": "http://mihomo:7890"

engines:
  - name: bing
    disabled: false
    shortcut: bi

  - name: bing news
    disabled: false
    shortcut: bin

  - name: google
    disabled: false
    shortcut: go
    timeout: 8.0

  - name: brave
    disabled: false
    shortcut: br
    timeout: 8.0

  - name: duckduckgo
    disabled: false
    shortcut: ddg
    timeout: 8.0

  - name: startpage
    disabled: false
    shortcut: sp
    timeout: 8.0

  - name: wikipedia
    disabled: false
    shortcut: wp

  - name: arxiv
    disabled: false
    shortcut: arx

  - name: sogou
    disabled: true

  - name: 360search
    disabled: true

ui:
  static_use_hash: true

ここでちょっとした落とし穴があります:SearXNG のデフォルトエンジン設定では、Bing ニュースの namebing news であり、enginebing_news です。use_default_settings: true の場合、enginesname でマージおよび上書きされるため、ここでは name: bing news と記述し、name: bing_news とは記述しないでください。

Mihoma がやることはただ1つ:SearXNG に安定した出口を与えること

Mihomo はサーバー全体を引き継ぐ必要はなく、Docker をグローバルにプロキシ経由にする必要もありません。mixed port を 1 つだけ開いて、同じ Docker ネットワーク内の SearXNG がアクセスできるようにします:

mixed-port: 7890
allow-lan: true
bind-address: '*'
mode: rule
log-level: info
ipv6: false

external-controller: 0.0.0.0:9090
secret: "请替换成随机密钥"

profile:
  store-selected: true
  store-fake-ip: true

proxy-providers:
  sub:
    type: http
    url: "你的订阅地址"
    interval: 3600
    path: ./proxy_providers/sub.yaml
    health-check:
      enable: true
      url: https://www.gstatic.com/generate_204
      interval: 300
      timeout: 5000
      lazy: false
      expected-status: 204

proxy-groups:
  - name: AUTO
    type: url-test
    use:
      - sub
    url: https://www.gstatic.com/generate_204
    interval: 300
    timeout: 5000
    tolerance: 50
    lazy: false
    expected-status: 204

  - name: PROXY
    type: select
    proxies:
      - AUTO
      - DIRECT
    use:
      - sub

rules:
  - MATCH,PROXY

この設定の意味はとても限定的です:購読は 3600 秒ごとに更新され、ノードは 300 秒ごとにヘルスチェックを行い、AUTOurl-test でレイテンシに基づいてノードを選択し、すべてのトラフィックは最終的に PROXY にマッチします。新しい設定が最初に起動されたとき、PROXY リストの最初にあるのは AUTO です。今後、コントロールパネルでノードを手動で切り替えた場合、store-selected: true が選択を保持するため、「デフォルトで AUTO を通る」とは限りなくなります。

また、サブスクリプション形式にも注意が必要です。proxy-providers は provider 形式、あるいは Mihomo が provider として解析できるサブスクリプションに適しています。サービス事業者によっては、完全な Clash 設定を返すものもあれば、ノードリストを返すものもあります。ログに解析失敗のエラーが出た場合、SearXNG を疑うのではなく、まずサービス事業者の管理画面で Clash MetaMihomoProxy Provider 形式に切り替えてください。

MOD スクリプト

このスクリプトは、SearXNG を /opt/searxng に配置済みのマシンを対象としています。既存の docker-compose.ymlsearxng/settings.yml をバックアップし、本機にあらかじめ用意されている metacubex/mihomo イメージをそのまま利用し、SearXNG、Valkey、Mihomo を同じ compose プロジェクトにまとめます。

このスクリプトはMihomoのイメージを自動的にプルしません。SearXNGとValkeyについても、ローカルに対応するイメージがない場合、docker compose upが起動できるかどうかは、ローカルのイメージとネットワーク環境によって異なります。完全にオフラインの環境では、3つのイメージを事前にすべて用意しておいてください。

sudo bash -s <<'EOF'
set -euo pipefail

APP_DIR="/opt/searxng"
COMPOSE_FILE="$APP_DIR/docker-compose.yml"
SETTINGS_FILE="$APP_DIR/searxng/settings.yml"
MIHOMO_DIR="$APP_DIR/mihomo"

if [ ! -f "$COMPOSE_FILE" ]; then
  echo "$COMPOSE_FILE が見つかりません。SearXNG が /opt/searxng にデプロイされているか確認してください"
  exit 1
fi

if [ ! -f "$SETTINGS_FILE" ]; then
  echo "$SETTINGS_FILE が見つかりません。SearXNG settings.yml のパスを確認してください"
  exit 1
fi

if docker image inspect metacubex/mihomo:latest >/dev/null 2>&1; then
  MIHOMO_IMAGE="metacubex/mihomo:latest"
else
  MIHOMO_IMAGE="$(docker images --format '{{.Repository}}:{{.Tag}}' \
    | awk -F: '$1=="metacubex/mihomo" && $2!="<none>" {print; exit}')"
fi

if [ -z "${MIHOMO_IMAGE:-}" ]; then
  echo "利用可能なローカル metacubex/mihomo イメージが検出されませんでした。"
  echo "先に確認してください:docker images | grep -i mihomo"
  exit 1
fi

echo "ローカル Mihomo イメージを検出しました:$MIHOMO_IMAGE"

LOCAL_MIHOMO_IMAGE="searxng-mihomo-local:latest"
docker tag "$MIHOMO_IMAGE" "$LOCAL_MIHOMO_IMAGE"

printf "Clash/Mihomo の購読 URL を入力してください: " > /dev/tty
IFS= read -r -s SUB_URL < /dev/tty
printf "\n" > /dev/tty

if [ -z "$SUB_URL" ]; then
  echo "購読 URL が空のため、終了します。"
  exit 1
fi

mkdir -p "$MIHOMO_DIR/proxy_providers"
chmod 700 "$MIHOMO_DIR"

TS="$(date +%Y%m%d-%H%M%S)"

cp -a "$COMPOSE_FILE" "$COMPOSE_FILE.bak.$TS"
cp -a "$SETTINGS_FILE" "$SETTINGS_FILE.bak.$TS"

echo "バックアップを作成しました:"
echo "  $COMPOSE_FILE.bak.$TS"
echo "  $SETTINGS_FILE.bak.$TS"

MIHOMO_SECRET="$(openssl rand -hex 16)"

cat > "$MIHOMO_DIR/config.yaml" <<YAML
mixed-port: 7890
allow-lan: true
bind-address: '*'
mode: rule
log-level: info
ipv6: false

external-controller: 0.0.0.0:9090
secret: "$MIHOMO_SECRET"

profile:
  store-selected: true
  store-fake-ip: true

proxy-providers:
  sub:
    type: http
    url: "$SUB_URL"
    interval: 3600
    path: ./proxy_providers/sub.yaml
    health-check:
      enable: true
      url: https://www.gstatic.com/generate_204
      interval: 300
      timeout: 5000
      lazy: false
      expected-status: 204

proxy-groups:
  - name: AUTO
    type: url-test
    use:
      - sub
    url: https://www.gstatic.com/generate_204
    interval: 300
    timeout: 5000
    tolerance: 50
    lazy: false
    expected-status: 204

  - name: PROXY
    type: select
    proxies:
      - AUTO
      - DIRECT
    use:
      - sub

rules:
  - MATCH,PROXY
YAML

chmod 600 "$MIHOMO_DIR/config.yaml"

OLD_SECRET="$(grep -E '^[[:space:]]*secret_key:' "$SETTINGS_FILE" | head -n1 | sed -E "s/.*secret_key:[[:space:]]*['\"]?([^'\"]+)['\"]?.*/\1/" || true)"

if [ -z "$OLD_SECRET" ] || [ "$OLD_SECRET" = "secret_key:" ]; then
  OLD_SECRET="$(openssl rand -hex 32)"
fi

cat > "$SETTINGS_FILE" <<YAML
use_default_settings: true

general:
  instance_name: "Hermes SearXNG"

search:
  safe_search: 0
  autocomplete: ""
  formats:
    - html
    - json

server:
  secret_key: "$OLD_SECRET"
  limiter: yml:ro
      - searxng-cache:/var/cache/searxng:rw
    depends_on:
      - valkey
      - mihomo
    networks:
      - searxng-net
    logging:
      driver: json-file
      options:
        max-size: "2m"
        max-file: "3"

  valkey:
    image: docker.io/valkey/valkey:8-alpine
    container_name: searxng-valkey
    restart: unless-stopped
    command: valkey-server --save 30 1 --loglevel warning
    volumes:
      - valkey-data:/data
    networks:
      - searxng-net
    logging:
      driver: json-file
      options:
        max-size: "2m"
        max-file: "3"

  mihomo:
    image: $LOCAL_MIHOMO_IMAGE
    container_name: searxng-mihomo
    restart: unless-stopped
    command: ["-d", "/root/.config/mihomo"]
    ports:
      - "127.0.0.1:7897:7890"
      - "127.0.0.1:9097:9090"
    volumes:
      - ./mihomo:/root/.config/mihomo
    networks:
      - searxng-net
    logging:
      driver: json-file
      options:
        max-size: "2m"
        max-file: "3"

networks:
  searxng-net:

volumes:
  searxng-cache:
  valkey-data:
YAML

cd "$APP_DIR"

if docker compose version >/dev/null 2>&1; then
  DC="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
  DC="docker-compose"
else
  echo "docker compose / docker-compose が見つかりません"
  exit 1
fi

echo
echo "サービスを開始しています..."
$DC up -d --no-build

echo
echo "サービスの起動を待機しています..."
sleep 12

echo
echo "コンテナーの状態:"
$DC ps

echo
echo "Mihomo の最新ログ:"
$DC logs --tail=80 mihomo || true

echo
echo "Mihomo ローカルプロキシーポート 127.0.0.1:7897 をテストしています:"
curl -I -sS --max-time 25 --proxy http://127.0.0.1:7897 https://www.gstatic.com/generate_204 || true

echo
echo
echo "SearXNG JSON 検索をテストしています:"
curl -sS --max-time 50 "http://127.0.0.1:8888/search?q=openai%20gpt&format=json" \
  | python3 -c 'import sys,json; d=json.load(sys.stdin); print("OK:", len(d.get("results", [])), "results")' || true

echo
echo "完了しました。"
echo "Hermes は引き続き次の設定を使用します: SEARXNG_URL=http://localhost:8888"
echo "SearXNG 送信プロキシー: searxng -> http://mihomo:7890"
echo "Mihomo ローカルデバッグプロキシー: http://127.0.0.1:7897"
echo "Mihomo 制御ポート: http://127.0.0.1:9097"
echo "Mihomo 設定ファイル: $MIHOMO_DIR/config.yaml"
EOF

Hermes は検索エントリのみを保持する

Hermes 側は Mihomo について知る必要はありません。必要なのは、ローカルマシンに SearXNG があるということだけです。

nano ~/.hermes/.env

書き込み:

SEARXNG_URL=http://localhost:8888

次に、~/.hermes/config.yaml で検索バックエンドを指定します:

web:
  search_backend: "searxng"

Hermes 公式の SearXNG skill をインストールしている場合は、引き続き使用できます。

hermes skills install official/research/searxng-search

もし Hermes がユーザーサービスであれば:

systemctl --user restart hermes
systemctl --user status hermes --no-pager

ここにも境界があります:SearXNG は検索を担当し、ウェブページ本文の抽出は担当しません。Hermes のドキュメントでも search backend と extract backend は分けられています。後で Hermes に検索結果ページの本文を読み込ませたい場合は、web.extract_backend に Firecrawl、Tavily、Exa、Parallel などの抽出ソリューションを別途設定する必要があります。

検証:階層をスキップしないこと

まず Mihomo をテストしてください。すぐに Hermes に問い合わせないでください。

curl -I --proxy http://127.0.0.1:7897 https://www.gstatic.com/generate_204

HTTP ステータスを返せる場合、ホストのデバッグプロキシポートが利用可能であることを示します。次に SearXNG の JSON をテストします。

curl -s --max-time 50 \
  "http://127.0.0.1:8888/search?q=openai%20gpt&format=json" \
  | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d.get("results", [])), "results")'

ここで 403 が発生した場合は、まず search.formatsjson が含まれているか確認してください。ここでタイムアウトが発生したり結果が空の場合は、SearXNG のログを確認してください。

cd /opt/searxng
docker compose logs -f searxng

次に、Hermes で検索をトリガーするリクエストを送信します。

OpenAI GPT の最新情報をインターネットで検索し、ソースリンクを列挙してください。

ログに /search?...format=json が表示されている場合、Hermes がローカルの SearXNG を呼び出していることを示しています。次に Mihomo を確認します。

cd /opt/searxng
docker compose logs -f mihomo

サブスクリプションの更新、provider、health check、AUTO といった情報に注目してください。 Mihomo がサブスクリプションを解析できていなければ、SearXNG をどれだけ正しく設定しても意味がありません。

省いてはいけない境界線いくつか

まず、ポートをパブリックネットワークに公開しないでください。本記事の compose はローカルホストのみにバインドしています:

ports:
  - "127.0.0.1:8888:8080"
  - "127.0.0.1:7897:7890"
  - "127.0.0.1:9097:9090"

「変更しないでください:」のように変更しないでください。

0.0.0.0:8888:8080
0.0.0.0:7897:7890
0.0.0.0:9097:9090

そうでない場合、検索サービス、プロキシポート、および Mihomo のコントロールポートは、いずれもパブリックネットワークから悪用されるリスクがあります。external-controller: 0.0.0.0:9090 はコンテナ内でのリッスンであり、外部からアクセスできるかどうかを実際に決定するのは compose のポートバインディングです。

第二に、Google や Startpage が頻繁にタイムアウトする場合、すぐにリンク構成全体を否定的に見直さないでください。まず Bing、Brave、DuckDuckGo、Wikipedia、arXiv などをそのまま残しておき、Mihomo のサブスクリプションとヘルスチェックが安定してから、验证码やタイムアウトを引き起こしやすいエンジンを有効にしてください。

第三に、スクリプトのロールバックポイントが明確である点です。

cd /opt/searxng

cp docker-compose.yml.bak.あなたのタイムスタンプ docker-compose.yml
cp searxng/settings.yml.bak.あなたのタイムスタンプ searxng/settings.yml

docker compose up -d

私はサーバー全体にグローバルプロキシを適用するよりも、このように役割を分割するアプローチを好みます。グローバルプロキシの問題は影響範囲が広すぎることであり、障害が発生した際に、それがシステム環境、Docker、アプリケーション設定、それともプロキシノード自体が原因なのかを判断するのが難しくなります。Hermes、SearXNG、Mihomo はそれぞれ一つの役割だけを担うため、トラブルシューティング時にはむしろ楽になります。

参考資料

写作附记

この文章では、元の素材の中で最も実用的な部分を保持しています。具体的には、階層化アーキテクチャ、SearXNG JSON、明示的な outgoing proxy、Mihomo の provider とヘルスチェック、Docker Compose、Hermes 設定、検証コマンド、よくある質問、そしてロールバックです。削減したのは繰り返しの説明や、誤解を招きやすい絶対的な表現です。たとえば all://: は汎用的に無効な設定ではなく、この環境では期待どおりにマッチしなかっただけです。

また、設定名を1か所修正しました:SearXNG の Bing ニュースエンジンは name: bing news と記述する必要があります。name: bing_news と書くと、デフォルトのエンジン名と一致しないため、通常のスペルミスよりもリスクが高くなります。

元のプロンプト

ユーザーは、「中国国内サーバーへの Hermes + SearXNG + Mihomo デプロイ:Hermes 検索をプロキシ経由にし、利用可能なノードを自動選択させる」と題した完全なデプロイ資料を提供し、整理・分析して記事に誤りがないことを確認した上で、ブログ執筆スキルを呼び出して記事にまとめるよう求めています。

資料には、背景、目標アーキテクチャ、なぜシステムプロキシを使用しないのか、SearXNG settings.yml、Mihomo config.yaml、Docker Compose、改造スクリプト、Hermes 設定、検証フロー、よくある質問、ロールバック方法、最終的な効果が含まれています。

この位置までスクロールするとコメントを読み込みます。