(() => { 'use strict'; if (globalThis.__dww11ExtensionLoaded) return; globalThis.__dww11ExtensionLoaded = true; const HOME = 'https://www.dww11.com/'; const PROBE_HOME = `${location.origin}/`; const SITE_ORIGINS = new Set(['https://www.dww11.com', 'https://house.dww11.com', 'https://dww11.com']); const isSiteURL = url => SITE_ORIGINS.has(new URL(url, location.href).origin); const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const visible = el => !!el && !!el.getClientRects().length && getComputedStyle(el).visibility !== 'hidden'; let flow = null, busy = false, running = false, panel, message, nextButton, resumeButton, retryButton, stopButton; let actions, toolbar, panelToggle, hideButton, restoreButton, panelHidden = false, visibilitySaving = false; class Stopped extends Error {} async function request(route, body = {}) { let result; try { result = await chrome.runtime.sendMessage({ app: 'dww11-v3', route, body }); } catch { throw new Error('扩展连接已断开,请刷新网页;已提交的抽取进度保留。'); } if (!result?.ok) throw new Error(result?.error || '扩展未返回结果,请刷新后继续。'); return result.data; } async function call(route, body = {}) { const run = flow?.id; if (!run || flow.paused) throw new Stopped(); const data = await request(route, { ...body, run }); if (flow?.id !== run || flow.paused) throw new Stopped(); return data; } async function save(patch) { const data = await call('/save', { patch }); flow = data.flow; } const phase = name => save({ phase: name }); function check(run) { if (flow?.id !== run || flow.paused) throw new Stopped(); } function show(text, error = false) { message.textContent = text; panel.style.borderColor = error ? '#d86d20' : '#d3d9e2'; controls(); } function controls() { if (!nextButton) return; nextButton.disabled = busy || !!flow; resumeButton.hidden = !flow || (!flow.paused && running); resumeButton.disabled = busy || running; retryButton.hidden = !flow?.paused || !['submitted', 'login', 'logout'].includes(flow.phase); retryButton.disabled = busy || running; stopButton.disabled = !flow || busy; } function panelVisibility(hidden) { panelHidden = hidden; panel.hidden = hidden; if (panelToggle) { panelToggle.textContent = hidden ? '显示换号卡片' : '隐藏换号卡片'; panelToggle.setAttribute('aria-expanded', String(!hidden)); } // 登录子页可能没有顶栏,仍需保留重新打开卡片的入口。 restoreButton.hidden = !hidden || !!toolbar?.isConnected; } async function changePanelVisibility(hidden) { if (visibilitySaving) return; visibilitySaving = true; panelVisibility(hidden); for (const b of [panelToggle, hideButton, restoreButton]) if (b) b.disabled = true; try { await request('/panel', { hidden }); } catch (e) { panelVisibility(false); show(e.message, true); } finally { visibilitySaving = false; for (const b of [panelToggle, hideButton, restoreButton]) if (b) b.disabled = false; } } function mountTopbar() { const anchors = [...document.querySelectorAll('.topwp a')]; const anchor = anchors.find(a => /欢迎您的登[录陆]/.test(a.textContent.replace(/\s/g, ''))) || anchors.find(a => /立即登[录陆]/.test(a.textContent.replace(/\s/g, ''))); if (!anchor) { if (toolbar) { actions.prepend(nextButton); nextButton.removeAttribute('style'); toolbar.remove(); toolbar = null; panelToggle = null; } panelVisibility(panelHidden); return; } if (toolbar?.isConnected && toolbar.nextElementSibling === anchor) return; if (!toolbar) { toolbar = document.createElement('span'); toolbar.id = 'dww11-extension-topbar'; // 与原站点左浮动的链接保持一致,DOM 中在欢迎文字之前即可显示在其左侧。 toolbar.style.cssText = 'float:left;display:inline-flex;align-items:center;gap:6px;margin:4px 10px 0 0;'; panelToggle = document.createElement('button'); panelToggle.type = 'button'; panelToggle.id = 'dww11-extension-panel-toggle'; panelToggle.style.cssText = 'padding:2px 8px;border:1px solid #ccc;border-radius:4px;background:#fff;color:#666;font:inherit;line-height:24px;cursor:pointer;'; panelToggle.addEventListener('click', () => changePanelVisibility(!panelHidden)); toolbar.append(panelToggle, nextButton); } nextButton.style.cssText = 'padding:2px 10px;border:1px solid #f28518;border-radius:4px;background:#fff;color:#e66d00;font:inherit;line-height:24px;cursor:pointer;'; anchor.before(toolbar); panelVisibility(panelHidden); } async function guarded(fn) { if (busy) return; busy = true; controls(); try { await fn(); } catch (e) { if (!(e instanceof Stopped)) show(e.message, true); } finally { busy = false; controls(); } } async function suspend(reason) { if (flow && !flow.paused) { const run = flow.id; try { await request('/suspend', { run, reason }); } finally { if (flow?.id === run) { flow.paused = true; flow.reason = reason; } } } show(reason + '\n进度已保留,可继续或在管理页结束流程。', true); } function signedIn() { return !!document.querySelector('#mumucms_username'); } function usernameOf(doc = document) { const el = doc.querySelector('#mumucms_username'); return el ? [...el.childNodes].filter(n => n.nodeType === Node.TEXT_NODE).map(n => n.textContent).join('').trim() : ''; } function restrictionFromText(text) { const match = String(text || '').match(/(?:您(?:当前的|的)?|你(?:当前的|的)?)?(?:账号|帐号|账户|用户)[^。\n!?]{0,80}?(?:被冻结|已冻结|被封禁|已封禁|被禁止访问|已禁止访问|被禁用|已禁用|被停用|已停用)[^。\n!?]{0,140}/); if (!match) return null; const message = match[0].trim(); return { code: /冻结/.test(message) ? 'account_frozen' : /封禁|禁止访问/.test(message) ? 'account_banned' : 'account_disabled', message }; } function restriction(doc = document, url = location.href) { const nodes = [...doc.querySelectorAll('#messagetext, .layui-layer-content, .alert_error, .alert_info, .alert_warning, [role="alert"]')]; const parsed = new URL(url, HOME); if (parsed.pathname === '/home.php' && parsed.searchParams.get('ac') === 'profile' && parsed.searchParams.get('op') === 'password') { const content = doc.querySelector('#ct'); if (content) nodes.push(content); } for (const node of nodes) { if (doc === document && !visible(node)) continue; const error = restrictionFromText(node.innerText || node.textContent); if (error) return error; } return null; } async function recordRestriction() { const error = restriction(), username = usernameOf(); if (error && username) { await request('/restriction', { username, error }); show('当前账号被冻结或封禁,已标记失效并保存错误信息。', true); } } function classify(text) { if (/验证码|安全验证|滑块|拼图|操作.{0,5}频繁|次数.{0,5}(过多|限制|超)|稍后再试|分钟后|访问受限|网络.{0,5}(错误|异常)/.test(text)) return 'pause'; if (/密码.{0,8}(错误|不正确)|用户名.{0,8}(不存在|错误)|用户不存在|账号.{0,8}(不存在|禁用|冻结)|帐号.{0,8}(不存在|禁用|冻结)|用户.{0,8}禁止登录|账户.{0,8}停用/.test(text)) return 'invalid'; return null; } function messages() { const texts = [...document.querySelectorAll('.layui-layer-content, #messagetext, .alert_error, .alert_info, [role="alert"]')].filter(visible).map(e => e.textContent); for (const frame of document.querySelectorAll('iframe[name="submitiframe"]')) { try { if (frame.contentDocument?.body) texts.push(frame.contentDocument.body.innerText); } catch { /* 跨域验证框不可读。 */ } } return texts.join('\n'); } function captchaVisible() { return [...document.querySelectorAll('iframe')].some(f => visible(f) && /captcha|tcaptcha|turing/i.test(f.src)); } function fill(el, value) { Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(el, value); el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } async function begin() { if (location.hostname === 'dww11.com') { location.assign(HOME); return; } await recordRestriction(); flow = (await request('/start')).flow; await drive(); } async function resume(retry = false) { if (location.hostname === 'dww11.com') { location.assign(HOME); return; } if (retry && !confirm('将重新提交本次抽到的账号(不抽新账号)。如上次提交已成功,可能再次登录。确认重试?')) return; flow = (await request('/resume')).flow; if (retry) { await save({ phase: 'logout', hops: 0, submittedAt: null, probedAt: null, invalidError: null }); location.assign(HOME); return; } await drive(); } async function drive() { if (running || !flow || flow.paused) return; running = true; controls(); const run = flow.id; const waitingSince = Date.now(); // 重启后给已提交请求留出核对结果的时间。 try { while (flow && !flow.paused) { check(run); const current = flow.current; const label = current ? `第 ${current.round} 轮 · 第 ${current.position} 个账号` : ''; if (['submitted', 'success'].includes(flow.phase)) { const error = restriction(); if (error) { if (signedIn() && usernameOf() !== current.username) throw new Error('限制提示所属账号与本次账号不一致,已暂停。'); await save({ invalidError: error, phase: 'invalid' }); continue; } } if (flow.phase === 'select') { flow = (await call('/next')).flow; } else if (flow.phase === 'logout') { show(`准备登录 ${label}……`); if (signedIn()) { const link = document.querySelector('#mumucms_username_menu a[href*="action=logout"]'); if (!link) throw new Error('未找到头像菜单中的退出链接。'); const url = new URL(link.href); if (!isSiteURL(url.href)) throw new Error('退出链接不是受支持的站点地址。'); location.assign(url.href); break; } await phase('login'); } else if (flow.phase === 'login') { if (signedIn()) throw new Error('退出未完成,请回首页后继续。'); if (captchaVisible()) throw new Error('需要人工完成验证码。'); const form = document.querySelector('form#login'); const username = form?.querySelector('input[name="username"]'), password = form?.querySelector('input[name="password"]'); const submit = form?.querySelector('button[type="submit"]'); if (!username || !password || !submit) { const hops = (flow.hops || 0) + 1; if (hops > 5) throw new Error('未找到账号密码登录表单,请打开“立即登录”后继续。'); await save({ hops }); const link = [...document.querySelectorAll('.topwp a')].find(a => /立即登[录陆]/.test(a.textContent)); if (link) { if (!isSiteURL(link.href)) throw new Error('登录链接不是受支持的站点地址。'); link.click(); } else location.assign(HOME); break; } const deadline = Date.now() + 15000; while (document.readyState !== 'complete' && Date.now() < deadline) { await sleep(200); check(run); } if (document.readyState !== 'complete') throw new Error('登录页仍在加载,请稍后继续。'); await sleep(600); check(run); let { account } = await call('/next'); if (account.username !== current.username) throw new Error('账号位置发生变化。'); await call('/attempt', { account: current.username }); check(run); fill(username, account.username); fill(password, account.password); account = null; await save({ phase: 'submitted', submittedAt: Date.now(), hops: 0, oldMessage: messages(), probedAt: null }); show(`正在登录 ${label}……`); submit.click(); // 若在写入 submitted 后、点击前崩溃,恢复时只核对结果,由用户明确选择重试。 } else if (flow.phase === 'submitted') { if (signedIn()) { if (usernameOf() !== current.username) throw new Error('当前用户名与本次账号不一致。'); await phase('success'); continue; } const text = messages(); const result = text !== flow.oldMessage || Date.now() - flow.submittedAt > 1500 ? classify(text) : null; if (captchaVisible() || result === 'pause') throw new Error('网站要求验证或暂时限制登录,请人工处理后继续。'); if (result === 'invalid') { await save({ phase: 'invalid', invalidError: { code: 'login_failed', message: text.trim().slice(0, 500) } }); continue; } if (Date.now() - flow.submittedAt > 1500 && Date.now() - (flow.probedAt || 0) > 2500) { await save({ probedAt: Date.now() }); let response, html; try { response = await fetch(PROBE_HOME, { credentials: 'include', cache: 'no-store', signal: AbortSignal.timeout(5000) }); html = response.ok ? await response.text() : null; } catch { /* 超时保持未确认,不据此标记失效。 */ } check(run); if (html && new URL(response.url).origin === location.origin) { const doc = new DOMParser().parseFromString(html, 'text/html'), name = usernameOf(doc); const error = restriction(doc, response.url); if (name && name !== current.username) throw new Error('首页核对到其他账号,已暂停。'); if (error && name === current.username) { await save({ phase: 'invalid', invalidError: error }); continue; } if (name === current.username) { location.assign(PROBE_HOME); break; } } } if (Date.now() - Math.max(flow.submittedAt || 0, waitingSince) > 30000) throw new Error('未能确认登录结果,账号未标记失效。可核对网站后继续,或手动重试本次登录。'); await sleep(250); } else if (flow.phase === 'invalid') { await call('/result', { account: current.username, result: 'invalid', error: flow.invalidError }); show(`${label} 不可用,已保存错误信息,继续下一个……`); await phase('select'); location.assign(HOME); break; } else if (flow.phase === 'success') { await call('/result', { account: current.username, result: 'available' }); flow = null; show(`${label} 登录成功。可继续点击“登陆下一帐号”。`); } else throw new Error('流程阶段不正确,请在管理页结束后重试。'); } } catch (e) { if (!(e instanceof Stopped) && flow?.id === run) { try { await suspend(e.message); } catch (error) { show(error.message, true); } } } finally { running = false; controls(); } } function mount() { const host = document.createElement('div'); host.id = 'dww11-extension-panel'; const root = host.attachShadow({ mode: 'closed' }); const style = document.createElement('style'); style.textContent = ':host{all:initial;position:fixed;right:20px;bottom:24px;z-index:2147483647}section{width:340px;max-width:calc(100vw - 60px);padding:16px;border:1px solid #d3d9e2;border-radius:12px;background:#fff;box-shadow:0 6px 30px #0002;color:#253147;font:14px/1.65 system-ui,sans-serif}strong{font-size:15px}p{white-space:pre-wrap;margin:10px 0}.heading{display:flex;align-items:center;justify-content:space-between;gap:12px}.actions{display:flex;flex-wrap:wrap;gap:8px}button{padding:6px 10px;border:1px solid #ccd3df;border-radius:6px;background:#f7f9fc;color:#253147;font:13px system-ui;cursor:pointer}.actions button:first-child{background:#ed7b16;border-color:#ed7b16;color:#fff}button:disabled{opacity:.5;cursor:default}button[hidden],section[hidden]{display:none}'; root.append(style); panel = document.createElement('section'); panel.hidden = true; const title = document.createElement('strong'); title.textContent = '广谈大湾网 · 随机换号'; const heading = document.createElement('div'); heading.className = 'heading'; hideButton = document.createElement('button'); hideButton.type = 'button'; hideButton.textContent = '隐藏卡片'; hideButton.addEventListener('click', () => changePanelVisibility(true)); heading.append(title, hideButton); message = document.createElement('p'); message.textContent = '正在读取本地进度……'; actions = document.createElement('div'); actions.className = 'actions'; const add = (text, fn) => { const b = document.createElement('button'); b.type = 'button'; b.textContent = text; b.addEventListener('click', () => guarded(fn)); actions.append(b); return b; }; nextButton = add('登陆下一帐号', begin); nextButton.id = 'dww11-extension-next'; resumeButton = add('继续未完成流程', () => resume()); retryButton = add('重试本次登录', () => resume(true)); add('管理 / 导入 CSV', () => request('/manager')); stopButton = add('暂停换号', () => suspend('自动换号已暂停。')); restoreButton = document.createElement('button'); restoreButton.type = 'button'; restoreButton.textContent = '显示换号卡片'; restoreButton.hidden = true; restoreButton.addEventListener('click', () => changePanelVisibility(false)); panel.append(heading, message, actions); root.append(panel, restoreButton); document.body.append(host); mountTopbar(); new MutationObserver(mountTopbar).observe(document.body, { childList: true, subtree: true }); controls(); } async function init() { mount(); const info = await request('/hello'); panelVisibility(info.panelHidden); flow = info.flow; if (flow && !info.owned) { flow.paused = true; show('存在未完成流程。可在原标签页继续;原标签关闭后可在此接续。', true); } else if (flow?.paused) show(flow.reason || '存在未完成流程,点击继续恢复。', true); else if (flow) await drive(); else { show(info.total ? `本地 ${info.total} 个账号,可用 ${info.active} 个。\n第 ${info.round} 轮,剩余 ${info.remaining} 个。` : '请先点击“管理 / 导入 CSV”导入账号。'); await recordRestriction(); } } init().catch(e => { if (message) { panelVisibility(false); show('扩展启动失败:' + e.message, true); } }); })();