# Design.md ## 1. 設計概要 本拡張は、Webページとの接点をContent Scriptに限定し、権限を伴うOllama通信と設定管理をBackgroundへ分離する。 ```text Web Page │ │ selection ▼ Content Script │ runtime.Port ▼ Background │ fetch / HTTP ▼ Ollama │ streaming ▼ Background │ runtime.Port ▼ Content Script │ ▼ Shadow DOM Popover ``` 設計原則: - ページ由来データは信頼しない。 - Content Scriptは最小責務とする。 - Ollama通信はBackgroundのみ(Ollama REST API + Fetch API + NDJSON streaming)。 - UI文字列はHTMLとして評価しない。 - 翻訳要求は常に1-shot。 - 同時翻訳は1件に制限。 - 設定値はUIだけでなくBackgroundでも検証する。 - Ollama接続先は初期版ではlocalhost / 127.0.0.1 + 任意ポートのみ許可する。 --- ## 2. コンポーネント構成 | コンポーネント | 主責務 | |---|---| | `content/selection-controller` | Selection検出、Range位置取得 | | `content/action-button` | 翻訳アイコン表示/非表示 | | `content/translation-popover` | 翻訳結果UI、stream描画、copy/regenerate/close | | `content/port-client` | Backgroundとの長期接続 | | `background/message-router` | Content Scriptからのrequest受信・validation | | `background/translation-session` | requestId、AbortController、active stream管理 | | `background/ollama-client` | Ollama REST API呼び出し(fetch + NDJSON streaming) | | `background/settings-service` | storage読書き、default、validation | | `popup` | 有効/無効、簡易設定、Options起動 | | `options` | 全設定UI | | `shared/settings-schema` | 設定型、既定値、範囲 | | `shared/messages` | runtime message型定義 | | `shared/constants` | 最大選択長等 | --- ## 3. 推奨ディレクトリ構成 ```text / ├─ manifest.json ├─ package.json ├─ src/ │ ├─ background/ │ │ ├─ index.js │ │ ├─ message-router.js │ │ ├─ translation-session.js │ │ ├─ ollama-client.js │ │ └─ settings-service.js │ ├─ content/ │ │ ├─ index.js │ │ ├─ selection-controller.js │ │ ├─ action-button.js │ │ ├─ translation-popover.js │ │ ├─ port-client.js │ │ └─ content.css │ ├─ popup/ │ │ ├─ popup.html │ │ ├─ popup.js │ │ └─ popup.css │ ├─ options/ │ │ ├─ options.html │ │ ├─ options.js │ │ └─ options.css │ └─ shared/ │ ├─ settings-schema.js │ ├─ messages.js │ ├─ prompt.js │ └─ constants.js ├─ tests/ │ ├─ unit/ │ └─ integration/ └─ docs/ ├─ Specifications.md └─ Design.md ``` 実際のビルド方式に応じて配置は調整してよいが、責務分離は維持する。 --- ## 4. Manifest設計 概念例: ```json { "manifest_version": 3, "name": "Selected Text Translator", "version": "0.1.0", "permissions": ["storage"], "host_permissions": ["http://localhost/*", "http://127.0.0.1/*"], "background": { "scripts": ["background.js"], "type": "module" }, "content_scripts": [ { "matches": ["http://*/*", "https://*/*"], "js": ["content.js"], "css": ["content.css"], "run_at": "document_idle" } ], "action": { "default_popup": "popup.html" }, "options_ui": { "page": "options.html", "open_in_tab": true } } ``` ### 4.1 Manifest実装ルール - `background.service_worker` は使用しない。 - `browser_action` は使用しない。 - `scripting` 権限は初期版では付与しない。 - `tabs` 権限は付与しない。 - localhost以外のhost permissionは初期版では付与しない。 - Firefoxのmatch patternはポート番号指定に対応していない(Firefox bug 1362809 / 1468162)。`http://localhost/*` および `http://127.0.0.1/*` は任意ポートに一致するため、実際に通信してよい接続先はアプリ側Host validatorで `http://localhost:` / `http://127.0.0.1:` のみに限定する。 --- ## 5. Content Script設計 ### 5.1 Selection取得 標準DOM APIを使用する。 ```js const selection = window.getSelection(); const text = selection?.toString().trim(); if (!text) return; const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); ``` ### 5.2 検出イベント 初期実装候補: - `selectionchange`: 選択状態更新用 - `mouseup`: 選択確定後の表示タイミング用 - `keyup`: Shift + Arrow等のキーボード選択対応用 過剰発火を避けるため、UI更新には軽いdebounceを適用してよい。 ### 5.3 Action Button表示条件 ```text selection exists AND trimmed text length > 0 AND text length <= MAX_SELECTION_LENGTH AND extension enabled ``` ### 5.4 位置計算 - `Range.getBoundingClientRect()` を基本とする。 - 選択が複数行の場合でも最後/全体矩形を基準に配置する。 - viewport端ではポップオーバーが画面外へ出ないよう補正する。 - scroll/resize時は再配置または非表示化する。 --- ## 6. UI分離設計 ### 6.1 Shadow DOM ページCSSとの衝突を避けるため、Content Scriptが専用host要素を追加し、その内部にShadow DOMを作る。 ```js const host = document.createElement('div'); document.documentElement.append(host); const shadow = host.attachShadow({ mode: 'open' }); ``` `open/closed` は実装都合で選択可能だが、セキュリティ境界として依存しない。 ### 6.2 DOM挿入ルール 禁止: ```js element.innerHTML = translatedText; shadowRoot.innerHTML = translatedText; ``` 許可: ```js element.textContent = translatedText; ``` 静的テンプレートHTMLを自前固定文字列として構築する場合も、外部入力を文字列連結しない。 --- ## 7. 翻訳ポップオーバー設計 ### 7.1 状態 ```text HIDDEN READY LOADING STREAMING COMPLETED ERROR ``` ### 7.2 表示内容 | 要素 | 内容 | |---|---| | Header | 翻訳結果 | | Meta | Model名 / Ollama | | Body | streaming翻訳本文 | | Copy | 完了/途中結果をclipboardへコピー | | Regenerate | 同じ選択文字列で再要求 | | Close | ポップオーバー非表示 | ### 7.3 UI挙動 - Icon押下直後にLOADING表示。 - 最初のchunk受信後STREAMINGへ遷移。 - 完了イベントでCOMPLETED。 - Abortされた旧requestのchunkはrequestId不一致なら無視。 - Error時は再生成可能な状態に戻す。 --- ## 8. Runtime通信設計 Streamingには `browser.runtime.connect()` / `runtime.Port` を使用する。 ### 8.1 Port名 ```text translation-stream ``` ### 8.2 Content → Background message #### translate ```js { type: 'translate', requestId: 'uuid-or-random-id', text: 'selected text' } ``` #### cancel ```js { type: 'cancel', requestId: '...' } ``` ### 8.3 Background → Content message #### started ```js { type: 'started', requestId: '...', model: '...' } ``` #### chunk ```js { type: 'chunk', requestId: '...', text: 'partial translation' } ``` #### completed ```js { type: 'completed', requestId: '...' } ``` #### error ```js { type: 'error', requestId: '...', code: 'OLLAMA_UNAVAILABLE', message: '...' } ``` ### 8.4 Validation Backgroundは最低限以下を確認する。 - `type` が既知値か - `requestId` がstringか - `text` がstringか - text lengthが上限以内か - 現在のsender/portがContent Script由来か --- ## 9. TranslationSession設計 Backgroundではactive translationを原則1件だけ保持する。 ```js class TranslationSession { requestId; abortController; sourceText; startedAt; } ``` 新しい翻訳要求: ```text translate(request B) ↓ active request Aあり? ├─ yes → Abort A └─ no ↓ request Bをactiveに設定 ↓ Ollama streaming開始 ``` ### 9.1 古いchunk対策 受信chunkごとにactive requestIdを確認し、不一致なら破棄する。 --- ## 10. OllamaClient設計 ### 10.1 API Ollama REST APIをFetch APIで直接呼び出す。`ollama-js`等のサードパーティSDKは使用しない。 概念例: ```js const response = await fetch(`${host}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: settings.model, messages: [ { role: 'system', content: buildSystemPrompt(settings) }, { role: 'user', content: selectedText } ], stream: true, think: settings.thinking, // off時は省略 options: buildOptions(settings) }), signal: abortController.signal }); // NDJSONストリームをバッファリングしつつ行単位でパースする const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; for (;;) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // bufferを行単位で分割し、完全な行のみJSON.parseする // 末尾の不完全行は次回チャンクに持ち越す } ``` ### 10.2 System Prompt構築 ```text BUILTIN_SYSTEM_PROMPT [User translation preferences] USER_SYSTEM_PROMPT ``` 固定promptは常に先頭に置く。 ### 10.3 buildOptions 未設定値はrequest objectへ含めない。 例: ```js { temperature: 0.2, num_ctx: 16384, seed: 42, top_k: 40, top_p: 0.9, min_p: 0.05, typical_p: 1.0, repeat_last_n: 64, repeat_penalty: 1.1, presence_penalty: 0.0, frequency_penalty: 0.0, num_keep: 4, num_predict: -1, stop: [''] } ``` Seed可変時は`seed`をoptionsへ含めない設計を優先する。 --- ## 11. 設定スキーマ 推奨形: ```js { enabled: true, ollamaHost: 'http://localhost:11434', model: '', targetLanguage: 'ja', thinking: false, temperature: 0.2, contextLength: 16384, seedMode: 'random', seedValue: null, userSystemPrompt: '', advanced: { numKeep: null, numPredict: null, topK: null, topP: null, minP: null, typicalP: null, repeatLastN: null, repeatPenalty: null, presencePenalty: null, frequencyPenalty: null, stop: [] } } ``` --- ## 12. 設定値validation ### 12.1 Basic | enabled | boolean | | ollamaHost | URL形式。**`http://localhost:` または `http://127.0.0.1:` のみ許可(schemeはhttp固定、外部ホスト・LAN IP・httpsは拒否)。必須制限** | | model | non-empty string | | targetLanguage | 定義済み値 | | thinking | `false/true/low/medium/high` 相当 | | temperature | 0.0〜2.0、0.1刻み | | contextLength | `4096/8192/16384/32768/131072/262144` のいずれか | | seedMode | `random/fixed` | | seedValue | fixed時のみinteger | | userSystemPrompt | string、最大長を設定 | ### 12.2 Advanced 推奨UI step: | 項目 | step | validation方針 | |---|---:|---| | num_keep | 1 | integer | | num_predict | 1 | integer、-1許容 | | top_k | 1 | integer、0以上 | | top_p | 0.05 | float | | min_p | 0.01 | float | | typical_p | 0.05 | float | | repeat_last_n | 1 | integer、-1許容 | | repeat_penalty | 0.05 | float | | presence_penalty | 0.05 | float | | frequency_penalty | 0.05 | float | | stop | - | string[] | Ollamaが明示的な最小/最大値を保証していない項目について、UI側で過度に狭い範囲を独自設定しない。型と明らかな不正値を中心に検証する。 --- ## 13. Context Length UI設計 固定プリセット: ```js const CONTEXT_PRESETS = [ 4096, 8192, 16384, 32768, 131072, 262144 ]; ``` UI表示: ```text 4k ─ 8k ─ 16k ─ 32k ─ 128k ─ 256k ``` 実装上はrange inputのindex値を使用してよい。 ```text 0 => 4096 1 => 8192 2 => 16384 3 => 32768 4 => 131072 5 => 262144 ``` モデル自身が対応する最大contextを超える場合、Ollama側でエラーまたは制約される可能性があるため、そのエラーをUIへ返す。 --- ## 14. Temperature UI設計 ```text min=0.0 max=2.0 step=0.1 ``` スライダー値は表示用数値と同期する。 ```html ``` --- ## 15. Seed UI設計 ```text Seed [ 可変 ] [ 固定 ] 固定選択時: 固定値 (整数) [ 42 ▲▼ ] ``` - random時: seed optionを送らない。 - fixed時: `seedValue` を整数として送る。 - Seed Valueはnumber input + spinnerでよい。 --- ## 16. Advanced UI設計 数値項目は`input type="number"`相当のUIとする。 例: ```text top_p [ 0.90 ][▲] [▼] ``` HTML native spinnerを使用できる場合は活用し、独自実装を増やさない。 stopは複数行textareaとし、1行1sequenceとして保存する。 --- ## 17. SettingsService設計 ### 17.1 保存 `browser.storage.local` を使用。 ### 17.2 読み込み 1. default settings生成 2. storage値取得 3. merge 4. validation 5. 不正値はdefaultへフォールバック ### 17.3 設定変更通知 設定変更時に `browser.storage.onChanged` を使用し、Content Script / Background双方が必要な設定を更新できるようにする。 --- ## 18. Security Design ### 18.1 Trust Boundary ```text UNTRUSTED Web Page / DOM / Selection │ ▼ Content Script │ structured messages only ▼ PRIVILEGED Background / Ollama access / storage ``` ### 18.2 XSS対策 - 選択文字列をHTML化しない。 - LLM出力をHTML化しない。 - Markdown rendererを初期版では導入しない。 - DOM挿入は`textContent`中心。 ### 18.3 Prompt Injection対策 翻訳用途ではページ本文を命令ではなくデータとして固定system promptで扱う。 ただしLLMの挙動を完全保証するセキュリティ境界としてPromptのみへ依存しない。 ### 18.4 Remote Code禁止 - CDN script禁止。 - `eval`禁止。 - `new Function`禁止。 - remote import禁止。 - npm依存はbundleに含める。 ### 18.5 Storage - `storage.local` は暗号化ストレージではない前提。 - API key等の秘密情報は初期版では扱わない。 ### 18.6 Message Validation Content側のデータをBackgroundで再検証する。 ### 18.7 Resource Abuse - MAX_SELECTION_LENGTHを設ける。 - active translationは1件。 - 新規requestで旧request abort。 - 選択だけではrequestを送らない。 --- ## 19. Error Code設計 | Code | 意味 | |---|---| | `INVALID_REQUEST` | message形式不正 | | `TEXT_EMPTY` | 空文字 | | `TEXT_TOO_LONG` | 選択長超過 | | `OLLAMA_UNAVAILABLE` | Ollama接続不可 | | `MODEL_NOT_FOUND` | モデルなし | | `INVALID_SETTINGS` | 設定値不正 | | `REQUEST_ABORTED` | 新規要求等で中断 | | `STREAM_ERROR` | streaming中エラー | | `UNKNOWN_ERROR` | その他 | UIでは内部stack traceを表示しない。 --- ## 20. Copy設計 Clipboard APIのみを使用する。`document.execCommand('copy')` はdeprecatedのため使用しない。 - コピーはポップオーバーのコピーボタンclick(user activation内)で発生するため、`navigator.clipboard.writeText()` をそのハンドラ内で直接呼び出す。 - 書き込み失敗時は「コピーできませんでした」等の簡潔なメッセージをポップオーバーへ表示するのみとし、deprecated APIへフォールバックしない。 - Firefox実機でclipboard動作を確認する。 コピー対象は現在表示中の翻訳プレーンテキストのみ。 --- ## 21. モデル一覧取得 BackgroundのOllamaClientからモデル一覧取得APIを呼び、Settings UIへ返す。 要件: - Ollama未起動時は空一覧 + 接続エラー表示。 - 取得成功時はselect候補へ反映。 - 保存済みmodelが一覧にない場合は警告表示。 --- ## 22. 実装順序 ### Phase 1: Skeleton - Manifest V3 - Background - Content Script - Popup / Options - build/test基盤 ### Phase 2: Selection UI - Selection検出 - Action button - Shadow DOM - Popover skeleton ### Phase 3: Messaging - runtime.Port - message schema - validation - requestId ### Phase 4: Ollama - Ollama REST client(fetch + NDJSON streaming) - model list - chat stream - Abort ### Phase 5: Settings - storage.local - Basic settings - Advanced settings - Context slider - Temperature slider - Seed modes ### Phase 6: Security Hardening - textContent確認 - CSP確認 - message validation - input length limits - no remote code - permissions audit ### Phase 7: Tests - unit - integration - Firefox実機 ### Phase 8: Final Verification - lint/test/build - temporary add-on install - acceptance criteriaを全件確認 --- ## 23. テスト設計 ### 23.1 Unit | 対象 | ケース | |---|---| | settings schema | default / valid / invalid | | temperature | 0.1刻み / 範囲外 | | contextLength | presetのみ許可 | | seed | random / fixed / invalid integer | | advanced options | null除外、数値反映 | | prompt builder | fixed + user prompt結合 | | message validation | malformed message拒否 | | length limit | 境界値 | ### 23.2 Integration Mock Ollama serverまたはmock clientを使い以下を確認。 ```text Content request → Background validation → Ollama stream → chunk forwarding → complete ``` Abortテスト: ```text request A start request B start A abort B chunks only displayed ``` ### 23.3 Firefox Manual/E2E 対象ページ: - 単純HTML - 長文記事 - 選択文字列にHTMLタグ表現を含むページ - CSSが強く指定されたページ - スクロール位置が深いページ 確認: - 選択位置近傍UI - viewport端補正 - scroll時挙動 - selection解除 - copy - regenerate - close - settings persistence - Ollama down/up --- ## 24. Firefox API監査チェックリスト 実装完了時に以下を確認する。 - [ ] Manifest V3 - [ ] `action` 使用 - [ ] `background.scripts` 使用 - [ ] `background.service_worker` 不使用 - [ ] `browser_action` 不使用 - [ ] `tabs.executeScript()` 不使用 - [ ] `runtime.getBackgroundPage()` 不使用 - [ ] `window.localStorage` 不使用 - [ ] `storage.local` 使用 - [ ] `runtime.Port` streaming通信 - [ ] Content ScriptからOllama fetchなし - [ ] `eval` / `new Function`なし - [ ] Remote JSなし - [ ] LLM出力への`innerHTML`なし - [ ] 不要permissionsなし - [ ] iframe初期対応なし --- ## 25. 完了時にLLMへ要求する成果物 実装担当LLMは最終的に以下を提示すること。 1. 実装済みソースコード一式 2. `manifest.json` 3. build手順 4. Firefox一時アドオンとしてのロード手順 5. Ollama起動/モデル準備手順 6. 単体テスト結果 7. 結合テスト結果 8. Firefox実機動作確認結果 9. 使用WebExtensions API一覧 10. 要求permissions一覧と理由 11. セキュリティチェック結果 12. 未解決事項・既知制限 テストを実行せず「実装したので動くはず」として完了扱いにしてはならない。実際に実行可能なテストは実行し、Firefox実機確認項目も可能な範囲で実行結果を記録する。