This commit is contained in:
dengqichen 2025-11-16 20:04:47 +08:00
parent bf55bcee27
commit c1d1381edb
9 changed files with 839 additions and 77 deletions

View File

@ -22,7 +22,9 @@
"license": "MIT",
"dependencies": {
"commander": "^11.0.0",
"puppeteer": "^21.11.0"
"puppeteer": "npm:rebrowser-puppeteer@^23.9.0",
"imap": "^0.8.19",
"mailparser": "^3.6.5"
},
"engines": {
"node": ">=14.0.0"

View File

@ -56,6 +56,7 @@ if (registerTool) {
.option('-o, --output <file>', '保存账号数据到文件')
.option('--from-step <number>', '从第几步开始执行', '1')
.option('--to-step <number>', '执行到第几步(不指定则执行全部)')
.option('--password-strategy <strategy>', '密码策略 (email=使用邮箱, random=随机)', 'email')
.option('--keep-browser-open', '保持浏览器打开', false)
.action(async (options) => {
// 转换步骤参数为数字

View File

@ -0,0 +1,242 @@
/**
* IMAP Connector - IMAP邮箱连接器
* 支持QQ邮箱GmailOutlook等支持IMAP的邮箱服务
*/
const Imap = require('imap');
const { simpleParser } = require('mailparser');
const logger = require('../../../../shared/logger');
class ImapConnector {
constructor(config) {
this.config = config;
this.imap = null;
this.connected = false;
}
/**
* 连接到邮箱
*/
async connect() {
return new Promise((resolve, reject) => {
this.imap = new Imap({
user: this.config.user,
password: this.config.password,
host: this.config.host,
port: this.config.port,
tls: this.config.tls,
tlsOptions: this.config.tlsOptions || { rejectUnauthorized: false }
});
this.imap.once('ready', () => {
this.connected = true;
logger.success('IMAP', `已连接到邮箱: ${this.config.user}`);
resolve();
});
this.imap.once('error', (err) => {
logger.error('IMAP', `连接失败: ${err.message}`);
reject(err);
});
this.imap.once('end', () => {
this.connected = false;
logger.info('IMAP', '连接已关闭');
});
this.imap.connect();
});
}
/**
* 断开连接
*/
disconnect() {
if (this.imap && this.connected) {
this.imap.end();
}
}
/**
* 获取最新的邮件
* @param {number} count - 获取数量
* @param {number} sinceDays - 获取几天内的邮件
* @returns {Promise<Array>}
*/
async getLatestEmails(count = 10, sinceDays = 1) {
if (!this.connected) {
await this.connect();
}
return new Promise((resolve, reject) => {
this.imap.openBox('INBOX', false, (err, box) => {
if (err) {
reject(err);
return;
}
// 计算日期范围
const sinceDate = new Date();
sinceDate.setDate(sinceDate.getDate() - sinceDays);
// 搜索条件最近N天的邮件
this.imap.search(['UNSEEN', ['SINCE', sinceDate]], async (err, results) => {
if (err) {
reject(err);
return;
}
if (!results || results.length === 0) {
resolve([]);
return;
}
// 只取最新的N封
const uids = results.slice(-count);
const emails = [];
const fetch = this.imap.fetch(uids, {
bodies: '',
markSeen: false
});
fetch.on('message', (msg) => {
let buffer = '';
msg.on('body', (stream) => {
stream.on('data', (chunk) => {
buffer += chunk.toString('utf8');
});
});
msg.once('end', async () => {
try {
const parsed = await simpleParser(buffer);
emails.push({
uid: msg.uid,
from: parsed.from?.text || '',
to: parsed.to?.text || '',
subject: parsed.subject || '',
date: parsed.date,
text: parsed.text || '',
html: parsed.html || '',
headers: parsed.headers
});
} catch (e) {
logger.warn('IMAP', `解析邮件失败: ${e.message}`);
}
});
});
fetch.once('error', (err) => {
reject(err);
});
fetch.once('end', () => {
resolve(emails);
});
});
});
});
}
/**
* 搜索包含特定关键词的邮件
* @param {string} subject - 主题关键词
* @param {number} sinceDays - 几天内
* @returns {Promise<Array>}
*/
async searchBySubject(subject, sinceDays = 1) {
if (!this.connected) {
await this.connect();
}
return new Promise((resolve, reject) => {
this.imap.openBox('INBOX', false, (err, box) => {
if (err) {
reject(err);
return;
}
const sinceDate = new Date();
sinceDate.setDate(sinceDate.getDate() - sinceDays);
this.imap.search([['SINCE', sinceDate], ['SUBJECT', subject]], async (err, results) => {
if (err) {
reject(err);
return;
}
if (!results || results.length === 0) {
resolve([]);
return;
}
const emails = [];
const fetch = this.imap.fetch(results, {
bodies: '',
markSeen: false
});
fetch.on('message', (msg) => {
let buffer = '';
msg.on('body', (stream) => {
stream.on('data', (chunk) => {
buffer += chunk.toString('utf8');
});
});
msg.once('end', async () => {
try {
const parsed = await simpleParser(buffer);
emails.push({
uid: msg.uid,
from: parsed.from?.text || '',
to: parsed.to?.text || '',
subject: parsed.subject || '',
date: parsed.date,
text: parsed.text || '',
html: parsed.html || '',
headers: parsed.headers
});
} catch (e) {
logger.warn('IMAP', `解析邮件失败: ${e.message}`);
}
});
});
fetch.once('error', (err) => {
reject(err);
});
fetch.once('end', () => {
resolve(emails);
});
});
});
});
}
/**
* 标记邮件为已读
* @param {number} uid - 邮件UID
*/
async markAsRead(uid) {
if (!this.connected) {
return;
}
return new Promise((resolve, reject) => {
this.imap.addFlags(uid, ['\\Seen'], (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
}
module.exports = ImapConnector;

View File

@ -0,0 +1,33 @@
/**
* Email Configuration
* 邮箱配置
*/
module.exports = {
// 主邮箱配置QQ邮箱 - 接收所有验证码)
primary: {
type: 'imap',
host: 'imap.qq.com',
port: 993,
user: '1695551@qq.com',
password: 'iogmboamejdsbjdh', // QQ邮箱授权码
tls: true,
tlsOptions: {
rejectUnauthorized: false
}
},
// 可以配置备用邮箱(未来扩展)
backup: {
// type: 'imap',
// host: '...',
// ...
},
// 邮件搜索配置
search: {
maxWaitTime: 60, // 最长等待时间(秒)
checkInterval: 3, // 检查间隔(秒)
maxEmails: 10 // 每次获取的邮件数量
}
};

View File

@ -0,0 +1,161 @@
/**
* Email Verification Service
* 邮箱验证码服务 - 统一入口
*/
const ImapConnector = require('./connectors/imap-connector');
const WindsurfParser = require('./parsers/windsurf-parser');
const emailConfig = require('./email-config');
const logger = require('../../../shared/logger');
class EmailVerificationService {
constructor(config = null) {
this.config = config || emailConfig.primary;
this.connector = null;
this.parsers = [
new WindsurfParser()
// 未来添加更多解析器
// new GitHubParser(),
// new TwitterParser(),
];
}
/**
* 获取验证码
* @param {string} siteName - 网站名称 'windsurf'
* @param {string} recipientEmail - 接收验证码的邮箱地址
* @param {number} timeout - 超时时间
* @returns {Promise<string>} - 验证码
*/
async getVerificationCode(siteName, recipientEmail, timeout = 60) {
logger.info('EmailVerification', `开始获取 ${siteName} 的验证码...`);
logger.info('EmailVerification', `接收邮箱: ${recipientEmail}`);
try {
// 1. 连接邮箱
this.connector = new ImapConnector(this.config);
await this.connector.connect();
// 2. 等待验证码邮件
const startTime = Date.now();
const checkInterval = emailConfig.search.checkInterval * 1000; // 转换为毫秒
let attempts = 0;
while (Date.now() - startTime < timeout * 1000) {
attempts++;
logger.info('EmailVerification', `${attempts} 次检查邮件...`);
// 获取最新邮件
const emails = await this.connector.getLatestEmails(10, 1);
if (emails && emails.length > 0) {
// 3. 查找匹配的邮件并提取验证码
for (const email of emails.reverse()) { // 从最新的开始
for (const parser of this.parsers) {
if (parser.canParse(email)) {
logger.success('EmailVerification', `找到匹配的邮件: ${email.subject}`);
const code = parser.extractCode(email);
if (code) {
logger.success('EmailVerification', `成功提取验证码: ${code}`);
// 标记为已读
try {
await this.connector.markAsRead(email.uid);
} catch (e) {
// 忽略标记失败
}
// 断开连接
this.connector.disconnect();
return code;
}
}
}
}
}
// 等待一段时间后再检查
logger.info('EmailVerification', `等待 ${emailConfig.search.checkInterval} 秒后重试...`);
await this.sleep(checkInterval);
}
// 超时
this.connector.disconnect();
throw new Error(`获取验证码超时(${timeout}秒内未收到邮件)`);
} catch (error) {
if (this.connector) {
this.connector.disconnect();
}
throw error;
}
}
/**
* 搜索特定主题的邮件并提取验证码
* @param {string} subject - 邮件主题关键词
* @param {number} timeout - 超时时间
* @returns {Promise<string>}
*/
async getCodeBySubject(subject, timeout = 60) {
logger.info('EmailVerification', `搜索主题包含 "${subject}" 的邮件...`);
try {
this.connector = new ImapConnector(this.config);
await this.connector.connect();
const startTime = Date.now();
const checkInterval = emailConfig.search.checkInterval * 1000;
while (Date.now() - startTime < timeout * 1000) {
const emails = await this.connector.searchBySubject(subject, 1);
if (emails && emails.length > 0) {
for (const email of emails.reverse()) {
for (const parser of this.parsers) {
if (parser.canParse(email)) {
const code = parser.extractCode(email);
if (code) {
logger.success('EmailVerification', `提取到验证码: ${code}`);
this.connector.disconnect();
return code;
}
}
}
}
}
await this.sleep(checkInterval);
}
this.connector.disconnect();
throw new Error(`获取验证码超时`);
} catch (error) {
if (this.connector) {
this.connector.disconnect();
}
throw error;
}
}
/**
* 添加新的解析器
* @param {BaseParser} parser - 解析器实例
*/
addParser(parser) {
this.parsers.push(parser);
}
/**
* 休眠
* @param {number} ms - 毫秒
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
module.exports = EmailVerificationService;

View File

@ -0,0 +1,53 @@
/**
* Base Parser - 邮件解析器基类
* 所有网站的邮件解析器都继承此类
*/
class BaseParser {
constructor(siteName) {
this.siteName = siteName;
}
/**
* 判断是否能解析此邮件
* @param {Object} email - 邮件对象
* @returns {boolean}
*/
canParse(email) {
throw new Error('Must implement canParse() method');
}
/**
* 从邮件中提取验证码
* @param {Object} email - 邮件对象
* @returns {string|null} - 验证码或null
*/
extractCode(email) {
throw new Error('Must implement extractCode() method');
}
/**
* 通用的验证码提取方法
* @param {string} content - 邮件内容
* @param {RegExp} pattern - 正则表达式
* @returns {string|null}
*/
extractByRegex(content, pattern) {
if (!content) return null;
const match = content.match(pattern);
return match ? match[1] : null;
}
/**
* 从HTML中提取文本
* @param {string} html - HTML内容
* @returns {string}
*/
stripHtml(html) {
if (!html) return '';
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
}
}
module.exports = BaseParser;

View File

@ -0,0 +1,124 @@
/**
* Windsurf Parser - Windsurf邮件解析器
* 用于解析Windsurf发送的验证码邮件
*/
const BaseParser = require('./base-parser');
class WindsurfParser extends BaseParser {
constructor() {
super('Windsurf');
// Windsurf邮件的特征
this.senderKeywords = ['windsurf', 'codeium', 'exafunction'];
this.subjectKeywords = ['verify', 'verification', 'code', '验证', 'welcome'];
// 验证码的正则表达式(根据实际邮件调整)
this.codePatterns = [
// HTML格式: <h1 class="code_xxx">866172</h1>
/<h1[^>]*class="code[^"]*"[^>]*>(\d{6})<\/h1>/i,
// 常见格式
/6 digit code[^0-9]*(\d{6})/i,
/verification code[^0-9]*(\d{6})/i,
/verify.*code:?\s*(\d{6})/i,
// 纯6位数字最后尝试
/\b(\d{6})\b/
];
}
/**
* 判断是否是Windsurf的验证码邮件
*/
canParse(email) {
if (!email) return false;
const from = (email.from || '').toLowerCase();
const subject = (email.subject || '').toLowerCase();
// 检查发件人
const hasSender = this.senderKeywords.some(keyword =>
from.includes(keyword)
);
// 检查主题
const hasSubject = this.subjectKeywords.some(keyword =>
subject.includes(keyword)
);
return hasSender || hasSubject;
}
/**
* 从邮件中提取验证码
*/
extractCode(email) {
if (!email) return null;
// 优先从HTML提取
let code = this.extractFromHtml(email.html);
if (code) return code;
// 其次从纯文本提取
code = this.extractFromText(email.text);
if (code) return code;
return null;
}
/**
* 从HTML内容提取验证码
*/
extractFromHtml(html) {
if (!html) return null;
// 先尝试直接从HTML提取保留HTML标签
for (const pattern of this.codePatterns) {
const code = this.extractByRegex(html, pattern);
if (code && this.validateCode(code)) {
return code;
}
}
// 如果HTML提取失败再去除标签后尝试
const text = this.stripHtml(html);
return this.extractFromText(text);
}
/**
* 从文本内容提取验证码
*/
extractFromText(text) {
if (!text) return null;
// 尝试所有正则表达式
for (const pattern of this.codePatterns) {
const code = this.extractByRegex(text, pattern);
if (code && this.validateCode(code)) {
return code;
}
}
return null;
}
/**
* 验证提取的验证码是否合理
*/
validateCode(code) {
if (!code) return false;
// Windsurf验证码是6位数字
if (code.length !== 6) {
return false;
}
// 应该是纯数字
if (!/^\d{6}$/.test(code)) {
return false;
}
return true;
}
}
module.exports = WindsurfParser;

View File

@ -12,6 +12,7 @@
const AccountDataGenerator = require('../generator');
const HumanBehavior = require('../utils/human-behavior');
const EmailVerificationService = require('../email-verification');
const logger = require('../../../shared/logger');
class WindsurfRegister {
@ -20,6 +21,7 @@ class WindsurfRegister {
this.siteUrl = 'https://windsurf.com/account/register';
this.dataGen = new AccountDataGenerator();
this.human = new HumanBehavior();
this.emailService = new EmailVerificationService();
this.browser = null;
this.page = null;
this.currentStep = 0;
@ -66,7 +68,10 @@ class WindsurfRegister {
name: options.name,
email: options.email,
username: options.username,
password: options.password,
password: {
strategy: options.passwordStrategy || 'email',
...options.password
},
includePhone: false // Windsurf第一步不需要手机号
});
@ -181,38 +186,44 @@ class WindsurfRegister {
// 点击Continue按钮使用人类行为
logger.info(this.siteName, ' → 点击Continue按钮...');
// 尝试查找按钮
let buttonSelector = null;
const possibleSelectors = [
'button:has-text("Continue")',
'button[type="submit"]',
'button.continue-button',
'button[class*="continue"]'
];
// 等待按钮变为可点击状态不再disabled
try {
await this.page.waitForFunction(
() => {
const button = document.querySelector('button');
if (!button) return false;
const text = button.textContent.trim();
return text === 'Continue' && !button.disabled;
},
{ timeout: 10000 }
);
for (const selector of possibleSelectors) {
try {
const button = await this.page.$(selector);
if (button) {
buttonSelector = selector;
break;
logger.info(this.siteName, ' → 按钮已激活');
// 点击未禁用的Continue按钮
const button = await this.page.$('button:not([disabled])');
if (button) {
const text = await this.page.evaluate(el => el.textContent.trim(), button);
if (text === 'Continue') {
// 同时等待导航和点击
await Promise.all([
this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 }).catch(() => {}),
this.human.humanClick(this.page, 'button:not([disabled])')
]);
logger.success(this.siteName, ' → 已点击Continue按钮并等待跳转');
}
} catch (e) {
// 继续尝试下一个
}
}
if (buttonSelector) {
await this.human.visualSearch(this.page, buttonSelector);
await this.human.humanClick(this.page, buttonSelector);
} else {
logger.warn(this.siteName, ' → 未找到Continue按钮尝试按Enter键');
} catch (error) {
logger.warn(this.siteName, ' → 按钮等待超时尝试按Enter键');
await this.human.randomDelay(500, 1000);
await this.page.keyboard.press('Enter');
await Promise.all([
this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 }).catch(() => {}),
this.page.keyboard.press('Enter')
]);
}
// 等待跳转或响应
await this.human.randomDelay(2000, 4000);
// 额外等待确保页面稳定
await this.human.randomDelay(1000, 2000);
this.currentStep = 1;
logger.success(this.siteName, `步骤 1 完成`);
@ -230,63 +241,185 @@ class WindsurfRegister {
// 填写密码
logger.info(this.siteName, ' → 填写密码...');
await this.page.waitForSelector('#password', { timeout: 10000 });
// 先清空密码框
await this.page.evaluate(() => {
const elem = document.querySelector('#password');
if (elem) elem.value = '';
});
await this.human.humanType(this.page, '#password', this.accountData.password);
// 填写确认密码
logger.info(this.siteName, ' → 填写确认密码...');
await this.page.waitForSelector('#passwordConfirmation', { timeout: 10000 });
// 先清空确认密码框(防止有残留)
await this.page.evaluate(() => {
const elem = document.querySelector('#passwordConfirmation');
if (elem) elem.value = '';
});
await this.human.humanType(this.page, '#passwordConfirmation', this.accountData.password);
// 等待验证通过
logger.info(this.siteName, ' → 等待密码验证...');
await this.human.randomDelay(1000, 2000);
// 查找并点击Continue按钮
logger.info(this.siteName, ' → 点击Continue按钮...');
const possibleSelectors = [
'button:has-text("Continue")',
'button[type="submit"]',
'button.continue-button',
'button[class*="continue"]'
];
// 等待按钮变为可点击状态不再disabled
try {
await this.page.waitForFunction(
() => {
const button = document.querySelector('button');
if (!button) return false;
const text = button.textContent.trim();
return text === 'Continue' && !button.disabled;
},
{ timeout: 10000 }
);
let buttonSelector = null;
for (const selector of possibleSelectors) {
try {
const button = await this.page.$(selector);
if (button) {
buttonSelector = selector;
break;
logger.info(this.siteName, ' → 按钮已激活');
// 使用更精确的选择器
const button = await this.page.$('button:not([disabled])');
if (button) {
const text = await this.page.evaluate(el => el.textContent.trim(), button);
if (text === 'Continue') {
// 同时等待导航和点击
await Promise.all([
this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 }).catch(() => {}),
this.human.humanClick(this.page, 'button:not([disabled])')
]);
logger.success(this.siteName, ' → 已点击Continue按钮并等待跳转');
}
} catch (e) {
// 继续尝试
}
}
if (buttonSelector) {
await this.human.visualSearch(this.page, buttonSelector);
await this.human.humanClick(this.page, buttonSelector);
} else {
logger.warn(this.siteName, ' → 未找到Continue按钮尝试按Enter键');
} catch (error) {
logger.warn(this.siteName, ' → 按钮等待超时尝试按Enter键');
await this.human.randomDelay(500, 1000);
await this.page.keyboard.press('Enter');
await Promise.all([
this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 }).catch(() => {}),
this.page.keyboard.press('Enter')
]);
}
// 等待跳转或响应
await this.human.randomDelay(2000, 4000);
// 额外等待确保页面稳定
await this.human.randomDelay(1000, 2000);
this.currentStep = 2;
logger.success(this.siteName, `步骤 2 完成`);
}
/**
* 步骤3: 邮箱验证待实现
* 步骤3: 邮箱验证
*/
async step3_emailVerification() {
logger.info(this.siteName, `[步骤 3/${this.getTotalSteps()}] 邮箱验证`);
// 邮箱验证逻辑
// ...
try {
// 等待验证码页面加载
await this.human.readPage(1, 2);
this.currentStep = 3;
logger.warn(this.siteName, '步骤 3 待实现(需要邮箱验证码)');
// 获取验证码(从邮箱)
logger.info(this.siteName, ' → 正在从邮箱获取验证码...');
logger.info(this.siteName, ` → 接收邮箱: ${this.accountData.email}`);
const code = await this.emailService.getVerificationCode(
'windsurf',
this.accountData.email,
90 // 90秒超时
);
logger.success(this.siteName, ` → 验证码: ${code}`);
// 等待验证码输入框
await this.human.randomDelay(1000, 2000);
// 查找验证码输入框(可能有多种选择器)
const possibleSelectors = [
'#code',
'#verificationCode',
'input[name="code"]',
'input[name="verificationCode"]',
'input[type="text"][placeholder*="code" i]',
'input[type="text"][placeholder*="验证" i]'
];
let codeInputSelector = null;
for (const selector of possibleSelectors) {
try {
const input = await this.page.$(selector);
if (input) {
codeInputSelector = selector;
logger.info(this.siteName, ` → 找到验证码输入框: ${selector}`);
break;
}
} catch (e) {
// 继续尝试
}
}
if (codeInputSelector) {
// 填写验证码
logger.info(this.siteName, ' → 填写验证码...');
await this.human.humanType(this.page, codeInputSelector, code);
// 查找并点击提交按钮
logger.info(this.siteName, ' → 点击提交按钮...');
const buttonSelectors = [
'button:has-text("Verify")',
'button:has-text("Continue")',
'button:has-text("Submit")',
'button[type="submit"]',
'button.verify-button',
'button[class*="verify"]'
];
let buttonSelector = null;
for (const selector of buttonSelectors) {
try {
const button = await this.page.$(selector);
if (button) {
buttonSelector = selector;
break;
}
} catch (e) {
// 继续尝试
}
}
if (buttonSelector) {
await this.human.visualSearch(this.page, buttonSelector);
await this.human.humanClick(this.page, buttonSelector);
} else {
logger.warn(this.siteName, ' → 未找到提交按钮尝试按Enter键');
await this.human.randomDelay(500, 1000);
await this.page.keyboard.press('Enter');
}
// 等待验证完成
await this.human.randomDelay(3000, 5000);
this.currentStep = 3;
logger.success(this.siteName, `步骤 3 完成`);
} else {
logger.error(this.siteName, ' → 未找到验证码输入框!');
logger.warn(this.siteName, ' → 请手动输入验证码: ' + code);
// 等待用户手动输入
await this.human.randomDelay(30000, 30000);
this.currentStep = 3;
}
} catch (error) {
logger.error(this.siteName, `邮箱验证失败: ${error.message}`);
throw error;
}
}
/**

View File

@ -27,23 +27,36 @@ class HumanBehavior {
if (element) element.value = '';
}, selector);
// 逐字输入,随机延迟
for (let i = 0; i < text.length; i++) {
const char = text[i];
// 每个字符的输入延迟
const delay = this.randomInt(80, 180);
await page.type(selector, char, { delay });
// 偶尔停顿(模拟思考或查看)
if (Math.random() < 0.15) {
await this.randomDelay(300, 900);
// 使用更可靠的方式直接设置value然后触发input事件
await page.evaluate((sel, value) => {
const element = document.querySelector(sel);
if (element) {
element.value = value;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
}, selector, text);
// 偶尔有短暂的快速输入(模拟熟悉的词语)
if (Math.random() < 0.1 && i < text.length - 3) {
i += 2; // 跳过几个字符,快速输入
}
// 模拟输入延迟
await this.randomDelay(text.length * 100, text.length * 150);
// 验证输入是否正确
const actualValue = await page.evaluate((sel) => {
const element = document.querySelector(sel);
return element ? element.value : '';
}, selector);
if (actualValue !== text) {
console.warn(`输入验证失败: 期望 "${text}", 实际 "${actualValue}"`);
// 重试一次
await page.evaluate((sel, value) => {
const element = document.querySelector(sel);
if (element) {
element.value = value;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
}, selector, text);
}
// 输入完成后的短暂停顿