first commit

This commit is contained in:
adrapier03
2026-06-29 22:25:58 +07:00
commit 0ea4265045
9 changed files with 3395 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
# MiMo Chain Bot
Bot otomatis untuk Xiaomi MiMo Open Platform (`platform.xiaomimimo.com`).
Chain-loop: register akun → redeem invite code → create API key → apply
Ultraspeed beta → ambil ref code → ulangi dengan ref code akun sebelumnya.
## Install
```bash
npm install
npx playwright install chrome
```
## Configuration
Edit `config.json`:
```json
{
"tempmail": {
"apiUrl": "https://tempmail.adrnode.com/api"
},
"captcha": {
"provider": "2captcha",
"apiKey": "YOUR_2CAPTCHA_API_KEY"
},
"xiaomi": {
"referralLink": "https://platform.xiaomimimo.com/?ref=XXXXXX",
"inviteCode": "XXXXXX",
"password": "MimoAuto2024!",
"betaApplication": "MiMo-V2.5-Pro-UltraSpeed"
},
"browser": {
"headless": false,
"timeout": 60000,
"screenshots": false
}
}
```
Yang **wajib** diisi:
- `captcha.apiKey` — API key dari [2captcha.com](https://2captcha.com), butuh balance
- `xiaomi.inviteCode` — kode referral untuk seed iterasi pertama (6 karakter)
- `xiaomi.referralLink` — URL referral, format `?ref=XXXXXX` (sama dengan inviteCode)
Yang opsional:
- `xiaomi.password` — password untuk semua akun baru (default OK)
- `browser.headless` — `true` tanpa UI, `false` (default) buat lihat browser
## Run
```bash
node chain-loop.js --count 5
```
| Flag | Default | Keterangan |
|---|---|---|
| `--count N` | 1 | jumlah akun yang dibuat |
| `--seed XXXXXX` | `config.xiaomi.inviteCode` | invite code untuk akun pertama |
| `--output file.txt` | `chain-result.txt` | file output |
```bash
# 5 akun, seed dari config
node chain-loop.js --count 5
# 10 akun, seed manual
node chain-loop.js --count 10 --seed P9LHUK
# Output ke file lain
node chain-loop.js --count 3 --output batch1.txt
```
## Per-akun flow
```
[1] Bikin temp email
[2] Launch browser dengan random fingerprint (UA, viewport, locale, GPU, dst)
[3] Buka referral link (?ref=<currentSeed>)
[4] Sign up form (email + password + confirm + terms checkbox)
[5] Solve captcha (reCAPTCHA via 2Captcha + image captcha)
[6] Verify email (kode 6 digit dari tempmail)
[7] Klik Console → Balance → "Enter invite code" → isi 6-box → Redeem
→ cek restriction (kalau kena, STOP)
→ reload halaman, verify balance bertambah ~$2
[8] Create API key (atau ambil existing kalau sudah ada)
[9] Fill Ultraspeed beta application form
[10] Klik "Refer & earn" → ambil ref code akun ini → seed untuk akun berikut
```
## Output
```bash
# chain-result.txt (auto-create, append)
# Format: email:password:refCode:apiKey:invitedBy
[email protected]:MimoAuto2024!:K3M2P8:sk-aaa...bbb:T9K59J
[email protected]:MimoAuto2024!:LX8N2A:sk-ccc...ddd:K3M2P8
[email protected]:MimoAuto2024!:Q9R3FZ:sk-eee...fff:LX8N2A
```
Tiap baris dichain — `invitedBy` baris N+1 = `refCode` baris N.
## Features
**Random fingerprint per akun** — User-Agent, viewport, locale, timezone, hardwareConcurrency,
deviceMemory, WebGL vendor/renderer, canvas noise seed, navigator.plugins. Sehingga Xiaomi
tidak mengenali fingerprint yang sama antar akun.
**Human-like typing** — Form signup, captcha code, email verification code, dan OTP
redeem diketik per-karakter dengan delay acak 60-180ms + thinking pause.
Form Ultraspeed pakai `.fill()` langsung (mode cepat, cukup untuk form internal).
**Idempotent** — Akun yang sudah redeem invite code / sudah punya API key
akan di-skip otomatis, aman untuk re-run.
**Verifikasi balance** — Setelah redeem invite code, halaman di-reload dan balance
dibaca ulang. Harus bertambah ~$2.00. Kalau tidak, iterasi dianggap gagal.
**Restriction detection** — Kalau setelah redeem muncul notif "risk control restrictions",
loop berhenti total (akun kena cap, gak bisa lanjut).
## Stop conditions
Loop berhenti kalau:
| Condition | Action |
|---|---|
| Akun restricted (risk control) | STOP, gak retry |
| Balance tidak bertambah ~$2 | STOP, gak retry |
| Ref code tidak tertangkap | STOP, mencegah duplikasi seed |
| Ref code = seed lama (clipboard leak) | STOP |
| Iter total fail (captcha timeout, dll) | Retry pakai seed yang sama |
## Fingerprint
Tiap akun randomize:
- **User-Agent**: Chrome 122-131 di Windows NT 10.0
- **Viewport**: 8 ukuran (1280x720 → 1920x1080)
- **Locale**: en-US, en-GB, id-ID, en-AU, en-CA, en-SG
- **Timezone**: Jakarta, Singapore, Bangkok, KL, Manila, Ho_Chi_Minh
- **Hardware**: 4-16 cores, 4-16 GB memory
- **WebGL**: pasangan vendor + renderer NVIDIA/Intel/AMD realistis
- **Canvas**: seed unik, noise 1 pixel pada toDataURL & getImageData
- **navigator.webdriver** di-undefine, plugins palsu
## Troubleshooting
**Captcha solve lama (>2 menit)**
- 2Captcha workers sibuk, coba lagi nanti. Balance 2Captcha cek di dashboard.
**"Account restricted" muncul**
- IP mungkin sudah ke-flag Xiaomi. Coba jeda beberapa jam atau pakai VPN.
- Screenshot `error-restriction-*.png` ke-dump otomatis untuk referensi.
**Balance tidak bertambah**
- Screenshot `error-balance-*.png` ke-dump otomatis.
- Kemungkinan saldo ter-delay (Xiaomi bilang "Balance updates in near-real time, ≤5 min delay").
- Atau invite code expired / invalid.
**Ref code tidak tertangkap**
- Screenshot `error-refcode-*.png` dan `chain-refcode-leak-*.png` ke-dump otomatis.
- Cek format modal "Refer & Earn" di Xiaomi — kalau label berubah, pattern di `extras.js`
perlu ditambah.
**Browser zombie (chrome.exe nyangkut)**
- Ctrl+C di terminal akan auto-close browser (SIGINT handler aktif).
- Kalau masih ada: `taskkill //IM chrome.exe //F` (hati-hati: juga tutup chrome pribadi).
## Estimasi durasi
| Akun | Estimasi |
|---|---|
| 1 | 2-4 menit |
| 5 | 10-20 menit |
| 10 | 20-40 menit |
| 50 | 2-3 jam |
Bottleneck utama: solving captcha (60-120s per akun via 2Captcha).
## Files
```
├── chain-loop.js entry point (node chain-loop.js)
├── chain-loop-config.js CLI args + save/log helpers
├── index.js MimoRegistration core class
├── extras.js getReferralCode method
├── fingerprint.js random browser fingerprint
├── human.js human-like typing helpers
├── package.json
├── config.json ← edit ini
└── README.md
```
Runtime output (gak masuk git):
- `chain-result.txt` — akun sukses
- `chain-fail.log` — error log
- `*.png` — screenshot debug
## License
MIT
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env node
/**
* Chain loop: register → redeem invite → ambil ref code → create API key →
* fill Ultraspeed form. Tiap iterasi kode invite-nya pake ref code akun
* sebelumnya (chained). Iterasi pertama pake --seed atau config.xiaomi.inviteCode.
*
* Cara pakai:
* node chain-loop.js --count 5
* node chain-loop.js --count 3 --seed HWPMXZ
* node chain-loop.js --count 10 --output chain.txt
*
* Output (chain.txt) format:
* email:password:refCode:apiKey:invitedBy
*/
import { chromium } from 'playwright';
import { readFileSync, appendFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { MimoRegistration } from './index.js';
import './extras.js'; // attach getReferralCode
import { generateFingerprint, buildInitScript, buildExtraHeaders } from './fingerprint.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Output dir bisa di-override via env var (dipake oleh wrapper bot-mimo
// supaya output file masuk ke folder bot-mimo, bukan parent).
const outputDir = process.env.HERMES_BOT_MIMO_CWD || __dirname;
// Config path juga bisa di-override
const configPath = process.env.HERMES_BOT_MIMO_CONFIG || join(__dirname, 'config.json');
const config = JSON.parse(readFileSync(configPath, 'utf8'));
// ---- CLI args ---------------------------------------------------------
const args = process.argv.slice(2);
const countIdx = args.indexOf('--count');
const count = countIdx !== -1 ? parseInt(args[countIdx + 1], 10) : 1;
const seedIdx = args.indexOf('--seed');
const seedRef = seedIdx !== -1 ? args[seedIdx + 1] : config.xiaomi.inviteCode;
const outputIdx = args.indexOf('--output');
const outputFile = outputIdx !== -1
? join(outputDir, args[outputIdx + 1])
: join(outputDir, 'chain-result.txt');
const failLog = join(outputDir, 'chain-fail.log');
// ---- File helpers -----------------------------------------------------
function saveResult(row) {
if (!existsSync(outputFile)) {
appendFileSync(
outputFile,
'# Chain loop results. Format: email:password:refCode:apiKey:invitedBy\n',
'utf8'
);
}
const line = [
row.email,
row.password,
row.refCode || '',
row.apiKey || '',
row.invitedBy || '',
].join(':') + '\n';
appendFileSync(outputFile, line, 'utf8');
}
function logFail(email, error) {
const line = `[${new Date().toISOString()}] ${email || 'unknown'} | ${error}\n`;
appendFileSync(failLog, line, 'utf8');
}
export { config, saveResult, logFail, count, seedRef, outputFile, failLog, __dirname };
+283
View File
@@ -0,0 +1,283 @@
#!/usr/bin/env node
/**
* Chain loop. Lihat chain-loop-config.js untuk dokumentasi.
*
* Per iterasi:
* 1. Bikin tempmail
* 2. Override config.xiaomi.referralLink + .inviteCode dengan ref akun sebelumnya
* 3. Launch browser dengan random fingerprint
* 4. registerOnly() — sign up + verify email
* 5. redeemInviteCode() — pakai ref code dari akun sebelumnya (sudah otomatis
* lewat config override + flow di MimoRegistration)
* 6. getReferralCode() — ambil ref code akun ini buat iterasi berikutnya
* 7. createApiKey()
* 8. fillUltraspeedForm()
*/
import { chromium } from 'playwright';
import { join } from 'path';
import { MimoRegistration } from './index.js';
import './extras.js'; // attach getReferralCode
import { generateFingerprint, buildInitScript, buildExtraHeaders } from './fingerprint.js';
import {
config, saveResult, logFail, count, seedRef, outputFile, failLog, __dirname,
} from './chain-loop-config.js';
// ---- One-iteration runner --------------------------------------------
async function runIteration(idx, total, currentRef) {
console.log('\n' + '═'.repeat(70));
console.log(` [${idx + 1}/${total}] Chain Iteration (using ref: ${currentRef})`);
console.log('═'.repeat(70));
// Per-iter: clone config + override invite code & referral link
const iterConfig = JSON.parse(JSON.stringify(config));
iterConfig.xiaomi.inviteCode = currentRef;
iterConfig.xiaomi.referralLink =
`https://platform.xiaomimimo.com/?ref=${encodeURIComponent(currentRef)}`;
const reg = new MimoRegistration(iterConfig);
const cleanup = async () => {
if (reg.browser) {
console.log('\n Caught SIGINT, closing browser...');
try { await reg.browser.close(); } catch (e) {}
}
process.exit(130);
};
process.once('SIGINT', cleanup);
process.once('SIGTERM', cleanup);
let email = null, refCode = null, apiKey = null;
try {
// 1. Tempmail
console.log('[1/8] Creating temporary email...');
email = await reg.tempmail.createInbox();
console.log(`✓ Email: ${email}`);
// 2. Browser
console.log('[2/8] Launching browser...');
const fp = generateFingerprint();
reg.fingerprint = fp;
console.log(` ↳ Chrome ${fp.chromeMajor}, ${fp.viewport.width}x${fp.viewport.height}, ${fp.locale}/${fp.timezone}`);
reg.browser = await chromium.launch({
headless: iterConfig.browser.headless,
channel: 'chrome',
args: [
`--window-size=${fp.viewport.width},${fp.viewport.height}`,
'--disable-blink-features=AutomationControlled',
],
});
const ctx = await reg.browser.newContext({
userAgent: fp.userAgent,
viewport: fp.viewport,
deviceScaleFactor: fp.deviceScaleFactor,
locale: fp.locale,
timezoneId: fp.timezone,
screen: { width: fp.screen.width, height: fp.screen.height },
extraHTTPHeaders: buildExtraHeaders(fp),
});
await ctx.addInitScript({ content: buildInitScript(fp) });
reg.page = await ctx.newPage();
// Skip screenshot dump
const origScreenshot = reg.page.screenshot.bind(reg.page);
reg.page.screenshot = async (opts = {}) => {
const isError = opts.path && opts.path.includes('error');
if (isError || iterConfig.browser.screenshots === true) return origScreenshot(opts);
return Buffer.alloc(0);
};
// 3. Open referral link (refCode chained)
console.log('[3/8] Opening referral page...');
await reg.page.goto(iterConfig.xiaomi.referralLink, {
waitUntil: 'networkidle',
timeout: iterConfig.browser.timeout,
});
// 4. Sign up form
console.log('[4/8] Filling Sign up form...');
await reg.fillRegistrationForm(email);
await reg.submitRegistration();
// 5. Captcha
console.log('[5/8] Handling captcha...');
await reg.handleXiaomiCaptcha();
await reg.handleImageCaptcha();
// 6. Verify email
console.log('[6/8] Verifying email...');
await reg.verifyEmail(email);
// 7. Redeem invite + create API key (idempotent — gpp meskipun udah)
// flow di MimoRegistration: clickConsoleMenu → balance → handleTerms →
// cek "Enter invite code" → kalau ada redeem, kalau tidak skip.
let redeemRestricted = false;
try {
await reg.redeemInviteCode();
} catch (e) {
if (e.code === 'ACCOUNT_RESTRICTED' || e.code === 'BALANCE_NOT_CREDITED') {
// Akun kena risk control / balance gak nambah — STOP propagate
// ke caller biar chain-loop berhenti.
const tag = e.code === 'ACCOUNT_RESTRICTED' ? 'restricted' : 'balance not credited';
console.log(` ❌ Stopping iteration — ${tag}: ${e.message}`);
redeemRestricted = true;
throw e;
}
console.log(` ! redeem error (lanjut): ${e.message}`);
}
// 8. Create API key (atau ambil existing)
console.log('[7/8] Creating API key...');
try {
apiKey = await reg.createApiKey();
console.log(`✓ API key: ${apiKey || '(none)'}`);
} catch (e) {
console.log(` ! createApiKey error: ${e.message}`);
}
// 9. Fill Ultraspeed form (best-effort)
console.log('[Bonus] Fill Ultraspeed form...');
try {
await reg.fillUltraspeedForm(email);
} catch (e) {
console.log(` ! fillUltraspeedForm error: ${e.message}`);
}
// 10. Ambil ref code akun ini PALING AKHIR — biar clipboard clean dari
// side-effect step redeem/apikey/ultraspeed sebelumnya.
console.log('[8/8] Fetching referral code for next iteration...');
try {
const captured = await reg.getReferralCode();
if (captured && captured.toUpperCase() === currentRef.toUpperCase()) {
// Bug guard: kalau refCode == seed yg baru di-redeem, artinya
// getReferralCode tertipu (clipboard leak / modal salah / dst).
console.log(` ! refCode "${captured}" == seed "${currentRef}" — invalid, treating as null`);
try {
await reg.page.screenshot({
path: join(__dirname, `chain-refcode-leak-${Date.now()}.png`),
fullPage: true,
});
} catch (e) {}
refCode = null;
} else {
refCode = captured;
console.log(`✓ My ref code: ${refCode || '(not found)'}`);
}
} catch (e) {
console.log(` ! getReferralCode error: ${e.message}`);
}
saveResult({
email,
password: iterConfig.xiaomi.password,
refCode,
apiKey,
invitedBy: currentRef,
});
console.log('═'.repeat(70));
console.log(`✅ ITERATION ${idx + 1} DONE`);
console.log(` Email : ${email}`);
console.log(` RefCode : ${refCode || '(not found)'}`);
console.log(` ApiKey : ${apiKey || '(none)'}`);
console.log('═'.repeat(70));
return { ok: true, email, refCode, apiKey };
} catch (err) {
console.error(`\n❌ Iteration ${idx + 1} failed: ${err.message}`);
if (reg.page) {
try {
await reg.page.screenshot({
path: join(__dirname, `chain-error-${Date.now()}.png`),
fullPage: true,
});
} catch (e) {}
}
logFail(email, err.message);
return {
ok: false,
email,
error: err.message,
restricted: err.code === 'ACCOUNT_RESTRICTED' || err.code === 'BALANCE_NOT_CREDITED',
stopReason: err.code,
};
} finally {
if (reg.browser) {
await reg.browser.close().catch(() => {});
}
}
}
// ---- Main loop -------------------------------------------------------
async function main() {
console.log(`\nChain loop config:`);
console.log(` Count : ${count}`);
console.log(` Seed ref : ${seedRef}`);
console.log(` Output file : ${outputFile}`);
console.log(` Fail log : ${failLog}\n`);
let currentRef = seedRef;
let okCount = 0, failCount = 0;
for (let i = 0; i < count; i++) {
const result = await runIteration(i, count, currentRef);
if (result.ok) {
okCount++;
// Chain: kalau berhasil dapet refCode, pake itu untuk iterasi berikut
if (result.refCode) {
console.log(`\n→ Next iteration will use ref: ${result.refCode}`);
currentRef = result.refCode;
} else {
// refCode gak ke-capture — chain rusak. STOP biar gak bikin
// beberapa akun pakai invite code yang sama (bisa dianggap fraud
// sama Xiaomi).
console.error(`\n❌ Iteration ${i + 1} succeeded but didn't capture refCode.`);
console.error(` STOPPING chain to prevent reusing seed ${currentRef}.`);
console.error(` Run lagi dengan --seed ${currentRef} kalau mau lanjut manual.`);
break;
}
} else {
failCount++;
if (result.restricted) {
// Akun kena risk control / balance gak nambah — STOP loop, jangan retry
const reason = result.stopReason || 'unknown';
console.error(`\n❌ Chain stopped: ${reason}`);
console.error(` Email: ${result.email}`);
console.error(` Detail: ${result.error}`);
console.error(` Last seed used: ${currentRef}`);
break;
}
// Iter total fail (non-restriction) — currentRef gak di-update, retry pakai seed yang sama
console.log(`\n! Iteration failed, retrying with same seed ${currentRef}`);
}
if (i < count - 1) {
const wait = 8000 + Math.floor(Math.random() * 6000);
console.log(` Waiting ${(wait / 1000).toFixed(1)}s before next...`);
await new Promise(r => setTimeout(r, wait));
}
}
console.log('\n' + '═'.repeat(70));
console.log(' CHAIN LOOP SUMMARY');
console.log('═'.repeat(70));
console.log(` Total : ${count}`);
console.log(` Successful : ${okCount}`);
console.log(` Failed : ${failCount}`);
console.log(` Output : ${outputFile}`);
console.log('═'.repeat(70) + '\n');
process.exit(okCount > 0 ? 0 : 1);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});
+20
View File
@@ -0,0 +1,20 @@
{
"tempmail": {
"apiUrl": "https://tempmail.adrnode.com/api"
},
"captcha": {
"provider": "2captcha",
"apiKey": "YOUR_2CAPTCHA_API_KEY"
},
"xiaomi": {
"referralLink": "https://platform.xiaomimimo.com/?ref=XXXXXX",
"inviteCode": "XXXXXX",
"password": "MimoAuto2024!",
"betaApplication": "MiMo-V2.5-Pro-UltraSpeed"
},
"browser": {
"headless": false,
"timeout": 60000,
"screenshots": false
}
}
+299
View File
@@ -0,0 +1,299 @@
/**
* Method tambahan untuk MimoRegistration yang dipakai bersama oleh
* register-only.js dan chain-loop.js. Di-attach via prototype biar gak
* perlu modifikasi index.js.
*/
import { humanDelay } from './human.js';
import { MimoRegistration } from './index.js';
// Common English words yg sering ke-match regex code uppercase.
const REF_BLACKLIST = new Set([
'YOUR', 'CODE', 'INVITE', 'REFERRAL', 'ENTER', 'COPY', 'SHARE',
'EARN', 'NULL', 'NONE', 'TRUE', 'FALSE', 'EMPTY',
]);
/**
* Validate ref code — Xiaomi MiMo invite codes are always EXACTLY 6 chars
* alphanumeric uppercase. Sample valid: T9K59J, 4G7RWD, K3M2P8, HWPMXZ,
* AXQZ3F, TQB65X, P9LHUK — semua 6 char.
*
* Pattern lebih dari 6 char (mis. "DIMASSARI59" = 11 char username email)
* harus ditolak.
*/
function isValidRefCode(s) {
if (!s) return false;
const up = String(s).toUpperCase().trim();
if (up.length !== 6) return false;
if (REF_BLACKLIST.has(up)) return false;
return /^[A-Z0-9]{6}$/.test(up);
}
/**
* Ambil kode referal milik akun yang sedang login.
* Strategi:
* 1. Scan semua link/href yang ada `?ref=XXXXXX`
* 2. Cari teks "Invite code: XXXXXX" di body
* 3. Klik tombol "Refer & earn" / "Invite friends" → modal → scan + clipboard
*/
MimoRegistration.prototype.getReferralCode = async function () {
// Navigate ke balance page (kalau belum di sana)
const currentUrl = this.page.url();
if (!currentUrl.includes('/console/balance')) {
try { await this.clickConsoleMenu(); } catch (e) {}
if (!this.page.url().includes('/console/balance')) {
await this.page.goto('https://platform.xiaomimimo.com/console/balance', {
waitUntil: 'networkidle',
timeout: this.config.browser.timeout,
});
}
await this.handleOAuthRedirect();
await humanDelay(1500, 2500);
await this.handleTermsModal();
await this.waitForOverlaysGone();
}
// Strategi 1: scan ?ref= di link/anchor/data-clipboard
console.log(' Scanning for ?ref= links...');
let refCode = await this.page.evaluate(() => {
const anchors = Array.from(document.querySelectorAll('a, [data-href], [data-clipboard-text]'));
for (const a of anchors) {
const href = a.href || a.getAttribute('data-href') || a.getAttribute('data-clipboard-text') || '';
const m = href.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m) return m[1].toUpperCase();
}
const inputs = Array.from(document.querySelectorAll('input[readonly], textarea[readonly]'));
for (const inp of inputs) {
const v = inp.value || '';
const m = v.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m) return m[1].toUpperCase();
}
return null;
});
if (isValidRefCode(refCode)) return refCode;
if (refCode) console.log(` ! Strategi 1 hasil "${refCode}" ditolak (blacklist/invalid)`);
refCode = null;
// Strategi 2: regex teks plain — 3 pattern, hasil divalidasi
console.log(' Scanning page text for ref code patterns...');
refCode = await this.page.evaluate(() => {
const text = document.body.innerText;
// a) URL share di plain text: "...?ref=XXXXXX"
const m1 = text.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m1) return m1[1].toUpperCase();
// b) "Invite code\nXXX" (modal Xiaomi nampilin label terus code di bawah,
// tanpa colon. Pakai [\s:\n]+ supaya match newline juga)
const m2 = text.match(/(?:invite\s+code|referral\s+code|your\s+code)[\s:\n]+([A-Z0-9]{6})\b/i);
if (m2) return m2[1].toUpperCase();
// c) "Code: XXX" generic dengan colon
const m3 = text.match(/\bcode\s*:\s*([A-Z0-9]{6})\b/i);
if (m3) return m3[1].toUpperCase();
return null;
});
if (isValidRefCode(refCode)) return refCode;
if (refCode) console.log(` ! Strategi 2 hasil "${refCode}" ditolak (blacklist/invalid)`);
refCode = null;
// Strategi 3: klik Refer & earn / Invite → modal → scan + clipboard
console.log(' Trying to click Refer & earn / Invite button...');
const opened = await this.page.evaluate(() => {
const all = Array.from(document.querySelectorAll('button, a, [role="button"]'));
const target = all.find(el => {
const txt = (el.textContent || '').trim();
return /^(Refer\s*&\s*earn|Refer\s*and\s*earn|Invite( friends)?|Share|Refer( friends)?)$/i.test(txt) && el.offsetHeight > 0;
});
if (target) {
target.scrollIntoView({ block: 'center' });
target.click();
return true;
}
return false;
});
if (opened) {
console.log(' ✓ Clicked Refer & earn / Invite button, waiting for modal...');
// Tunggu modal Refer & Earn beneran muncul (cari teks "Invite code" /
// "Invite builders" di salah satu modal yg visible)
await this.page.waitForFunction(() => {
const modals = Array.from(document.querySelectorAll('.ant-modal, .ant-modal-content, .ant-modal-wrap, [role="dialog"]'));
return modals.some(m => {
if (m.offsetHeight === 0 || m.style.display === 'none') return false;
const t = (m.innerText || '').toLowerCase();
return t.includes('invite code') || t.includes('invite builder') || t.includes('refer & earn');
});
}, { timeout: 8000 }).catch(() => {});
await humanDelay(800, 1400);
// Clear clipboard biar gak baca residue dari step sebelumnya
try {
await this.page.evaluate(async () => {
try { await navigator.clipboard.writeText(''); } catch (e) {}
});
} catch (e) {}
// Re-scan modal — coba beberapa kali kalau pertama belum ke-render
for (let attempt = 1; attempt <= 3 && !refCode; attempt++) {
const candidate = await this.page.evaluate(() => {
const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-modal-content, .ant-modal-wrap, [role="dialog"]'))
.find(m => m.offsetHeight > 0 && (m.style.display !== 'none'));
const scope = modal || document.body;
// 1. Link share di anchor / data-clipboard / readonly input / value
const els = Array.from(scope.querySelectorAll('a, [data-clipboard-text], [data-href], input, textarea, span, div'));
for (const el of els) {
const sources = [
el.href, el.value, el.getAttribute('data-clipboard-text'),
el.getAttribute('data-href'), el.getAttribute('data-link'),
el.textContent,
];
for (const s of sources) {
if (!s) continue;
const m = s.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m) return m[1].toUpperCase();
}
}
// 2. Plain text di modal — coba 4 pattern (modal Xiaomi nampilin code
// sebagai standalone text dengan label "Invite code" di atasnya)
const text = scope.innerText || '';
// a) URL ?ref=XXX (paling reliable)
const m1 = text.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m1) return m1[1].toUpperCase();
// b) "Invite code" label + code di baris berikut (format modal aktual)
const m2 = text.match(/(?:invite\s+code|referral\s+code|your\s+code)[\s:\n]+([A-Z0-9]{6})\b/i);
if (m2) return m2[1].toUpperCase();
// c) "Code: XXX" generic dengan colon
const m3 = text.match(/\bcode\s*:\s*([A-Z0-9]{6})\b/i);
if (m3) return m3[1].toUpperCase();
return null;
});
if (isValidRefCode(candidate)) {
refCode = candidate;
break;
}
if (candidate) {
console.log(` ! Modal attempt ${attempt} hasil "${candidate}" ditolak (blacklist)`);
}
if (attempt < 3) {
console.log(` Modal scan attempt ${attempt} empty, retry in 1.5s...`);
await humanDelay(1200, 1800);
}
}
if (!refCode) {
console.log(' ! Ref code not in modal text, trying Copy button + clipboard...');
try {
const ctx = this.page.context();
await ctx.grantPermissions(['clipboard-read', 'clipboard-write']).catch(() => {});
// Strategi 3a: baca attribute langsung dari button Copy di modal
// REFER & EARN (bukan modal lain). Filter modal scope dulu
// dengan teks "Invite code" / "Invite builder".
const copyBtnText = await this.page.evaluate(() => {
const modals = Array.from(document.querySelectorAll('.ant-modal, .ant-modal-content, [role="dialog"]'))
.filter(m => m.offsetHeight > 0);
// Cari modal yg teksnya mengandung "Invite code" / "Invite builder"
const referModal = modals.find(m => {
const t = (m.innerText || '').toLowerCase();
return t.includes('invite code') || t.includes('invite builder') || t.includes('refer & earn');
}) || modals[modals.length - 1]; // fallback ke modal terakhir
if (!referModal) return null;
const btns = Array.from(referModal.querySelectorAll('button, a, [role="button"], [data-clipboard-text]'));
const copy = btns.find(b => /^(copy|copy link|salin)/i.test((b.textContent || '').trim()));
if (!copy) return null;
return {
clipText: copy.getAttribute('data-clipboard-text'),
link: copy.getAttribute('data-link') || copy.getAttribute('data-href'),
value: copy.value,
};
});
if (copyBtnText) {
console.log(` Copy button attrs: ${JSON.stringify(copyBtnText)}`);
for (const v of Object.values(copyBtnText)) {
if (!v) continue;
const s = String(v);
// a) URL share dengan ?ref=XXX
const m = s.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m && isValidRefCode(m[1])) { refCode = m[1].toUpperCase(); break; }
// b) Bare code (TQB65X / P9LHUK)
const trimmed = s.trim().toUpperCase();
if (isValidRefCode(trimmed)) { refCode = trimmed; break; }
}
}
// Strategi 3b: kalau attribute kosong, klik Copy + bringToFront + readText
if (!refCode) {
await this.page.bringToFront().catch(() => {});
await this.page.evaluate(() => {
const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-modal-content, .ant-modal-wrap, [role="dialog"]'))
.find(m => m.offsetHeight > 0);
if (!modal) return false;
const btns = Array.from(modal.querySelectorAll('button, a, [role="button"]'));
const copy = btns.find(b => /^(copy|copy link|salin)/i.test((b.textContent || '').trim()));
if (copy) { copy.click(); return true; }
return false;
});
await humanDelay(900, 1400);
const clipboardText = await this.page.evaluate(async () => {
// Pastikan window punya focus dulu
try { window.focus(); } catch (e) {}
try {
return await navigator.clipboard.readText();
} catch (e) {
return '';
}
});
if (clipboardText) {
console.log(` Clipboard: ${clipboardText.substring(0, 100)}`);
// Coba ekstrak ref code dari clipboard:
// a) URL share dengan ?ref=XXX
// b) Bare code (TQB65X / P9LHUK / dst) — modal Xiaomi cuma copy code
const m1 = clipboardText.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m1 && isValidRefCode(m1[1])) {
refCode = m1[1].toUpperCase();
} else {
// Bare code: trim, uppercase, validasi
const trimmed = clipboardText.trim().toUpperCase();
if (isValidRefCode(trimmed)) {
refCode = trimmed;
} else {
// Cari token alphanumeric 4-12 char di mana pun di clipboard
const m2 = clipboardText.match(/\b([A-Z0-9]{6})\b/i);
if (m2 && isValidRefCode(m2[1])) refCode = m2[1].toUpperCase();
}
}
} else {
console.log(' ! Clipboard empty (page may have lost focus)');
}
}
} catch (clipErr) {
console.log(` ! Clipboard read error: ${clipErr.message}`);
}
}
// Kalau masih null, dump screenshot + html modal untuk debug
if (!refCode) {
console.log(' ! All strategies failed — dumping debug artifacts');
try {
const ts = Date.now();
await this.page.screenshot({
path: `error-refcode-${ts}.png`,
fullPage: false,
});
const modalHtml = await this.page.evaluate(() => {
const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-modal-content, [role="dialog"]'))
.find(m => m.offsetHeight > 0);
return modal ? modal.outerHTML.substring(0, 2000) : 'NO_MODAL';
});
console.log(` Modal HTML (preview): ${modalHtml.substring(0, 500)}`);
} catch (e) {}
}
await this.page.keyboard.press('Escape').catch(() => {});
}
return refCode;
};
+266
View File
@@ -0,0 +1,266 @@
/**
* Browser fingerprint randomizer for Playwright.
*
* generateFingerprint() -> profile object
* buildInitScript(fp) -> JS string yang di-inject ke setiap halaman
* via context.addInitScript() biar override jalan
* sebelum kode situs sempat baca navigator/screen/canvas.
*
* Catatan: ini ngacak permukaan-permukaan yang dibaca skrip anti-bot umum
* (FingerprintJS, Akamai BMP, dsb). Bukan stealth penuh — kalau target
* pasang deteksi level network (TLS JA3, IP, ASN), butuh proxy + tools lain.
*/
import crypto from 'crypto';
// ---- Pool data ----------------------------------------------------------
const CHROME_VERSIONS = [
'122.0.0.0', '123.0.0.0', '124.0.0.0', '125.0.0.0', '126.0.0.0',
'127.0.0.0', '128.0.0.0', '129.0.0.0', '130.0.0.0', '131.0.0.0',
];
// Windows 10 dan 11 dua-duanya report "10.0" di UA string
const WINDOWS_NT_VERSION = '10.0';
const VIEWPORTS = [
{ width: 1920, height: 1080 },
{ width: 1536, height: 864 },
{ width: 1440, height: 900 },
{ width: 1366, height: 768 },
{ width: 1600, height: 900 },
{ width: 1680, height: 1050 },
{ width: 1280, height: 720 },
{ width: 1280, height: 800 },
];
const LOCALES = ['en-US', 'en-GB', 'id-ID', 'en-AU', 'en-CA', 'en-SG'];
const TIMEZONES = [
'Asia/Jakarta', 'Asia/Singapore', 'Asia/Bangkok',
'Asia/Kuala_Lumpur', 'Asia/Manila', 'Asia/Ho_Chi_Minh',
];
const HARDWARE_CONCURRENCY = [4, 6, 8, 12, 16];
const DEVICE_MEMORY = [4, 8, 16];
const COLOR_DEPTHS = [24, 30];
// Pasangan vendor + renderer realistis (gak campur — kalau vendor NVIDIA,
// renderer-nya juga GPU NVIDIA biar konsisten saat skrip cocokkan keduanya)
const WEBGL_RENDERERS = [
{ vendor: 'Google Inc. (NVIDIA)', renderer: 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
{ vendor: 'Google Inc. (NVIDIA)', renderer: 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
{ vendor: 'Google Inc. (NVIDIA)', renderer: 'ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 Ti Direct3D11 vs_5_0 ps_5_0, D3D11)' },
{ vendor: 'Google Inc. (NVIDIA)', renderer: 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 Ti Direct3D11 vs_5_0 ps_5_0, D3D11)' },
{ vendor: 'Google Inc. (Intel)', renderer: 'ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
{ vendor: 'Google Inc. (Intel)', renderer: 'ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)' },
{ vendor: 'Google Inc. (Intel)', renderer: 'ANGLE (Intel, Intel(R) HD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
{ vendor: 'Google Inc. (AMD)', renderer: 'ANGLE (AMD, AMD Radeon RX 6600 XT Direct3D11 vs_5_0 ps_5_0, D3D11)' },
{ vendor: 'Google Inc. (AMD)', renderer: 'ANGLE (AMD, AMD Radeon Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)' },
];
// ---- Helpers ------------------------------------------------------------
function pick(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// ---- Fingerprint generator ---------------------------------------------
export function generateFingerprint() {
const chromeVersion = pick(CHROME_VERSIONS);
const chromeMajor = chromeVersion.split('.')[0];
const viewport = pick(VIEWPORTS);
const locale = pick(LOCALES);
const timezone = pick(TIMEZONES);
const cores = pick(HARDWARE_CONCURRENCY);
const memory = pick(DEVICE_MEMORY);
const webgl = pick(WEBGL_RENDERERS);
const colorDepth = pick(COLOR_DEPTHS);
const deviceScaleFactor = pick([1, 1, 1, 1.25, 1.5]);
const userAgent =
`Mozilla/5.0 (Windows NT ${WINDOWS_NT_VERSION}; Win64; x64) ` +
`AppleWebKit/537.36 (KHTML, like Gecko) ` +
`Chrome/${chromeVersion} Safari/537.36`;
// Screen biasanya >= viewport. Kasih taskbar offset acak.
const screenWidth = viewport.width;
const screenHeight = viewport.height + randInt(40, 120);
const availHeight = screenHeight - randInt(30, 60);
// Seed unik buat noise canvas / audio
const seed = crypto.randomBytes(16).toString('hex');
return {
userAgent,
chromeMajor,
chromeVersion,
viewport,
deviceScaleFactor,
screen: {
width: screenWidth,
height: screenHeight,
availWidth: screenWidth,
availHeight,
colorDepth,
},
locale,
timezone,
platform: 'Win32',
hardwareConcurrency: cores,
deviceMemory: memory,
webgl,
seed,
pluginsCount: randInt(2, 5),
};
}
// HTTP headers tambahan untuk konsisten dengan UA yang diset.
// Sec-CH-UA dkk biasanya dikirim Chrome berdasar binary version aslinya;
// kita override eksplisit biar match sama UA yang kita rotasikan.
export function buildExtraHeaders(fp) {
const major = fp.chromeMajor;
const baseLang = fp.locale.split('-')[0];
return {
'Sec-CH-UA': `"Chromium";v="${major}", "Not_A Brand";v="24", "Google Chrome";v="${major}"`,
'Sec-CH-UA-Mobile': '?0',
'Sec-CH-UA-Platform': '"Windows"',
'Accept-Language': `${fp.locale},${baseLang};q=0.9`,
};
}
// Init script di-inject sebelum semua script halaman jalan.
// Pakai Object.defineProperty(..., { configurable: true }) supaya pemeriksa
// fingerprint yang nge-cek descriptor gak langsung curiga.
export function buildInitScript(fp) {
const safe = (v) => JSON.stringify(v);
return `
(() => {
try {
const seed = ${safe(fp.seed)};
// ---- navigator overrides ----
Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true });
Object.defineProperty(navigator, 'platform', { get: () => ${safe(fp.platform)}, configurable: true });
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => ${fp.hardwareConcurrency}, configurable: true });
Object.defineProperty(navigator, 'deviceMemory', { get: () => ${fp.deviceMemory}, configurable: true });
Object.defineProperty(navigator, 'languages', { get: () => [${safe(fp.locale)}, ${safe(fp.locale.split('-')[0])}], configurable: true });
// Plugins palsu, panjangnya berbeda-beda
const fakePlugins = Array.from({ length: ${fp.pluginsCount} }, (_, i) => ({
name: 'Plugin ' + i,
filename: 'plugin' + i + '.dll',
description: '',
length: 1,
}));
Object.defineProperty(navigator, 'plugins', { get: () => fakePlugins, configurable: true });
// ---- screen overrides ----
Object.defineProperty(screen, 'width', { get: () => ${fp.screen.width}, configurable: true });
Object.defineProperty(screen, 'height', { get: () => ${fp.screen.height}, configurable: true });
Object.defineProperty(screen, 'availWidth', { get: () => ${fp.screen.availWidth}, configurable: true });
Object.defineProperty(screen, 'availHeight', { get: () => ${fp.screen.availHeight}, configurable: true });
Object.defineProperty(screen, 'colorDepth', { get: () => ${fp.screen.colorDepth}, configurable: true });
Object.defineProperty(screen, 'pixelDepth', { get: () => ${fp.screen.colorDepth}, configurable: true });
// ---- WebGL vendor/renderer ----
const spoofWebGL = (proto) => {
if (!proto) return;
const orig = proto.getParameter;
proto.getParameter = function (parameter) {
// UNMASKED_VENDOR_WEBGL
if (parameter === 37445) return ${safe(fp.webgl.vendor)};
// UNMASKED_RENDERER_WEBGL
if (parameter === 37446) return ${safe(fp.webgl.renderer)};
return orig.apply(this, arguments);
};
};
if (typeof WebGLRenderingContext !== 'undefined') spoofWebGL(WebGLRenderingContext.prototype);
if (typeof WebGL2RenderingContext !== 'undefined') spoofWebGL(WebGL2RenderingContext.prototype);
// ---- Canvas noise ----
// Geser 1 pixel dengan alpha sangat rendah, posisi & warna ditentukan seed.
// Nilai akhir toDataURL/getImageData jadi unik per fingerprint.
const sx = parseInt(seed.substring(0, 4), 16);
const sy = parseInt(seed.substring(4, 8), 16);
const sr = parseInt(seed.substring(8, 10), 16);
const sg = parseInt(seed.substring(10, 12), 16);
const sb = parseInt(seed.substring(12, 14), 16);
const _toDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function () {
try {
if (this.width > 16 && this.height > 16) {
const ctx = this.getContext('2d');
if (ctx) {
ctx.save();
ctx.fillStyle = 'rgba(' + sr + ',' + sg + ',' + sb + ',0.005)';
ctx.fillRect(sx % this.width, sy % this.height, 1, 1);
ctx.restore();
}
}
} catch (e) {}
return _toDataURL.apply(this, arguments);
};
const _getImageData = CanvasRenderingContext2D.prototype.getImageData;
CanvasRenderingContext2D.prototype.getImageData = function () {
const data = _getImageData.apply(this, arguments);
try {
// Geser sedikit di byte tertentu — tetap deterministik per seed.
const offset = (sx + sy) % Math.max(1, data.data.length - 1);
data.data[offset] = (data.data[offset] + (sr % 3)) & 0xff;
} catch (e) {}
return data;
};
// ---- AudioContext noise ----
if (typeof AnalyserNode !== 'undefined') {
const _getFloat = AnalyserNode.prototype.getFloatFrequencyData;
AnalyserNode.prototype.getFloatFrequencyData = function (array) {
_getFloat.apply(this, arguments);
const drift = ((sr / 256) - 0.5) * 0.0001;
for (let i = 0; i < array.length; i++) array[i] += drift;
};
}
// ---- Permissions API tweak (notifications gak boleh "denied" + webdriver true) ----
if (navigator.permissions && navigator.permissions.query) {
const origQuery = navigator.permissions.query.bind(navigator.permissions);
navigator.permissions.query = (params) => {
if (params && params.name === 'notifications') {
return Promise.resolve({ state: Notification.permission, onchange: null });
}
return origQuery(params);
};
}
} catch (e) {
// Jangan biarkan error fingerprint mematikan halaman — log aja.
console.error('[fp-init] error:', e && e.message);
}
})();
`;
}
// CLI test: `node fingerprint.js` — print 3 contoh fingerprint
if (import.meta.url === `file:///${process.argv[1].replace(/\\/g, '/')}` ||
(process.argv[1] && process.argv[1].endsWith('fingerprint.js'))) {
for (let i = 0; i < 3; i++) {
const fp = generateFingerprint();
console.log(`\n--- Sample ${i + 1} ---`);
console.log('UA :', fp.userAgent);
console.log('Viewport :', `${fp.viewport.width}x${fp.viewport.height} (DPR ${fp.deviceScaleFactor})`);
console.log('Screen :', `${fp.screen.width}x${fp.screen.height} @ ${fp.screen.colorDepth}bpp`);
console.log('Locale / TZ :', fp.locale, '/', fp.timezone);
console.log('CPU / Mem :', fp.hardwareConcurrency, 'cores /', fp.deviceMemory, 'GB');
console.log('GPU vendor :', fp.webgl.vendor);
console.log('GPU renderer :', fp.webgl.renderer);
console.log('Plugins count :', fp.pluginsCount);
console.log('Seed :', fp.seed.substring(0, 16) + '…');
}
}
+145
View File
@@ -0,0 +1,145 @@
/**
* Helper untuk interaksi browser ala manusia.
*
* Tujuan: menghindari pola bot yang kentara (paste instan via .fill(),
* klik tanpa hover/jeda, dsb). Bukan stealth penuh — cuma menambah
* jitter di layer event yang biasa diukur skrip antifraud
* (waktu antar keystroke, klik tanpa pointermove, fokus tanpa klik).
*
* Pakai begini:
* import { humanType, humanClick, humanDelay, humanFill } from './human.js';
* await humanFill(page, 'input[name="email"]', email);
* await humanClick(page, 'button[type="submit"]');
*/
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function randInt(min, max) {
return Math.floor(rand(min, max + 1));
}
export function humanDelay(min = 80, max = 220) {
return new Promise((resolve) => setTimeout(resolve, randInt(min, max)));
}
/**
* Resolve target ke ElementHandle/Locator yang punya method click/fill/type.
* - String → page.$(selector) (ElementHandle)
* - Locator → return apa adanya
* - ElementHandle → return apa adanya
*/
async function resolveTarget(pageOrFrame, target) {
if (typeof target === 'string') {
const handle = await pageOrFrame.$(target);
if (!handle) throw new Error(`humanFill: selector tidak ketemu: ${target}`);
return handle;
}
return target;
}
/**
* Klik element dengan sedikit hover dulu. Hover memicu pointermove,
* yang sering dipakai sebagai sinyal "user benerian" oleh script antibot.
*/
export async function humanClick(pageOrFrame, target, options = {}) {
const el = await resolveTarget(pageOrFrame, target);
try {
// Move pointer ke element pelan-pelan (Playwright hover sudah
// terjemahkan jadi serangkaian pointermove)
await el.hover({ timeout: options.hoverTimeout || 4000 }).catch(() => {});
} catch (e) {}
await humanDelay(60, 180);
await el.click({ ...options, delay: randInt(40, 120) });
}
/**
* Ketik teks ala manusia: per karakter dengan delay acak,
* sesekali jeda 200-500ms seolah-olah "mikir".
*/
export async function humanType(pageOrFrame, text, options = {}) {
const minDelay = options.minDelay ?? 60;
const maxDelay = options.maxDelay ?? 180;
const thinkProb = options.thinkProb ?? 0.07; // 7% chance jeda mikir per char
const thinkMin = options.thinkMin ?? 220;
const thinkMax = options.thinkMax ?? 520;
const keyboard = pageOrFrame.keyboard;
for (const ch of text) {
await keyboard.type(ch, { delay: randInt(minDelay, maxDelay) });
if (Math.random() < thinkProb) {
await humanDelay(thinkMin, thinkMax);
}
}
}
/**
* Fokus ke field, bersihkan isi lama secara natural, lalu ketik nilai baru
* karakter-per-karakter. Pengganti drop-in untuk .fill() yang lebih meyakinkan.
*
* options:
* - clear: 'select-all' (default) atau 'backspace' atau 'none'
* - clickFirst: true (default) — klik field dulu sebelum ngetik
*/
export async function humanFill(pageOrFrame, target, value, options = {}) {
const el = await resolveTarget(pageOrFrame, target);
const clearMode = options.clear ?? 'select-all';
if (options.clickFirst !== false) {
try { await el.hover({ timeout: 3000 }); } catch (e) {}
await humanDelay(80, 200);
try {
await el.click({ delay: randInt(40, 120) });
} catch (e) {
// beberapa input ke-overlay; coba focus saja
try { await el.focus(); } catch (e2) {}
}
} else {
try { await el.focus(); } catch (e) {}
}
await humanDelay(80, 200);
// Bersihkan isi lama
if (clearMode === 'select-all') {
await pageOrFrame.keyboard.press('Control+A');
await humanDelay(40, 100);
await pageOrFrame.keyboard.press('Backspace');
} else if (clearMode === 'backspace') {
// Hapus per karakter, max 100 (jaga-jaga)
for (let i = 0; i < 100; i++) {
const v = await el.evaluate((node) => node.value || '').catch(() => '');
if (!v) break;
await pageOrFrame.keyboard.press('Backspace');
await humanDelay(20, 60);
}
}
await humanDelay(60, 160);
await humanType(pageOrFrame, String(value), options);
await humanDelay(80, 200);
}
/**
* Versi humanFill khusus Locator API (page.locator(...)) — kadang lebih
* stabil daripada selector string karena Locator auto-wait.
*/
export async function humanFillLocator(page, locator, value, options = {}) {
await locator.scrollIntoViewIfNeeded().catch(() => {});
await locator.hover({ timeout: 3000 }).catch(() => {});
await humanDelay(80, 200);
await locator.click({ delay: randInt(40, 120) }).catch(async () => {
// fallback: focus aja
await locator.focus().catch(() => {});
});
await humanDelay(80, 200);
await page.keyboard.press('Control+A');
await humanDelay(40, 100);
await page.keyboard.press('Backspace');
await humanDelay(60, 160);
await humanType(page, String(value), options);
await humanDelay(80, 200);
}
+2085
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
{
"name": "xiaomi-mimo-autoreg",
"version": "1.0.0",
"description": "Xiaomi MiMo Auto-Registration - Browser Automation",
"type": "module",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "node index.js --test"
},
"dependencies": {
"playwright": "^1.40.0",
"node-fetch": "^3.3.0"
},
"keywords": ["xiaomi", "mimo", "automation", "registration"],
"author": "",
"license": "MIT"
}