first commit
This commit is contained in:
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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) + '…');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user