mirror of
https://github.com/Mac-XK/Windsurf-Tool.git
synced 2026-08-29 01:21:29 +08:00
Add files via upload
This commit is contained in:
+423
@@ -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 个并发窗口,并具备完善的错误处理和重试机制。
|
||||
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
@@ -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>
|
||||
Generated
+7172
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+4450
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
ignoredBuiltDependencies:
|
||||
- electron
|
||||
- esbuild
|
||||
- puppeteer
|
||||
- sleep
|
||||
- vue-demi
|
||||
+143
@@ -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
@@ -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')
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user