使用AI开发属于自己的多开浏览器——奇异博士浏览器实战

指纹浏览器管理多开AI开发指南-奇异博士

前言

市面上的指纹浏览器(多开浏览器)价格昂贵,功能臃肿。以某主流浏览器为例:

  • 启动速度慢,体验笨拙
  • 免费版仅支持5个浏览器实例
  • 配置项繁杂,99%都是多余的
  • 企业版高达617美刀/月(约4000+人民币)
  • 专业版也要9美刀/月,且数量有限

实际上,对于大多数多账号管理场景,真正需要的核心配置只有:

  • WebRTC
  • 时区
  • 地理位置
  • 语言

其他如噪音、厂商、CPU等配置基本用不上。

既然是AI时代,为什么不自己动手打造一个轻量、快速、免费的多开浏览器呢?

技术选型与架构

遇到的技术难题

  1. 浏览器指纹隔离:每个浏览器实例需要独立的指纹环境
  2. 数据持久化:关闭后再次打开需要保留登录状态和历史记录
  3. 多实例管理:支持创建无限个浏览器实例并分组管理
  4. 跨平台打包:需要打包成桌面应用

技术方案

基于 Electron + Node.js 架构:

奇异博士浏览器
├── main/                 # 主进程
│   ├── index.js         # 入口文件
│   ├── browser-manager.js   # 浏览器实例管理
│   └── profile-manager.js   # 配置文件管理
├── renderer/            # 渲染进程(UI界面)
│   ├── index.html
│   ├── styles.css
│   └── app.js
├── profiles/            # 浏览器配置存储目录
└── package.json

核心实现代码

1. 浏览器实例管理器

// browser-manager.js
const { BrowserWindow, session } = require('electron');
const path = require('path');
const fs = require('fs');

class BrowserManager {
  constructor() {
    this.browsers = new Map();
    this.profilesDir = path.join(__dirname, '../profiles');
    this.ensureProfilesDir();
  }

  ensureProfilesDir() {
    if (!fs.existsSync(this.profilesDir)) {
      fs.mkdirSync(this.profilesDir, { recursive: true });
    }
  }

  // 创建新的浏览器实例
  async createBrowser(config) {
    const { id, name, country, proxy, group } = config;
    const profilePath = path.join(this.profilesDir, id);
    
    // 创建独立的session分区,实现数据隔离
    const partition = `persist:browser_${id}`;
    const ses = session.fromPartition(partition);
    
    // 设置代理
    if (proxy) {
      await ses.setProxy({ proxyRules: proxy });
    }

    // 根据国家设置语言
    const language = this.getLanguageByCountry(country);
    
    const win = new BrowserWindow({
      width: 1280,
      height: 800,
      title: name,
      webPreferences: {
        partition: partition,
        nodeIntegration: false,
        contextIsolation: true,
        // 注入指纹配置
        preload: path.join(__dirname, 'preload.js')
      }
    });

    // 设置语言
    win.webContents.session.setSpellCheckerLanguages([language]);
    
    // 保存配置
    this.saveProfile(id, { name, country, proxy, group, language });
    
    this.browsers.set(id, win);
    
    win.on('closed', () => {
      this.browsers.delete(id);
    });

    return win;
  }

  // 启动已有浏览器
  async launchBrowser(id) {
    const profile = this.loadProfile(id);
    if (!profile) {
      throw new Error('Profile not found');
    }
    
    const partition = `persist:browser_${id}`;
    const ses = session.fromPartition(partition);
    
    if (profile.proxy) {
      await ses.setProxy({ proxyRules: profile.proxy });
    }

    const win = new BrowserWindow({
      width: 1280,
      height: 800,
      title: profile.name,
      webPreferences: {
        partition: partition,
        nodeIntegration: false,
        contextIsolation: true,
        preload: path.join(__dirname, 'preload.js')
      }
    });

    this.browsers.set(id, win);
    
    win.on('closed', () => {
      this.browsers.delete(id);
    });

    // 加载上次的页面或默认页面
    win.loadURL('https://www.google.com');
    
    return win;
  }

