瀏覽器擴展開發:一鍵填充從虛擬號碼讀取的短信驗證碼
一個專注瀏覽器擴展的資深前端工程師的「從零到發布」完整實戰——每一步都有代碼,每一段代碼都有註釋,每一個坑都提前替你踩平了。
📑 目錄
一、擴展架構圖
三個核心組件各司其職,數據流清晰分明:
- Popup:用戶界面,顯示號碼、驗證碼和操作按鈕。
- Background Service Worker:業務邏輯與 API 通信中心,負責請求號碼、輪詢短信、推送通知。
- Content Script:與目標網頁交互,智能識別輸入框並填充驗證碼。
二、Step 1:項目初始化與 Manifest V3 配置
建立專案目錄,初始文件結構如下:
sms-filler-extension/
├── manifest.json
├── popup.html
├── popup.js
├── popup.css
├── background.js
├── content-script.js
├── services/
│ └── sms-api.js
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
以下是完整的 manifest.json,注意 Manifest V3 的關鍵權限聲明:
{
"manifest_version": 3,
"name": "SMS Auto Fill",
"version": "1.0.0",
"description": "一鍵從虛擬號碼讀取短信驗證碼並自動填充",
"permissions": [
"storage",
"scripting",
"activeTab",
"notifications",
"alarms"
],
"host_permissions": [
"https://5sim.net/*",
"https://api.smspool.net/*"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_title": "SMS Auto Fill"
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"content_scripts": [
{
"matches": [""],
"js": ["content-script.js"],
"run_at": "document_idle"
}
]
}
Manifest V3 與 V2 的關鍵差異:
| 特性 | Manifest V2 | Manifest V3 |
|---|---|---|
| Background | 持久背景頁 | Service Worker(非持久,可能休眠) |
| 權限 | 權限較寬鬆 | 需顯式聲明 host_permissions |
| 遠端代碼 | 允許 | 禁止,所有邏輯必須打包在擴展內 |
| 生命週期 | 背景頁長駐 | Service Worker 閒置 30 秒後卸載 |
三、Step 2:核心 API 通信模組——對接接碼平台
在 services/sms-api.js 中封裝獲取號碼、輪詢短信、釋放號碼三個核心函數。以 5sim API 為例,錯誤處理涵蓋超時、餘額不足、號碼被封等情況。
const BASE_URL = 'https://5sim.net/v1';
let API_KEY = '';
// 初始化 API Key(從 storage 讀取)
async function initApiKey() {
const result = await chrome.storage.local.get('apiKey');
API_KEY = result.apiKey || '';
}
initApiKey();
// 請求頭
function headers() {
return {
'Authorization': `Bearer ${API_KEY}`,
'Accept': 'application/json'
};
}
// 獲取虛擬號碼 (country: usa, service: google)
async function buyNumber(country = 'usa', service = 'google') {
try {
const resp = await fetch(
`${BASE_URL}/user/buy/activation/${country}/any/${service}`,
{ headers: headers() }
);
if (!resp.ok) {
if (resp.status === 403) throw new Error('API Key 無效或餘額不足');
if (resp.status === 429) throw new Error('請求過於頻繁,請稍後重試');
throw new Error(`獲取號碼失敗: ${resp.status}`);
}
const data = await resp.json();
return { phone: data.phone, activationId: data.id };
} catch (e) {
console.error('buyNumber error:', e);
throw e;
}
}
// 輪詢短信狀態 (每 5 秒一次,最長 120 秒)
async function waitForSms(activationId, timeout = 120000) {
const start = Date.now();
while (Date.now() - start < timeout) {
try {
const resp = await fetch(`${BASE_URL}/user/check/${activationId}`, {
headers: headers()
});
if (!resp.ok) throw new Error(`查詢失敗: ${resp.status}`);
const data = await resp.json();
if (data.status === 'RECEIVED' && data.sms && data.sms.length > 0) {
return data.sms[0].text; // 返回最新短信內容
}
} catch (e) {
console.warn('輪詢錯誤:', e);
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
throw new Error('等待短信超時');
}
// 釋放號碼(取消或標記完成)
async function cancelNumber(activationId) {
try {
await fetch(`${BASE_URL}/user/cancel/${activationId}`, {
headers: headers()
});
} catch (e) {
console.error('釋放號碼失敗:', e);
}
}
// 導出給 background 使用
export { buyNumber, waitForSms, cancelNumber };
四、Step 3:構建 Popup UI——控制面板
Popup 是使用者直接互動的介面。它通過 chrome.storage.local 與 Background 雙向同步數據,實時顯示號碼與驗證碼。
popup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<div class="section">
<div class="label">📞 當前號碼</div>
<div id="phoneDisplay" class="phone">尚無號碼</div>
<div id="expiryDisplay" class="expiry"></div>
</div>
<div class="section">
<div class="label">🔑 最新驗證碼</div>
<div id="otpDisplay" class="otp">等待中...</div>
<button id="copyBtn" class="btn secondary">複製驗證碼</button>
</div>
<div class="actions">
<button id="getNumberBtn" class="btn primary">獲取新號碼</button>
<button id="fillBtn" class="btn success" disabled>填充至當前頁</button>
</div>
<div id="status" class="status"></div>
</div>
<script src="popup.js" type="module"></script>
</body>
</html>
popup.js(核心邏輯)
import { buyNumber, waitForSms } from './services/sms-api.js';
const phoneEl = document.getElementById('phoneDisplay');
const otpEl = document.getElementById('otpDisplay');
const expiryEl = document.getElementById('expiryDisplay');
const copyBtn = document.getElementById('copyBtn');
const getNumberBtn = document.getElementById('getNumberBtn');
const fillBtn = document.getElementById('fillBtn');
const statusEl = document.getElementById('status');
let currentPhone = '';
let currentActivationId = '';
let countdownTimer = null;
// 從 storage 加載狀態
async function loadState() {
const { phone, activationId, otp, timestamp } = await chrome.storage.local.get(['phone', 'activationId', 'otp', 'timestamp']);
if (phone) {
currentPhone = phone;
currentActivationId = activationId;
phoneEl.textContent = phone;
expiryEl.textContent = timestamp ? `取得時間: ${new Date(timestamp).toLocaleTimeString()}` : '';
fillBtn.disabled = !otp;
}
if (otp) {
otpEl.textContent = otp;
}
}
loadState();
// 監聽 storage 變化實時更新
chrome.storage.onChanged.addListener((changes) => {
if (changes.phone) {
phoneEl.textContent = changes.phone.newValue || '尚無號碼';
currentPhone = changes.phone.newValue;
}
if (changes.otp) {
otpEl.textContent = changes.otp.newValue || '等待中...';
fillBtn.disabled = !changes.otp.newValue;
if (changes.otp.newValue) copyBtn.disabled = false;
}
});
// 獲取新號碼
getNumberBtn.addEventListener('click', async () => {
getNumberBtn.disabled = true;
statusEl.textContent = '正在獲取號碼...';
try {
const { phone, activationId } = await buyNumber('usa', 'google');
const timestamp = Date.now();
await chrome.storage.local.set({ phone, activationId, otp: '', timestamp });
currentPhone = phone;
currentActivationId = activationId;
statusEl.textContent = '✅ 號碼已獲取,正在等待短信...';
// 觸發 background 開始輪詢
chrome.runtime.sendMessage({ action: 'startPolling', activationId });
// 8 分鐘倒計時
startCountdown(480);
} catch (e) {
statusEl.textContent = `❌ ${e.message}`;
} finally {
getNumberBtn.disabled = false;
}
});
// 填充到當前頁面
fillBtn.addEventListener('click', async () => {
fillBtn.disabled = true;
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) {
chrome.tabs.sendMessage(tab.id, { action: 'fillOtp' }, (response) => {
if (chrome.runtime.lastError) {
statusEl.textContent = '❌ 無法注入此頁面(可能為 chrome:// 頁面)';
} else {
statusEl.textContent = '✓ 已填充';
setTimeout(() => { statusEl.textContent = ''; }, 2000);
}
fillBtn.disabled = false;
});
}
});
// 複製驗證碼
copyBtn.addEventListener('click', () => {
const otp = otpEl.textContent;
if (otp && otp !== '等待中...') {
navigator.clipboard.writeText(otp).then(() => {
statusEl.textContent = '✓ 已複製';
setTimeout(() => { statusEl.textContent = ''; }, 1500);
});
}
});
function startCountdown(seconds) {
clearInterval(countdownTimer);
let remaining = seconds;
expiryEl.textContent = `有效期剩餘: ${Math.floor(remaining / 60)}:${(remaining % 60).toString().padStart(2, '0')}`;
countdownTimer = setInterval(() => {
remaining--;
if (remaining <= 0) {
clearInterval(countdownTimer);
expiryEl.textContent = '⏰ 號碼已過期';
return;
}
expiryEl.textContent = `有效期剩餘: ${Math.floor(remaining / 60)}:${(remaining % 60).toString().padStart(2, '0')}`;
}, 1000);
}
五、Step 4:Content Script——自動填充驗證碼
Content Script 負責智能識別頁面上的驗證碼輸入框,並使用原生事件觸發來確保 React/Vue 受控組件正常更新。
// content-script.js
// 通用的 setNativeValue 函數,繞過 React/Vue 的屬性劫持
function setNativeValue(element, value) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
nativeInputValueSetter.call(element, value);
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
// 某些框架需要額外觸發
element.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
element.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
}
// 智能尋找驗證碼輸入框
function findOtpInput() {
const keywords = ['code', 'token', 'otp', 'verification', 'captcha', 'mfa', 'pin', 'one-time'];
const selectors = keywords.map(k => `input[type="text"][id*="${k}"], input[type="text"][name*="${k}"],
input[type="text"][placeholder*="${k}"], input[type="tel"][id*="${k}"],
input[autocomplete="one-time-code"], input[autocomplete="otp"]`).join(', ');
let input = document.querySelector(selectors);
if (!input) {
// 遍歷所有 input,根據屬性關鍵字模糊匹配
const allInputs = document.querySelectorAll('input[type="text"], input[type="tel"], input[type="number"]');
for (const el of allInputs) {
const attrs = (el.getAttribute('id') || '') + (el.getAttribute('name') || '') +
(el.getAttribute('placeholder') || '') + (el.getAttribute('autocomplete') || '');
if (keywords.some(k => attrs.toLowerCase().includes(k))) {
input = el;
break;
}
}
}
return input;
}
// 監聽來自 popup 或 background 的填充指令
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'fillOtp') {
chrome.storage.local.get('otp', (data) => {
const otp = data.otp;
if (!otp) {
sendResponse({ success: false, error: '無可用驗證碼' });
return;
}
const input = findOtpInput();
if (!input) {
sendResponse({ success: false, error: '找不到驗證碼輸入框' });
return;
}
setNativeValue(input, otp);
// 短暫視覺反饋
input.style.transition = 'background-color 0.3s';
input.style.backgroundColor = '#c6f6d5';
setTimeout(() => { input.style.backgroundColor = ''; }, 1500);
sendResponse({ success: true });
});
return true; // 保持訊息通道開啟以進行非同步回應
}
});
setNativeValue 的設計原因:React 和 Vue 使用受控組件,直接賦值 input.value = otp 不會觸發框架的狀態更新。我們必須取得原生 value setter 並主動觸發 input 和 change 事件,框架才能正確同步內部狀態。
六、Step 5:打包與本地安裝
本地測試
- 打開 Chrome,網址列輸入
chrome://extensions/。 - 開啟右上角「開發人員模式」。
- 點擊「載入未封裝項目」,選擇專案根目錄(包含
manifest.json的文件夾)。 - 擴展圖標會出現在工具列,點擊即可測試。
打包發布
產生 .zip 包上傳 Chrome Web Store:
zip -r sms-filler.zip . -x "*.git*" "node_modules/*"
提交到開發者控制台時需準備:說明、截圖、隱私政策 URL、應用類別等。
七、進階功能優化建議
- 多平台切換:在 popup 增加下拉選單選擇接碼平台(5sim / SMSPool),動態調用不同 API 模組。
- 驗證碼歷史記錄:使用
chrome.storage.local保存最近 20 條記錄,7 天後自動清除。 - 快捷鍵支持:在 manifest 中聲明
commands,例如Ctrl+Shift+S一鍵讀取最新驗證碼並嘗試填充。 - 自動輪詢模式:獲取號碼後 Background 自動輪詢,收到驗證碼後通過
chrome.notifications彈出桌面通知。
八、排坑指南
坑一:Content Script 無法注入某些頁面
症狀:在 chrome://extensions/ 或 Chrome Web Store 等特權頁面中,content script 無法執行,fill 按鈕點擊後無反應。
解法:在 popup.js 的 fillBtn 回調中檢測 chrome.runtime.lastError,並提示用戶「此頁面不支援自動填充」。此外,可在 manifest.json 的 content_scripts.matches 中排除 chrome:// 和 https://chrome.google.com/webstore/*。
坑二:React/Vue 輸入框值已更新但組件狀態未同步
症狀:驗證碼已顯示在輸入框內,但頁面上的「提交」按鈕仍為禁用狀態,表單校驗未通過。
解法:必須使用 setNativeValue 函數,通過原生 setter 設置值並觸發 input 和 change 事件。僅設置 element.value 或僅觸發 input 事件可能不足以通知框架。
坑三:Background Service Worker 休眠
症狀:擴展安裝後一段時間,輪詢過程突然中斷,短信不再接收。這是因為 Manifest V3 的 Service Worker 在閒置約 30 秒後會被瀏覽器卸載。
解法:使用 chrome.alarms API 建立定時喚醒機制。在 background.js 中:
chrome.alarms.create('keepAlive', { periodInMinutes: 0.5 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'keepAlive') {
console.log('Service Worker 喚醒檢查');
// 此處可檢查是否有正在進行的輪詢,若無需繼續則可省略
}
});
同時在進行輪詢時,可通過 chrome.storage.session 保持少量數據以延長生命。
坑四:API Key 安全存儲
症狀:在 popup 或 background 中硬編碼 API Key,代碼一旦被解析即洩漏。
解法:讓使用者在擴展的選項頁面輸入 API Key,並存儲在 chrome.storage.local(本地加密存儲,不會同步到雲端)。每次調用 API 時從 storage 讀取。
九、總結與倫理提醒
至此,你已經擁有了一個完整的瀏覽器擴展,它可以在你瀏覽任何網站時,一鍵從接碼平台拉取虛擬號碼的短信驗證碼,並自動填充到正確的輸入框中。這套架構的核心價值在於:將接碼平台的能力融入瀏覽器,省去切換設備的認知負擔。