上篇文章写到用 Vibe Coding 写了一个网页,后面再想,能不能用这个网页直接变成浏览器的插件,顺便也体验下怎么把插件上传到谷歌的插件市场上去。
整个流程现在都跑通了,插件也上传到了 Chrome 到应用市场上去,可以去体验下

虽然比较简陋,但还是能使用的。
详细的思路是这样的,我在原来的网页生成对话里,直接问“根据这个能开发一个浏览器插件嘛,我要上传到谷歌插件市场,要怎么操作”
根据这个进行了一段分析之后,生成了3段代码,分别是manifest.json, popup.html, popup.js 的代码,还有一个icon,因为匆忙就随便截了个128*128尺寸的图片。
运行第一次,不成功,运行第二次,也不成功,后面分析出来是API的问题,DeepSeek 直接换了一个API源。
你遇到的 所有 API 统一返回 403 Forbidden,是因为这些免费 IP 归属地服务(ipwhois / ipapi / ip-api)大多禁止来自浏览器扩展的请求(请求头的
Origin是chrome-extension://…,触发反滥用机制)。换成更宽松的api.ip.sb即可解决。
更新的代码
{
"manifest_version": 3,
"name": "IP Lookup Tool",
"version": "1.0",
"description": "Look up public IP address information with ip.sb API.",
"icons": {
"128": "icon.png"
},
"action": {
"default_popup": "popup.html",
"default_title": "IP Lookup"
},
"host_permissions": [
"https://api.ipify.org/*",
"https://api.ip.sb/*"
]
}
以及这块的代码部分
(function() {
// ==================== DOM 元素 ====================
const myIpDisplay = document.getElementById('myIpDisplay');
const myIpTags = document.getElementById('myIpTags');
const statusDot = document.getElementById('statusDot');
const statusLabel = document.getElementById('statusLabel');
const searchInput = document.getElementById('searchInput');
const searchBtn = document.getElementById('searchBtn');
const clearBtn = document.getElementById('clearBtn');
const queryResult = document.getElementById('queryResult');
const resultIp = document.getElementById('resultIp');
const resultTags = document.getElementById('resultTags');
const statusMessage = document.getElementById('statusMessage');
const langBtns = document.querySelectorAll('.lang-btn');
// ==================== 多语言 ====================
const i18n = {
zh: {
title: 'IP 查询工具',
subtitle: '查询公网 IP 地址归属信息',
loading: '正在获取本机IP...',
loading_text: '查询中...',
your_ip: '您的公网 IP 信息',
fetch_failed: '获取失败',
cannot_get: '无法获取',
divider: '查询其他 IP',
placeholder: '输入 IP 地址,如 8.8.8.8',
search_btn: '查询',
querying: '查询中...',
result_title: '查询结果',
footer: '数据来源:api.ip.sb',
error_prefix: '本机IP获取失败:',
query_error_prefix: '查询失败:',
invalid_ip: '请输入一个有效的 IP 地址',
invalid_format: 'IP 地址格式不正确',
private_ip: '这是私有 IP 地址,无法查询。',
no_details: '暂无详细信息',
timeout: '请求超时',
network_error: '网络连接失败'
},
en: {
title: 'IP Lookup',
subtitle: 'Look up public IP address info',
loading: 'Fetching your IP...',
loading_text: 'Looking up...',
your_ip: 'Your Public IP Info',
fetch_failed: 'Failed',
cannot_get: 'Unavailable',
divider: 'Look Up Other IP',
placeholder: 'Enter IP, e.g. 8.8.8.8',
search_btn: 'Search',
querying: 'Searching...',
result_title: 'Query Result',
footer: 'Data from api.ip.sb',
error_prefix: 'Failed to get your IP: ',
query_error_prefix: 'Query failed: ',
invalid_ip: 'Please enter a valid IP address',
invalid_format: 'Invalid IP format',
private_ip: 'This is a private IP.',
no_details: 'No details available',
timeout: 'Request timed out',
network_error: 'Network error'
}
};
let currentLang = 'zh';
function t(key) { return i18n[currentLang]?.[key] || key; }
const DYNAMIC_IDS = new Set(['myIpDisplay', 'statusLabel', 'resultIp', 'searchBtn']);
function applyLanguage() {
document.querySelectorAll('[data-i18n]').forEach(el => {
if (el.id && DYNAMIC_IDS.has(el.id)) return;
const key = el.getAttribute('data-i18n');
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
const placeholderKey = el.getAttribute('data-i18n-placeholder');
if (placeholderKey) el.placeholder = t(placeholderKey);
} else {
el.textContent = t(key);
}
});
const btnTextSpan = document.querySelector('.search-btn .btn-text');
if (btnTextSpan) {
const innerSpan = btnTextSpan.querySelector('span[data-i18n]');
if (innerSpan) innerSpan.textContent = t('search_btn');
else btnTextSpan.innerHTML = '🔍 ' + t('search_btn');
}
langBtns.forEach(btn => btn.classList.toggle('active', btn.getAttribute('data-lang') === currentLang));
updateDynamicTexts();
}
function updateDynamicTexts() {
if (statusDot.classList.contains('loading')) {
statusLabel.textContent = t('loading');
myIpDisplay.textContent = t('loading_text');
} else if (statusDot.classList.contains('error')) {
statusLabel.textContent = t('fetch_failed');
} else {
statusLabel.textContent = t('your_ip');
}
if (searchBtn.classList.contains('loading')) {
searchBtn.querySelector('.btn-text').innerHTML = '<span class="btn-spinner"></span> ' + t('querying');
} else {
searchBtn.querySelector('.btn-text').innerHTML = '🔍 ' + t('search_btn');
}
}
function saveLangPreference() {
localStorage.setItem('ipquery-lang', currentLang);
}
function switchLanguage(lang) {
if (lang === currentLang) return;
currentLang = lang;
applyLanguage();
saveLangPreference();
}
// ==================== 唯一 API:ip.sb ====================
// 本机查询用基础 URL + 自定义字段格式
async function fetchIpInfo(ip) {
const base = 'https://api.ip.sb/geoip';
const url = ip ? `${base}/${encodeURIComponent(ip)}` : base;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
// ip.sb 字段映射
return {
ip: data.ip,
city: data.city,
region: data.region,
country: data.country,
country_code: data.country_code,
isp: data.isp,
org: data.organization,
timezone: data.timezone,
lat: data.latitude,
lon: data.longitude
};
} catch (err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') throw new Error(t('timeout'));
throw err;
}
}
function renderIpTags(container, data) {
container.innerHTML = '';
const tags = [];
if (data.city && data.region) tags.push({ icon: '📍', text: `${data.city}, ${data.region}`, highlight: true });
else if (data.city) tags.push({ icon: '📍', text: data.city, highlight: true });
if (data.country) {
const emoji = getCountryEmoji(data.country_code);
tags.push({ icon: emoji || '🌍', text: data.country, highlight: false });
}
if (data.isp) tags.push({ icon: '📡', text: data.isp, highlight: true });
if (data.org && data.org !== data.isp) tags.push({ icon: '🏢', text: data.org, highlight: false });
if (data.timezone) tags.push({ icon: '🕐', text: data.timezone, highlight: false });
if (data.lat != null && data.lon != null) tags.push({ icon: '🗺️', text: `${data.lat}°, ${data.lon}°`, highlight: false });
if (tags.length === 0) {
const span = document.createElement('span');
span.className = 'info-tag';
span.textContent = t('no_details');
container.appendChild(span);
return;
}
tags.forEach(tag => {
const span = document.createElement('span');
span.className = 'info-tag' + (tag.highlight ? ' highlight' : '');
span.innerHTML = `<span class="tag-icon">${tag.icon}</span> ${tag.text}`;
container.appendChild(span);
});
}
function getCountryEmoji(code) {
if (!code) return '';
const upper = code.toUpperCase();
const codePoints = [...upper].map(c => 0x1F1E6 + c.charCodeAt(0) - 65);
return codePoints.length === 2 ? String.fromCodePoint(...codePoints) : '';
}
function isValidIpFormat(ip) {
const trimmed = ip.trim();
if (!trimmed) return false;
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(trimmed)) {
return trimmed.split('.').every(p => {
const num = parseInt(p, 10);
return num >= 0 && num <= 255 && String(num) === p;
});
}
const ipv6Regex = /^[0-9a-fA-F:]+$/;
return ipv6Regex.test(trimmed) && trimmed.includes(':');
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function setMyIpLoadingState(state) {
statusDot.className = 'dot';
switch (state) {
case 'loading':
statusDot.classList.add('loading');
statusLabel.textContent = t('loading');
myIpDisplay.textContent = t('loading_text');
myIpDisplay.classList.add('loading-text');
break;
case 'success':
statusDot.classList.remove('loading', 'error');
statusLabel.textContent = t('your_ip');
myIpDisplay.classList.remove('loading-text');
break;
case 'error':
statusDot.classList.add('error');
statusLabel.textContent = t('fetch_failed');
myIpDisplay.textContent = t('cannot_get');
myIpDisplay.classList.remove('loading-text');
break;
}
}
function setSearchBtnLoading(isLoading) {
if (isLoading) {
searchBtn.classList.add('loading');
searchBtn.disabled = true;
searchBtn.querySelector('.btn-text').innerHTML = '<span class="btn-spinner"></span> ' + t('querying');
} else {
searchBtn.classList.remove('loading');
searchBtn.disabled = false;
searchBtn.querySelector('.btn-text').innerHTML = '🔍 ' + t('search_btn');
}
}
function showStatusMessage(msg, type = 'info') {
statusMessage.textContent = msg;
statusMessage.className = 'status-message visible ' + type;
}
function hideStatusMessage() {
statusMessage.classList.remove('visible', 'error', 'warning', 'info');
}
async function loadMyIp() {
setMyIpLoadingState('loading');
hideStatusMessage();
try {
// ip.sb 的 base URL 不传 IP 即返回本机信息
const data = await fetchIpInfo(null);
setMyIpLoadingState('success');
myIpDisplay.textContent = data.ip;
renderIpTags(myIpTags, data);
} catch (err) {
setMyIpLoadingState('error');
myIpDisplay.textContent = t('cannot_get');
myIpTags.innerHTML = '<span class="info-tag" style="color:#fca5a5;">⚠️ ' + escapeHtml(err.message) + '</span>';
showStatusMessage(t('error_prefix') + err.message, 'error');
}
}
async function querySpecificIp(ip) {
const trimmedIp = ip.trim();
if (!trimmedIp) {
showStatusMessage(t('invalid_ip'), 'warning');
return;
}
if (!isValidIpFormat(trimmedIp)) {
showStatusMessage(t('invalid_format'), 'warning');
return;
}
queryResult.classList.remove('visible');
hideStatusMessage();
setSearchBtnLoading(true);
try {
const data = await fetchIpInfo(trimmedIp);
resultIp.textContent = data.ip;
renderIpTags(resultTags, data);
queryResult.classList.add('visible');
} catch (err) {
queryResult.classList.remove('visible');
if (err.message.includes('private') || err.message.includes('reserved')) {
showStatusMessage(t('private_ip'), 'warning');
} else {
showStatusMessage(t('query_error_prefix') + err.message, 'error');
}
} finally {
setSearchBtnLoading(false);
}
}
// ==================== 事件 ====================
langBtns.forEach(btn => btn.addEventListener('click', function() {
switchLanguage(this.getAttribute('data-lang'));
}));
searchBtn.addEventListener('click', () => querySpecificIp(searchInput.value));
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
querySpecificIp(searchInput.value);
}
});
searchInput.addEventListener('input', () => {
if (searchInput.value.trim()) clearBtn.classList.add('visible');
else clearBtn.classList.remove('visible');
if (queryResult.classList.contains('visible')) queryResult.classList.remove('visible');
if (statusMessage.classList.contains('visible')) hideStatusMessage();
});
clearBtn.addEventListener('click', () => {
searchInput.value = '';
clearBtn.classList.remove('visible');
queryResult.classList.remove('visible');
hideStatusMessage();
searchInput.focus();
});
// 初始化
const savedLang = localStorage.getItem('ipquery-lang');
if (savedLang && (savedLang === 'zh' || savedLang === 'en')) currentLang = savedLang;
applyLanguage();
loadMyIp();
})();
重新生成之后,替换原来的文件,就能直接运行了。就有想法把这个插件上传到谷歌插件市场的想法。谷歌上搜索了下这么注册以及上传插件,不是很难,就把这个插件上传到了谷歌插件市场,后面专门写个文章怎么注册。