  getLanguageByCountry(country) {
    const languageMap = {
      'CN': 'zh-CN',
      'US': 'en-US',
      'ES': 'es-ES',
      'JP': 'ja-JP',
      'KR': 'ko-KR',
      'FR': 'fr-FR',
      'DE': 'de-DE',
      'RU': 'ru-RU'
    };
    return languageMap[country] || 'en-US';
  }

  saveProfile(id, profile) {
    const profilePath = path.join(this.profilesDir, `${id}.json`);
    fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
  }

  loadProfile(id) {
    const profilePath = path.join(this.profilesDir, `${id}.json`);
    if (fs.existsSync(profilePath)) {
      return JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
    }
    return null;
  }

  getAllProfiles() {
    const profiles = [];
    const files = fs.readdirSync(this.profilesDir);
    for (const file of files) {
      if (file.endsWith('.json')) {
        const id = file.replace('.json', '');
        const profile = this.loadProfile(id);
        if (profile) {
          profiles.push({ id, ...profile });
        }
      }
    }
    return profiles;
  }

  deleteProfile(id) {
    const profilePath = path.join(this.profilesDir, `${id}.json`);
    if (fs.existsSync(profilePath)) {
      fs.unlinkSync(profilePath);
    }
    // 删除session数据
    const partition = `persist:browser_${id}`;
    session.fromPartition(partition).clearStorageData();
  }
}

module.exports = new BrowserManager();

2. 指纹注入脚本

// preload.js
const { contextBridge } = require('electron');

// 注入时区和地理位置
contextBridge.exposeInMainWorld('fingerprint', {
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
  language: navigator.language
});

// 覆盖WebRTC,防止真实IP泄露
const originalRTCPeerConnection = window.RTCPeerConnection;
window.RTCPeerConnection = function(...args) {
  const pc = new originalRTCPeerConnection(...args);
  // 可以在这里添加更多的WebRTC控制逻辑
  return pc;
};

3. 主进程入口

// main/index.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const browserManager = require('./browser-manager');
const { v4: uuidv4 } = require('uuid');

let mainWindow;

function createMainWindow() {
  mainWindow = new BrowserWindow({
    width: 1000,
    height: 700,
    title: 'Dr.Strange Browser',
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  });

  mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
}

