SMART-CONTACT Chrome拡張機能 要件定義書¶
| 項目 | 内容 |
|---|---|
| プロジェクト名 | SMART-CONTACT |
| ドキュメント種別 | Chrome拡張機能 機能要件 |
| バージョン | 1.1 |
| 作成日 | 2026-02-17 |
| ステータス | 開発中 (develop ブランチ) |
1. 概要¶
SMART-CONTACT Chrome拡張機能は、Webサイトのお問い合わせフォームへの自動入力・送信を行うBtoB営業支援ツールである。事前に登録したテンプレート情報をもとに、複数のURLに対して順次フォーム入力・送信を実行し、営業活動における問い合わせ業務を自動化する。
1.1 対象ユーザー¶
- 法人営業担当者(BtoB営業)
- マーケティング担当者
- 事業開発担当者
1.2 解決する課題¶
- 手作業による大量のお問い合わせフォーム入力の工数削減
- 送信先の重複管理・スケジュール管理の自動化
- ライセンス管理によるSaaS型ビジネスモデルの実現
1.3 技術構成¶
| レイヤー | 技術 |
|---|---|
| 拡張機能フレームワーク | WXT 0.20.17 (Manifest V3) |
| 拡張機能 UI | React 19 + TypeScript 5.9 |
| スタイリング | Tailwind CSS 4 + PostCSS |
| ビルドツール | Vite 7 |
| 認証 | Laravel Sanctum 4.0 (APIトークン) |
1.4 コンポーネント構成¶
┌──────────────────────────────────────────┐
│ Chrome Browser Extension │
│ ┌───────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Options │ │Background│ │ Content │ │
│ │ Dashboard │ │ Service │ │ Script │ │
│ │ (React) │ │ Worker │ │ │ │
│ └─────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ └──────┬──────┘ │ │
│ │ chrome.runtime │ │
│ ├───────────────────┘ │
└───────────────┼───────────────────────────┘
│ HTTP (Sanctum Token)
▼
Laravel 12 Backend
| コンポーネント | 役割 |
|---|---|
| Options Dashboard | React製のUI画面。ログイン、テンプレート編集、実行、設定、アカウント管理を提供 |
| Background Service Worker | バッチ実行エンジン。URLリストの逐次処理、タブ管理、ライセンスチェック、ログ記録を担当 |
| Content Script | 各Webページに注入されるスクリプト。フォーム検出、フィールドマッチング、自動入力、送信ボタン押下を実行 |
2. 認証・ライセンス管理¶
FR-AUTH-001: ログイン¶
| 項目 | 内容 |
|---|---|
| 説明 | メールアドレスとパスワードでログインし、APIトークンを取得する |
| 入力 | email (必須), password (必須) |
| 処理 | Sanctumトークン発行、既存トークン削除、ユーザー情報返却、チケット情報取得 |
| 出力 | token, user (id, name, email, is_white_user, contract_period, plan_type, ticket_balance, monthly_free_limit, monthly_free_remaining, subscription_expires_at, is_banned) |
| 保存先 | Chrome Storage Local (access_token, user_info) |
APIエンドポイント: POST /api/login
リクエスト:
{
"email": "user@example.com",
"password": "password"
}
レスポンス (200):
{
"status": "success",
"token": "1|abc123...",
"user": {
"id": 1,
"name": "ユーザー名",
"email": "user@example.com",
"is_white_user": true,
"contract_period": "2026/02/07 〜 無期限 (Premium)",
"plan_type": "subscription",
"ticket_balance": 0,
"monthly_free_limit": 10,
"monthly_free_remaining": 10,
"subscription_expires_at": null,
"is_banned": false
}
}
追加フィールド (v1.1):
-
plan_type:"subscription"(ホワイトユーザー) /"ticket"(一般ユーザー) -
ticket_balance: 購入チケット残枚数 -
monthly_free_limit: 月間無料URL枠 (SystemSettingから) -
monthly_free_remaining: 今月の無料枠残数 -
subscription_expires_at: サブスク期限 (ISO 8601 / null) -
is_banned: BAN状態
ログイン成功後、拡張機能はこれらの情報を user_info として Chrome Storage Local に保存し、以後の認証チェックやURL件数制限をローカルで行う (APIコール不要)。また、保留中の同期データ (pending_sync_user_{userId}) があれば自動リトライする。
レスポンス (401):
{
"status": "error",
"message": "メールアドレスまたはパスワードが間違っています。"
}
契約期間フォーマット:
| ユーザー種別 | フォーマット |
|---|---|
| ホワイトユーザー | {created_at} 〜 無期限 (Premium) |
| トライアル | {created_at} 〜 {trial_ends_at} (Trial) |
| 未契約 | 未契約 |
FR-AUTH-002: ライセンスチェック¶
| 項目 | 内容 |
|---|---|
| 説明 | 実行前にデバイスのライセンス状態を確認する |
| 入力 | device_uuid (必須), device_name (任意) |
| 判定ロジック | 0. BANユーザー (banned_at IS NOT NULL) → 拒否 (account_banned) |
1. ホワイトユーザー → 無条件で許可 (white_unlimited) |
|
2. 既存登録デバイス → 許可 + last_active_at 更新 |
|
| 3. 新規デバイス + 枠あり → デバイス登録 + 許可 | |
4. 新規デバイス + 枠なし → 拒否 (device_limit_reached) |
優先度0: BANチェック
BANユーザー (banned_at IS NOT NULL) の判定は全ての他チェックより先に実行される。BANされたユーザーはホワイトユーザーであっても利用不可となる。
APIエンドポイント: POST /api/check-license
リクエスト:
{
"device_uuid": "550e8400-e29b-41d4-a716-446655440000",
"device_name": "Chrome on MacOS"
}
レスポンス (200 - 許可):
{
"status": "allowed",
"plan": "white_unlimited",
"message": "ホワイトユーザー認証OK"
}
レスポンス (403 - 拒否):
{
"status": "denied",
"reason": "device_limit_reached",
"current_usage": 2,
"limit": 2,
"message": "利用台数の上限(2台)に達しています。管理画面で整理するか、追加ライセンスを購入してください。"
}
FR-AUTH-003: デバイスID管理¶
| 項目 | 内容 |
|---|---|
| 説明 | 拡張機能初回起動時にUUID v4を生成し、ブラウザに紐付ける。ユーザー画面でIDとブラウザの再紐付けも担保する |
| 生成タイミング | 初回起動時 (存在しない場合のみ) |
| 生成方法 | crypto.randomUUID() |
| 保存先 | Chrome Storage Local (device_id) |
| サーバー登録 |
user_devices テーブルに (user_id, device_uuid) として登録 |
export const getDeviceId = async (): Promise<string> => {
const result = await chrome.storage.local.get(['device_id']);
if (result.device_id) return result.device_id;
// UUIDを生成して保存
const newId = crypto.randomUUID();
await chrome.storage.local.set({ device_id: newId });
return newId;
};
FR-AUTH-004: ログアウト¶
| 項目 | 内容 |
|---|---|
| 説明 | 現在のデバイスのトークンを無効化し、ローカル情報を削除する |
| 処理 | サーバー側: 現在のアクセストークン削除 |
クライアント側: access_token, user_info を Chrome Storage から削除 |
APIエンドポイント: POST /api/logout
レスポンス (200):
{
"message": "ログアウトしました"
}
FR-AUTH-005: ユーザー種別¶
| 種別 | is_white_user |
デバイス制限 | 契約期間 |
|---|---|---|---|
| ホワイトユーザー (Premium) | true |
無制限 | 無期限 |
| 一般ユーザー (Standard) | false |
stripe_subscription_quantity 台まで (デフォルト1) |
trial_ends_at またはStripeサブスクリプション |
3. テンプレート管理¶
FR-TPL-001: テンプレートデータ構造¶
interface Template {
id: string;
name: string; // テンプレート管理名
// 送信者情報
companyName: string; // 会社名
companyKana: string; // 会社名カナ
department: string; // 部署名
// 氏名 (分割入力)
lastName: string; // 姓
firstName: string; // 名
lastKana: string; // セイ (カタカナ)
firstKana: string; // メイ (カタカナ)
lastNameHira: string; // せい (ひらがな)
firstNameHira: string; // めい (ひらがな)
// 連絡先
email: string; // メールアドレス
tel1: string; // 電話番号 (市外局番)
tel2: string; // 電話番号 (市内局番)
tel3: string; // 電話番号 (加入者番号)
// 住所
zip1: string; // 郵便番号 (3桁)
zip2: string; // 郵便番号 (4桁)
prefecture: string; // 都道府県
city: string; // 市区町村
addressOther: string; // 番地・建物名
siteUrl: string; // WebサイトURL
// メッセージ
subject: string; // 件名
body: string; // 本文
}
全25フィールド。氏名はフォームの姓名分割入力に対応するため lastName / firstName に分割し、カタカナ (lastKana / firstKana) とひらがな (lastNameHira / firstNameHira) も個別に保持する。電話番号は市外局番 (tel1) / 市内局番 (tel2) / 加入者番号 (tel3) の3分割、郵便番号は3桁 (zip1) / 4桁 (zip2) の2分割とする。
FR-TPL-002: テンプレートCRUD操作¶
| 操作 | 説明 |
|---|---|
| 新規作成 | 空のテンプレートを作成 |
| 編集 | 既存テンプレートの各フィールドを編集 |
| 複製 | 既存テンプレートをコピーして新規作成 |
| 削除 | テンプレートを削除 (最低1件は残す必要あり) |
| 保存先 | Chrome Storage Local (templates) |
FR-TPL-003: 差し込みタグ¶
本文内で以下のタグを使用でき、実行時にテンプレート値に置換される。
| タグ | 置換値 |
|---|---|
{会社名} |
companyName |
{会社名カナ} |
companyKana |
{部署} |
department |
{担当者名} |
lastName + firstName
|
{担当者カナ} |
lastKana + firstKana
|
{サイトURL} |
siteUrl |
{メール} |
email |
{電話番号} |
tel1-tel2-tel3 |
{郵便番号} |
zip1-zip2 |
{住所} |
prefecture + city + addressOther
|
4. フォーム自動入力エンジン (Content Script)¶
FR-FILL-001: フォーム検出¶
| 項目 | 内容 |
|---|---|
| 対象 | 全Webページ (*://*/*) |
| Google Forms |
docs.google.com/forms は専用ロジックで処理 (質問ラベルベースのマッチング + カスタムUI要素対応) |
| 有効なフォーム判定条件 | 送信ボタンが存在 AND (textarea OR select OR 入力欄2個以上) |
| 入力欄カウント除外 | 検索ボックス (search, keyword, q, query)、header/footer/nav内の入力欄 |
フォーム準備待機: 最大4秒 (200ms x 20回ポーリング)。textarea が存在するか、visible な input が2個以上になるまで待機する。
async function waitForFormReady() {
for (let i = 0; i < 20; i++) {
const hasTextarea = Array.from(document.querySelectorAll('textarea')).some(el => isVisible(el));
const inputs = Array.from(document.querySelectorAll('input:not([type="hidden"])')).filter(el => isVisible(el));
if (hasTextarea || inputs.length >= 2) return;
await new Promise(r => setTimeout(r, 200));
}
}
有効フォーム判定ロジック (detectValidFormStructure):
async function detectValidFormStructure(): Promise<boolean> {
const submitBtn = findSubmitButton();
const hasTextarea = document.querySelectorAll('textarea').length > 0;
const hasSelect = document.querySelectorAll('select').length > 0;
const inputs = document.querySelectorAll(
'input:not([type="hidden"]):not([type="submit"]):not([type="checkbox"]):not([type="radio"]), textarea'
);
let realInputs = 0;
inputs.forEach(input => {
const el = input as HTMLInputElement;
if (!isVisible(el)) return;
if (el.type === 'search' || /search|検索|keyword/i.test(el.placeholder || '')
|| /search|検索/i.test(el.name || '')) return;
if (el.closest('header, footer, .search, nav')) return;
realInputs++;
});
return !!submitBtn && (hasTextarea || hasSelect || realInputs >= 2);
}
FR-FILL-002: フィールドマッチングロジック¶
フォームの各入力欄に対して、以下の優先度でフィールドを特定する。
-
構造的マッチング (
fillByStructure): 親要素内の入力欄数と周辺テキストからグループ単位で判定- 2入力グループ → 姓名分割、郵便番号分割、メール+確認、氏名+カナ
- 3入力グループ → 電話番号3分割
- 1入力 + textarea → 本文
-
属性ベースマッチング (
performAutoFill): name属性、id、placeholder、周辺テキストのスコアリング- 100点: name/id属性がパターンに一致
- 80点: 周辺テキスト (label、placeholder、aria-label、親要素テキスト) がパターンに一致
- 追加ボーナス: email型inputに
+80点、メール確認フィールドに+200点 - 50点以上で入力対象として採用
構造的マッチング詳細:
| グループ入力数 | 周辺ラベル | マッピング |
|---|---|---|
| 2個 | 氏名/名前/Name |
[0]=lastName, [1]=firstName
|
| 2個 | フリガナ/カナ |
[0]=lastKana, [1]=firstKana
|
| 2個 | ひらがな |
[0]=lastNameHira, [1]=firstNameHira
|
| 2個 | 郵便/〒 |
[0]=zip1, [1]=zip2 (入力後1.5秒待機) |
| 2個 | メール/mail |
[0]=email, [1]=email (確認用) |
| 2個 | (2番目にkana/furi/カナ) |
[0]=lastName firstName, [1]=lastKana firstKana
|
| 3個 | 電話/tel |
[0]=tel1, [1]=tel2, [2]=tel3
|
| 1個 (textarea) | 本文/内容/詳細/message/body | [0]=body |
| 1個 | 氏名/名前/Name |
[0]=lastName firstName (結合) |
| 1個 | フリガナ/カナ |
[0]=lastKana firstKana (結合) |
FR-FILL-003: フィールドマッピング一覧¶
| フィールド | キー | 検出パターン (正規表現) | 除外パターン |
|---|---|---|---|
| 郵便番号 | zip |
/zip|郵便|postal|〒/i |
/1|2/i |
| 本文 | body |
/body|content|message|text|inquiry|本文|内容|詳細|備考|お問い合わせ内容/i |
- |
| 会社名 | company |
/会社|貴社|法人|company|御社|所属/i |
/kana|ふりがな|フリガナ/i |
| 部署 | department |
/部署|部門|department|事業部/i |
- |
| 姓 | lastName |
/姓|苗字|名字|last.?name|name1/i |
/kana|hira|furi|カナ|フリガナ|first|名$|ビル|建物|bldg/i |
| 名 | firstName |
/名|first.?name|name2|メイ/i |
/last|姓|kana|hira|カナ|フリガナ|mail|email|confirm|check|確認|ビル|建物|bldg/i |
| 氏名(結合) | fullName |
/氏名|フルネーム|お名前|^名前|name/i |
/kana|カナ|フリガナ|ふりがな|last|first|姓|名$|name[12]|ビル|建物|bldg/i |
| カナ(結合) | fullKana |
/フリガナ|ふりがな|カナ|かな|^kana/i |
/セイ|メイ|last|first/i |
| セイ | lastKana |
/セイ|フリガナ1|カナ1|kana1/i |
/メイ|名/i |
| メイ | firstKana |
/メイ|フリガナ2|カナ2|kana2/i |
/セイ|姓/i |
| メール確認 | emailConf |
/確認|confirm|verify|再入力|mail.*(confirm|check)|メール.*(再|確認)/i |
- |
| メール | email |
/mail|メール|アドレス/i |
/confirm|check|再|確認/i |
| 電話 | tel |
/tel|phone|電話|携帯/i |
/1|2|3|zip|fax/i |
| 都道府県 | pref |
/pref|県|都|道|府/i |
- (select要素) |
| 市区町村 | city |
/市区|町村|city|municipality/i |
/zip|郵便|postal|code|〒|地域|area|ビル|building|bldg|street|番地/i |
| 番地 | street |
/番地|street/i |
/zip|郵便|postal|code|〒|地域|area|ビル|building|bldg|市区|町村|city/i |
| 住所(結合) | addr |
/住所|所在地/i |
/zip|郵便|postal|code|〒|地域|area|ビル|building|bldg|市区|町村|city|番地/i |
住所フィールドの結合ロジック:
- 都道府県が既にselect入力済み →
city + addressOtherを入力 - 都道府県が未入力 →
prefecture + city + addressOtherを結合入力 - 既存値がある場合 → 既存値 +
addressOtherを追記
FR-FILL-004: 補助入力処理¶
| 対象 | 処理内容 |
|---|---|
| selectボックス | 都道府県は値マッチング (テキスト一致 or 都道府県末尾除去で一致)。その他のselectは「その他」→「ビジネス/協業」系→最後のオプション順に選択 |
| ラジオボタン | 必須の場合、「その他」を優先選択。見つからない場合は最後の選択肢 |
| チェックボックス | 「同意/規約」系は自動チェック。必須の場合も自動チェック |
| プライバシーポリシー同意 |
role="checkbox", role="switch", 通常のcheckboxすべて対応 |
selectボックスの選択優先度:
- 都道府県selectの場合: テンプレートの
prefecture値でマッチング → 末尾の「都道府県」を除去してマッチング → 「その他/other」 - その他のselectの場合:
-
その他/Other/other(完全一致) -
/その他|other|お問い合わせ/i(部分一致) /協業|ビジネス|business|partner|提携|案件|事業|営業|内容|content|subject/i- 最後のオプション (フォールバック)
-
ラジオボタン / チェックボックスの処理:
- プライバシーポリシー判定:
/同意|規約|policy|term|承諾|確認/i - プライバシー系の場合:
/同意|承諾|規約|agree|確認/iに一致する選択肢を優先 - 必須判定:
required属性、aria-required="true"、周辺テキストに/必須|required|[*]|\*/i、親要素クラスにrequire、.required/.req/img[alt*="必須"]の存在
ARIA対応:
-
role="radiogroup"内のrole="radio"要素に対応 -
role="checkbox"要素に対応 -
role="switch"要素に対応 (プライバシーポリシー同意) -
aria-checked,aria-required,aria-labelledby属性を参照
FR-FILL-005: 値の設定方法¶
- React等のフレームワーク対応:
Object.getOwnPropertyDescriptor(prototype, "value").setを使用したネイティブセッター呼び出し - イベント発火:
input,change,blur,focusイベントをバブリング付きで発火 - 入力済みフィールドのハイライト: 背景色を
rgba(212, 175, 55, 0.15)に変更
function setValue(el: HTMLElement, val: string) {
if (el.tagName === 'SELECT') {
const select = el as HTMLSelectElement;
const match = Array.from(select.options).find(o => o.text.includes(val) || o.value === val);
if (match) select.value = match.value;
} else {
const input = el as HTMLInputElement | HTMLTextAreaElement;
const prototype = el.tagName === 'TEXTAREA'
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype;
const nativeSetter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
if (nativeSetter && nativeSetter !== Object.getOwnPropertyDescriptor(input, "value")?.set) {
nativeSetter.call(input, val);
} else {
input.value = val;
}
}
['input','change','blur', 'focus'].forEach(ev => el.dispatchEvent(new Event(ev, { bubbles: true })));
el.style.backgroundColor = 'rgba(212, 175, 55, 0.15)';
}
FR-FILL-006: ページ遷移・探索ロジック¶
フォームが見つからない場合の探索処理(優先度順):
-
サイトマップ探索 (
findLinksFromSitemap):/sitemap.xml,/sitemap_index.xmlから問い合わせページURLを検出- 検索キーワード:
/contact/i,/inquiry/i,/form/i,/otoiawase/i,/お問い合わせ/
- 検索キーワード:
-
リンク探索 (
analyzeSiteStructure): ページ内のリンクをスコアリング- 200点: サイトマップ内のリンク
- 100点: リンクテキストに
/お問い合わせ|フォーム|相談|入力|Contact|Inquiry|Mail/i - 70点: hrefに
/contact|inquiry|form|otoiawase|support/i - +50点: header/nav/footer内のリンク
- +20点: クラス名に
btnまたはprimaryを含む - -50点:
/会社概要|about|company/i - -150点:
/faq|question|privacy|policy|guideline|login|recruit|採用/i - -1000点: パスが
/(ルートページへのリンク)
-
iframe検出:
form/contact/inquiry/entryを含むiframeのsrcへ遷移 -
メールアドレス抽出 → mailto:リンク蓄積: ページ内のメールアドレスを正規表現
/[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}/で抽出し、collectedEmails配列に蓄積する。バッチ完了後にmailto:リンクを一括生成 -
手詰まり: フォーム・リンク・メールアドレスすべて見つからない場合はエラー (
NO_FORM_FOUND)
5. バッチ実行エンジン (Background Service Worker)¶
FR-EXEC-001: 実行フロー¶
1. ローカル認証チェック (Chrome Storage の user_info を参照)
- is_banned=true → 利用拒否
- plan_type="subscription" → subscription_expires_at 期限チェック
- plan_type="ticket" → ticket_balance + monthly_free_remaining > 0 を確認
- is_white_user=true → 無制限許可
2. 設定読み込み (Chrome Storage)
3. 稼働制限チェック (曜日・時間)
4. URLリストのループ処理:
a. 除外ドメインチェック
b. 重複送信チェック
c. タブ作成 → ページ読み込み待機
d. ページ解析待機 (pageLoadDelay秒)
e. コンテンツスクリプトへメッセージ送信 (FILL_FORM)
f. レスポンス処理:
- LINKS_FOUND / MOVE_PAGE → 再帰的にページ遷移
- FOUND_MAIL_SEND_SERVER → メールアドレス抽出成功 → collectedEmails配列に蓄積
- CAPTCHA_DETECTED → 保留キューへ追加 (テストモードも同一動作)
- BUTTON_CLICKED → 送信完了待機 (本番モードのみ)
→ CHECK_PAGE_STATUS で完了確認
→ 確認画面の場合は確定ボタン押下
- TEST_COMPLETED → テストモード時、送信ボタン非押下で完了
- ERROR → 失敗記録
g. 送信履歴記録 (成功時)
h. トランザクション記録 (url_hash, status, type, timestamp)
i. 1秒待機 → 次のURL
5. 蓄積メールがあれば mailto:リンクを生成 (BCC一括・メーラー起動)
6. トランザクション記録をUIに送信 (EXECUTION_SYNC_READY)
7. UIが POST /api/sync-execution でサーバーと同期 → チケット消費
8. 保留キューの通知 (CAPTCHA件数)
9. 実行履歴の保存
v1.1 変更点: ステップ1はサーバーAPIコール (
POST /api/check-license) からローカルストレージ参照に変更。ステップ5でmailtoリンク生成、ステップ6-7でバッチ同期を追加。
レスポンスステータス一覧:
| ステータス | 意味 | Background側の処理 |
|---|---|---|
MOVE_PAGE |
お問い合わせページへ遷移 | 再帰的にページ遷移して処理続行 |
LINKS_FOUND |
リンク候補を検出 | 最高スコアリンクへ遷移 |
FOUND_MAIL_SEND_SERVER |
メールアドレス抽出成功 | メールアドレスを collectedEmails に蓄積 (本番/テスト共通) |
BUTTON_CLICKED |
送信ボタン押下完了 (本番のみ) | 5秒待機後 CHECK_PAGE_STATUS で確認 |
TEST_COMPLETED |
テストモードでフォーム入力完了 | 2秒待機後タブ閉じ→成功記録 |
CAPTCHA_DETECTED |
reCAPTCHA検知 | 保留キューへ追加 (テスト/本番共通) |
NO_FORM_FOUND |
フォーム/リンク/メール全て不検出 | 失敗記録 |
ERROR |
エラー発生 | 失敗記録 |
COMPLETED |
送信完了ページ確認 | 成功記録 |
CONFIRM_SUBMITTED |
確認画面で確定ボタン押下 | 再度 CHECK_PAGE_STATUS で完了確認 |
FR-EXEC-002: テストモード (Dry Run)¶
基本方針: テストモードと本番モードの唯一の違いは「送信ボタンを押さない」「メール送信しない」のみ。CAPTCHA検知・音声通知・保留キュー・稼働制限チェック・送信履歴記録・エラー処理・タブの自動閉じ等、送信以外の全処理は本番モードと完全に同一に動作する。
| 項目 | テストモード | 本番モード |
|---|---|---|
| フォーム自動入力 | 実行する | 実行する |
| 送信ボタン | 押さない (赤枠 10px solid #FF0000 でハイライト) |
自動クリック |
| メール送信 (フォーム未検出時) | 蓄積しない (ログのみ記録) | メールアドレスを蓄積し、完了後に mailto:リンクを生成 |
| CAPTCHA検知 | 本番と同一 (保留キューへ追加、音声アラート再生) | 保留キューへ追加、音声アラート再生 |
| タブ動作 | 自動閉じ (本番と同一) | 自動閉じ |
送信履歴 (sentHistory) |
記録する (本番と同一) | 記録する |
| エラー処理 | 本番と同一 (タブ閉じ→次へ) | タブ閉じ→次へ |
| 無限ループ検知 | 本番と同一 (即停止) | 即停止 |
| 実行履歴 |
isTestMode: true フラグ付きで記録 |
isTestMode: false で記録 |
レスポンスの違い: テストモードでは送信ボタン発見時に TEST_COMPLETED ステータスを返す。本番モードでは送信ボタンをクリックし BUTTON_CLICKED ステータスを返す。
FR-EXEC-003: 重複送信防止¶
| スコープ | 説明 |
|---|---|
none |
制限なし (常に送信) |
batch |
同一実行バッチ内で重複しない |
1day |
24時間以内の送信済みURLをスキップ |
2days |
2日以内 |
3days |
3日以内 |
4days |
4日以内 |
5days |
5日以内 |
6days |
6日以内 |
7days |
1週間以内 |
2weeks |
2週間以内 |
month |
1ヶ月以内 (デフォルト推奨値) |
3months |
3ヶ月以内 |
year |
1年以内 |
forever |
永久にスキップ |
送信履歴は Chrome Storage Local の sentHistory に { [url]: ISO日時文字列 } 形式で保存。
function shouldSkipUrl(url: string, scope: string, history: Record<string, string>, currentBatch: Set<string>): boolean {
if (!scope || scope === 'none') return false;
if (scope === 'batch' || currentBatch.has(url)) return true;
const lastSentStr = history[url];
if (!lastSentStr) return false;
if (scope === 'forever') return true;
const lastSentTime = new Date(lastSentStr).getTime();
const diffMs = Date.now() - lastSentTime;
const oneDay = 24 * 60 * 60 * 1000;
switch (scope) {
case '1day': return diffMs < oneDay;
case '2days': return diffMs < oneDay * 2;
case '3days': return diffMs < oneDay * 3;
case '4days': return diffMs < oneDay * 4;
case '5days': return diffMs < oneDay * 5;
case '6days': return diffMs < oneDay * 6;
case '7days': return diffMs < oneDay * 7;
case '2weeks': return diffMs < oneDay * 14;
case 'month': return diffMs < oneDay * 30;
case '3months': return diffMs < oneDay * 90;
case 'year': return diffMs < oneDay * 365;
default: return false;
}
}
FR-EXEC-004: 稼働制限¶
| 制限種別 | 説明 | デフォルト |
|---|---|---|
| 曜日制限 | チェックした曜日は実行不可 | 日曜 (sun)・土曜 (sat) を制限 |
| 時間帯制限 | 指定時間帯外は実行不可 | 09:00 ~ 18:00 (無効状態: enableTimeLimit: false) |
function checkRestrictions(settings: any, logFn: any): boolean {
const now = new Date();
const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const currentDayKey = days[now.getDay()];
if (settings.restrictedDays && settings.restrictedDays[currentDayKey] === true) {
logFn('ERROR', `本日は送信制限曜日(${currentDayKey.toUpperCase()})のため、処理を停止します。`);
return false;
}
if (settings.enableTimeLimit) {
const currentHm = now.getHours() * 60 + now.getMinutes();
const [startH, startM] = (settings.timeLimitStart || '00:00').split(':').map(Number);
const [endH, endM] = (settings.timeLimitEnd || '23:59').split(':').map(Number);
const startHm = startH * 60 + startM;
const endHm = endH * 60 + endM;
if (currentHm < startHm || currentHm > endHm) {
logFn('ERROR', `現在時刻は送信許可時間外です (${settings.timeLimitStart} ~ ${settings.timeLimitEnd})`);
return false;
}
}
return true;
}
FR-EXEC-005: 除外ドメイン¶
URLに以下のキーワードが含まれる場合はスキップする。1行1キーワードで設定。
デフォルト除外リスト:
houjin
prtimes
initial
wantedly
wiki
hrmos
goo.ne.jp
baseconnect
mapion
itp.ne.jp
除外判定: url.includes(ignoreWord) で部分一致チェック。
FR-EXEC-006: 無限ループ検知¶
- 訪問済みURLを
Setで管理 - URLの正規化: フラグメント (
#) 除去、末尾スラッシュ除去 - 同一URLへの再訪問を検知した場合、処理を停止 (テストモード・本番モード共通)
const normalizedUrl = url.split('#')[0].replace(/\/$/, '');
if (visitedUrls.has(normalizedUrl)) {
logFn('ERROR', `無限ループ検知: 既に訪問済みのページのため停止します (${url})`);
return 'FAILED';
}
visitedUrls.add(normalizedUrl);
FR-EXEC-007: CAPTCHA対応¶
| 検知対象 | 判定方法 |
|---|---|
| reCAPTCHA |
iframe[src*="recaptcha"] または .g-recaptcha の存在 (可視状態) |
| reCAPTCHA v3 (非表示) |
.grecaptcha-badge → 非表示版のため無視 (通過可能) |
function hasRecaptcha() {
if (document.querySelector('.grecaptcha-badge')) return false;
const iframe = document.querySelector('iframe[src*="recaptcha"]');
if (iframe && isVisible(iframe as HTMLElement)) return true;
const container = document.querySelector('.g-recaptcha');
if (container && isVisible(container as HTMLElement)) return true;
return false;
}
| モード | 動作 |
|---|---|
| 手動解決モード (推奨) | CAPTCHA検知 → 処理を保留キューへ移動 → 後で一括手動解決 |
| スキップモード | CAPTCHA検知 → 送信失敗として次のURLへ進む |
手動解決モードのタイムアウト: 設定可能 (デフォルト5分)。タイムアウト時は全処理を中断 (TIMEOUT_STOP)。
CAPTCHA解決の待機ロジック:
async function handleCaptchaMode(timeout: number) {
return new Promise((resolve, reject) => {
const start = Date.now();
const timer = setInterval(() => {
if (document.querySelector('[name="g-recaptcha-response"]')?.getAttribute('value')) {
clearInterval(timer); resolve(true);
}
if (Date.now() - start > timeout * 60000) {
clearInterval(timer); reject(new Error('TIMEOUT_STOP'));
}
}, 1000);
});
}
FR-EXEC-008: 音声通知¶
| 通知タイミング | 音声ファイル | 説明 |
|---|---|---|
| CAPTCHA検知時 | startAlertLoop.mp3 |
最初に1回鳴る |
| 待機中 | alertAudio.mp3 |
ループ再生 |
| 完了/タイムアウト時 | stopAlert.mp3 |
1回鳴る |
- カスタム音声ファイルのアップロード可能 (上限1MB、audio/*形式)
- Base64エンコードで Chrome Storage に保存 (
audioStart,audioLoop,audioStop) - 有効/無効の切り替え可能 (
enableSound) - デフォルト音声は
chrome.runtime.getURL(defaultFileName)で取得
FR-EXEC-009: ページ読み込み待機¶
- 設定可能 (デフォルト3秒、最小1秒)
-
chrome.tabs.onUpdatedでstatus: 'complete'を待機後、追加で設定秒数待機
function waitForTabLoad(tabId: number): Promise<void> {
return new Promise((resolve) => {
const listener = (tid: number, changeInfo: any) => {
if (tid === tabId && changeInfo.status === 'complete') {
chrome.tabs.onUpdated.removeListener(listener);
resolve();
}
};
chrome.tabs.onUpdated.addListener(listener);
});
}
FR-EXEC-010: 実行制御¶
| アクション | 説明 |
|---|---|
START_PROCESS |
バッチ実行開始。実行前に確認ダイアログ表示 (モード・テンプレート名・件数) |
STOP_PROCESS |
AbortController による強制停止 |
GET_STATUS |
現在の実行状態を返す (isRunning) |
CLEAR_LOGS |
ログ履歴を消去 |
START_PROCESS ペイロード:
{
action: 'START_PROCESS',
data: {
targets: string[], // URLリスト
template: Template, // 使用テンプレート
settings: Settings, // 設定値
userId: string | number // ユーザーID
}
}
実行セッション記録:
{
id: `hist_${Date.now()}`,
userId: activeUserId,
templateName: template?.name || '名称未設定',
startTime: new Date().toISOString(),
targetCount: targets?.length || 0,
logs: [],
isTestMode: boolean
}
実行履歴は executionHistory_user_{userId} に最大50件保存。
6. 送信完了判定¶
FR-COMPLETE-001: 完了ページ判定¶
ページ内テキストに以下のキーワードが含まれる場合、送信完了と判定する。
送信完了 | 送信しました | ありがとうございます | complete | success |
registered | received | 承りました | 記録しました | 受け付けました
正規表現:
/送信完了|送信しました|ありがとうございます|complete|success|registered|received|承りました|記録しました|受け付けました/i
FR-COMPLETE-002: 確認画面判定¶
ページ内テキストに以下のキーワードが含まれる場合、確認画面と判定し自動で確定ボタンを押下する。
確認画面 | 確認して | confirm
正規表現:
/確認画面|確認して|confirm/i
確認画面検出後の処理:
- 確定ボタン (
findSubmitButton) を検出してクリック →CONFIRM_SUBMITTED - 確定ボタンが見つからない場合 →
STUCK_ON_CONFIRM - 確定ボタン押下後5秒待機 → 再度
CHECK_PAGE_STATUSで完了確認
7. メール送信機能 (mailto:リンク方式)¶
v1.1 変更: サーバーAPI経由のメール送信を廃止し、
mailto:リンク生成方式に変更。サーバー負荷を排除し、ユーザーのデフォルトメーラーを利用する。
FR-MAIL-001: 概要¶
フォーム自動入力の探索過程でお問い合わせフォームが見つからず、ページ上にメールアドレスのみが検出された場合、メールアドレスを collectedEmails 配列に蓄積する。バッチ処理完了後、蓄積したメールアドレスをBCCにまとめた mailto: リンクを生成し、ユーザーがクリックすることでデフォルトメーラーを起動する。
FR-MAIL-002: 処理フロー¶
1. Content Script がフォーム・リンクを検出できない
2. ページ内からメールアドレスを正規表現で抽出
正規表現: /[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}/
3. Background へ FOUND_MAIL_SEND_SERVER ステータス + 抽出メールアドレスを返却
4. Background が collectedEmails 配列にメールアドレスを蓄積
5. バッチ処理全URL完了後:
a. user_info からユーザーのメールアドレスを取得
b. template から subject/body を取得
c. mailto:{userEmail}?subject={subject}&body={body}&bcc={emails} 形式のリンクを生成
d. URL長制限対応: 1900文字を超える場合は複数リンクに分割
e. MAILTO タイプのログエントリとして出力
f. MAILTO_LINK_READY メッセージでUIに送信
6. ユーザーがログ内のmailtoリンクをクリック → デフォルトメーラーが起動
FR-MAIL-003: テストモード時の動作¶
| 項目 | 内容 |
|---|---|
| 動作 | メールアドレス抽出のみ (蓄積もスキップ) |
| ログ | [TEST] メールアドレス抽出: {mail} (送信スキップ) |
| 送信履歴 | 記録する (本番と同一) |
FR-MAIL-004: mailto:リンク生成仕様¶
| 項目 | ソース |
|---|---|
| 宛先 (to) | ユーザー自身のメールアドレス (user_info.email) |
| BCC | バッチ処理で抽出した全メールアドレス (カンマ区切り) |
| 件名 (subject) | テンプレートの subject
|
| 本文 (body) | テンプレートの body
|
URL長制限: OS/ブラウザごとに mailto: URLには約2000文字の制限がある。安全マージンとして1900文字を上限とし、超える場合は複数のmailtoリンクに自動分割する。
function generateMailtoLinks(userEmail: string, emails: string[], subject: string, body: string): string[] {
const MAX_URL_LENGTH = 1900;
const basePrefix = `mailto:${encodeURIComponent(userEmail)}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}&bcc=`;
const links: string[] = [];
let currentBcc: string[] = [];
for (const email of emails) {
const testBcc = [...currentBcc, email];
const testUrl = basePrefix + encodeURIComponent(testBcc.join(','));
if (testUrl.length > MAX_URL_LENGTH && currentBcc.length > 0) {
links.push(basePrefix + encodeURIComponent(currentBcc.join(',')));
currentBcc = [email];
} else {
currentBcc.push(email);
}
}
if (currentBcc.length > 0) {
links.push(basePrefix + encodeURIComponent(currentBcc.join(',')));
}
return links;
}
FR-MAIL-005: UI表示¶
ログ表示部分で MAILTO タイプのログエントリをクリック可能な <a> タグとして描画する。
| 項目 | 内容 |
|---|---|
| 表示スタイル | ゴールド背景、大きめフォント、クリック可能なリンク |
| リンク属性 |
href="mailto:...", target="_blank"
|
| ラベル | 「メール送信リンク(BCC: XX件)- クリックでメーラー起動」 |
| 複数分割時 | 「メール送信リンク 1/N(BCC: 約XX件)」の形式で表示 |
8. 画面一覧¶
8.1 ログイン画面¶
| 項目 | 内容 |
|---|---|
| コンポーネント | Login.tsx |
| 表示条件 |
access_token が Chrome Storage に存在しない場合 |
| 入力項目 | メールアドレス、パスワード |
| 動作 |
POST /api/login → トークン取得 → Dashboard へ遷移 |
| エラー表示 | 認証失敗メッセージ、サーバー接続エラーメッセージ |
8.2 ダッシュボード (DASHBOARD)¶
| 項目 | 内容 |
|---|---|
| コンポーネント |
Dashboard.tsx 内の DashboardPanel
|
| タブキー | dash |
| 表示内容 | ステータスカード (TARGETS / SUCCESS / ERROR / YIELD RATE) |
| 実行履歴リスト (ページネーション: 5件/ページ) | |
| セッションログ (リアルタイム更新) | |
| データソース |
executionHistory_user_{userId}, executionLogs_user_{userId}
|
8.3 メッセージエディタ (MESSAGE EDITOR)¶
| 項目 | 内容 |
|---|---|
| コンポーネント | MessageEditor.tsx |
| タブキー | input |
| レイアウト | 左: テンプレート一覧サイドバー (260px)、右: 編集フォーム |
| 機能 | 新規作成、編集、複製、削除、差し込みタグ挿入 |
| 入力セクション | 送信者情報 (分割入力)、メッセージ本文 |
8.4 実行画面 (EXECUTION)¶
| 項目 | 内容 |
|---|---|
| コンポーネント | Execution.tsx |
| タブキー | send |
| レイアウト | 左: テンプレート選択 + URLリスト入力、右: コンソールログ |
| 入力 | テンプレート選択 (ドロップダウン)、ターゲットURL (1行1URL、textarea) |
| 実行ボタン | START PROCESS / STOP |
| 前提条件 | 設定が一度保存されていること (lastSavedSettings の存在チェック) |
| 実行前確認 | モード (テスト/本番)、テンプレート名、件数を表示して confirm |
8.5 設定画面 (SETTINGS)¶
| 項目 | 内容 |
|---|---|
| コンポーネント | Settings.tsx |
| タブキー | set |
| セクション構成 | 基本動作設定 / ロボット認証対策 & 通知 / 稼働制限設定 / 除外ドメイン設定 |
設定項目一覧:
| カテゴリ | 設定項目 | キー | 型 | デフォルト |
|---|---|---|---|---|
| 基本動作 | テストモード | testMode |
boolean | false |
| 基本動作 | 重複送信防止期間 | duplicateCheckScope |
enum | month |
| 基本動作 | ページ読み込み待機 (秒) | pageLoadDelay |
number | 3 |
| CAPTCHA | 手動解決モード | solveCaptcha |
boolean | true |
| CAPTCHA | タイムアウト (分) | captchaTimeout |
number | 5 |
| 通知 | 音声通知有効化 | enableSound |
boolean | true |
| 通知 | 開始通知音 | audioStart |
audio/base64 | null |
null (デフォルトMP3使用) |
| 通知 | 待機ループ音 | audioLoop |
audio/base64 | null |
null (デフォルトMP3使用) |
| 通知 | 完了/終了音 | audioStop |
audio/base64 | null |
null (デフォルトMP3使用) |
| 稼働制限 | 時間帯制限有効化 | enableTimeLimit |
boolean | false |
| 稼働制限 | 開始時刻 | timeLimitStart |
string (HH:mm) | 09:00 |
| 稼働制限 | 終了時刻 | timeLimitEnd |
string (HH:mm) | 18:00 |
| 稼働制限 | 曜日制限 | restrictedDays |
object | { sun: true, mon: false, tue: false, wed: false, thu: false, fri: false, sat: true } |
| 除外 | 除外ドメインリスト | ignoreDomains |
string (改行区切り) | 10件のデフォルト値 (上記参照) |
8.6 アカウント設定画面 (ACCOUNT)¶
| 項目 | 内容 |
|---|---|
| コンポーネント | AccountSettings.tsx |
| タブキー | account |
| 表示情報 | ログインユーザーのメールアドレス、ユーザー種別、契約期間 |
| 外部リンク | 登録情報 (/dashboard/profile)、契約確認 (/dashboard/billing) → ユーザーWebパネルへ |
| 操作 | ログアウトボタン |
9. 拡張機能UIテーマ¶
ダークテーマ (黒/ゴールド) を基調とした統一デザイン。
| 項目 | 値 |
|---|---|
| メイン背景 |
#000000 (黒) |
| カード背景 | #141414 |
| パネル背景 | #1E1E1E |
| アクセントカラー |
#D4AF37 (ゴールド) |
| ゴールドグラデーション | linear-gradient(135deg, #d4af37, #c5a028) |
| テキスト色 (メイン) | #E0E0E0 |
| テキスト色 (サブ) | #888888 |
| ボーダー色 | #333333 |
| 成功色 | #00cc00 |
| エラー色 | #ff4444 |
| 入力済みフィールドハイライト | rgba(212, 175, 55, 0.15) |
| テストモード送信ボタン枠 | 10px solid #FF0000 |
10. Chrome Storage データ構造¶
| キー | 型 | 説明 |
|---|---|---|
access_token |
string | Sanctum APIトークン |
user_info |
UserInfo | ユーザー情報 (id, name, email, is_white_user, contract_period, plan_type, ticket_balance, monthly_free_limit, monthly_free_remaining, subscription_expires_at, is_banned) |
device_id |
string | デバイスUUID (UUIDv4) |
templates |
Template[] | テンプレート一覧 |
settings |
object | 全設定値 |
lastSavedSettings |
string | 設定の最終保存日時 |
sentHistory |
Record<string, string> | 送信済みURL履歴 { url: ISO日時 }
|
executionHistory_user_{userId} |
object[] | 実行履歴 (最大50件) |
executionLogs_user_{userId} |
LogItem[] | 実行ログ (最大500件) |
executionUrls_user_{userId} |
string | 最後に入力したURLリスト |
lastSelectedTemplateId_user_{userId} |
string | 最後に選択したテンプレートID |
readNoticeIds |
number[] | 既読通知IDリスト |
pending_sync_user_{userId} |
object | 同期失敗時の保留データ (次回ログイン時にリトライ) |
LogItem 型定義:
interface LogItem {
time: string;
type: 'INFO' | 'SUCCESS' | 'ERROR' | 'SYSTEM' | 'WAITING' | 'MAILTO';
msg: string;
}
ログレベル表示色:
| ログレベル | 用途 | 色 |
|---|---|---|
INFO |
処理状況の通知 | ゴールド (#D4AF37) |
SUCCESS |
送信成功 | 緑 (#00cc00) |
ERROR |
エラー・失敗 | 赤 (#ff4444) |
SYSTEM |
システムメッセージ (開始/終了等) | ゴールド (#D4AF37) |
WAITING |
待機中 | ゴールド (#D4AF37) |
MAILTO |
mailto:リンク (クリック可能) | ゴールド背景 + リンク装飾 |
11. Chrome API利用¶
11.1 パーミッション¶
| パーミッション | 用途 |
|---|---|
storage |
テンプレート・設定・履歴・トークン等の永続化 |
tabs |
タブの作成・更新・削除 |
activeTab |
現在のタブへのアクセス |
scripting |
コンテンツスクリプトの注入 |
manifest設定:
manifest: {
name: 'SMART-CONTACT',
permissions: ['storage', 'tabs', 'activeTab', 'scripting'],
}
11.2 Tabs API¶
| API | 用途 |
|---|---|
chrome.tabs.create |
新規タブ作成 (URL処理開始時) |
chrome.tabs.update |
既存タブのURL更新 (ページ遷移時) |
chrome.tabs.remove |
タブ削除 (処理完了/エラー後) |
chrome.tabs.sendMessage |
コンテンツスクリプトへのメッセージ送信 (FILL_FORM, CHECK_PAGE_STATUS, SHOW_OVERLAY) |
chrome.tabs.onUpdated |
タブ読み込み完了の監視 (status: 'complete') |
chrome.tabs.query |
タブ存在確認 |
11.3 Runtime API¶
| API | 用途 |
|---|---|
chrome.runtime.onMessage |
バックグラウンド ⇄ ダッシュボード間のメッセージング (START_PROCESS, STOP_PROCESS, GET_STATUS, CLEAR_LOGS, LOG_UPDATE, PROCESS_COMPLETE) |
chrome.runtime.sendMessage |
ログ更新、プロセス完了通知等のブロードキャスト |
chrome.runtime.getURL |
拡張機能内リソース (音声ファイル等) のURL取得 |
chrome.action.onClicked |
拡張機能アイコンクリック時にオプションページを開く |
11.4 Storage API¶
| API | 用途 |
|---|---|
chrome.storage.local.get |
テンプレート、設定、履歴、トークン等の読み込み |
chrome.storage.local.set |
テンプレート、設定、履歴、トークン等の保存 |
chrome.storage.local.remove |
ログアウト時のトークン削除等 |
- 容量制限: Chrome Storage Local はデフォルトで約5MB
- 非同期API (
get,set,remove) -
chrome.storage.syncも補助的に参照 (テストモード判定等)