summaryrefslogtreecommitdiff
path: root/doc/Design.md
blob: 20de3e5f53e1c9bd53581ea9a3d0a92331fda94f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
# 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:<port>` / `http://127.0.0.1:<port>` のみに限定する。

---

## 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: ['<END>']
}
```

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:<port>` または `http://127.0.0.1:<port>` のみ許可(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
<input type="range" min="0" max="2" step="0.1">
```

---

## 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実機確認項目も可能な範囲で実行結果を記録する。