AIタスク #10469
未完了エピック #10493: [WEBサイト自動販売機] 操作可能MVP:ヒアリングからサイト生成・プレビューまで
LLM対策診断ツール実装(website-vending-machine 診断コンテンツ機能)
100%
## 受入基準(検収条件)
server.js/crawlability.js/scoring.js/db.js/Dockerfile/package.jsonが仕様通りに実装されていること。safeFetchが本文fetch/robots.txt/llms.txtの全てを必ず経由していること。異常系が全て200+error_reasonで返っていること(500にならない)。既存/sessions系の動作を壊さないこと。total_scoreが上記重み式(direct_answer0.30+authority0.25+original_info0.20+schema0.10+freshness0.10+llms_txt0.05)で算出されていること。diagnosis_resultsにscore_llms_txt列が存在すること。rate_limit_ipテーブルが存在し、24時間ウィンドウでリセットされ、attempt_count>=3でGeminiに切り替わること。diagnosis_resultsにllm_provider列が存在し、成功時に'claude'または'gemini'が記録されること。Gemini容量制限時のerror_reasonが`llm:gemini_capacity`で固定されていること。
説明
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で追記、別ファイルにしない)¶
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/llms.txtの全てを必ず経由していること。異常系が全て200+error_reasonで返っていること(500にならない)。既存/sessions系の動作を壊さないこと。total_scoreが上記重み式(direct_answer0.30+authority0.25+original_info0.20+schema0.10+freshness0.10+llms_txt0.05)で算出されていること。diagnosis_resultsにscore_llms_txt列が存在すること。rate_limit_ipテーブルが存在し、24時間ウィンドウでリセットされ、attempt_count>=3でGeminiに切り替わること。diagnosis_resultsにllm_provider列が存在し、成功時に'claude'または'gemini'が記録されること。Gemini容量制限時のerror_reasonがllm:gemini_capacityで固定されていること。
レビュー反映(2回目)¶
- 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);
- リセットは日次(24時間)ウィンドウ。window_startから24h経過でattempt_count=1・window_startリセット。未経過ならインクリメント。attempt_count>3でGemini切替。
- diagnosis_resultsにllm_provider TEXT列追加('claude'/'gemini')。
- Gemini容量制限時のerror_reasonは
llm:gemini_capacityで固定。 - Gemini APIキーは.envのGEMINI_API_KEY。
レビュー反映(3回目)¶
- 重み確定: total_score = direct_answer0.30 + authority0.25 + original_info0.20 + schema0.10 + freshness0.10 + llms_txt0.05(各score_*は0〜100、合計重み100)。
- llms_txt実装: crawlability.jsのsafeFetchで/llms.txtを取得、2xxで取得できればscore_llms_txt=100、それ以外(404含む)は0。diagnosis_resultsテーブルにscore_llms_txt REAL列を追加。
- Gemini切替え閾値を統一: attempt_count>=3でGemini切替え(本文の「3回以上」に合わせ、以前の「attempt_count>3」記述を上書き訂正)。
スコープ確定宣言(これ以上の仕様化は行わない)¶
以下は確定事項(必須実装): total_score重み(direct_answer0.30/authority0.25/original_info0.20/schema0.10/freshness0.10/llms_txt0.05)、diagnosis_resultsのscore_llms_txt列とllm_provider列、rate_limit_ipテーブル(attempt_count>=3でGemini切替え、24時間ウィンドウ)、LLM呼び出し失敗時のtotal_score=null+error_reason+200応答。DBスキーマのCREATE TABLE本体ブロックにこれら全列を統合して実装すること(追記別ブロックではなく本体の1つのCREATE TABLE文にまとめる)。
以下はCodexの裁量に委ねる(レビューで大きく外れていなければ差し戻さないこと): score_authority/score_freshness/score_schemaの0〜100正規化の具体的な閾値・分割点、および機械判定系(freshness/schema/authority)の取得失敗時の細部挿作は実装者(Codex)自身の合理的な判断に委ねる(根拠はコードコメントに残す)。異常系は必ず200+error_reasonで返すという大方針のみ遵守すればよい。