プロジェクト

全般

プロフィール

AIタスク #10469

Redmine Admin さんが14日前に更新

# LLM対策診断ツール実装 詳細仕様 

 ## 背景 
 集客ページとして、URL入力→AI検索(LLM)に引用されやすいかを自動診断するツールをvend01(/root/website-vending-machine/vend01)に追加する。 

 ## スコアリング方針 
 ゲート判定(クロール可否)→本体スコア(重み付け加重平均)の2段階。重みはAIクローラー(GPTBot/PerplexityBot/ClaudeBot/Google-Extended)の実挙動とGEO研究ベースの確度で設定(表示速度・UXは除外、llms.txtは重み最小)。 

 ## ファイル構成(既存vend01の少ファイル主義を尊重、2ファイルに限定) 
 - crawlability.js: safeFetch(url)単一実装(本文fetchとrobots.txt取得の両方が必ずこれを経由)、gate_crawlable/gate_robots_blocked判定 
 - scoring.js: スコア集計(機械的判定+LLM呼び出し) 

 ## SSRF対策(safeFetch必須仕様) 
 - スキームはhttp/httpsのみ許可 
 - redirect: 'manual'を指定、3xx応答を自前でハンドルしLocationを再度safeFetchに通して各ホップごとにIP検証(最大リダイレクト3回) 
 - DNS rebinding対策: dns.lookup()で名前解決したIPを検証後、undiciのカスタムdispatcherでconnect先IPを固定し同一接続内で再解決させない(ただしTLSのSNI/証明書検証は元ホスト名ベースを使うこと) 
 - プライベートレンジ拒否(IPv4): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, 0.0.0.0/8, 100.64.0.0/10 
 - プライベートレンジ拒否(IPv6): ::1, fe80::/10, fc00::/7, ::ffff:127.0.0.1等 
 - proxy-network内コンテナ名解決も拒否、AbortSignal.timeout(8000)をsafeFetch内に内蔵 

 ## 判定方式の分離 
 機械的判定: gate_crawlable, gate_robots_blocked, score_schema(cheerioでJSON-LD解析), score_freshness(sitemap lastmod), score_authority(外部リンクユニークドメイン数、nofollow除外) 
 LLM判定: 抽出済み本文をLLMに渡しscore_direct_answerとscore_original_infoを1回の呼び出しでJSON一括出力 

 ## LLM接続先(重要) 
 既存vend01のcallClaude(Anthropic API直呼び)は使わず、VPS-root内部APIを使用: 
 - 通常時: http://mcp-gateway-v2:8000/v1/chat/completions (proxy-network内、OpenAI Chat Completions互換)。modelに`claude-3-sonnet`を渡すと内部で`sonnet`エイリアスにマッピングされ常に最新Sonnetに自動追従。認証はX-API-Keyヘッダ(.envのCLAUDE_API_KEYS)。注意: 内部でClaude Code CLIをサブプロセス起動する重い処理でレイテンシ数秒〜。messagesはフラット化されるため単発呼び出し前提の実装にすること。 
 - 同一IPから3回以上のトライがあった場合: Gemini APIの無料枠に切り替える(IP別試行回数カウントが必要)。Gemini無料枠の容量制限に到達した場合はエラーメッセージとして「システムが混雑しています。日を改めてご利用ください」を表示する。 
 - error_reasonはgate:/llm:の接頭辞で区別し、llm:claude / llm:geminiのようにどちらで判定したかも記録する。 

 ## LLM失敗フォールバック 
 タイムアウト/例外/JSONパース失敗はすべてfetch失敗と同じパターン(total_score=null, error_reason記録, エンドポイントは200)に合流させる。 

 ## DBスキーマ(既存単一DBファイルにdb.js内でCREATE TABLE IF NOT EXISTSで追記、別ファイルにしない) 
 ```sql 
 CREATE TABLE IF NOT EXISTS diagnosis_results ( 
   id INTEGER PRIMARY KEY AUTOINCREMENT, 
   url TEXT NOT NULL, 
   gate_crawlable INTEGER, 
   gate_robots_blocked INTEGER, 
   score_direct_answer REAL, 
   score_authority REAL, 
   score_original_info REAL, 
   score_schema REAL, 
   score_freshness REAL, 
   total_score REAL, 
   error_reason TEXT, 
   created_at TEXT DEFAULT (datetime('now')) 
 ); 
 ``` 
 UNIQUE制約なしで同一URLの再診断は毎回新規行として履歴保持。raw_html本文はDB保存しない。 

 ## APIエンドポイント 
 - POST /api/diagnosis body:{url} → {gate_crawlable, gate_robots_blocked, breakdown:{...}, total_score, error_reason} 
 - GET /api/diagnosis/:id → 過去診断結果の再取得 
 - 既存/sessions系と同じプレフィックスなし規約(Nginx側で/vend/01/api/を付与する前提) 

 ## Dockerfile/package.jsonの明示変更 
 - Dockerfile COPY行に crawlability.js, scoring.js を明示追記 
 - package.jsonに cheerio を追加(robots.txtパースは自前実装、依存追加不要) 

 ## 受入基準(検収条件) 
 - server.js/crawlability.js/scoring.js/db.js/Dockerfile/package.jsonが上記仕様通りに実装されていること 
 - safeFetchが本文fetchとrobots.txt取得の両方を必ず経由していること 
 - 異常系(fetch失敗/タイムアウト/LLM失敗)がすべて200+error_reasonで返っていること(500にならない) 
 - 既存/sessions系エンドポイントの動作を壊さないこと 
 - 同一IP3回以上でGemini無料枠へのフォールバック、容量制限到達時のエラーメッセージ表示が実装されていること 


 ## レビュー反映(2回目) 
 1. rate_limit_ipテーブル新規: CREATE TABLE IF NOT EXISTS rate_limit_ip (ip TEXT PRIMARY KEY, attempt_count INTEGER NOT NULL DEFAULT 0, window_start TEXT NOT NULL); 
 2. リセットは日次(24時間)ウィンドウ。window_startから24h経過でattempt_count=1・window_startリセット。未経過ならインクリメント。attempt_count>3でGemini切替。 
 3. diagnosis_resultsにllm_provider TEXT列追加('claude'/'gemini')。 
 4. Gemini容量制限時のerror_reasonは`llm:gemini_capacity`で固定。 
 5. Gemini APIキーは.envのGEMINI_API_KEY。

戻る