Add files via upload

This commit is contained in:
MacXK
2025-12-26 12:59:26 +08:00
committed by GitHub
parent 1dbd783679
commit 1c1d7a1f85
26 changed files with 19761 additions and 0 deletions
+423
View File
@@ -0,0 +1,423 @@
# Windsurf Tool 账号注册流程文档
## 1. 项目概述
本项目是一个 **Windsurf 账号批量管理工具**,使用以下技术栈:
| 层级 | 技术 | 用途 |
|------|------|------|
| 前端框架 | Vue 3 + Vite | 用户界面 |
| UI 组件库 | Element Plus | UI 组件 |
| 状态管理 | Pinia | 账号数据管理 |
| 桌面框架 | Electron 27 | 桌面应用容器 |
| 浏览器自动化 | puppeteer-real-browser | 绕过 Cloudflare 检测 |
| 邮件接收 | imap + mailparser | IMAP 协议获取验证码 |
---
## 2. 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ Vue 3 前端 (渲染进程) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │
│ │ RegisterView.vue│ │ AccountsView.vue│ │SettingsView │ │
│ │ (批量注册UI) │ │ (账号管理) │ │ (IMAP配置) │ │
│ └────────┬────────┘ └─────────────────┘ └──────────────┘ │
│ │ │
│ │ ipcRenderer.invoke('batch-register') │
└───────────┼─────────────────────────────────────────────────┘
│ IPC 通信
┌─────────────────────────────────────────────────────────────┐
│ Electron 主进程 (main.js) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ipcMain.handle('batch-register') │ │
│ └─────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ RegistrationBot (核心注册机器人) │ │
│ │ • puppeteer-real-browser (浏览器自动化) │ │
│ │ • 表单自动填写 │ │
│ │ • Cloudflare Turnstile 验证处理 │ │
│ └─────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ EmailReceiver (邮件验证码接收) │ │
│ │ • IMAP 协议连接邮箱 │ │
│ │ • 自动解析验证码 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 3. 核心文件结构
```
windsurf-tool-vue/
├── src/
│ └── views/
│ ├── RegisterView.vue # 注册页面 UI
│ └── SettingsView.vue # IMAP 配置页面
├── electron/
│ ├── main.js # Electron 主进程 & IPC 处理
│ └── services/
│ ├── registrationBot.js # 🔑 核心注册机器人
│ └── emailReceiver.js # 📧 邮箱验证码接收器
```
---
## 4. 详细注册流程
### 4.1 用户操作流程
```
[用户] 在设置页配置邮箱域名和 IMAP
[用户] 进入批量注册页,设置注册数量 (1-10)
[用户] 选择是否启用无头模式
[用户] 点击「开始批量注册」按钮
```
### 4.2 系统内部流程 (5 个步骤)
#### **步骤 1: 填写基本信息**
```javascript
// 文件: electron/services/registrationBot.js
// 1. 生成临时邮箱 (格式: 编号 + 8位随机字符 + @配置域名)
const email = await this.generateTempEmail();
// 示例: 1abc12def@yourdomain.com
// 2. 生成随机英文姓名
const { firstName, lastName } = this.generateRandomName();
// 3. 访问注册页面
await page.goto('https://windsurf.com/account/register');
// 4. 填写 First Name, Last Name, Email
await firstNameInput.type(firstName, { delay: 100 });
await lastNameInput.type(lastName, { delay: 100 });
await emailInput.type(email, { delay: 100 });
// 5. 勾选同意条款复选框
await checkbox.click();
// 6. 点击 Continue 按钮
await submitBtn.click();
```
#### **步骤 2: 填写密码**
```javascript
// 密码设置为与邮箱相同 (简化管理)
const password = email;
// 等待密码输入页面加载
await page.waitForSelector('input[type="password"]');
// 填写密码和确认密码
await passwordInputs[0].type(password, { delay: 100 });
await passwordInputs[1].type(password, { delay: 100 });
// 点击 Continue
await submitBtn.click();
```
#### **步骤 3: Cloudflare Turnstile 验证**
```javascript
// puppeteer-real-browser 会自动处理 Cloudflare 验证
// 使用以下配置绕过检测:
const response = await connect({
headless: false, // 有头模式更易通过
fingerprint: true, // 启用浏览器指纹
turnstile: true, // 自动处理 Turnstile
tf: true,
args: [
'--disable-blink-features=AutomationControlled',
'--disable-features=IsolateOrigins,site-per-process'
]
});
// 等待验证完成 (约 10 秒)
await this.sleep(10000);
```
#### **步骤 4: 邮箱验证码**
```javascript
// 文件: electron/services/emailReceiver.js
// 1. 创建 IMAP 连接
const imap = new Imap({
user: this.config.user,
password: this.config.password,
host: this.config.host, // 如: imap.qq.com
port: this.config.port || 993,
tls: true
});
// 2. 搜索未读邮件
imap.search(['UNSEEN'], callback);
// 3. 识别 Windsurf 验证邮件 (关键词过滤)
const isWindsurfEmail =
subject.includes('windsurf') ||
subject.includes('verify') ||
from.includes('codeium') ||
from.includes('exafunction');
// 4. 正则提取验证码
const patterns = [
/\b(\d{6})\b/, // 6位数字
/\b([A-Z0-9]{6})\b/, // 6位字母数字
/code[:]\s*(\w+)/i // code: xxx 格式
];
// 5. 重试机制: 最多 3 次,每次间隔 30 秒
const MAX_RETRIES = 3;
const RETRY_DELAY = 30000;
```
#### **步骤 5: 完成注册**
```javascript
// 输入 6 位验证码
for (let i = 0; i < 6; i++) {
await codeInputs[i].type(verificationCode[i], { delay: 100 });
}
// 点击 Create Account
await createBtn.click();
// 检查注册结果
const currentUrl = page.url();
const isSuccess = !currentUrl.includes('/login');
// 保存账号到本地 JSON 文件
if (isSuccess) {
const account = { email, password, firstName, lastName, createdAt };
accounts.push(account);
await fs.writeFile(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2));
}
```
---
## 5. 批量注册并发控制
```javascript
// 文件: electron/services/registrationBot.js - batchRegister()
const MAX_CONCURRENT = 4; // 最大同时 4 个浏览器窗口
// 分批执行
for (let i = 0; i < count; i += MAX_CONCURRENT) {
const batchSize = Math.min(MAX_CONCURRENT, count - i);
// 每个窗口延迟 3 秒启动 (避免验证码混淆)
const startDelay = j * 3000;
// 等待当前批次完成
await Promise.all(batchTasks);
// 批次间等待 10 秒
await this.sleep(10000);
}
```
---
## 6. IPC 通信流程
### 6.1 前端发起注册
```javascript
// 文件: src/views/RegisterView.vue
const startRegister = async () => {
const params = {
count: Number(registerCount.value),
headless: Boolean(headlessMode.value)
};
// 调用主进程
const result = await ipcRenderer.invoke('batch-register', params);
};
```
### 6.2 主进程处理
```javascript
// 文件: electron/main.js
ipcMain.handle('batch-register', async (event, params) => {
const { count, headless } = params;
// 读取配置
const config = await fs.readFile(CONFIG_FILE());
// 创建注册机器人
const bot = new RegistrationBot({
emailDomains: config.domains,
emailConfig: config.imap,
headless: headless
});
// 执行批量注册
const results = await bot.batchRegister(count, progressCallback, logCallback);
return { success: true, results };
});
```
### 6.3 实时日志推送
```javascript
// 主进程 → 渲染进程
mainWindow.webContents.send('register-log', message);
mainWindow.webContents.send('register-progress', { current, total });
// 渲染进程监听
ipcRenderer.on('register-log', (event, message) => {
addLog(message, type);
});
ipcRenderer.on('register-progress', (event, data) => {
progress.value = data.percent;
});
```
---
## 7. 关键技术点
### 7.1 绕过 Cloudflare 检测
使用 `puppeteer-real-browser` 而非原生 Puppeteer,它提供:
- 真实浏览器指纹
- 自动处理 Turnstile 验证
- 隐藏 WebDriver 特征
```javascript
const { connect } = require('puppeteer-real-browser');
const response = await connect({
fingerprint: true,
turnstile: true,
tf: true
});
```
### 7.2 邮箱验证码接收
支持主流邮箱服务商:
| 邮箱 | IMAP 服务器 | 端口 |
|------|-------------|------|
| QQ 邮箱 | imap.qq.com | 993 |
| Gmail | imap.gmail.com | 993 |
| 163 邮箱 | imap.163.com | 993 |
| Outlook | outlook.office365.com | 993 |
### 7.3 数据持久化
账号和配置存储在 Electron 的 userData 目录:
```javascript
const ACCOUNTS_FILE = path.join(app.getPath('userData'), 'accounts.json');
const CONFIG_FILE = path.join(app.getPath('userData'), 'config.json');
```
---
## 8. 流程图
```
┌──────────────┐
│ 用户点击注册 │
└──────┬───────┘
┌──────────────┐
│ 检查配置完整性 │ ─── 失败 ──→ 提示配置邮箱
└──────┬───────┘
│ 成功
┌──────────────┐
│ 生成邮箱/密码 │
└──────┬───────┘
┌──────────────┐
│ 启动浏览器 │ ←─ puppeteer-real-browser
└──────┬───────┘
┌──────────────┐
│ 访问注册页面 │ https://windsurf.com/account/register
└──────┬───────┘
┌──────────────┐
│ 步骤1: 填基本信息 │ → First Name / Last Name / Email
└──────┬───────┘
┌──────────────┐
│ 步骤2: 填密码 │ → Password / Confirm
└──────┬───────┘
┌──────────────┐
│ 步骤3: CF验证 │ → Cloudflare Turnstile (自动处理)
└──────┬───────┘
┌──────────────┐
│ 步骤4: 邮箱验证 │ → IMAP 接收 → 正则提取 6 位验证码
└──────┬───────┘
┌──────────────┐
│ 步骤5: 完成注册 │ → 保存到 accounts.json
└──────┬───────┘
┌──────────────┐
│ 更新前端状态 │ → 刷新账号列表
└──────────────┘
```
---
## 9. 错误处理
| 错误场景 | 处理方式 |
|---------|---------|
| 未配置邮箱域名 | 返回错误提示,引导配置 |
| 未配置 IMAP | 返回错误提示,引导配置 |
| IMAP 连接失败 | 显示具体错误,支持重新测试 |
| 验证码获取超时 | 最多重试 3 次,每次间隔 30 秒 |
| Cloudflare 检测 | 建议关闭无头模式 |
| 按钮点击失败 | 多种选择器备选方案 |
---
## 10. 总结
该项目实现了 Windsurf 账号的全自动批量注册,核心技术:
1. **前端**: Vue 3 + Element Plus + Pinia
2. **桌面框架**: Electron (IPC 通信)
3. **浏览器自动化**: puppeteer-real-browser (绕过检测)
4. **邮件处理**: imap + mailparser (IMAP 协议)
注册流程分为 5 个步骤,支持最多 4 个并发窗口,并具备完善的错误处理和重试机制。
+281
View File
@@ -0,0 +1,281 @@
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs').promises
// 服务模块
const RegistrationBot = require('./services/registrationBot')
const WindsurfManager = require('./services/windsurfManager')
const EmailReceiver = require('./services/emailReceiver')
const CardBindingBot = require('./services/cardBindingBot')
let mainWindow
const isDev = process.argv.includes('--dev') || !app.isPackaged
// 数据文件路径
const getDataPath = (filename) => path.join(app.getPath('userData'), filename)
const ACCOUNTS_FILE = () => getDataPath('accounts.json')
const CONFIG_FILE = () => getDataPath('config.json')
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 750,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
if (isDev) {
const devUrl = process.env.VITE_DEV_SERVER_URL || 'http://localhost:5173'
mainWindow.loadURL(devUrl)
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
}
}
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
// ==================== 账号管理 IPC ====================
// 获取所有账号
ipcMain.handle('get-accounts', async () => {
try {
const data = await fs.readFile(ACCOUNTS_FILE(), 'utf-8')
return JSON.parse(data)
} catch (error) {
return []
}
})
// 保存账号
ipcMain.handle('save-accounts', async (event, accounts) => {
try {
await fs.writeFile(ACCOUNTS_FILE(), JSON.stringify(accounts, null, 2))
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
})
// 添加账号
ipcMain.handle('add-account', async (event, account) => {
try {
let accounts = []
try {
const data = await fs.readFile(ACCOUNTS_FILE(), 'utf-8')
accounts = JSON.parse(data)
} catch (e) {}
accounts.push({
id: Date.now().toString(),
...account,
createdAt: account.createdAt || new Date().toISOString()
})
await fs.writeFile(ACCOUNTS_FILE(), JSON.stringify(accounts, null, 2))
return { success: true, accounts }
} catch (error) {
return { success: false, error: error.message }
}
})
// 删除账号
ipcMain.handle('delete-account', async (event, id) => {
try {
const data = await fs.readFile(ACCOUNTS_FILE(), 'utf-8')
let accounts = JSON.parse(data)
accounts = accounts.filter(acc => acc.id !== id)
await fs.writeFile(ACCOUNTS_FILE(), JSON.stringify(accounts, null, 2))
return { success: true, accounts }
} catch (error) {
return { success: false, error: error.message }
}
})
// 更新账号
ipcMain.handle('update-account', async (event, { id, updates }) => {
try {
const data = await fs.readFile(ACCOUNTS_FILE(), 'utf-8')
let accounts = JSON.parse(data)
const index = accounts.findIndex(acc => acc.id === id)
if (index !== -1) {
accounts[index] = { ...accounts[index], ...updates }
await fs.writeFile(ACCOUNTS_FILE(), JSON.stringify(accounts, null, 2))
return { success: true, accounts }
}
return { success: false, error: '账号不存在' }
} catch (error) {
return { success: false, error: error.message }
}
})
// ==================== 配置管理 IPC ====================
// 获取配置
ipcMain.handle('get-config', async () => {
try {
const data = await fs.readFile(CONFIG_FILE(), 'utf-8')
return JSON.parse(data)
} catch (error) {
return { domains: [], imap: { host: '', port: 993, user: '', password: '' } }
}
})
// 保存配置
ipcMain.handle('save-config', async (event, config) => {
try {
await fs.writeFile(CONFIG_FILE(), JSON.stringify(config, null, 2))
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
})
// 测试 IMAP 连接
ipcMain.handle('test-imap', async (event, imapConfig) => {
try {
const receiver = new EmailReceiver(imapConfig)
const result = await receiver.testConnection()
return result
} catch (error) {
return { success: false, message: error.message }
}
})
// ==================== 注册功能 IPC ====================
// 批量注册
ipcMain.handle('batch-register', async (event, params) => {
console.log('===== 收到批量注册请求 =====')
console.log('原始参数:', params)
const { count, headless = false } = params || {}
console.log('解析后 - count:', count, 'headless:', headless)
try {
// 获取配置
let config = {}
try {
const data = await fs.readFile(CONFIG_FILE(), 'utf-8')
config = JSON.parse(data)
} catch (e) {}
if (!config.domains || config.domains.length === 0) {
return { success: false, error: '请先配置邮箱域名' }
}
if (!config.imap || !config.imap.host) {
return { success: false, error: '请先配置 IMAP 邮箱' }
}
console.log('批量注册参数 - headless:', headless)
const bot = new RegistrationBot({
emailDomains: config.domains,
emailConfig: config.imap,
headless: headless
})
// 发送日志到前端
const logCallback = (message) => {
mainWindow.webContents.send('register-log', message)
}
const progressCallback = ({ current, total }) => {
mainWindow.webContents.send('register-progress', { current, total })
}
const results = await bot.batchRegister(count, progressCallback, logCallback)
return { success: true, results }
} catch (error) {
return { success: false, error: error.message }
}
})
// ==================== 切换账号 IPC ====================
// 切换账号
ipcMain.handle('switch-account', async (event, account) => {
try {
const logCallback = (message) => {
mainWindow.webContents.send('switch-log', message)
}
const manager = new WindsurfManager(logCallback)
// 1. 完整重置
logCallback('开始重置 Windsurf 配置...')
const resetResult = await manager.fullReset()
if (!resetResult.success) {
return { success: false, error: resetResult.error }
}
// 2. 启动 Windsurf 并自动登录
logCallback('启动 Windsurf 并自动登录...')
const loginResult = await manager.autoLogin(account.email, account.password)
return loginResult
} catch (error) {
return { success: false, error: error.message }
}
})
// 仅重置(不登录)
ipcMain.handle('reset-windsurf', async () => {
try {
const logCallback = (message) => {
mainWindow.webContents.send('switch-log', message)
}
const manager = new WindsurfManager(logCallback)
const result = await manager.fullReset()
return result
} catch (error) {
return { success: false, error: error.message }
}
})
// ==================== 绑卡 IPC ====================
let cardBindingBot = null
// 启动绑卡流程 - 登录账号
ipcMain.handle('bind-card-login', async (event, { account, cardInfo }) => {
try {
const logCallback = (message) => {
mainWindow.webContents.send('bind-card-log', message)
}
cardBindingBot = new CardBindingBot()
const result = await cardBindingBot.loginAndBind(account, cardInfo, logCallback)
return result
} catch (error) {
return { success: false, error: error.message }
}
})
// 关闭绑卡浏览器
ipcMain.handle('bind-card-close', async () => {
try {
if (cardBindingBot) {
await cardBindingBot.close()
cardBindingBot = null
}
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
})
File diff suppressed because it is too large Load Diff
+530
View File
@@ -0,0 +1,530 @@
/**
* 银行卡绑定机器人
* 使用 puppeteer-real-browser 自动登录并绑定银行卡
* 参考 AiGo绑卡 扩展的实现方式
*/
const { connect } = require('puppeteer-real-browser');
const CardGenerator = require('./cardGenerator');
class CardBindingBot {
constructor() {
this.browser = null;
this.page = null;
this.logCallback = null;
this.cardInfo = null;
}
log(message) {
console.log(message);
if (this.logCallback) {
this.logCallback(message);
}
}
async loginAndBind(account, cardInfo, logCallback) {
this.logCallback = logCallback;
try {
this.log('🚀 启动浏览器...');
const response = await connect({
headless: false,
fingerprint: true,
turnstile: false, // 禁用自动点击 Cloudflare Turnstile(会误点 Stripe 复选框)
tf: false, // 禁用自动处理验证
args: ['--disable-blink-features=AutomationControlled']
});
this.browser = response.browser;
this.page = response.page;
await this.page.setViewport({ width: 1280, height: 800 });
this.log('✓ 浏览器已启动');
// 访问登录页面
this.log('🌐 访问登录页面...');
await this.page.goto('https://windsurf.com/account/login', {
waitUntil: 'networkidle2',
timeout: 30000
});
await this.sleep(2000);
// 填写邮箱
this.log(`📧 填写邮箱: ${account.email}`);
await this.fillInput('input[type="email"], input[name="email"]', account.email);
await this.sleep(500);
// 填写密码
this.log('🔐 填写密码...');
await this.fillInput('input[type="password"]', account.password);
await this.sleep(500);
// 点击登录按钮
this.log('🔘 点击登录按钮...');
await this.page.evaluate(() => {
const btn = document.querySelector('button[type="submit"]') ||
Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Log in'));
if (btn) btn.click();
});
this.log('⏳ 等待登录完成(最长120秒)...');
try {
await this.page.waitForFunction(() => !window.location.href.includes('/login'), { timeout: 120000 });
this.log('✅ 登录成功!');
} catch (e) {
if (this.page.url().includes('/login')) {
return { success: false, message: '登录超时' };
}
}
await this.sleep(2000);
this.log('📍 登录后页面: ' + this.page.url());
// 生成卡信息
this.generateCardInfo(cardInfo);
// 处理 Cookie 弹窗
await this.acceptCookies();
// 导航到绑卡页面
await this.navigateToBilling();
// 填充支付表单
await this.fillPaymentForm();
// 自动点击提交按钮
await this.clickSubmitButton();
// 注意:这里只是表示流程完成,并不代表绑卡成功
// 返回 submitted 而不是 success,让前端知道需要手动确认
return { success: false, submitted: true, message: '表单已提交,请手动确认绑卡结果' };
} catch (error) {
this.log(`❌ 操作失败: ${error.message}`);
return { success: false, error: error.message };
}
}
generateCardInfo(cardInfo) {
if (cardInfo && cardInfo.mode === 'bin' && cardInfo.bin) {
this.log(`💳 使用 BIN ${cardInfo.bin} 生成卡信息...`);
this.cardInfo = CardGenerator.generateFullCardInfo(cardInfo.bin);
} else if (cardInfo && cardInfo.mode === 'full' && cardInfo.cardNumber) {
this.log('💳 使用完整卡号...');
this.cardInfo = {
cardNumber: cardInfo.cardNumber.replace(/\s/g, ''),
expMonth: cardInfo.expMonth,
expYear: cardInfo.expYear,
cvv: cardInfo.cvv,
name: CardGenerator.generateChineseName(),
address: CardGenerator.generateChinaAddress()
};
} else {
this.log('💳 使用默认 BIN 生成卡信息...');
this.cardInfo = CardGenerator.generateFullCardInfo('424242');
}
// 使用中国地址和中文名
const addr = this.cardInfo.address;
this.log(`📝 生成地址: ${addr.province || addr.region || 'N/A'} - ${addr.city || addr.district || 'N/A'}`);
this.log(`📝 持卡人: ${this.cardInfo.name}`);
this.log(`📝 邮编: ${addr.zipCode || 'N/A'}`);
}
async acceptCookies() {
try {
await this.page.evaluate(() => {
const btn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Accept all'));
if (btn) btn.click();
});
this.log('✓ 已接受 Cookie');
} catch (e) {}
}
async navigateToBilling() {
this.log('🌐 导航到账户页面...');
await this.page.goto('https://windsurf.com/account', { waitUntil: 'networkidle2', timeout: 30000 });
await this.sleep(2000);
if (this.page.url().includes('/login')) {
this.log('⚠️ 需要重新登录...');
await this.page.waitForFunction(() => !window.location.href.includes('/login'), { timeout: 60000 });
}
// 点击 Upgrade
this.log('🔘 查找 Upgrade 按钮...');
await this.page.evaluate(() => {
const el = Array.from(document.querySelectorAll('button, a')).find(e =>
e.textContent.includes('Upgrade') || e.textContent.includes('升级')
);
if (el) el.click();
});
await this.sleep(3000);
// 直接导航到绑卡页面
if (!this.page.url().includes('stripe.com') && !this.page.url().includes('billing')) {
this.log('🌐 导航到绑卡页面...');
await this.page.goto('https://windsurf.com/billing/individual?plan=2', { waitUntil: 'networkidle2', timeout: 30000 });
await this.sleep(3000);
}
this.log('📍 当前页面: ' + this.page.url());
}
async fillPaymentForm() {
this.log('💳 开始填写支付信息...');
const card = this.cardInfo;
this.log(`📝 卡号: ${card.cardNumber}`);
this.log(`📝 有效期: ${card.expMonth}/${card.expYear}`);
this.log(`📝 持卡人: ${card.name}`);
// 等待页面加载
await this.sleep(3000);
// // 先取消"保存信息"复选框,避免出现电话号码输入框
// this.log('📋 先取消保存信息复选框...');
// await this.uncheckSaveInfo();
// await this.sleep(1000);
// 选择银行卡支付方式
await this.selectCardPayment();
await this.sleep(1000);
// 点击手动输入地址
await this.clickManualAddress();
await this.sleep(1000);
// 逐个填充字段
await this.fillStripeFields();
this.log('✅ 支付信息填写完成!');
}
/**
* 自动点击提交按钮
*/
async clickSubmitButton() {
this.log('🔘 点击提交按钮...');
const clicked = await this.page.evaluate(() => {
// 查找提交按钮(多种可能的选择器)
const selectors = [
'button[type="submit"]',
'button:has-text("开始试用")',
'button:has-text("Start trial")',
'button:has-text("提交")',
'button:has-text("Submit")',
'button:has-text("Pay")',
'button:has-text("支付")'
];
// 尝试多种方式查找按钮
let submitBtn = null;
// 方法1: 通过按钮文本查找
const buttons = document.querySelectorAll('button');
for (const btn of buttons) {
const text = btn.textContent.trim();
if (text.includes('开始试用') ||
text.includes('Start trial') ||
text.includes('提交') ||
text.includes('Subscribe')) {
submitBtn = btn;
break;
}
}
if (submitBtn) {
submitBtn.click();
return { success: true, text: submitBtn.textContent.trim() };
}
return { success: false, message: '未找到提交按钮' };
});
if (clicked.success) {
this.log(`✅ 已点击: ${clicked.text}`);
} else {
this.log('⚠️ 未找到提交按钮,请手动点击');
}
await this.sleep(2000);
}
async selectCardPayment() {
this.log('🔍 选择银行卡支付...');
const clicked = await this.page.evaluate(() => {
const selectors = [
'button[data-testid=card-accordion-item-button]',
'button[data-testid="payment-method-card"]',
'.payment-method-card'
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) { el.click(); return sel; }
}
return null;
});
if (clicked) this.log(`✓ 已选择: ${clicked}`);
}
async clickManualAddress() {
const clicked = await this.page.evaluate(() => {
const btn = document.querySelector('.AddressAutocomplete-manual-entry .Button') ||
Array.from(document.querySelectorAll('button')).find(b =>
b.textContent.includes('Enter address manually') || b.textContent.includes('手动输入')
);
if (btn) { btn.click(); return true; }
return false;
});
if (clicked) this.log('✓ 已点击手动输入地址');
}
/**
* 取消保存信息复选框 - 只处理 enableStripePass
*/
async uncheckSaveInfo() {
try {
// 只处理特定的复选框,不要处理其他的
const unchecked = await this.page.evaluate(() => {
const checkbox = document.querySelector('input[name="enableStripePass"]') ||
document.querySelector('input#enableStripePass');
if (checkbox && checkbox.checked) {
checkbox.click();
return true;
}
return false;
});
if (unchecked) {
this.log('✓ 已取消保存信息复选框');
}
} catch (e) {
// 忽略错误
}
}
/**
* 使用 Puppeteer 原生方法填写 Stripe 表单(模拟真实键盘输入)
*/
async fillStripeFields() {
const card = this.cardInfo;
this.log('💳 开始填写 Stripe 表单...');
this.log(`📝 卡号: ${card.cardNumber}`);
this.log(`📝 有效期: ${card.expMonth}/${card.expYear}`);
this.log(`📝 持卡人: ${card.name}`);
// 1. 填写卡号 - 使用快速输入
await this.typeInStripeField('input[name="cardNumber"]', card.cardNumber);
// 2. 填写有效期
await this.typeInStripeField('input[name="cardExpiry"]', card.expMonth + card.expYear);
// 3. 填写 CVV
await this.typeInStripeField('input[name="cardCvc"]', card.cvv);
// 4. 填写持卡人姓名
await this.typeInStripeField('input[name="billingName"]', card.name);
// 5. 选择国家 - 中国
await this.selectStripeOption('select[name="billingCountry"]', 'CN');
await this.sleep(1000); // 等待地址字段更新
// 6. 填写邮编
await this.typeInStripeField('input[name="billingPostalCode"]', card.address.zipCode);
// 7. 选择省份
await this.selectStripeOption('select[name="billingAdministrativeArea"]', card.address.province);
await this.sleep(500);
// 8. 填写城市
await this.typeInStripeField('input[name="billingLocality"]', card.address.city);
// 9. 填写地区
const districtField = await this.page.$('input[name="billingDependentLocality"]');
if (districtField) {
await this.typeInStripeField('input[name="billingDependentLocality"]', card.address.district || '');
}
// 10. 填写地址第一行
await this.typeInStripeField('input[name="billingAddressLine1"]', card.address.addressLine1);
// 11. 填写地址第二行
if (card.address.addressLine2) {
await this.typeInStripeField('input[name="billingAddressLine2"]', card.address.addressLine2);
}
this.log('✅ 表单填写完成');
}
/**
* 使用 Puppeteer 真实键盘输入填写 Stripe 字段
*/
async typeInStripeField(selector, value) {
try {
await this.page.waitForSelector(selector, { timeout: 5000 });
const element = await this.page.$(selector);
if (element) {
// 点击元素获取焦点
await element.click();
await this.sleep(100);
// 清空现有内容 (Mac 使用 Meta/Command)
await this.page.keyboard.down('Meta');
await this.page.keyboard.press('a');
await this.page.keyboard.up('Meta');
await this.page.keyboard.press('Backspace');
await this.sleep(50);
// 使用 Puppeteer 的 type 方法真实输入,每个字符间隔 10ms
const strValue = String(value);
await element.type(strValue, { delay: 10 });
// 等待输入完成
await this.sleep(100);
this.log(`${selector}: ${strValue}`);
return true;
}
} catch (e) {
this.log(`${selector} 未找到或超时: ${e.message}`);
}
return false;
}
/**
* 选择 Stripe 下拉框选项
*/
async selectStripeOption(selector, value) {
try {
const selected = await this.page.evaluate((sel, val) => {
const select = document.querySelector(sel);
if (select) {
for (const opt of select.options) {
if (opt.value.includes(val) || opt.text.includes(val)) {
select.value = opt.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
return opt.value;
}
}
}
return null;
}, selector, value);
if (selected) {
this.log(`${selector}: ${selected}`);
return true;
}
} catch (e) {
this.log(`${selector} 选择失败`);
}
return false;
}
/**
* 在输入框中输入文本 - 使用 Puppeteer 原生方法
*/
async typeInField(selector, value) {
try {
const element = await this.page.$(selector);
if (element) {
// 清空现有内容
await element.click({ clickCount: 3 });
await this.page.keyboard.press('Backspace');
await this.sleep(100);
// 逐字符输入
await element.type(value, { delay: 30 });
this.log(`${selector}: ${value}`);
return true;
}
this.log(` ⚠️ 未找到: ${selector}`);
return false;
} catch (e) {
this.log(` ❌ 填写失败 ${selector}: ${e.message}`);
return false;
}
}
/**
* 选择下拉框选项
*/
async selectOption(selector, value) {
try {
const element = await this.page.$(selector);
if (element) {
await this.page.select(selector, value);
this.log(`${selector}: ${value}`);
return true;
}
return false;
} catch (e) {
// 尝试通过 evaluate 选择
const selected = await this.page.evaluate((sel, val) => {
const select = document.querySelector(sel);
if (select) {
for (const opt of select.options) {
if (opt.value === val || opt.text.includes(val)) {
select.value = opt.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
}
}
return false;
}, selector, value);
if (selected) this.log(`${selector}: ${value}`);
return selected;
}
}
/**
* 选择或输入省份
*/
async selectOrTypeProvince(province) {
// 先尝试作为下拉框选择
const selectSelector = '#billingAdministrativeArea, select[name="billingAdministrativeArea"]';
const inputSelector = 'input[name="billingAdministrativeArea"]';
const isSelect = await this.page.$(selectSelector);
if (isSelect) {
const tagName = await this.page.evaluate(sel => {
const el = document.querySelector(sel);
return el ? el.tagName : null;
}, selectSelector);
if (tagName === 'SELECT') {
await this.selectOption(selectSelector, province);
return;
}
}
// 作为输入框处理
await this.typeInField(inputSelector, province);
}
async fillInput(selector, value) {
const el = await this.page.$(selector);
if (el) {
await el.click({ clickCount: 3 });
await this.page.keyboard.press('Backspace');
await el.type(value, { delay: 50 });
}
}
async close() {
if (this.browser) {
try {
await this.browser.close();
this.log('🔒 浏览器已关闭');
} catch (e) {}
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
module.exports = CardBindingBot;
+548
View File
@@ -0,0 +1,548 @@
/**
* 银行卡生成器
* 使用 Luhn 算法生成有效的银行卡号
*/
class CardGenerator {
/**
* Luhn 算法校验
*/
static luhnCheck(cardNumber) {
let sum = 0;
let isEven = false;
for (let i = cardNumber.length - 1; i >= 0; i--) {
let digit = parseInt(cardNumber[i], 10);
if (isEven) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
}
/**
* 计算 Luhn 校验位
*/
static calculateLuhnCheckDigit(partialNumber) {
let sum = 0;
let isEven = true;
for (let i = partialNumber.length - 1; i >= 0; i--) {
let digit = parseInt(partialNumber[i], 10);
if (isEven) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
isEven = !isEven;
}
return (10 - (sum % 10)) % 10;
}
/**
* 根据 BIN 头生成完整卡号
*/
static generateCardNumber(bin) {
bin = String(bin);
const totalLength = 16;
const randomLength = totalLength - bin.length - 1;
let randomPart = '';
for (let i = 0; i < randomLength; i++) {
randomPart += Math.floor(Math.random() * 10);
}
const partialNumber = bin + randomPart;
const checkDigit = this.calculateLuhnCheckDigit(partialNumber);
return partialNumber + checkDigit;
}
/**
* 生成随机有效期(未来1-3年)
*/
static generateExpiry() {
const now = new Date();
const currentYear = now.getFullYear() % 100;
const yearsAhead = Math.floor(Math.random() * 3) + 1;
let year = currentYear + yearsAhead;
let month = Math.floor(Math.random() * 12) + 1;
return {
month: String(month).padStart(2, '0'),
year: String(year).padStart(2, '0')
};
}
/**
* 生成随机 CVV
*/
static generateCVV() {
return String(Math.floor(Math.random() * 900) + 100);
}
/**
* 中国姓氏(常见100个)
*/
static getChineseSurnames() {
return [
'王', '李', '张', '刘', '陈', '杨', '黄', '赵', '周', '吴',
'徐', '孙', '马', '胡', '朱', '郭', '何', '罗', '高', '林',
'郑', '梁', '谢', '宋', '唐', '许', '韩', '冯', '邓', '曹',
'彭', '曾', '肖', '田', '董', '袁', '潘', '于', '蒋', '蔡',
'余', '杜', '叶', '程', '苏', '魏', '吕', '丁', '任', '沈',
'姚', '卢', '姜', '崔', '钟', '谭', '陆', '汪', '范', '金',
'石', '廖', '贾', '夏', '韦', '付', '方', '白', '邹', '孟',
'熊', '秦', '邱', '江', '尹', '薛', '闫', '段', '雷', '侯',
'龙', '史', '陶', '黎', '贺', '顾', '毛', '郝', '龚', '邵',
'万', '钱', '严', '覃', '武', '戴', '莫', '孔', '向', '汤'
];
}
/**
* 中国名字(男性常见)
*/
static getChineseMaleNames() {
return [
'伟', '强', '磊', '军', '勇', '杰', '涛', '明', '超', '华',
'刚', '平', '辉', '鹏', '飞', '波', '斌', '宇', '浩', '凯',
'健', '俊', '峰', '龙', '亮', '建', '文', '博', '志', '海',
'威', '彬', '林', '成', '东', '旭', '阳', '晨', '帅', '康',
'毅', '昊', '然', '睿', '轩', '翔', '航', '鑫', '宁', '乐'
];
}
/**
* 中国名字(女性常见)
*/
static getChineseFemaleNames() {
return [
'芳', '娟', '敏', '静', '丽', '艳', '娜', '秀', '英', '华',
'慧', '巧', '美', '婷', '玲', '燕', '红', '春', '菊', '兰',
'凤', '洁', '梅', '琳', '素', '云', '莲', '真', '霞', '翠',
'雪', '荣', '爱', '妹', '霜', '香', '月', '莺', '媛', '艳',
'瑞', '凡', '佳', '嘉', '琼', '桂', '娣', '叶', '璧', '璐'
];
}
/**
* 生成随机中国姓名
*/
static generateChineseName() {
const surnames = this.getChineseSurnames();
const maleNames = this.getChineseMaleNames();
const femaleNames = this.getChineseFemaleNames();
const surname = surnames[Math.floor(Math.random() * surnames.length)];
const isMale = Math.random() > 0.5;
const names = isMale ? maleNames : femaleNames;
// 随机1-2个字的名
const nameLength = Math.random() > 0.3 ? 2 : 1;
let givenName = '';
for (let i = 0; i < nameLength; i++) {
givenName += names[Math.floor(Math.random() * names.length)];
}
return surname + givenName;
}
/**
* 中国省份城市数据(包含多个城市和区县)
*/
static getChinaProvinceData() {
return {
'北京市': {
cities: {
'北京市': {
districts: ['东城区', '西城区', '朝阳区', '丰台区', '石景山区', '海淀区', '顺义区', '通州区', '大兴区', '房山区', '门头沟区', '昌平区'],
zipBase: '100'
}
}
},
'天津市': {
cities: {
'天津市': {
districts: ['和平区', '河东区', '河西区', '南开区', '河北区', '红桥区', '东丽区', '西青区', '津南区', '北辰区', '武清区', '宝坻区'],
zipBase: '300'
}
}
},
'上海市': {
cities: {
'上海市': {
districts: ['黄浦区', '徐汇区', '长宁区', '静安区', '普陀区', '虹口区', '杨浦区', '闵行区', '宝山区', '嘉定区', '浦东新区', '金山区', '松江区'],
zipBase: '200'
}
}
},
'重庆市': {
cities: {
'重庆市': {
districts: ['渝中区', '大渡口区', '江北区', '沙坪坝区', '九龙坡区', '南岸区', '北碚区', '渝北区', '巴南区', '万州区', '涪陵区'],
zipBase: '400'
}
}
},
'广东省': {
cities: {
'广州市': { districts: ['越秀区', '海珠区', '荔湾区', '天河区', '白云区', '黄埔区', '番禺区', '花都区', '南沙区', '从化区', '增城区'], zipBase: '510' },
'深圳市': { districts: ['罗湖区', '福田区', '南山区', '宝安区', '龙岗区', '盐田区', '龙华区', '坪山区', '光明区'], zipBase: '518' },
'珠海市': { districts: ['香洲区', '斗门区', '金湾区'], zipBase: '519' },
'东莞市': { districts: ['莞城街道', '南城街道', '东城街道', '万江街道', '石碣镇', '石龙镇', '茶山镇', '石排镇'], zipBase: '523' },
'佛山市': { districts: ['禅城区', '南海区', '顺德区', '三水区', '高明区'], zipBase: '528' }
}
},
'浙江省': {
cities: {
'杭州市': { districts: ['上城区', '下城区', '江干区', '拱墅区', '西湖区', '滨江区', '萧山区', '余杭区', '富阳区', '临安区'], zipBase: '310' },
'宁波市': { districts: ['海曙区', '江北区', '北仑区', '镇海区', '鄞州区', '奉化区'], zipBase: '315' },
'温州市': { districts: ['鹿城区', '龙湾区', '瓯海区', '洞头区', '瑞安市', '乐清市'], zipBase: '325' },
'嘉兴市': { districts: ['南湖区', '秀洲区', '嘉善县', '海盐县', '海宁市', '平湖市', '桐乡市'], zipBase: '314' }
}
},
'江苏省': {
cities: {
'南京市': { districts: ['玄武区', '秦淮区', '建邺区', '鼓楼区', '浦口区', '栖霞区', '雨花台区', '江宁区', '六合区', '溧水区'], zipBase: '210' },
'苏州市': { districts: ['虎丘区', '吴中区', '相城区', '姑苏区', '吴江区', '昆山市', '太仓市', '常熟市', '张家港市'], zipBase: '215' },
'无锡市': { districts: ['锡山区', '惠山区', '滨湖区', '梁溪区', '新吴区', '江阴市', '宜兴市'], zipBase: '214' },
'常州市': { districts: ['天宁区', '钟楼区', '新北区', '武进区', '金坛区', '溧阳市'], zipBase: '213' }
}
},
'山东省': {
cities: {
'济南市': { districts: ['历下区', '市中区', '槐荫区', '天桥区', '历城区', '长清区', '章丘区', '济阳区'], zipBase: '250' },
'青岛市': { districts: ['市南区', '市北区', '黄岛区', '崂山区', '李沧区', '城阳区', '即墨区', '胶州市'], zipBase: '266' },
'烟台市': { districts: ['芝罘区', '福山区', '牟平区', '莱山区', '龙口市', '莱阳市', '莱州市', '招远市'], zipBase: '264' },
'潍坊市': { districts: ['潍城区', '寒亭区', '坊子区', '奎文区', '临朐县', '昌乐县', '青州市', '诸城市'], zipBase: '261' }
}
},
'四川省': {
cities: {
'成都市': { districts: ['锦江区', '青羊区', '金牛区', '武侯区', '成华区', '龙泉驿区', '青白江区', '新都区', '温江区', '双流区'], zipBase: '610' },
'绵阳市': { districts: ['涪城区', '游仙区', '安州区', '江油市', '三台县', '盐亭县', '梓潼县'], zipBase: '621' },
'德阳市': { districts: ['旌阳区', '罗江区', '广汉市', '什邡市', '绵竹市', '中江县'], zipBase: '618' }
}
},
'湖北省': {
cities: {
'武汉市': { districts: ['江岸区', '江汉区', '硚口区', '汉阳区', '武昌区', '青山区', '洪山区', '东西湖区', '蔡甸区', '江夏区'], zipBase: '430' },
'宜昌市': { districts: ['西陵区', '伍家岗区', '点军区', '猇亭区', '夷陵区', '宜都市', '当阳市', '枝江市'], zipBase: '443' },
'襄阳市': { districts: ['襄城区', '樊城区', '襄州区', '南漳县', '谷城县', '保康县', '老河口市', '枣阳市'], zipBase: '441' }
}
},
'湖南省': {
cities: {
'长沙市': { districts: ['芙蓉区', '天心区', '岳麓区', '开福区', '雨花区', '望城区', '长沙县', '浏阳市', '宁乡市'], zipBase: '410' },
'株洲市': { districts: ['荷塘区', '芦淞区', '石峰区', '天元区', '渌口区', '醴陵市'], zipBase: '412' },
'湘潭市': { districts: ['雨湖区', '岳塘区', '湘潭县', '湘乡市', '韶山市'], zipBase: '411' }
}
},
'河南省': {
cities: {
'郑州市': { districts: ['中原区', '二七区', '管城回族区', '金水区', '上街区', '惠济区', '中牟县', '巩义市', '荥阳市', '新密市'], zipBase: '450' },
'洛阳市': { districts: ['老城区', '西工区', '瀍河回族区', '涧西区', '吉利区', '洛龙区', '偃师区', '孟津区'], zipBase: '471' },
'开封市': { districts: ['龙亭区', '顺河回族区', '鼓楼区', '禹王台区', '祥符区', '杞县', '通许县', '尉氏县'], zipBase: '475' }
}
},
'福建省': {
cities: {
'福州市': { districts: ['鼓楼区', '台江区', '仓山区', '马尾区', '晋安区', '长乐区', '闽侯县', '连江县', '罗源县'], zipBase: '350' },
'厦门市': { districts: ['思明区', '海沧区', '湖里区', '集美区', '同安区', '翔安区'], zipBase: '361' },
'泉州市': { districts: ['鲤城区', '丰泽区', '洛江区', '泉港区', '惠安县', '安溪县', '永春县', '德化县'], zipBase: '362' }
}
},
'陕西省': {
cities: {
'西安市': { districts: ['新城区', '碑林区', '莲湖区', '灞桥区', '未央区', '雁塔区', '阎良区', '临潼区', '长安区', '高陵区'], zipBase: '710' },
'咸阳市': { districts: ['秦都区', '杨陵区', '渭城区', '三原县', '泾阳县', '乾县', '礼泉县', '永寿县'], zipBase: '712' },
'宝鸡市': { districts: ['渭滨区', '金台区', '陈仓区', '凤翔区', '岐山县', '扶风县', '眉县', '陇县'], zipBase: '721' }
}
},
'辽宁省': {
cities: {
'沈阳市': { districts: ['和平区', '沈河区', '大东区', '皇姑区', '铁西区', '苏家屯区', '浑南区', '沈北新区', '于洪区'], zipBase: '110' },
'大连市': { districts: ['中山区', '西岗区', '沙河口区', '甘井子区', '旅顺口区', '金州区', '普兰店区'], zipBase: '116' },
'鞍山市': { districts: ['铁东区', '铁西区', '立山区', '千山区', '海城市', '台安县', '岫岩满族自治县'], zipBase: '114' }
}
},
'吉林省': {
cities: {
'长春市': { districts: ['南关区', '宽城区', '朝阳区', '二道区', '绿园区', '双阳区', '九台区', '农安县'], zipBase: '130' },
'吉林市': { districts: ['昌邑区', '龙潭区', '船营区', '丰满区', '永吉县', '蛟河市', '桦甸市', '舒兰市'], zipBase: '132' }
}
},
'黑龙江省': {
cities: {
'哈尔滨市': { districts: ['道里区', '南岗区', '道外区', '平房区', '松北区', '香坊区', '呼兰区', '阿城区', '双城区'], zipBase: '150' },
'齐齐哈尔市': { districts: ['龙沙区', '建华区', '铁锋区', '昂昂溪区', '富拉尔基区', '碾子山区', '梅里斯达斡尔族区'], zipBase: '161' }
}
},
'安徽省': {
cities: {
'合肥市': { districts: ['瑶海区', '庐阳区', '蜀山区', '包河区', '长丰县', '肥东县', '肥西县', '庐江县', '巢湖市'], zipBase: '230' },
'芜湖市': { districts: ['镜湖区', '弋江区', '鸠江区', '湾沚区', '繁昌区', '南陵县', '无为市'], zipBase: '241' },
'蚌埠市': { districts: ['龙子湖区', '蚌山区', '禹会区', '淮上区', '怀远县', '五河县', '固镇县'], zipBase: '233' }
}
},
'江西省': {
cities: {
'南昌市': { districts: ['东湖区', '西湖区', '青云谱区', '青山湖区', '新建区', '红谷滩区', '南昌县', '安义县', '进贤县'], zipBase: '330' },
'九江市': { districts: ['濂溪区', '浔阳区', '柴桑区', '武宁县', '修水县', '永修县', '德安县', '都昌县'], zipBase: '332' },
'赣州市': { districts: ['章贡区', '南康区', '赣县区', '信丰县', '大余县', '上犹县', '崇义县', '安远县'], zipBase: '341' }
}
},
'河北省': {
cities: {
'石家庄市': { districts: ['长安区', '桥西区', '新华区', '井陉矿区', '裕华区', '藁城区', '鹿泉区', '栾城区'], zipBase: '050' },
'唐山市': { districts: ['路南区', '路北区', '古冶区', '开平区', '丰南区', '丰润区', '曹妃甸区', '滦南县'], zipBase: '063' },
'保定市': { districts: ['竞秀区', '莲池区', '满城区', '清苑区', '徐水区', '涞水县', '阜平县', '定兴县'], zipBase: '071' }
}
},
'山西省': {
cities: {
'太原市': { districts: ['小店区', '迎泽区', '杏花岭区', '尖草坪区', '万柏林区', '晋源区', '清徐县', '阳曲县', '娄烦县'], zipBase: '030' },
'大同市': { districts: ['新荣区', '平城区', '云冈区', '云州区', '阳高县', '天镇县', '广灵县', '灵丘县'], zipBase: '037' }
}
},
'内蒙古自治区': {
cities: {
'呼和浩特市': { districts: ['新城区', '回民区', '玉泉区', '赛罕区', '土默特左旗', '托克托县', '和林格尔县', '清水河县'], zipBase: '010' },
'包头市': { districts: ['东河区', '昆都仑区', '青山区', '石拐区', '白云鄂博矿区', '九原区', '土默特右旗', '固阳县'], zipBase: '014' }
}
},
'广西壮族自治区': {
cities: {
'南宁市': { districts: ['兴宁区', '青秀区', '江南区', '西乡塘区', '良庆区', '邕宁区', '武鸣区', '隆安县', '马山县'], zipBase: '530' },
'桂林市': { districts: ['秀峰区', '叠彩区', '象山区', '七星区', '雁山区', '临桂区', '阳朔县', '灵川县', '全州县'], zipBase: '541' }
}
},
'云南省': {
cities: {
'昆明市': { districts: ['五华区', '盘龙区', '官渡区', '西山区', '东川区', '呈贡区', '晋宁区', '富民县', '宜良县'], zipBase: '650' },
'大理白族自治州': { districts: ['大理市', '漾濞彝族自治县', '祥云县', '宾川县', '弥渡县', '南涧彝族自治县', '巍山彝族回族自治县'], zipBase: '671' }
}
},
'贵州省': {
cities: {
'贵阳市': { districts: ['南明区', '云岩区', '花溪区', '乌当区', '白云区', '观山湖区', '清镇市', '开阳县', '息烽县'], zipBase: '550' },
'遵义市': { districts: ['红花岗区', '汇川区', '播州区', '桐梓县', '绥阳县', '正安县', '道真仡佬族苗族自治县'], zipBase: '563' }
}
},
'甘肃省': {
cities: {
'兰州市': { districts: ['城关区', '七里河区', '西固区', '安宁区', '红古区', '永登县', '皋兰县', '榆中县'], zipBase: '730' },
'天水市': { districts: ['秦州区', '麦积区', '清水县', '秦安县', '甘谷县', '武山县', '张家川回族自治县'], zipBase: '741' }
}
},
'青海省': {
cities: {
'西宁市': { districts: ['城东区', '城中区', '城西区', '城北区', '湟中区', '大通回族土族自治县', '湟源县'], zipBase: '810' }
}
},
'宁夏回族自治区': {
cities: {
'银川市': { districts: ['兴庆区', '西夏区', '金凤区', '永宁县', '贺兰县', '灵武市'], zipBase: '750' }
}
},
'新疆维吾尔自治区': {
cities: {
'乌鲁木齐市': { districts: ['天山区', '沙依巴克区', '新市区', '水磨沟区', '头屯河区', '达坂城区', '米东区', '乌鲁木齐县'], zipBase: '830' }
}
},
'西藏自治区': {
cities: {
'拉萨市': { districts: ['城关区', '堆龙德庆区', '达孜区', '林周县', '当雄县', '尼木县', '曲水县', '墨竹工卡县'], zipBase: '850' }
}
},
'海南省': {
cities: {
'海口市': { districts: ['秀英区', '龙华区', '琼山区', '美兰区'], zipBase: '570' },
'三亚市': { districts: ['海棠区', '吉阳区', '天涯区', '崖州区'], zipBase: '572' }
}
}
};
}
/**
* 常见街道名称
*/
static getStreetNames() {
return [
'人民路', '解放路', '中山路', '建设路', '和平路', '文化路', '新华路', '胜利路',
'长江路', '黄河路', '北京路', '上海路', '南京路', '广州路', '深圳路', '杭州路',
'朝阳路', '光明路', '幸福路', '团结路', '友谊路', '民主路', '科技路', '创业路',
'学府路', '大学路', '青年路', '工业路', '商业街', '步行街', '金融街', '高新路',
'滨河路', '湖滨路', '海滨路', '山水路', '花园路', '公园路', '体育路', '文艺路',
'东风路', '西湖路', '南湖路', '北湖路', '中央大道', '世纪大道', '迎宾大道', '环城路'
];
}
/**
* 生成随机中国地址
*/
static generateChinaAddress() {
const provinceData = this.getChinaProvinceData();
const streetNames = this.getStreetNames();
// 随机选择省份
const provinces = Object.keys(provinceData);
const province = provinces[Math.floor(Math.random() * provinces.length)];
// 随机选择城市
const cities = Object.keys(provinceData[province].cities);
const city = cities[Math.floor(Math.random() * cities.length)];
// 随机选择区县
const cityData = provinceData[province].cities[city];
const district = cityData.districts[Math.floor(Math.random() * cityData.districts.length)];
// 随机选择街道
const street = streetNames[Math.floor(Math.random() * streetNames.length)];
// 随机门牌号
const streetNo = Math.floor(Math.random() * 500) + 1;
const buildingNo = Math.floor(Math.random() * 30) + 1;
const roomNo = Math.floor(Math.random() * 2500) + 101;
// 生成邮编(基于城市的邮编前缀 + 随机后缀)
const zipSuffix = String(Math.floor(Math.random() * 100)).padStart(3, '0');
const zipCode = cityData.zipBase + zipSuffix;
return {
country: '中国',
province: province,
city: city,
district: district,
addressLine1: `${street}${streetNo}`,
addressLine2: `${buildingNo}${roomNo}`,
zipCode: zipCode
};
}
/**
* 香港地区数据
*/
static getHongKongData() {
return {
regions: [
{ value: 'KOWLOON', label: '九龍 — Kowloon' },
{ value: 'HONG KONG', label: '香港島 — Hong Kong' },
{ value: 'NEW TERRITORIES', label: '新界 — New Territories' }
],
kowloonDistricts: [
'Mong Kok', 'Tsim Sha Tsui', 'Jordan', 'Yau Ma Tei', 'Sham Shui Po',
'Kowloon City', 'Wong Tai Sin', 'Kwun Tong', 'Hung Hom', 'To Kwa Wan'
],
hongKongDistricts: [
'Central', 'Wan Chai', 'Causeway Bay', 'North Point', 'Quarry Bay',
'Admiralty', 'Sheung Wan', 'Sai Ying Pun', 'Kennedy Town', 'Aberdeen'
],
newTerritoriesDistricts: [
'Sha Tin', 'Tsuen Wan', 'Tuen Mun', 'Yuen Long', 'Tai Po',
'Fanling', 'Sheung Shui', 'Kwai Chung', 'Tsing Yi', 'Ma On Shan'
],
streets: [
'Nathan Road', 'Queens Road', 'Des Voeux Road', 'Hennessy Road',
'Canton Road', 'Granville Road', 'Austin Road', 'Chatham Road',
'Boundary Street', 'Prince Edward Road', 'Argyle Street', 'Waterloo Road'
]
};
}
/**
* 生成随机英文名(用于持卡人)
*/
static generateEnglishName() {
const firstNames = [
'James', 'John', 'Michael', 'David', 'William', 'Richard', 'Joseph', 'Thomas',
'Mary', 'Jennifer', 'Linda', 'Patricia', 'Elizabeth', 'Susan', 'Jessica', 'Sarah',
'Wei', 'Ming', 'Ling', 'Hui', 'Fang', 'Yan', 'Hong', 'Jing'
];
const lastNames = [
'Wong', 'Chan', 'Lee', 'Cheung', 'Lau', 'Ho', 'Ng', 'Tam',
'Leung', 'Chow', 'Fung', 'Yip', 'Kwok', 'Tse', 'Mak', 'Hui'
];
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
return `${firstName} ${lastName}`;
}
/**
* 生成香港地址(适用于 Stripe 表单)
*/
static generateHongKongAddress() {
const hkData = this.getHongKongData();
// 随机选择地区
const regionIndex = Math.floor(Math.random() * hkData.regions.length);
const region = hkData.regions[regionIndex];
// 根据地区选择区域
let districts;
if (region.value === 'KOWLOON') {
districts = hkData.kowloonDistricts;
} else if (region.value === 'HONG KONG') {
districts = hkData.hongKongDistricts;
} else {
districts = hkData.newTerritoriesDistricts;
}
const district = districts[Math.floor(Math.random() * districts.length)];
const street = hkData.streets[Math.floor(Math.random() * hkData.streets.length)];
const streetNo = Math.floor(Math.random() * 500) + 1;
const floor = Math.floor(Math.random() * 30) + 1;
const unit = String.fromCharCode(65 + Math.floor(Math.random() * 8)); // A-H
return {
country: 'HK',
region: region.value,
regionLabel: region.label,
district: district,
addressLine1: `${streetNo} ${street}`,
addressLine2: `${floor}/F, Unit ${unit}`
};
}
/**
* 生成完整的卡信息(使用中国地址和中文名)
*/
static generateFullCardInfo(bin) {
const cardNumber = this.generateCardNumber(bin);
const expiry = this.generateExpiry();
const cvv = this.generateCVV();
const name = this.generateChineseName();
const address = this.generateChinaAddress();
return {
cardNumber,
expMonth: expiry.month,
expYear: expiry.year,
cvv,
name,
address
};
}
}
module.exports = CardGenerator;
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/osascript
-- Windsurf Log in 按钮自动点击脚本
-- 使用 macOS 原生 UI 自动化
on run
try
-- 方法1: 检查应用程序是否在运行(使用应用程序名称)
set windsurfRunning to false
try
tell application "System Events"
set appList to name of every application process
repeat with appName in appList
if appName is "Windsurf" then
set windsurfRunning to true
exit repeat
end if
end repeat
end tell
end try
-- 方法2: 如果方法1失败,尝试通过进程路径检测
if not windsurfRunning then
try
tell application "System Events"
set procList to every process whose bundle identifier is "com.exafunction.windsurf"
if (count of procList) > 0 then
set windsurfRunning to true
end if
end tell
end try
end if
-- 方法3: 尝试通过 Electron 进程检测(检查所有 Electron 进程)
if not windsurfRunning then
try
tell application "System Events"
set electronProcs to every process whose name is "Electron"
repeat with proc in electronProcs
try
set procPath to POSIX path of (application file of proc)
if procPath contains "Windsurf.app" then
set windsurfRunning to true
exit repeat
end if
end try
end repeat
end tell
end try
end if
if not windsurfRunning then
log "❌ Windsurf 未运行"
return false
end if
-- 找到正确的进程名
set windsurfProcess to "Electron"
try
tell application "System Events"
set appList to name of every application process
repeat with appName in appList
if appName is "Windsurf" then
set windsurfProcess to "Windsurf"
exit repeat
end if
end repeat
end tell
end try
-- 如果没找到 Windsurf 进程名,尝试通过 Electron 进程路径确认
if windsurfProcess is "Electron" then
try
tell application "System Events"
set electronProcs to every process whose name is "Electron"
repeat with proc in electronProcs
try
set procPath to POSIX path of (application file of proc)
if procPath contains "Windsurf.app" then
set windsurfProcess to "Electron"
exit repeat
end if
end try
end repeat
end tell
end try
end if
-- 激活 Windsurf 应用(尝试多种方式)
try
tell application "Windsurf" to activate
on error
try
tell application "System Events"
set frontmost of process windsurfProcess to true
end tell
on error
-- 如果都失败,继续尝试
end try
end try
delay 0.5
-- 方法1: 使用键盘快捷键(最可靠)
tell application "System Events"
tell process windsurfProcess
try
-- 按 Tab 键移动焦点到按钮(通常焦点已经在按钮上)
repeat 3 times
key code 48 -- Tab 键
delay 0.15
end repeat
-- 按 Enter 或 Space 键点击
key code 36 -- Enter 键
delay 0.2
log "✓ 使用键盘快捷键点击"
return true
on error errMsg
log "⚠️ 键盘快捷键失败: " & errMsg
end try
end tell
end tell
-- 方法2: 尝试通过 UI 元素查找并点击按钮
tell application "System Events"
tell process windsurfProcess
try
-- 获取所有窗口
set winList to windows
if (count of winList) > 0 then
set mainWin to window 1
-- 查找按钮
tell mainWin
set allButtons to buttons
repeat with btn in allButtons
try
set btnName to name of btn
if btnName contains "Log" or btnName contains "log" or btnName contains "登录" then
click btn
log "✓ 成功点击按钮: " & btnName
return true
end if
end try
end repeat
end tell
end if
on error errMsg2
log "⚠️ UI元素方法失败: " & errMsg2
end try
end tell
end tell
log "❌ 所有方法都失败了"
return false
on error errMsg
log "❌ 脚本执行失败: " & errMsg
return false
end try
end run
+197
View File
@@ -0,0 +1,197 @@
const Imap = require('imap');
const { simpleParser } = require('mailparser');
/**
* 本地邮箱验证码接收器
*/
class EmailReceiver {
constructor(config) {
this.config = config;
}
/**
* 获取验证码(本地IMAP实现)
*/
async getVerificationCode(targetEmail, maxWaitTime = 120000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
// 创建IMAP连接
const imap = new Imap({
user: this.config.user,
password: this.config.password,
host: this.config.host,
port: this.config.port || 993,
tls: true,
tlsOptions: { rejectUnauthorized: false }
});
let checkInterval;
let isResolved = false;
const checkMail = () => {
if (Date.now() - startTime > maxWaitTime) {
clearInterval(checkInterval);
imap.end();
if (!isResolved) {
isResolved = true;
reject(new Error('获取验证码超时'));
}
return;
}
imap.openBox('INBOX', false, (err, box) => {
if (err) {
console.log('打开邮箱失败:', err.message);
return;
}
// 搜索未读邮件(不限制发件人,因为可能从不同域名发送)
const searchCriteria = ['UNSEEN'];
imap.search(searchCriteria, (err, results) => {
if (err) {
console.log('搜索邮件失败:', err.message);
return;
}
if (!results || results.length === 0) {
console.log('暂无新邮件');
return;
}
console.log(`找到 ${results.length} 封未读邮件`);
const fetch = imap.fetch(results, { bodies: '', markSeen: true });
fetch.on('message', (msg) => {
msg.on('body', (stream) => {
simpleParser(stream, (err, parsed) => {
if (err || isResolved) return;
// 检查邮件主题或内容是否包含Windsurf相关关键词
const subject = parsed.subject || '';
const from = parsed.from?.text || '';
const to = parsed.to?.text || '';
console.log(`邮件信息 - 主题: ${subject}, 发件人: ${from}, 收件人: ${to}`);
// 检查是否是Windsurf相关邮件
const isWindsurfEmail = subject.toLowerCase().includes('windsurf') ||
subject.toLowerCase().includes('verify') ||
from.toLowerCase().includes('windsurf') ||
from.toLowerCase().includes('codeium') ||
from.toLowerCase().includes('exafunction');
if (!isWindsurfEmail) {
console.log('不是Windsurf验证邮件,跳过');
return;
}
// 检查邮件是否发送给目标邮箱
if (!to.includes(targetEmail)) {
console.log(`收件人不匹配: ${to} != ${targetEmail}`);
return;
}
// 从邮件内容中提取验证码
const emailBody = (parsed.text || parsed.html || '').replace(/<[^>]*>/g, '');
console.log('邮件内容:', emailBody.substring(0, 200));
// 多种验证码格式匹配
const patterns = [
/\b(\d{6})\b/, // 6位数字
/\b([A-Z0-9]{6})\b/, // 6位字母数字
/验证码[:]\s*(\w+)/, // 中文验证码
/code[:]\s*(\w+)/i, // 英文code
/verification code[:]\s*(\w+)/i // verification code
];
for (const pattern of patterns) {
const match = emailBody.match(pattern);
if (match) {
clearInterval(checkInterval);
imap.end();
if (!isResolved) {
isResolved = true;
console.log(`✓ 找到验证码: ${match[1]}`);
resolve(match[1]);
}
return;
}
}
console.log('未能从邮件中提取验证码');
});
});
});
fetch.once('error', (err) => {
console.log('获取邮件失败:', err.message);
});
});
});
};
imap.once('ready', () => {
console.log('IMAP连接成功,开始监听验证码邮件...');
checkMail();
checkInterval = setInterval(checkMail, 5000); // 每5秒检查一次
});
imap.once('error', (err) => {
clearInterval(checkInterval);
if (!isResolved) {
isResolved = true;
reject(new Error(`IMAP连接失败: ${err.message}`));
}
});
imap.once('end', () => {
clearInterval(checkInterval);
console.log('IMAP连接已关闭');
});
imap.connect();
});
}
/**
* 测试IMAP连接
*/
async testConnection() {
return new Promise((resolve) => {
const imap = new Imap({
user: this.config.user,
password: this.config.password,
host: this.config.host,
port: this.config.port || 993,
tls: true,
tlsOptions: { rejectUnauthorized: false },
connTimeout: 10000, // 10秒连接超时
authTimeout: 10000 // 10秒认证超时
});
// 设置总超时
const timeout = setTimeout(() => {
imap.end();
resolve({ success: false, message: '连接超时,请检查服务器地址和端口' });
}, 15000);
imap.once('ready', () => {
clearTimeout(timeout);
imap.end();
resolve({ success: true, message: 'IMAP连接成功' });
});
imap.once('error', (err) => {
clearTimeout(timeout);
resolve({ success: false, message: `IMAP连接失败: ${err.message}` });
});
imap.connect();
});
}
}
module.exports = EmailReceiver;
+646
View File
@@ -0,0 +1,646 @@
const { connect } = require('puppeteer-real-browser');
const puppeteer = require('puppeteer');
class RegistrationBot {
constructor(config) {
this.config = config;
// 自定义域名邮箱列表
this.emailDomains = config.emailDomains || ['example.com'];
// 邮箱编号计数器(1-999)
this.emailCounter = 1;
// 无头模式
this.headless = config.headless === true;
console.log('RegistrationBot 初始化 - headless:', this.headless, '原始值:', config.headless);
}
/**
* 生成域名邮箱
* 格式: 编号(1-999) + 随机字母数字组合
*/
async generateTempEmail() {
// 获取当前编号
const number = this.emailCounter;
// 生成随机字母数字组合(8位)
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let randomStr = '';
for (let i = 0; i < 8; i++) {
randomStr += chars[Math.floor(Math.random() * chars.length)];
}
// 组合用户名: 编号 + 随机字符串
const username = `${number}${randomStr}`;
// 随机选择配置的域名
const randomIndex = Math.floor(Math.random() * this.emailDomains.length);
const domain = this.emailDomains[randomIndex];
// 递增计数器(1-999循环)
this.emailCounter++;
if (this.emailCounter > 999) {
this.emailCounter = 1;
}
return `${username}@${domain}`;
}
/**
* 获取邮箱验证码(使用本地EmailReceiver
* 支持重试机制:最多重试3次,每次间隔30秒
*/
async getVerificationCode(email, maxWaitTime = 120000) {
const emailConfig = this.config.emailConfig;
if (!emailConfig) {
throw new Error('未配置邮箱IMAP信息');
}
const EmailReceiver = require('./emailReceiver');
const receiver = new EmailReceiver(emailConfig);
const MAX_RETRIES = 3;
const RETRY_DELAY = 30000; // 30秒
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
if (this.logCallback) {
this.logCallback(`📬 第 ${attempt} 次尝试获取验证码...`);
}
console.log(`[尝试 ${attempt}/${MAX_RETRIES}] 等待 ${email} 的验证码邮件...`);
const code = await receiver.getVerificationCode(email, maxWaitTime);
if (code) {
if (this.logCallback) {
this.logCallback(`✓ 成功获取验证码: ${code}`);
}
return code;
}
} catch (error) {
console.error(`[尝试 ${attempt}/${MAX_RETRIES}] 获取验证码失败:`, error.message);
if (attempt < MAX_RETRIES) {
if (this.logCallback) {
this.logCallback(`⚠️ 第 ${attempt} 次获取失败,${RETRY_DELAY/1000} 秒后重试...`);
}
console.log(`等待 ${RETRY_DELAY/1000} 秒后重试...`);
await this.sleep(RETRY_DELAY);
} else {
if (this.logCallback) {
this.logCallback(`❌ 已重试 ${MAX_RETRIES} 次,仍未获取到验证码`);
}
throw new Error(`获取验证码失败,已重试 ${MAX_RETRIES} 次: ${error.message}`);
}
}
}
throw new Error('获取验证码失败,已达到最大重试次数');
}
/**
* 生成随机英文名
*/
generateRandomName() {
const firstNames = [
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Charles',
'Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth', 'Barbara', 'Susan', 'Jessica', 'Sarah', 'Karen',
'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Paul', 'Andrew', 'Joshua', 'Kenneth',
'Emily', 'Ashley', 'Kimberly', 'Melissa', 'Donna', 'Michelle', 'Dorothy', 'Carol', 'Amanda', 'Betty'
];
const lastNames = [
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez',
'Wilson', 'Anderson', 'Taylor', 'Thomas', 'Moore', 'Jackson', 'Martin', 'Lee', 'Thompson', 'White',
'Harris', 'Clark', 'Lewis', 'Robinson', 'Walker', 'Young', 'Allen', 'King', 'Wright', 'Scott',
'Green', 'Baker', 'Adams', 'Nelson', 'Hill', 'Carter', 'Mitchell', 'Roberts', 'Turner', 'Phillips'
];
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
return { firstName, lastName };
}
/**
* 输出日志(同时发送到前端)
*/
log(message) {
console.log(message);
if (this.logCallback) {
this.logCallback(message);
}
}
/**
* 注册单个账号
*/
async registerAccount(logCallback, stepCallback) {
this.logCallback = logCallback;
this.stepCallback = stepCallback; // 步骤进度回调
let browser, page;
try {
this.log('🚀 开始连接浏览器...');
let response;
if (this.headless) {
// 无头模式:使用 puppeteer-real-browser 但设置 headless
this.log('🔇 无头模式启动...');
this.log('⚠️ 注意:无头模式可能被 Cloudflare 检测,如失败请关闭无头模式');
response = await connect({
headless: true,
fingerprint: true,
turnstile: true,
tf: true,
args: [
'--disable-blink-features=AutomationControlled',
'--disable-features=IsolateOrigins,site-per-process',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu'
]
});
browser = response.browser;
page = response.page;
} else {
// 有头模式:使用 puppeteer-real-browser(更好的反检测)
this.log('🖥️ 有头模式启动(使用 Real Browser...');
response = await connect({
headless: false,
fingerprint: true,
turnstile: true,
tf: true,
args: [
'--disable-blink-features=AutomationControlled',
'--disable-features=IsolateOrigins,site-per-process',
'--no-sandbox',
'--disable-setuid-sandbox'
]
});
browser = response.browser;
page = response.page;
}
this.log('✓ 浏览器连接成功');
if (!browser || !page) {
throw new Error('浏览器或页面对象未创建');
}
this.log('✓ 浏览器已启动');
// 生成临时邮箱和密码
const email = await this.generateTempEmail();
const password = email; // 密码和邮箱一样
const { firstName, lastName } = this.generateRandomName();
this.log(`📧 邮箱: ${email}`);
this.log(`👤 姓名: ${firstName} ${lastName}`);
// 访问注册页面
this.log('🌐 正在访问注册页面...');
await page.goto('https://windsurf.com/account/register', {
waitUntil: 'networkidle2',
timeout: 30000
});
await this.sleep(2000);
// ========== 第一步: 填写基本信息 ==========
this.log('📝 步骤1: 填写基本信息');
if (this.stepCallback) this.stepCallback(1, 5); // 步骤1/5
// 等待表单加载
await page.waitForSelector('input', { timeout: 15000 });
await this.sleep(1000);
// 填写First name
const firstNameInput = await page.$('input[name="firstName"], input[placeholder*="First"], input[placeholder*="first"]');
if (firstNameInput) {
await firstNameInput.click();
await firstNameInput.type(firstName, { delay: 100 });
}
// 填写Last name
const lastNameInput = await page.$('input[name="lastName"], input[placeholder*="Last"], input[placeholder*="last"]');
if (lastNameInput) {
await lastNameInput.click();
await lastNameInput.type(lastName, { delay: 100 });
}
// 填写Email
const emailInput = await page.$('input[type="email"], input[name="email"]');
if (emailInput) {
await emailInput.click({ clickCount: 3 });
await page.keyboard.press('Backspace');
await emailInput.type(email, { delay: 100 });
}
// 同意条款复选框
const checkbox = await page.$('input[type="checkbox"]');
if (checkbox) {
const isChecked = await page.evaluate(el => el.checked, checkbox);
if (!isChecked) {
await checkbox.click();
}
}
await this.sleep(1000);
// 点击Continue按钮
this.log('🔘 点击Continue按钮...');
let clicked = false;
// 尝试多种方式找到并点击按钮
try {
// 方式1: 通过type=submit
const submitBtn = await page.$('button[type="submit"]');
if (submitBtn) {
await submitBtn.click();
clicked = true;
this.log('✓ Continue按钮点击成功');
}
} catch (e) {
this.log('⚠️ submit按钮点击失败,尝试其他方式');
}
if (!clicked) {
try {
// 方式2: 通过文本内容查找
const buttons = await page.$$('button');
for (const btn of buttons) {
const text = await page.evaluate(el => el.textContent, btn);
if (text && (text.includes('Continue') || text.includes('继续'))) {
await btn.click();
clicked = true;
this.log('✓ 通过文本查找点击成功');
break;
}
}
} catch (e) {
this.log('⚠️ 文本查找失败');
}
}
if (!clicked) {
throw new Error('无法找到Continue按钮');
}
await this.sleep(3000);
// ========== 第二步: 填写密码 ==========
this.log('🔐 步骤2: 填写密码信息');
if (this.stepCallback) this.stepCallback(2, 5); // 步骤2/5
// 等待密码输入页面
await page.waitForSelector('input[type="password"]', { timeout: 15000 });
await this.sleep(1000);
// 再次填写Email(如果需要)
const emailInput2 = await page.$('input[type="email"], input[name="email"]');
if (emailInput2) {
const emailValue = await page.evaluate(el => el.value, emailInput2);
if (!emailValue) {
await emailInput2.click();
await emailInput2.type(email, { delay: 100 });
}
}
// 填写密码
const passwordInputs = await page.$$('input[type="password"]');
if (passwordInputs.length >= 1) {
await passwordInputs[0].click();
await passwordInputs[0].type(password, { delay: 100 });
}
// 填写确认密码
if (passwordInputs.length >= 2) {
await passwordInputs[1].click();
await passwordInputs[1].type(password, { delay: 100 });
}
await this.sleep(1000);
// 点击Continue按钮
this.log('🔘 点击第二个Continue按钮...');
let clicked2 = false;
try {
const submitBtn2 = await page.$('button[type="submit"]');
if (submitBtn2) {
await submitBtn2.click();
clicked2 = true;
this.log('✓ 第二个Continue按钮点击成功');
}
} catch (e) {
this.log('⚠️ 尝试其他方式');
}
if (!clicked2) {
try {
const buttons = await page.$$('button');
for (const btn of buttons) {
const text = await page.evaluate(el => el.textContent, btn);
if (text && (text.includes('Continue') || text.includes('继续'))) {
await btn.click();
clicked2 = true;
this.log('✓ 通过文本找到按钮');
break;
}
}
} catch (e) {
this.log('⚠️ 查找失败');
}
}
if (!clicked2) {
throw new Error('无法找到第二个Continue按钮');
}
await this.sleep(3000);
// ========== 第三步: Cloudflare人机验证 ==========
this.log('🛡️ 步骤3: 等待Cloudflare验证...');
if (this.stepCallback) this.stepCallback(3, 5); // 步骤3/5
// puppeteer-real-browser会自动处理Cloudflare Turnstile验证
// 等待验证完成
await this.sleep(10000);
// 点击Continue按钮(验证后)
this.log('🔘 查找验证后的Continue按钮...');
let clicked3 = false;
// 尝试多次查找按钮
for (let attempt = 0; attempt < 3; attempt++) {
try {
// 方式1: 通过submit按钮
const submitBtn = await page.$('button[type="submit"]');
if (submitBtn) {
await submitBtn.click();
clicked3 = true;
this.log('✓ 验证后Continue按钮点击成功');
break;
}
} catch (e) {
// 忽略错误,继续尝试
}
if (!clicked3) {
try {
// 方式2: 通过文本查找
const buttons = await page.$$('button');
for (const btn of buttons) {
const text = await page.evaluate(el => el.textContent, btn);
if (text && (text.includes('Continue') || text.includes('继续') || text.includes('Next'))) {
await btn.click();
clicked3 = true;
this.log('✓ 通过文本找到Continue按钮');
break;
}
}
} catch (e) {
// 忽略错误
}
}
if (clicked3) break;
// 等待1秒后重试
this.log(`⚠️ 第${attempt + 1}次未找到按钮,等待后重试...`);
await this.sleep(2000);
}
if (!clicked3) {
this.log('⚠️ 未找到Continue按钮,可能已自动跳转');
}
await this.sleep(3000);
// ========== 第四步: 输入验证码 ==========
this.log('📮 步骤4: 等待邮箱验证码...');
if (this.stepCallback) this.stepCallback(4, 5); // 步骤4/5
// 等待验证码输入框
await page.waitForSelector('input[type="text"], input[name="code"]', { timeout: 15000 });
// 延迟15秒后再获取验证码,避免批量注册时验证码混淆
this.log('⏱️ 延迟 15 秒后获取验证码,避免混淆...');
await this.sleep(15000);
// 获取验证码
this.log('📬 正在接收验证码...');
const verificationCode = await this.getVerificationCode(email);
this.log(`✓ 获取到验证码: ${verificationCode}`);
// 输入6位验证码
const codeInputs = await page.$$('input[type="text"], input[name="code"]');
if (codeInputs.length === 6) {
// 如果是6个独立输入框
for (let i = 0; i < 6; i++) {
await codeInputs[i].click();
await codeInputs[i].type(verificationCode[i], { delay: 100 });
}
} else if (codeInputs.length === 1) {
// 如果是单个输入框
await codeInputs[0].click();
await codeInputs[0].type(verificationCode, { delay: 100 });
}
await this.sleep(1000);
// 点击Create account按钮
console.log('点击Create account按钮...');
const createBtn = await page.$('button[type="submit"]');
if (createBtn) {
await createBtn.click();
}
await this.sleep(5000);
// ========== 第五步: 检查注册是否成功 ==========
if (this.stepCallback) this.stepCallback(5, 5); // 步骤5/5
const currentUrl = page.url();
const isSuccess = !currentUrl.includes('/login') && !currentUrl.includes('/signup');
if (isSuccess) {
console.log('✓ 注册成功!');
// 保存账号到本地
const fs = require('fs').promises;
const path = require('path');
const { app } = require('electron');
const ACCOUNTS_FILE = path.join(app.getPath('userData'), 'accounts.json');
let accounts = [];
try {
const data = await fs.readFile(ACCOUNTS_FILE, 'utf-8');
accounts = JSON.parse(data);
} catch (error) {
// 文件不存在,使用空数组
}
const account = {
id: Date.now().toString(),
email,
password,
firstName,
lastName,
createdAt: new Date().toISOString()
};
accounts.push(account);
await fs.writeFile(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2));
console.log('账号已保存到本地');
return {
success: true,
email,
password,
firstName,
lastName,
createdAt: account.createdAt
};
} else {
throw new Error('注册失败,请检查页面');
}
} catch (error) {
console.error('注册过程出错:', error);
console.error('错误堆栈:', error.stack);
return {
success: false,
error: error.message || '未知错误',
errorStack: error.stack
};
} finally {
if (browser) {
try {
await browser.close();
console.log('浏览器已关闭');
} catch (e) {
console.error('关闭浏览器失败:', e);
}
}
}
}
/**
* 批量注册(控制并发数量)
* 最多同时4个窗口,每个注册完成后才开始下一个
*/
async batchRegister(count, progressCallback, logCallback) {
const MAX_CONCURRENT = 4; // 最大并发数
if (logCallback) {
logCallback(`🚀 开始批量注册 ${count} 个账号`);
logCallback(`📊 最大并发数: ${MAX_CONCURRENT} 个窗口`);
logCallback(`⏱️ 验证码延迟: 15 秒`);
}
const results = [];
let completed = 0;
// 分批执行,每批最多 MAX_CONCURRENT 个
for (let i = 0; i < count; i += MAX_CONCURRENT) {
const batchSize = Math.min(MAX_CONCURRENT, count - i);
const batchTasks = [];
if (logCallback) {
logCallback(`\n========== 第 ${Math.floor(i/MAX_CONCURRENT) + 1} 批次,注册 ${batchSize} 个账号 ==========`);
}
// 创建当前批次的任务
for (let j = 0; j < batchSize; j++) {
const taskIndex = i + j + 1;
// 为每个任务创建独立的日志回调
const taskLogCallback = (log) => {
if (logCallback) {
logCallback(`[窗口${taskIndex}] ${log}`);
}
};
// 每个窗口间隔启动,避免验证码混淆
const startDelay = j * 3000; // 每个窗口延迟3秒启动
const task = (async () => {
await this.sleep(startDelay);
if (logCallback) {
logCallback(`\n[窗口${taskIndex}] 开始注册...`);
}
// 步骤进度回调 - 更新细粒度进度
const stepCallback = (step, totalSteps) => {
// 计算当前任务的进度贡献
const taskProgress = (step / totalSteps);
const overallProgress = ((completed + taskProgress) / count) * 100;
if (progressCallback) {
progressCallback({ current: completed + taskProgress, total: count, percent: Math.round(overallProgress) });
}
};
const result = await this.registerAccount(taskLogCallback, stepCallback);
completed++;
if (progressCallback) {
progressCallback({ current: completed, total: count, percent: Math.round((completed / count) * 100) });
}
if (logCallback) {
if (result.success) {
logCallback(`✅ [窗口${taskIndex}] 注册成功! 邮箱: ${result.email}`);
} else {
logCallback(`❌ [窗口${taskIndex}] 注册失败: ${result.error}`);
}
}
return result;
})();
batchTasks.push(task);
}
// 等待当前批次完成
const batchResults = await Promise.all(batchTasks);
results.push(...batchResults);
// 如果还有下一批,等待一段时间再开始
if (i + MAX_CONCURRENT < count) {
if (logCallback) {
logCallback(`\n⏸️ 等待10秒后开始下一批次...`);
}
await this.sleep(10000);
}
}
if (logCallback) {
const successCount = results.filter(r => r.success).length;
const failedCount = results.filter(r => !r.success).length;
logCallback(`\n========== 批量注册完成 ==========`);
logCallback(`✅ 成功: ${successCount}`);
logCallback(`❌ 失败: ${failedCount}`);
}
return results;
}
/**
* 延迟函数
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
module.exports = RegistrationBot;
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Windsurf Go</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+7172
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
{
"name": "windsurf-tool-vue",
"version": "2.0.0",
"description": "Windsurf Go - Vue 3 + Electron 现代化账号管理工具",
"main": "electron/main.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"electron:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
"electron:build": "vite build && electron-builder",
"electron:build:mac": "vite build && electron-builder --mac",
"electron:build:win": "vite build && electron-builder --win"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.5",
"pinia": "^2.1.7",
"element-plus": "^2.4.4",
"@element-plus/icons-vue": "^2.3.1",
"imap": "^0.8.19",
"mailparser": "^3.6.5",
"puppeteer": "^21.11.0",
"puppeteer-real-browser": "^1.3.8"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"vite": "^5.0.10",
"electron": "^27.1.0",
"electron-builder": "^24.9.1",
"concurrently": "^8.2.2",
"wait-on": "^7.2.0"
},
"pnpm": {
"onlyBuiltDependencies": ["electron", "esbuild", "puppeteer"]
},
"build": {
"appId": "com.windsurf.tool.vue",
"productName": "Windsurf-Tool",
"directories": {
"output": "dist-electron"
},
"files": [
"dist/**/*",
"electron/**/*",
"node_modules/**/*",
"!node_modules/.cache/**/*"
],
"asar": false,
"mac": {
"target": ["dmg", "zip"]
},
"win": {
"target": ["nsis", "portable"]
}
}
}
+4450
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
ignoredBuiltDependencies:
- electron
- esbuild
- puppeteer
- sleep
- vue-demi
+143
View File
@@ -0,0 +1,143 @@
<template>
<el-container class="app-container">
<el-aside width="220px" class="sidebar">
<div class="logo">
<span class="logo-icon"></span>
<span class="logo-text">Windsurf Go</span>
</div>
<nav class="nav-menu">
<router-link to="/accounts" class="nav-item" :class="{ active: currentRoute === '/accounts' }">
<el-icon><User /></el-icon>
<span>账号管理</span>
</router-link>
<router-link to="/register" class="nav-item" :class="{ active: currentRoute === '/register' }">
<el-icon><Plus /></el-icon>
<span>批量注册</span>
</router-link>
<router-link to="/switch" class="nav-item" :class="{ active: currentRoute === '/switch' }">
<el-icon><Switch /></el-icon>
<span>切换账号</span>
</router-link>
<router-link to="/settings" class="nav-item" :class="{ active: currentRoute === '/settings' }">
<el-icon><Setting /></el-icon>
<span>系统配置</span>
</router-link>
<router-link to="/tutorial" class="nav-item" :class="{ active: currentRoute === '/tutorial' }">
<el-icon><Document /></el-icon>
<span>使用教程</span>
</router-link>
</nav>
<!-- 主题切换 -->
<div class="theme-toggle" @click="themeStore.toggleTheme()">
<el-icon v-if="themeStore.isDark"><Sunny /></el-icon>
<el-icon v-else><Moon /></el-icon>
<span>{{ themeStore.isDark ? '浅色模式' : '深色模式' }}</span>
</div>
</el-aside>
<el-main class="main-content">
<router-view />
</el-main>
</el-container>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useThemeStore } from '@/stores/theme'
const route = useRoute()
const themeStore = useThemeStore()
const currentRoute = computed(() => route.path)
onMounted(() => {
themeStore.initTheme()
})
</script>
<style scoped>
.app-container {
height: 100vh;
background: var(--bg-primary);
}
.sidebar {
background: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
transition: background-color 0.3s, border-color 0.3s;
}
.logo {
padding: 24px 20px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid var(--border-color);
}
.logo-icon {
font-size: 24px;
}
.logo-text {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.nav-menu {
padding: 16px 12px;
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 8px;
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
transition: all 0.2s;
}
.nav-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.nav-item.active {
background: var(--primary-color);
color: #fff;
}
.theme-toggle {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 20px;
border-top: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.theme-toggle:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.main-content {
background: var(--bg-primary);
padding: 32px;
overflow-y: auto;
transition: background-color 0.3s;
}
</style>
+20
View File
@@ -0,0 +1,20 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
import './styles/global.css'
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')
+15
View File
@@ -0,0 +1,15 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
{ path: '/', redirect: '/accounts' },
{ path: '/accounts', name: 'Accounts', component: () => import('@/views/AccountsView.vue') },
{ path: '/register', name: 'Register', component: () => import('@/views/RegisterView.vue') },
{ path: '/switch', name: 'Switch', component: () => import('@/views/SwitchView.vue') },
{ path: '/settings', name: 'Settings', component: () => import('@/views/SettingsView.vue') },
{ path: '/tutorial', name: 'Tutorial', component: () => import('@/views/TutorialView.vue') }
]
export default createRouter({
history: createWebHashHistory(),
routes
})
+114
View File
@@ -0,0 +1,114 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
const { ipcRenderer } = window.require ? window.require('electron') : { ipcRenderer: null }
export const useAccountsStore = defineStore('accounts', () => {
const accounts = ref([])
const PRO_TRIAL_DAYS = 13
// 计算账号剩余天数
const getDaysRemaining = (createdAt) => {
const created = new Date(createdAt)
const now = new Date()
const diffTime = PRO_TRIAL_DAYS * 24 * 60 * 60 * 1000 - (now - created)
return Math.ceil(diffTime / (24 * 60 * 60 * 1000))
}
// 获取账号状态
const getStatus = (daysRemaining) => {
if (daysRemaining <= 0) return 'expired'
if (daysRemaining <= 3) return 'warning'
return 'active'
}
// 统计数据
const stats = computed(() => {
const total = accounts.value.length
let active = 0, warning = 0, expired = 0
accounts.value.forEach(acc => {
const days = getDaysRemaining(acc.createdAt)
const status = getStatus(days)
if (status === 'active') active++
else if (status === 'warning') warning++
else expired++
})
return { total, active, warning, expired }
})
// 从 Electron 加载账号
const loadFromStorage = async () => {
if (ipcRenderer) {
accounts.value = await ipcRenderer.invoke('get-accounts')
} else {
// 浏览器环境使用 localStorage
const data = localStorage.getItem('windsurf_accounts')
if (data) accounts.value = JSON.parse(data)
}
}
// 添加账号
const addAccount = async (account) => {
if (ipcRenderer) {
const result = await ipcRenderer.invoke('add-account', account)
if (result.success) {
accounts.value = result.accounts
}
return result
} else {
accounts.value.push({
id: Date.now().toString(),
...account,
createdAt: account.createdAt || new Date().toISOString()
})
localStorage.setItem('windsurf_accounts', JSON.stringify(accounts.value))
return { success: true }
}
}
// 删除账号
const removeAccount = async (id) => {
if (ipcRenderer) {
const result = await ipcRenderer.invoke('delete-account', id)
if (result.success) {
accounts.value = result.accounts
}
return result
} else {
accounts.value = accounts.value.filter(acc => acc.id !== id)
localStorage.setItem('windsurf_accounts', JSON.stringify(accounts.value))
return { success: true }
}
}
// 更新账号信息
const updateAccount = async (id, updates) => {
if (ipcRenderer) {
const result = await ipcRenderer.invoke('update-account', { id, updates })
if (result.success) {
accounts.value = result.accounts
}
return result
} else {
const index = accounts.value.findIndex(acc => acc.id === id)
if (index !== -1) {
accounts.value[index] = { ...accounts.value[index], ...updates }
localStorage.setItem('windsurf_accounts', JSON.stringify(accounts.value))
}
return { success: true }
}
}
return {
accounts,
stats,
getDaysRemaining,
getStatus,
addAccount,
removeAccount,
updateAccount,
loadFromStorage
}
})
+39
View File
@@ -0,0 +1,39 @@
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
export const useThemeStore = defineStore('theme', () => {
const isDark = ref(false)
// 初始化主题
const initTheme = () => {
const saved = localStorage.getItem('windsurf_theme')
if (saved) {
isDark.value = saved === 'dark'
} else {
// 跟随系统
isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches
}
applyTheme()
}
// 应用主题
const applyTheme = () => {
document.documentElement.setAttribute('data-theme', isDark.value ? 'dark' : 'light')
}
// 切换主题
const toggleTheme = () => {
isDark.value = !isDark.value
localStorage.setItem('windsurf_theme', isDark.value ? 'dark' : 'light')
applyTheme()
}
// 监听变化
watch(isDark, applyTheme)
return {
isDark,
initTheme,
toggleTheme
}
})
+121
View File
@@ -0,0 +1,121 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
transition: background-color 0.3s, color 0.3s;
}
/* 浅色主题变量 */
:root,
[data-theme="light"] {
--bg-primary: #f5f7fa;
--bg-secondary: #ffffff;
--bg-tertiary: #f9fafb;
--bg-hover: #f3f4f6;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-muted: #9ca3af;
--border-color: #e5e7eb;
--primary-color: #2563eb;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--sidebar-bg: #ffffff;
--card-bg: #ffffff;
}
/* 深色主题变量 */
[data-theme="dark"] {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--bg-hover: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border-color: #334155;
--primary-color: #3b82f6;
--success-color: #22c55e;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--sidebar-bg: #1e293b;
--card-bg: #1e293b;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
/* Element Plus 深色模式覆盖 */
[data-theme="dark"] .el-table {
--el-table-bg-color: var(--card-bg);
--el-table-tr-bg-color: var(--card-bg);
--el-table-header-bg-color: var(--bg-tertiary);
--el-table-row-hover-bg-color: var(--bg-hover);
--el-table-border-color: var(--border-color);
--el-table-text-color: var(--text-primary);
--el-table-header-text-color: var(--text-secondary);
}
[data-theme="dark"] .el-input__wrapper {
background-color: var(--bg-tertiary);
box-shadow: 0 0 0 1px var(--border-color) inset;
}
[data-theme="dark"] .el-input__inner {
color: var(--text-primary);
}
[data-theme="dark"] .el-select__wrapper {
background-color: var(--bg-tertiary);
box-shadow: 0 0 0 1px var(--border-color) inset;
}
[data-theme="dark"] .el-dialog {
--el-dialog-bg-color: var(--bg-secondary);
--el-dialog-title-font-size: 16px;
}
[data-theme="dark"] .el-dialog__title {
color: var(--text-primary);
}
[data-theme="dark"] .el-form-item__label {
color: var(--text-secondary);
}
[data-theme="dark"] .el-button--default {
--el-button-bg-color: var(--bg-tertiary);
--el-button-border-color: var(--border-color);
--el-button-text-color: var(--text-primary);
--el-button-hover-bg-color: var(--bg-hover);
}
[data-theme="dark"] .el-input-number {
--el-input-bg-color: var(--bg-tertiary);
}
[data-theme="dark"] .el-menu {
--el-menu-bg-color: transparent;
--el-menu-text-color: var(--text-secondary);
--el-menu-active-color: var(--primary-color);
--el-menu-hover-bg-color: var(--bg-hover);
}
[data-theme="dark"] .el-popper.is-light {
background: var(--bg-secondary);
border-color: var(--border-color);
}
[data-theme="dark"] .el-select-dropdown__item {
color: var(--text-primary);
}
[data-theme="dark"] .el-select-dropdown__item.hover {
background-color: var(--bg-hover);
}
+649
View File
@@ -0,0 +1,649 @@
<template>
<div class="accounts-view">
<div class="page-header">
<h1>账号管理</h1>
<div class="header-actions">
<el-button @click="refreshList">
<el-icon><Refresh /></el-icon> 刷新
</el-button>
<el-button @click="showImportDialog = true">
<el-icon><Upload /></el-icon> 导入
</el-button>
<el-button type="primary" @click="showAddDialog = true">
<el-icon><Plus /></el-icon> 添加账号
</el-button>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ store.stats.total }}</div>
<div class="stat-label">总账号</div>
</div>
<div class="stat-card success">
<div class="stat-value">{{ store.stats.active }}</div>
<div class="stat-label">可用</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ store.stats.warning }}</div>
<div class="stat-label">即将到期</div>
</div>
<div class="stat-card danger">
<div class="stat-value">{{ store.stats.expired }}</div>
<div class="stat-label">已到期</div>
</div>
</div>
<!-- 账号列表 -->
<div class="card">
<el-table :data="accountsWithStatus" style="width: 100%" empty-text="暂无账号">
<el-table-column prop="email" label="邮箱" min-width="200" />
<el-table-column label="密码" min-width="140">
<template #header>
<div class="password-header">
<span>密码</span>
<el-icon class="toggle-icon" @click="showPassword = !showPassword">
<View v-if="showPassword" />
<Hide v-else />
</el-icon>
</div>
</template>
<template #default="{ row }">
<span>{{ showPassword ? row.password : '••••••••' }}</span>
</template>
</el-table-column>
<el-table-column label="绑卡" width="80" align="center">
<template #default="{ row }">
<span :class="['bindcard-badge', row.cardBound ? 'bound' : 'unbound']">
{{ row.cardBound ? '已绑' : '未绑' }}
</span>
</template>
</el-table-column>
<el-table-column label="到期日期" width="120">
<template #default="{ row }">
<span class="expire-date">{{ formatExpireDate(row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<span :class="['status-badge', row.status]">
{{ row.daysRemaining > 0 ? `${row.daysRemaining}` : '已到期' }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<div class="action-buttons">
<el-tooltip content="复制账号" placement="top">
<el-button link type="primary" @click="copyAccount(row)">
<el-icon :size="18"><CopyDocument /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="绑定银行卡" placement="top">
<el-button link type="warning" @click="bindCard(row)">
<el-icon :size="18"><CreditCard /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="删除账号" placement="top">
<el-button link type="danger" @click="deleteAccount(row.id)">
<el-icon :size="18"><Delete /></el-icon>
</el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 添加账号对话框 -->
<el-dialog v-model="showAddDialog" title="添加账号" width="400px">
<el-form :model="newAccount" label-width="80px">
<el-form-item label="邮箱">
<el-input v-model="newAccount.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="newAccount.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="注册日期">
<el-date-picker
v-model="newAccount.createdAt"
type="date"
placeholder="选择日期(默认今天)"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddDialog = false">取消</el-button>
<el-button type="primary" @click="handleAddAccount">确定</el-button>
</template>
</el-dialog>
<!-- 导入账号对话框 -->
<el-dialog v-model="showImportDialog" title="导入账号" width="500px">
<el-form label-width="80px">
<el-form-item label="格式说明">
<div class="import-hint">
每行一个账号格式<code>邮箱,密码</code> <code>邮箱----密码</code>
</div>
</el-form-item>
<el-form-item label="账号数据">
<el-input
v-model="importText"
type="textarea"
:rows="8"
placeholder="user1@example.com,password1&#10;user2@example.com----password2"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showImportDialog = false">取消</el-button>
<el-button type="primary" @click="handleImport">导入</el-button>
</template>
</el-dialog>
<!-- 绑卡日志对话框 -->
<el-dialog v-model="showBindCardLogDialog" title="绑卡日志" width="600px">
<div class="bind-card-logs">
<div v-for="(log, index) in bindCardLogs" :key="index" class="log-item">
<span class="log-time">{{ log.time }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
<div v-if="bindCardLogs.length === 0" class="no-logs">暂无日志</div>
</div>
<template #footer>
<el-button @click="bindCardLogs = []; showBindCardLogDialog = false">清空并关闭</el-button>
<el-button type="primary" @click="showBindCardLogDialog = false">关闭</el-button>
</template>
</el-dialog>
<!-- 绑定银行卡对话框 -->
<el-dialog v-model="showBindCardDialog" title="绑定银行卡" width="500px">
<div class="bind-card-info">
<p>当前账号: <strong>{{ currentBindAccount?.email }}</strong></p>
</div>
<el-form :model="cardInfo" label-width="100px">
<el-form-item label="输入方式">
<el-radio-group v-model="cardInputMode">
<el-radio value="bin">BIN头生成</el-radio>
<el-radio value="full">完整卡号</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="cardInputMode === 'bin'">
<el-form-item label="BIN头">
<el-input v-model="cardInfo.bin" placeholder="请输入6-8位BIN头,如 453256" maxlength="8" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="卡号">
<el-input v-model="cardInfo.cardNumber" placeholder="请输入16位卡号" maxlength="19" />
</el-form-item>
<el-form-item label="有效期">
<div class="expire-inputs">
<el-input v-model="cardInfo.expMonth" placeholder="MM" maxlength="2" style="width: 80px" />
<span class="expire-separator">/</span>
<el-input v-model="cardInfo.expYear" placeholder="YY" maxlength="2" style="width: 80px" />
</div>
</el-form-item>
<el-form-item label="CVV">
<el-input v-model="cardInfo.cvv" placeholder="3位CVV" maxlength="4" style="width: 100px" />
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="showBindCardDialog = false">取消</el-button>
<el-button type="primary" @click="handleBindCard">绑定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useAccountsStore } from '@/stores/accounts'
const { ipcRenderer } = window.require ? window.require('electron') : { ipcRenderer: null }
const store = useAccountsStore()
const showAddDialog = ref(false)
const showImportDialog = ref(false)
const showBindCardDialog = ref(false)
const showPassword = ref(false)
const newAccount = ref({ email: '', password: '', createdAt: null })
const importText = ref('')
const currentBindAccount = ref(null)
const cardInputMode = ref('bin')
const cardInfo = ref({
bin: '',
cardNumber: '',
expMonth: '',
expYear: '',
cvv: ''
})
const PRO_TRIAL_DAYS = 13
const bindCardLogs = ref([])
const showBindCardLogDialog = ref(false)
onMounted(() => {
store.loadFromStorage()
// 监听绑卡日志
if (ipcRenderer) {
ipcRenderer.on('bind-card-log', (event, message) => {
bindCardLogs.value.push({
time: new Date().toLocaleTimeString(),
message
})
})
}
})
const accountsWithStatus = computed(() => {
return store.accounts.map(acc => ({
...acc,
daysRemaining: store.getDaysRemaining(acc.createdAt),
status: store.getStatus(store.getDaysRemaining(acc.createdAt)),
cardBound: acc.cardBound || false
}))
})
// 格式化到期日期
const formatExpireDate = (createdAt) => {
const created = new Date(createdAt)
const expireDate = new Date(created.getTime() + PRO_TRIAL_DAYS * 24 * 60 * 60 * 1000)
return expireDate.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
}
// 刷新列表
const refreshList = async () => {
await store.loadFromStorage()
ElMessage.success('已刷新')
}
// 添加账号
const handleAddAccount = async () => {
if (!newAccount.value.email || !newAccount.value.password) {
ElMessage.warning('请填写完整信息')
return
}
const accountData = {
email: newAccount.value.email,
password: newAccount.value.password
}
// 如果指定了日期,使用指定日期
if (newAccount.value.createdAt) {
accountData.createdAt = new Date(newAccount.value.createdAt).toISOString()
}
await store.addAccount(accountData)
newAccount.value = { email: '', password: '', createdAt: null }
showAddDialog.value = false
ElMessage.success('添加成功')
}
// 导入账号
const handleImport = async () => {
if (!importText.value.trim()) {
ElMessage.warning('请输入账号数据')
return
}
const lines = importText.value.trim().split('\n')
let successCount = 0
let failCount = 0
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed) continue
let email, password
// 支持多种分隔符
if (trimmed.includes('----')) {
[email, password] = trimmed.split('----')
} else if (trimmed.includes(',')) {
[email, password] = trimmed.split(',')
} else if (trimmed.includes('\t')) {
[email, password] = trimmed.split('\t')
} else {
failCount++
continue
}
email = email?.trim()
password = password?.trim()
if (email && password) {
await store.addAccount({ email, password })
successCount++
} else {
failCount++
}
}
importText.value = ''
showImportDialog.value = false
if (successCount > 0) {
ElMessage.success(`成功导入 ${successCount} 个账号${failCount > 0 ? `${failCount} 个失败` : ''}`)
} else {
ElMessage.error('导入失败,请检查格式')
}
}
// 复制账号
const copyAccount = (account) => {
navigator.clipboard.writeText(`${account.email}\n${account.password}`)
ElMessage.success('已复制到剪贴板')
}
// 绑定银行卡 - 打开对话框
const bindCard = (account) => {
currentBindAccount.value = account
// 重置表单
cardInputMode.value = 'bin'
cardInfo.value = {
bin: '',
cardNumber: '',
expMonth: '',
expYear: '',
cvv: ''
}
showBindCardDialog.value = true
}
// 处理绑卡
const isBindingCard = ref(false)
const handleBindCard = async () => {
if (cardInputMode.value === 'bin') {
if (!cardInfo.value.bin || cardInfo.value.bin.length < 6) {
ElMessage.warning('请输入至少6位BIN头')
return
}
} else {
if (!cardInfo.value.cardNumber || cardInfo.value.cardNumber.length < 15) {
ElMessage.warning('请输入完整卡号')
return
}
if (!cardInfo.value.expMonth || !cardInfo.value.expYear) {
ElMessage.warning('请输入有效期')
return
}
if (!cardInfo.value.cvv || cardInfo.value.cvv.length < 3) {
ElMessage.warning('请输入CVV')
return
}
}
if (!ipcRenderer) {
ElMessage.warning('此功能仅在桌面应用中可用')
return
}
isBindingCard.value = true
showBindCardDialog.value = false
bindCardLogs.value = [] // 清空之前的日志
showBindCardLogDialog.value = true // 显示日志对话框
ElMessage.info('正在启动浏览器登录...')
try {
const result = await ipcRenderer.invoke('bind-card-login', {
account: {
email: currentBindAccount.value.email,
password: currentBindAccount.value.password
},
cardInfo: {
mode: cardInputMode.value,
...cardInfo.value
}
})
if (result.success) {
ElMessage.success(result.message || '绑卡成功')
// 更新账号绑卡状态
await store.updateAccount(currentBindAccount.value.id, {
cardBound: true,
cardBoundAt: new Date().toISOString()
})
} else if (result.submitted) {
// 表单已提交,但需要手动确认结果
ElMessage.warning(result.message || '表单已提交,请手动确认绑卡结果')
// 不自动更新绑卡状态
} else {
ElMessage.error(result.error || '操作失败')
}
} catch (error) {
ElMessage.error('绑卡失败: ' + error.message)
} finally {
isBindingCard.value = false
}
}
// 删除账号
const deleteAccount = async (id) => {
try {
await ElMessageBox.confirm('确定要删除这个账号吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await store.removeAccount(id)
ElMessage.success('删除成功')
} catch {
// 取消删除
}
}
</script>
<style scoped>
.accounts-view {
max-width: 1000px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-header h1 {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.header-actions {
display: flex;
gap: 12px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: var(--card-bg);
border-radius: 12px;
padding: 20px;
text-align: center;
border: 1px solid var(--border-color);
transition: background-color 0.3s, border-color 0.3s;
}
.stat-value {
font-size: 32px;
font-weight: 600;
color: var(--text-primary);
}
.stat-card.success .stat-value { color: var(--success-color); }
.stat-card.warning .stat-value { color: var(--warning-color); }
.stat-card.danger .stat-value { color: var(--danger-color); }
.stat-label {
font-size: 14px;
color: var(--text-secondary);
margin-top: 4px;
}
.card {
background: var(--card-bg);
border-radius: 12px;
padding: 20px;
border: 1px solid var(--border-color);
transition: background-color 0.3s, border-color 0.3s;
}
.expire-date {
font-size: 13px;
color: var(--text-secondary);
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-badge.active {
background: #d1fae5;
color: #059669;
}
.status-badge.warning {
background: #fef3c7;
color: #d97706;
}
.status-badge.expired {
background: #fee2e2;
color: #dc2626;
}
.import-hint {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
}
.import-hint code {
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
.password-header {
display: flex;
align-items: center;
gap: 8px;
}
.toggle-icon {
cursor: pointer;
color: var(--text-muted);
transition: color 0.2s;
}
.toggle-icon:hover {
color: var(--primary-color);
}
.bindcard-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.bindcard-badge.bound {
background: #d1fae5;
color: #059669;
}
.bindcard-badge.unbound {
background: #f3f4f6;
color: #9ca3af;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 4px;
}
.bind-card-info {
margin-bottom: 16px;
padding: 12px;
background: var(--bg-tertiary);
border-radius: 8px;
}
.bind-card-info p {
margin: 0;
color: var(--text-secondary);
}
.expire-inputs {
display: flex;
align-items: center;
gap: 8px;
}
.expire-separator {
color: var(--text-secondary);
font-size: 16px;
}
.bind-card-logs {
max-height: 400px;
overflow-y: auto;
background: var(--bg-tertiary);
border-radius: 8px;
padding: 12px;
}
.log-item {
display: flex;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid var(--border-color);
font-size: 13px;
}
.log-item:last-child {
border-bottom: none;
}
.log-time {
color: var(--text-muted);
flex-shrink: 0;
font-family: monospace;
}
.log-message {
color: var(--text-primary);
word-break: break-all;
}
.no-logs {
text-align: center;
color: var(--text-muted);
padding: 20px;
}
</style>
+447
View File
@@ -0,0 +1,447 @@
<template>
<div class="register-view">
<div class="page-header">
<h1>批量注册</h1>
<div class="config-status" :class="{ error: !hasConfig }">
<el-icon v-if="hasConfig"><CircleCheck /></el-icon>
<el-icon v-else><Warning /></el-icon>
<span>{{ hasConfig ? '配置就绪' : '请先配置邮箱域名' }}</span>
</div>
</div>
<!-- 注册设置卡片 -->
<div class="register-card">
<div class="register-header">
<div class="register-info">
<h3>自动批量注册</h3>
<p>自动生成邮箱注册账号接收验证码</p>
</div>
</div>
<div class="register-form">
<div class="form-item">
<span class="form-label">注册数量</span>
<div class="number-input">
<button class="num-btn" @click="registerCount > 1 && registerCount--"></button>
<input type="number" v-model.number="registerCount" min="1" max="10" />
<button class="num-btn" @click="registerCount < 10 && registerCount++">+</button>
</div>
<span class="form-hint">建议 1-10 </span>
</div>
<div class="form-item">
<span class="form-label">无头模式</span>
<el-switch v-model="headlessMode" />
<el-tooltip content="无头模式不显示浏览器窗口,但可能被 Cloudflare 检测" placement="top">
<el-icon class="help-icon"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
<el-button
type="primary"
size="large"
:loading="isRegistering"
:disabled="!hasConfig"
@click="startRegister"
class="register-btn"
>
<el-icon v-if="!isRegistering"><VideoPlay /></el-icon>
{{ isRegistering ? '注册中...' : '开始批量注册' }}
</el-button>
</div>
<!-- 进度条 -->
<div v-if="isRegistering" class="progress-section">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
</div>
<span class="progress-text">{{ progress }}%</span>
</div>
</div>
<!-- 注册日志 -->
<div class="log-card">
<div class="log-header">
<span class="log-title">📋 注册日志</span>
<el-button v-if="logs.length" link type="primary" @click="logs = []">清空</el-button>
</div>
<div class="log-content" ref="logContainer">
<div v-if="!logs.length" class="log-empty">
暂无日志点击开始注册后显示
</div>
<div v-for="(log, index) in logs" :key="index" :class="['log-item', log.type]">
<span class="log-time">{{ log.time }}</span>
<span class="log-msg">{{ log.message }}</span>
</div>
</div>
</div>
<!-- 使用提示 -->
<div class="tips-card">
<div class="tip-item">
<el-icon><InfoFilled /></el-icon>
<span>每个账号注册约需 1-2 分钟请耐心等待</span>
</div>
<div class="tip-item">
<el-icon><InfoFilled /></el-icon>
<span>注册前请确保已在配置页面设置好邮箱域名和 IMAP</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { useAccountsStore } from '@/stores/accounts'
const { ipcRenderer } = window.require ? window.require('electron') : { ipcRenderer: null }
const store = useAccountsStore()
const registerCount = ref(1)
const headlessMode = ref(false)
const isRegistering = ref(false)
const logs = ref([])
const logContainer = ref(null)
const progress = ref(0)
const hasConfig = ref(true)
// 检查配置
const checkConfig = async () => {
if (ipcRenderer) {
const config = await ipcRenderer.invoke('get-config')
hasConfig.value = config.domains && config.domains.length > 0 && config.imap && config.imap.host
}
}
onMounted(() => {
checkConfig()
})
const addLog = (message, type = 'info') => {
const time = new Date().toLocaleTimeString()
logs.value.push({ time, message, type })
nextTick(() => {
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight
}
})
}
const startRegister = async () => {
if (isRegistering.value) return
isRegistering.value = true
logs.value = []
progress.value = 0
addLog(`开始注册 ${registerCount.value} 个账号...`)
if (ipcRenderer) {
// 确保参数是普通值,避免 Vue Proxy 序列化问题
const params = {
count: Number(registerCount.value),
headless: Boolean(headlessMode.value)
}
console.log('发送注册请求,参数:', params)
const result = await ipcRenderer.invoke('batch-register', params)
if (result.success) {
const successCount = result.results.filter(r => r.success).length
addLog(`批量注册完成!成功 ${successCount}`, 'success')
await store.loadFromStorage()
ElMessage.success(`注册完成,成功 ${successCount}`)
} else {
addLog(`注册失败: ${result.error}`, 'error')
ElMessage.error(result.error)
}
} else {
for (let i = 0; i < registerCount.value; i++) {
addLog(`正在注册第 ${i + 1} 个账号...`)
await new Promise(r => setTimeout(r, 1000))
progress.value = Math.round(((i + 1) / registerCount.value) * 100)
const randomStr = Math.random().toString(36).substring(2, 8)
const email = `user_${randomStr}@example.com`
const password = `pass_${randomStr}`
await store.addAccount({ email, password })
addLog(`账号 ${email} 注册成功`, 'success')
}
addLog('批量注册完成!', 'success')
ElMessage.success('注册完成')
}
progress.value = 100
isRegistering.value = false
}
onMounted(() => {
if (ipcRenderer) {
ipcRenderer.on('register-log', (event, message) => {
let type = 'info'
if (message.includes('✓') || message.includes('成功')) type = 'success'
else if (message.includes('✗') || message.includes('失败') || message.includes('❌')) type = 'error'
addLog(message, type)
})
ipcRenderer.on('register-progress', (event, data) => {
// 支持细粒度进度更新
if (data.percent !== undefined) {
progress.value = data.percent
} else {
progress.value = Math.round((data.current / data.total) * 100)
}
})
}
})
onUnmounted(() => {
if (ipcRenderer) {
ipcRenderer.removeAllListeners('register-log')
ipcRenderer.removeAllListeners('register-progress')
}
})
</script>
<style scoped>
.register-view {
max-width: 700px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-header h1 {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.config-status {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 20px;
font-size: 13px;
background: #d1fae5;
color: #059669;
}
.config-status.error {
background: #fee2e2;
color: #dc2626;
}
.register-card {
background: var(--card-bg);
border-radius: 16px;
padding: 28px;
border: 1px solid var(--border-color);
margin-bottom: 20px;
transition: background-color 0.3s, border-color 0.3s;
}
.register-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.register-info h3 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.register-info p {
font-size: 13px;
color: var(--text-secondary);
}
.register-form {
display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
}
.form-item {
display: flex;
align-items: center;
gap: 12px;
}
.form-label {
font-size: 14px;
color: var(--text-secondary);
}
.number-input {
display: flex;
align-items: center;
background: var(--bg-tertiary);
border-radius: 8px;
overflow: hidden;
}
.number-input input {
width: 60px;
text-align: center;
border: none;
background: transparent;
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
padding: 8px 0;
}
.number-input input:focus {
outline: none;
}
.num-btn {
width: 36px;
height: 36px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 18px;
cursor: pointer;
transition: all 0.2s;
}
.num-btn:hover {
background: var(--bg-hover);
color: var(--primary-color);
}
.form-hint {
font-size: 12px;
color: var(--text-muted);
}
.help-icon {
color: var(--text-muted);
cursor: help;
font-size: 16px;
}
.register-btn {
margin-left: auto;
padding: 12px 28px;
font-size: 15px;
}
.progress-section {
display: flex;
align-items: center;
gap: 12px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
}
.progress-bar {
flex: 1;
height: 8px;
background: var(--bg-tertiary);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #2563eb);
border-radius: 4px;
transition: width 0.3s;
}
.progress-text {
font-size: 13px;
font-weight: 500;
color: var(--primary-color);
min-width: 40px;
}
.log-card {
background: var(--card-bg);
border-radius: 16px;
border: 1px solid var(--border-color);
margin-bottom: 20px;
overflow: hidden;
transition: background-color 0.3s, border-color 0.3s;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
}
.log-title {
font-size: 15px;
font-weight: 500;
color: var(--text-primary);
}
.log-content {
max-height: 280px;
overflow-y: auto;
padding: 16px 20px;
}
.log-empty {
text-align: center;
color: var(--text-muted);
font-size: 13px;
padding: 40px 0;
}
.log-item {
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
font-size: 13px;
display: flex;
gap: 12px;
}
.log-item:last-child {
border-bottom: none;
}
.log-item.success .log-msg { color: var(--success-color); }
.log-item.error .log-msg { color: var(--danger-color); }
.log-time { color: var(--text-muted); font-family: monospace; font-size: 12px; }
.log-msg { color: var(--text-primary); flex: 1; }
.tips-card {
background: var(--bg-tertiary);
border-radius: 12px;
padding: 16px 20px;
}
.tip-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-secondary);
padding: 6px 0;
}
.tip-item .el-icon {
color: var(--primary-color);
}
</style>
+377
View File
@@ -0,0 +1,377 @@
<template>
<div class="settings-view">
<div class="page-header">
<h1>系统配置</h1>
</div>
<!-- 邮箱域名配置 -->
<div class="config-card">
<div class="card-header">
<div class="header-icon">📧</div>
<div class="header-info">
<h3>邮箱域名</h3>
<p>配置 Cloudflare Email Routing 的域名用于生成注册邮箱</p>
</div>
</div>
<div class="domain-section">
<div class="domain-list" v-if="config.domains.length">
<span v-for="domain in config.domains" :key="domain" class="domain-tag">
{{ domain }}
<span class="remove-btn" @click="removeDomain(domain)">×</span>
</span>
</div>
<div v-else class="empty-hint">暂未添加域名</div>
<div class="add-domain">
<el-input v-model="newDomain" placeholder="输入域名,如 example.com" />
<el-button type="primary" @click="addDomain">添加</el-button>
</div>
</div>
</div>
<!-- IMAP 配置 -->
<div class="config-card">
<div class="card-header">
<div class="header-icon">📬</div>
<div class="header-info">
<h3>IMAP 邮箱配置</h3>
<p>配置接收验证码的邮箱Cloudflare 转发的目标邮箱</p>
</div>
</div>
<div class="form-section">
<div class="form-row">
<div class="form-item flex-2">
<label>IMAP 服务器</label>
<el-input v-model="config.imap.host" placeholder="如 imap.qq.com" />
</div>
<div class="form-item flex-1">
<label>端口</label>
<el-input-number v-model="config.imap.port" :min="1" :max="65535" style="width: 100%" />
</div>
</div>
<div class="form-row">
<div class="form-item flex-1">
<label>邮箱账号</label>
<el-input v-model="config.imap.user" placeholder="your@qq.com" />
</div>
<div class="form-item flex-1">
<label>密码 / 授权码</label>
<el-input v-model="config.imap.password" type="password" show-password placeholder="授权码" />
</div>
</div>
<div class="form-actions">
<el-button @click="testConnection" :loading="testing">
<el-icon v-if="!testing"><Connection /></el-icon>
测试连接
</el-button>
<el-button type="primary" @click="saveConfig">
<el-icon><Check /></el-icon>
保存配置
</el-button>
</div>
</div>
</div>
<!-- 常用邮箱配置参考 -->
<div class="config-card">
<div class="card-header">
<div class="header-icon">💡</div>
<div class="header-info">
<h3>常用邮箱 IMAP 配置</h3>
<p>点击快速填充服务器地址和端口</p>
</div>
</div>
<div class="preset-grid">
<div class="preset-item" @click="applyPreset('qq')">
<span class="preset-name">QQ 邮箱</span>
<span class="preset-host">imap.qq.com:993</span>
</div>
<div class="preset-item" @click="applyPreset('gmail')">
<span class="preset-name">Gmail</span>
<span class="preset-host">imap.gmail.com:993</span>
</div>
<div class="preset-item" @click="applyPreset('163')">
<span class="preset-name">163 邮箱</span>
<span class="preset-host">imap.163.com:993</span>
</div>
<div class="preset-item" @click="applyPreset('outlook')">
<span class="preset-name">Outlook</span>
<span class="preset-host">outlook.office365.com:993</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
const { ipcRenderer } = window.require ? window.require('electron') : { ipcRenderer: null }
const config = ref({
domains: [],
imap: { host: '', port: 993, user: '', password: '' }
})
const newDomain = ref('')
const testing = ref(false)
const presets = {
qq: { host: 'imap.qq.com', port: 993 },
gmail: { host: 'imap.gmail.com', port: 993 },
'163': { host: 'imap.163.com', port: 993 },
outlook: { host: 'outlook.office365.com', port: 993 }
}
onMounted(async () => {
if (ipcRenderer) {
config.value = await ipcRenderer.invoke('get-config')
} else {
const saved = localStorage.getItem('windsurf_config')
if (saved) config.value = JSON.parse(saved)
}
})
const addDomain = () => {
if (!newDomain.value) return
if (!config.value.domains.includes(newDomain.value)) {
config.value.domains.push(newDomain.value)
saveConfig()
}
newDomain.value = ''
}
const removeDomain = (domain) => {
config.value.domains = config.value.domains.filter(d => d !== domain)
saveConfig()
}
const saveConfig = async () => {
if (ipcRenderer) {
// 转换为普通对象,避免 Vue Proxy 序列化问题
const configData = JSON.parse(JSON.stringify(config.value))
const result = await ipcRenderer.invoke('save-config', configData)
if (result.success) {
ElMessage.success('配置已保存')
} else {
ElMessage.error(result.error)
}
} else {
localStorage.setItem('windsurf_config', JSON.stringify(config.value))
ElMessage.success('配置已保存')
}
}
const testConnection = async () => {
if (!config.value.imap.host || !config.value.imap.user || !config.value.imap.password) {
ElMessage.warning('请先填写完整的 IMAP 配置')
return
}
testing.value = true
try {
if (ipcRenderer) {
// 转换为普通对象,避免 Vue Proxy 序列化问题
const imapConfig = {
host: config.value.imap.host,
port: config.value.imap.port,
user: config.value.imap.user,
password: config.value.imap.password
}
const result = await ipcRenderer.invoke('test-imap', imapConfig)
if (result.success) {
ElMessage.success('连接测试成功')
} else {
ElMessage.error(result.message || '连接失败')
}
} else {
await new Promise(r => setTimeout(r, 1500))
ElMessage.success('连接测试成功(模拟)')
}
} catch (error) {
ElMessage.error('连接失败: ' + (error.message || '未知错误'))
} finally {
testing.value = false
}
}
const applyPreset = (name) => {
const preset = presets[name]
if (preset) {
config.value.imap.host = preset.host
config.value.imap.port = preset.port
ElMessage.success(`已应用 ${name.toUpperCase()} 配置`)
}
}
</script>
<style scoped>
.settings-view {
max-width: 700px;
margin: 0 auto;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.config-card {
background: var(--card-bg);
border-radius: 16px;
padding: 24px;
border: 1px solid var(--border-color);
margin-bottom: 20px;
transition: background-color 0.3s, border-color 0.3s;
}
.card-header {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.header-icon {
font-size: 28px;
}
.header-info h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.header-info p {
font-size: 13px;
color: var(--text-secondary);
}
.domain-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.domain-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.domain-tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: var(--bg-tertiary);
border-radius: 8px;
font-size: 13px;
color: var(--text-primary);
}
.remove-btn {
cursor: pointer;
color: var(--text-muted);
font-size: 16px;
line-height: 1;
transition: color 0.2s;
}
.remove-btn:hover {
color: var(--danger-color);
}
.empty-hint {
font-size: 13px;
color: var(--text-muted);
padding: 12px 0;
}
.add-domain {
display: flex;
gap: 12px;
}
.add-domain .el-input {
flex: 1;
}
.form-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-row {
display: flex;
gap: 16px;
}
.form-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-item.flex-1 { flex: 1; }
.form-item.flex-2 { flex: 2; }
.form-item label {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 8px;
}
.preset-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.preset-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 18px;
background: var(--bg-tertiary);
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
}
.preset-item:hover {
background: var(--bg-hover);
transform: translateY(-1px);
}
.preset-name {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
}
.preset-host {
font-size: 12px;
color: var(--text-muted);
font-family: monospace;
}
</style>
+453
View File
@@ -0,0 +1,453 @@
<template>
<div class="switch-view">
<div class="page-header">
<h1>切换账号</h1>
</div>
<!-- 账号选择卡片 -->
<div class="switch-card">
<div class="card-header">
<h3>选择要切换的账号</h3>
<p>选择一个账号后点击自动切换系统将自动完成登录</p>
</div>
<div class="account-selector">
<el-select
v-model="selectedAccount"
placeholder="请选择账号"
size="large"
style="width: 100%"
>
<el-option
v-for="acc in availableAccounts"
:key="acc.id"
:label="acc.email"
:value="acc.id"
>
<div class="account-option">
<span class="option-email">{{ acc.email }}</span>
<span :class="['option-badge', acc.status]">
{{ acc.daysRemaining > 0 ? `${acc.daysRemaining}` : '已到期' }}
</span>
</div>
</el-option>
</el-select>
</div>
<!-- 选中账号信息 -->
<div v-if="selectedAccountInfo" class="selected-info">
<div class="info-item">
<span class="info-label">邮箱</span>
<span class="info-value">{{ selectedAccountInfo.email }}</span>
</div>
<div class="info-item">
<span class="info-label">状态</span>
<span :class="['status-tag', selectedAccountInfo.status]">
{{ selectedAccountInfo.daysRemaining > 0 ? `剩余 ${selectedAccountInfo.daysRemaining}` : '已到期' }}
</span>
</div>
</div>
<div class="action-buttons">
<el-button
type="primary"
size="large"
:loading="isSwitching"
:disabled="!selectedAccount"
@click="switchAccount"
>
<el-icon v-if="!isSwitching"><VideoPlay /></el-icon>
{{ isSwitching ? '切换中...' : '自动切换账号' }}
</el-button>
<el-button
size="large"
:loading="isResetting"
@click="resetOnly"
>
<el-icon v-if="!isResetting"><RefreshRight /></el-icon>
{{ isResetting ? '重置中...' : '仅重置配置' }}
</el-button>
</div>
</div>
<!-- 切换流程说明 -->
<div class="process-card">
<div class="process-title">切换流程</div>
<div class="process-steps">
<div class="step-item">
<div class="step-num">1</div>
<div class="step-text">重置 Windsurf 配置和机器码</div>
</div>
<div class="step-arrow"></div>
<div class="step-item">
<div class="step-num">2</div>
<div class="step-text">启动 Windsurf 应用</div>
</div>
<div class="step-arrow"></div>
<div class="step-item">
<div class="step-num">3</div>
<div class="step-text">自动填写登录信息</div>
</div>
<div class="step-arrow"></div>
<div class="step-item">
<div class="step-num">4</div>
<div class="step-text">完成账号切换</div>
</div>
</div>
</div>
<!-- 切换日志 -->
<div v-if="logs.length" class="log-card">
<div class="log-header">
<span class="log-title">📋 切换日志</span>
<el-button link type="primary" @click="logs = []">清空</el-button>
</div>
<div class="log-content" ref="logContainer">
<div v-for="(log, index) in logs" :key="index" :class="['log-item', log.type]">
<span class="log-time">{{ log.time }}</span>
<span class="log-msg">{{ log.message }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { useAccountsStore } from '@/stores/accounts'
const { ipcRenderer } = window.require ? window.require('electron') : { ipcRenderer: null }
const store = useAccountsStore()
const selectedAccount = ref('')
const isSwitching = ref(false)
const isResetting = ref(false)
const logs = ref([])
const logContainer = ref(null)
onMounted(() => {
store.loadFromStorage()
})
const availableAccounts = computed(() => {
return store.accounts.map(acc => ({
...acc,
daysRemaining: store.getDaysRemaining(acc.createdAt),
status: store.getStatus(store.getDaysRemaining(acc.createdAt))
}))
})
const selectedAccountInfo = computed(() => {
if (!selectedAccount.value) return null
return availableAccounts.value.find(a => a.id === selectedAccount.value)
})
const addLog = (message, type = 'info') => {
const time = new Date().toLocaleTimeString()
logs.value.push({ time, message, type })
nextTick(() => {
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight
}
})
}
const switchAccount = async () => {
if (!selectedAccount.value || isSwitching.value) return
const account = store.accounts.find(a => a.id === selectedAccount.value)
if (!account) return
isSwitching.value = true
logs.value = []
if (ipcRenderer) {
addLog('开始切换账号...')
const result = await ipcRenderer.invoke('switch-account', account)
if (result.success) {
addLog('账号切换完成!', 'success')
ElMessage.success('切换完成')
} else {
addLog(`切换失败: ${result.error}`, 'error')
ElMessage.error(result.error)
}
} else {
addLog('开始切换账号...')
addLog('正在重置 Windsurf 配置...')
await new Promise(r => setTimeout(r, 1000))
addLog('正在清除机器码...')
await new Promise(r => setTimeout(r, 1000))
addLog('正在启动 Windsurf...')
await new Promise(r => setTimeout(r, 1000))
addLog(`正在登录账号: ${account.email}`)
await new Promise(r => setTimeout(r, 1000))
addLog('账号切换完成!', 'success')
ElMessage.success('切换完成')
}
isSwitching.value = false
}
const resetOnly = async () => {
if (isResetting.value) return
isResetting.value = true
logs.value = []
if (ipcRenderer) {
addLog('开始重置 Windsurf 配置...')
const result = await ipcRenderer.invoke('reset-windsurf')
if (result.success) {
addLog('重置完成!', 'success')
ElMessage.success('重置完成')
} else {
addLog(`重置失败: ${result.error}`, 'error')
ElMessage.error(result.error)
}
} else {
addLog('开始重置 Windsurf 配置...')
await new Promise(r => setTimeout(r, 2000))
addLog('重置完成!', 'success')
ElMessage.success('重置完成')
}
isResetting.value = false
}
onMounted(() => {
if (ipcRenderer) {
ipcRenderer.on('switch-log', (event, message) => {
let type = 'info'
if (message.includes('✓') || message.includes('成功') || message.includes('完成')) type = 'success'
else if (message.includes('✗') || message.includes('失败') || message.includes('❌')) type = 'error'
addLog(message, type)
})
}
})
onUnmounted(() => {
if (ipcRenderer) {
ipcRenderer.removeAllListeners('switch-log')
}
})
</script>
<style scoped>
.switch-view {
max-width: 800px;
margin: 0 auto;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.switch-card {
background: var(--card-bg);
border-radius: 16px;
padding: 28px;
border: 1px solid var(--border-color);
margin-bottom: 20px;
transition: background-color 0.3s, border-color 0.3s;
}
.card-header {
margin-bottom: 24px;
}
.card-header h3 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 6px;
}
.card-header p {
font-size: 13px;
color: var(--text-secondary);
}
.account-selector {
margin-bottom: 20px;
}
.account-option {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.option-email {
color: var(--text-primary);
}
.option-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
}
.option-badge.active { background: #d1fae5; color: #059669; }
.option-badge.warning { background: #fef3c7; color: #d97706; }
.option-badge.expired { background: #fee2e2; color: #dc2626; }
.selected-info {
background: var(--bg-tertiary);
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 24px;
display: flex;
gap: 32px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-label {
font-size: 12px;
color: var(--text-muted);
}
.info-value {
font-size: 14px;
color: var(--text-primary);
font-weight: 500;
}
.status-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}
.status-tag.active { background: #d1fae5; color: #059669; }
.status-tag.warning { background: #fef3c7; color: #d97706; }
.status-tag.expired { background: #fee2e2; color: #dc2626; }
.action-buttons {
display: flex;
gap: 12px;
}
.action-buttons .el-button {
padding: 12px 24px;
}
.process-card {
background: var(--card-bg);
border-radius: 16px;
padding: 24px;
border: 1px solid var(--border-color);
margin-bottom: 20px;
transition: background-color 0.3s, border-color 0.3s;
}
.process-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 20px;
}
.process-steps {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.step-item {
display: flex;
align-items: center;
gap: 10px;
}
.step-num {
width: 28px;
height: 28px;
background: var(--primary-color);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
}
.step-text {
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
}
.step-arrow {
color: var(--text-muted);
font-size: 16px;
}
.log-card {
background: var(--card-bg);
border-radius: 16px;
border: 1px solid var(--border-color);
overflow: hidden;
transition: background-color 0.3s, border-color 0.3s;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
}
.log-title {
font-size: 15px;
font-weight: 500;
color: var(--text-primary);
}
.log-content {
max-height: 240px;
overflow-y: auto;
padding: 16px 20px;
}
.log-item {
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
font-size: 13px;
display: flex;
gap: 12px;
}
.log-item:last-child {
border-bottom: none;
}
.log-item.success .log-msg { color: var(--success-color); }
.log-item.error .log-msg { color: var(--danger-color); }
.log-time { color: var(--text-muted); font-family: monospace; font-size: 12px; }
.log-msg { color: var(--text-primary); flex: 1; }
</style>
+318
View File
@@ -0,0 +1,318 @@
<template>
<div class="tutorial-view">
<div class="page-header">
<h1>📚 使用教程</h1>
</div>
<!-- 快速开始 -->
<div class="section">
<h2>🚀 快速开始</h2>
<div class="steps-card">
<div class="step">
<div class="step-num">1</div>
<div class="step-content">
<h4>配置邮箱</h4>
<p>前往配置页面配置 IMAP 邮箱用于接收验证码</p>
<ul>
<li>填写 IMAP 服务器地址imap.qq.com</li>
<li>填写邮箱账号和授权码</li>
<li>点击测试连接确保配置正确</li>
</ul>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-content">
<h4>批量注册账号</h4>
<p>批量注册页面进行账号注册</p>
<ul>
<li>设置注册数量1-10</li>
<li>点击开始批量注册</li>
<li>等待自动注册完成</li>
</ul>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-content">
<h4>切换账号</h4>
<p>前往切换账号页面切换账号</p>
<ul>
<li>选择要使用的账号</li>
<li>点击自动切换账号</li>
<li>等待自动完成登录流程</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 功能详解 -->
<div class="section">
<h2>💡 功能详解</h2>
<div class="faq-item">
<div class="faq-q">📝 批量注册</div>
<div class="faq-a">
<p>自动批量注册 Windsurf 账号无需手动操作</p>
<p>系统会自动生成邮箱注册账号接收验证码每个账号注册大约需要 1-2 分钟</p>
</div>
</div>
<div class="faq-item">
<div class="faq-q">👤 账号管理</div>
<div class="faq-a">
<p>管理所有已注册的账号查看到期时间</p>
<ul>
<li><span class="badge active">可用</span> 剩余 4 天以上</li>
<li><span class="badge warning">即将到期</span> 剩余 1-3 </li>
<li><span class="badge expired">已到期</span> 试用期已结束</li>
</ul>
</div>
</div>
<div class="faq-item">
<div class="faq-q">📥 导入账号</div>
<div class="faq-a">
<p>支持批量导入已有账号格式</p>
<code>邮箱,密码</code> <code>邮箱----密码</code>
<p style="margin-top: 8px;">每行一个账号</p>
</div>
</div>
<div class="faq-item">
<div class="faq-q">🔄 账号切换</div>
<div class="faq-a">
<p>一键切换 Windsurf 账号自动完成登录</p>
<p>流程关闭 Windsurf 清除配置和机器码 启动 Windsurf 自动登录</p>
</div>
</div>
</div>
<!-- 常见问题 -->
<div class="section">
<h2> 常见问题</h2>
<div class="faq-item">
<div class="faq-q">Q1: 批量注册失败怎么办</div>
<div class="faq-a">
<ul>
<li>检查 IMAP 邮箱配置是否正确</li>
<li>检查网络连接是否正常</li>
<li>验证码接收超时稍后重试</li>
</ul>
</div>
</div>
<div class="faq-item">
<div class="faq-q">Q2: 自动登录不成功</div>
<div class="faq-a">
<ul>
<li>确保 Windsurf 已完全关闭</li>
<li>检查账号是否有效未到期</li>
<li>手动清除配置后重试</li>
</ul>
</div>
</div>
<div class="faq-item">
<div class="faq-q">Q3: 账号试用期是多久</div>
<div class="faq-a">
<p>Windsurf Pro 试用期为 <strong>13 </strong>系统会自动计算到期时间并提醒</p>
</div>
</div>
<div class="faq-item">
<div class="faq-q">Q4: 数据存储在哪里</div>
<div class="faq-a">
<p>所有账号数据都存储在本地完全本地化不上传到任何服务器</p>
<p><strong>macOS:</strong> ~/Library/Application Support/windsurf-tool-vue/</p>
</div>
</div>
</div>
<!-- 注意事项 -->
<div class="section">
<h2> 注意事项</h2>
<div class="warning-box">
<ul>
<li>本工具仅供学习交流使用请遵守 Windsurf 服务条款</li>
<li>批量注册时请合理控制数量避免对服务器造成压力</li>
<li>账号数据存储在本地请妥善保管避免泄露</li>
<li>切换账号前请保存当前工作避免数据丢失</li>
<li>建议使用专用邮箱进行注册不要使用重要邮箱</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
</script>
<style scoped>
.tutorial-view {
max-width: 800px;
margin: 0 auto;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.section {
margin-bottom: 32px;
}
.section h2 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
}
.steps-card {
background: var(--card-bg);
border-radius: 16px;
padding: 24px;
border: 1px solid var(--border-color);
}
.step {
display: flex;
gap: 16px;
padding: 16px 0;
border-bottom: 1px solid var(--border-color);
}
.step:last-child {
border-bottom: none;
padding-bottom: 0;
}
.step:first-child {
padding-top: 0;
}
.step-num {
width: 32px;
height: 32px;
background: var(--primary-color);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.step-content h4 {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 6px;
}
.step-content p {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.step-content ul {
margin: 0;
padding-left: 18px;
}
.step-content li {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 4px;
}
.faq-item {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 12px;
}
.faq-q {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 10px;
}
.faq-a {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
}
.faq-a p {
margin-bottom: 6px;
}
.faq-a ul {
margin: 8px 0;
padding-left: 18px;
}
.faq-a li {
margin-bottom: 4px;
}
.faq-a code {
background: var(--bg-tertiary);
padding: 2px 8px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
margin-right: 6px;
}
.badge.active { background: #d1fae5; color: #059669; }
.badge.warning { background: #fef3c7; color: #d97706; }
.badge.expired { background: #fee2e2; color: #dc2626; }
.warning-box {
background: #fef3c7;
border: 1px solid #fcd34d;
border-radius: 12px;
padding: 16px 20px;
}
.warning-box ul {
margin: 0;
padding-left: 18px;
}
.warning-box li {
font-size: 13px;
color: #92400e;
margin-bottom: 8px;
line-height: 1.5;
}
.warning-box li:last-child {
margin-bottom: 0;
}
</style>
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
base: './',
build: {
outDir: 'dist'
}
})