记一次 Vibe Coding 谷歌插件的体验

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

整个流程现在都跑通了,插件也上传到了 Chrome 到应用市场上去,可以去体验下

https://chromewebstore.google.com/detail/ip-lookup-tool/nhjmdjidakohnoblneffeaoohnebebld?hl=zh-CN&utm_source=ext_sidebar

ip检查插件

虽然比较简陋,但还是能使用的。

详细的思路是这样的,我在原来的网页生成对话里,直接问“根据这个能开发一个浏览器插件嘛,我要上传到谷歌插件市场,要怎么操作”

根据这个进行了一段分析之后,生成了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();
})();

重新生成之后,替换原来的文件,就能直接运行了。就有想法把这个插件上传到谷歌插件市场的想法。谷歌上搜索了下这么注册以及上传插件,不是很难,就把这个插件上传到了谷歌插件市场,后面专门写个文章怎么注册。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注


滚动至顶部