app.whenReady().then(createMainWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// IPC通信:获取所有浏览器配置
ipcMain.handle('get-profiles', () => {
  return browserManager.getAllProfiles();
});

// IPC通信:创建新浏览器
ipcMain.handle('create-browser', async (event, config) => {
  const id = uuidv4();
  await browserManager.createBrowser({ id, ...config });
  return { id, ...config };
});

// IPC通信:启动浏览器
ipcMain.handle('launch-browser', async (event, id) => {
  await browserManager.launchBrowser(id);
  return true;
});

// IPC通信:删除浏览器
ipcMain.handle('delete-browser', (event, id) => {
  browserManager.deleteProfile(id);
  return true;
});

4. 前端界面

<!-- renderer/index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Dr.Strange Browser</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="container">
    <header>
      <h1>Dr.Strange Browser</h1>
      <button id="btn-new" class="btn-primary">+ New Environment</button>
    </header>
    
    <div class="filter-bar">
      <select id="group-filter">
        <option value="">All Groups</option>
      </select>
      <input type="text" id="search" placeholder="Search...">
    </div>
    
    <div id="browser-list" class="browser-list"></div>
  </div>

  <!-- 新建弹窗 -->
  <div id="modal" class="modal hidden">
    <div class="modal-content">
      <h2>Create New Environment</h2>
      <form id="create-form">
        <div class="form-group">
          <label>Name</label>
          <input type="text" id="input-name" required>
        </div>
        <div class="form-group">
          <label>Country</label>
          <select id="input-country">
            <option value="US">United States</option>
            <option value="CN">China</option>
            <option value="ES">Spain</option>
            <option value="JP">Japan</option>
            <option value="KR">Korea</option>
            <option value="FR">France</option>
            <option value="DE">Germany</option>
          </select>
        </div>
        <div class="form-group">
          <label>Proxy (optional)</label>
          <input type="text" id="input-proxy" placeholder="http://ip:port">
        </div>
        <div class="form-group">
          <label>Group</label>
          <input type="text" id="input-group" placeholder="e.g. Work, Personal">
        </div>
        <div class="form-actions">
          <button type="button" id="btn-cancel">Cancel</button>
          <button type="submit" class="btn-primary">Create</button>
        </div>
      </form>
    </div>
  </div>

  <script src="app.js"></script>
</body>
</html>
/* renderer/styles.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
  min-height: 100vh;
  color: #fff;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 30px;
}

header h1 {
  font-size: 28px;
  background: linear-gradient(90deg, #00d9ff, #00ff88);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.btn-primary {
  background: linear-gradient(90deg, #00d9ff, #00ff88);
  border: none;
  padding: 12px 24px;
  border-radius: 8px;
  color: #1a1a2e;
  font-weight: bold;
  cursor: pointer;
  transition: transform 0.2s;
}

.btn-primary:hover {
  transform: scale(1.05);
}

.filter-bar {
  display: flex;
  gap: 15px;
  margin-bottom: 20px;
}

.filter-bar select,
.filter-bar input {
  padding: 10px 15px;
  border-radius: 8px;
  border: 1px solid #333;
  background: #0f0f23;
  color: #fff;
}

.browser-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 20px;
}

.browser-card {
  background: rgba(255, 255, 255, 0.05);
  border-radius: 12px;
  padding: 20px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  transition: transform 0.2s, box-shadow 0.2s;
}

.browser-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 10px 30px rgba(0, 217, 255, 0.2);
}

.browser-card h3 {
  margin-bottom: 10px;
}

.browser-card .meta {
  color: #888;
  font-size: 14px;
  margin-bottom: 15px;
}

.browser-card .actions {
  display: flex;
  gap: 10px;
}

.browser-card .btn-launch {
  flex: 1;
  padding: 10px;
  border: none;
  border-radius: 6px;
  background: #00d9ff;
  color: #1a1a2e;
  font-weight: bold;
  cursor: pointer;
}

.browser-card .btn-delete {
  padding: 10px 15px;
  border: 1px solid #ff4757;
  border-radius: 6px;
  background: transparent;
  color: #ff4757;
  cursor: pointer;
}

.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal.hidden {
  display: none;
}

.modal-content {
  background: #1a1a2e;
  padding: 30px;
  border-radius: 16px;
  width: 400px;
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.form-group {
  margin-bottom: 20px;
}

.form-group label {
  display: block;
  margin-bottom: 8px;
  color: #888;
}

.form-group input,
.form-group select {
  width: 100%;
  padding: 12px;
  border-radius: 8px;
  border: 1px solid #333;
  background: #0f0f23;
  color: #fff;
}

.form-actions {
  display: flex;
  gap: 15px;
  justify-content: flex-end;
}

.form-actions button {
  padding: 12px 24px;
  border-radius: 8px;
  cursor: pointer;
}

#btn-cancel {
  background: transparent;
  border: 1px solid #666;
  color: #fff;
}
// renderer/app.js
const { ipcRenderer } = require('electron');

const browserList = document.getElementById('browser-list');
const modal = document.getElementById('modal');
const btnNew = document.getElementById('btn-new');
const btnCancel = document.getElementById('btn-cancel');
const createForm = document.getElementById('create-form');
const groupFilter = document.getElementById('group-filter');

let profiles = [];

async function loadProfiles() {
  profiles = await ipcRenderer.invoke('get-profiles');
  renderProfiles();
  updateGroupFilter();
}

function renderProfiles(filter = '') {
  const filtered = profiles.filter(p => {
    if (filter && p.group !== filter) return false;
    return true;
  });

  browserList.innerHTML = filtered.map(profile => `
    <div class="browser-card" data-id="${profile.id}">
      <h3>${profile.name}</h3>
      <div class="meta">
        <div>Country: ${profile.country}</div>
        <div>Group: ${profile.group || 'None'}</div>
        ${profile.proxy ? `<div>Proxy: ${profile.proxy}</div>` : ''}
      </div>
      <div class="actions">
        <button class="btn-launch" onclick="launchBrowser('${profile.id}')">Launch</button>
        <button class="btn-delete" onclick="deleteBrowser('${profile.id}')">Delete</button>
      </div>
    </div>
  `).join('');
}

function updateGroupFilter() {
  const groups = [...new Set(profiles.map(p => p.group).filter(Boolean))];
  groupFilter.innerHTML = `
    <option value="">All Groups</option>
    ${groups.map(g => `<option value="${g}">${g}</option>`).join('')}
  `;
}

async function launchBrowser(id) {
  await ipcRenderer.invoke('launch-browser', id);
}

async function deleteBrowser(id) {
  if (confirm('Are you sure you want to delete this browser?')) {
    await ipcRenderer.invoke('delete-browser', id);
    loadProfiles();
  }
}

btnNew.addEventListener('click', () => {
  modal.classList.remove('hidden');
});

btnCancel.addEventListener('click', () => {
  modal.classList.add('hidden');
});

createForm.addEventListener('submit', async (e) => {
  e.preventDefault();
  
  const config = {
    name: document.getElementById('input-name').value,
    country: document.getElementById('input-country').value,
    proxy: document.getElementById('input-proxy').value || null,
    group: document.getElementById('input-group').value || null
  };

  await ipcRenderer.invoke('create-browser', config);
  modal.classList.add('hidden');
  createForm.reset();
  loadProfiles();
});

groupFilter.addEventListener('change', (e) => {
  renderProfiles(e.target.value);
});

// 初始化
loadProfiles();

5. package.json 配置

{
  "name": "dr-strange-browser",
  "version": "1.0.0",
  "description": "A lightweight multi-browser manager",
  "main": "main/index.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder"
  },
  "dependencies": {
    "uuid": "^9.0.0"
  },
  "devDependencies": {
    "electron": "^28.0.0",
    "electron-builder": "^24.0.0"
  },
  "build": {
    "appId": "com.drstrange.browser",
    "productName": "Dr.Strange Browser",
    "directories": {
      "output": "dist"
    },
    "win": {
      "target": "nsis",
      "icon": "assets/icon.ico"
    },
    "mac": {
      "target": "dmg",
      "icon": "assets/icon.icns"
    }
  }
}

运行与打包

开发运行

# 安装依赖
npm install

# 启动开发模式
npm start

打包发布

# 打包为安装程序
npm run build

打包后:

  • 源代码约 900MB
  • 安装包约 200MB

实现效果

  1. 启动速度:不到1秒即可启动浏览器实例
  2. 无限实例:可创建无限个浏览器环境
  3. 数据隔离:每个实例独立的Cookie、缓存、历史记录
  4. 状态保持:关闭后再次打开,登录状态和历史记录完整保留
  5. 分组管理:支持按分组快速筛选
  6. 语言自动配置:选择国家后自动设置对应语言

避坑指南

  1. Electron版本选择:建议使用LTS版本,避免兼容性问题
  2. Session分区:使用 persist: 前缀确保数据持久化
  3. WebRTC泄露:需要在preload脚本中处理,否则会暴露真实IP
  4. 打包体积:可以使用 electron-builderasar 压缩减小体积
  5. 跨平台兼容:路径处理使用 path.join(),避免硬编码分隔符

总结和下载

在AI时代,只要有想法,就能把它变成现实。与其每月花费数百甚至数千元使用商业指纹浏览器,不如花几天时间用AI辅助开发一个属于自己的多开浏览器。

核心功能其实并不复杂:

  • Electron提供浏览器内核
  • Session分区实现数据隔离
  • 简单的配置管理实现多实例

这个项目的完整代码已开源,可以访问博客 xoxome.online 在免费原创工具箱中找到"奇异博士"进行下载使用。

免费下载使用

本文基于实际开发经验整理,如有问题欢迎评论区交流。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