import { transact as databaseTransaction } from './db.js'; import { draw, errorRecord, isRestriction, localDay, newSchedule, parseCSV, remaining } from './core.js'; const SITE = /^https:\/\/(?:(?:www|house)\.)?dww11\.com\//; const PHASES = new Set(['select', 'logout', 'login', 'submitted', 'invalid', 'success']); const managerURL = chrome.runtime.getURL('manager.html'); const publicFlow = f => f ? structuredClone(f) : null; const stamp = () => new Date().toISOString(); function transact(reducer, write = true) { return databaseTransaction((accounts, state, store) => { // 旧版登录已成功却卡在发布阶段的流程自动释放,不改账号或抽取记录。 if (state.flow && (['publish', 'publishing'].includes(state.flow.phase) || state.flow.current?.result === 'available')) state.flow = null; return reducer(accounts, state, store); }, write); } function summary(accounts, state) { const s = state.schedule || newSchedule(); const active = accounts.filter(a => a.status !== 'invalid').length; return { total: accounts.length, active, invalid: accounts.length - active, day: localDay(), seedDay: s.day || localDay(), round: s.round, drawn: s.used.length, remaining: remaining(accounts, s, localDay()).length, totalDraws: s.totalDraws, flow: publicFlow(state.flow), panelHidden: !!state.panelHidden }; } function owned(state, body, sender, allowPaused = false) { const f = state.flow; if (!f || f.id !== body.run || f.tabId !== sender.tab?.id || (f.paused && !allowPaused)) throw new Error('流程已暂停、转移或结束,请刷新页面后继续。'); if (f.documentId && sender.documentId && f.documentId !== sender.documentId) throw new Error('旧页面的流程已失效。'); return f; } export async function operate(route, body = {}, sender = {}) { const isManager = sender.id === chrome.runtime.id && sender.url === managerURL; const isContent = sender.frameId === 0 && Number.isInteger(sender.tab?.id) && SITE.test(sender.url || ''); if (!isManager && !isContent) throw new Error('不允许的消息来源。'); const managerRoutes = new Set(['/list', '/import', '/pause', '/restore']); if (managerRoutes.has(route) && !isManager) throw new Error('此操作只能在扩展管理页进行。'); if (!isContent && !managerRoutes.has(route) && !['/status', '/manager'].includes(route)) throw new Error('此操作只能在网站顶层页面进行。'); if (route === '/manager') { await chrome.runtime.openOptionsPage(); return { ok: true }; } if (route === '/import') { if (typeof body.text !== 'string' || body.text.length > 20 * 1024 * 1024) throw new Error('CSV 超过 20 MiB 字符上限。'); const rows = parseCSV(body.text); // 校验整批后再开启写事务。 return transact((accounts, state, store) => { if (state.flow) throw new Error('存在未完成流程,请先在管理页结束流程,再导入。'); const old = new Map(accounts.map(a => [a.username, a])); let added = 0, updated = 0; for (const row of rows) { const previous = old.get(row.username); if (previous) updated++; else added++; store.put(previous ? { ...previous, password: row.password } : { ...row, status: 'unknown', lastchecked: null, lastlogin: null, lastError: null, createdAt: stamp() }); } state.schedule ||= newSchedule(); return { added, updated }; }); } if (route === '/resume') { const snapshot = await transact((a, s) => publicFlow(s.flow), false); if (snapshot?.tabId != null && snapshot.tabId !== sender.tab.id) { let tab; try { tab = await chrome.tabs.get(snapshot.tabId); } catch { /* 原标签已关闭,可接续。 */ } if (tab && SITE.test(tab.url || tab.pendingUrl || '')) throw new Error('另一标签页持有流程,请在原标签页继续,或在管理页结束流程。'); } return transact((accounts, state) => { const f = state.flow; if (!f || f.id !== snapshot?.id) throw new Error('流程已改变,请刷新后重试。'); f.id = crypto.randomUUID(); // 使旧页面的异步操作立即失效。 f.tabId = sender.tab.id; f.documentId = sender.documentId; f.paused = false; f.reason = ''; f.hops = 0; return { flow: publicFlow(f) }; }); } return transact((accounts, state, store) => { state.schedule ||= newSchedule(); if (route === '/status') return summary(accounts, state); if (route === '/panel') { if (typeof body.hidden !== 'boolean') throw new Error('卡片显示设置无效。'); state.panelHidden = body.hidden; return { ok: true }; } if (route === '/list') return { ...summary(accounts, state), accounts: accounts.map(({ password, ...a }) => a) }; if (route === '/hello') { const f = state.flow; if (f?.tabId === sender.tab.id) f.documentId = sender.documentId; return { ...summary(accounts, state), owned: f?.tabId === sender.tab.id }; } if (route === '/pause') { // 管理页明确结束流程;抽取记录保留,下一次选择新的账号。 state.flow = null; return { ok: true }; } if (route === '/restore') { if (state.flow) throw new Error('请先结束未完成流程,再恢复账号。'); const a = accounts.find(a => a.username === body.username); if (!a) throw new Error('账号不存在。'); a.status = 'unknown'; a.lastError = null; store.put(a); return { ok: true }; } if (route === '/restriction') { if (state.flow) throw new Error('换号流程进行中,请先暂停并结束该流程,再记录当前账号。'); const a = accounts.find(a => a.username === body.username); const error = errorRecord(body.error); if (!a) throw new Error('当前账号不在数据库中。'); if (!isRestriction(error)) throw new Error('不是有效的冻结或封禁信息。'); if (a.status !== 'invalid' || a.lastError?.code !== error.code || a.lastError?.message !== error.message) { a.status = 'invalid'; a.lastError = error; store.put(a); } return { ok: true }; } if (route === '/start') { if (state.flow) throw new Error('存在未完成流程,请先继续或在管理页结束。'); if (!accounts.some(a => a.status !== 'invalid')) throw new Error('没有可用账号,请先在管理页导入 CSV。'); state.flow = { id: crypto.randomUUID(), tabId: sender.tab.id, documentId: sender.documentId, phase: 'select', paused: false, startedAt: stamp() }; return { flow: publicFlow(state.flow) }; } const f = owned(state, body, sender); if (route === '/save') { const patch = body.patch || {}; if (patch.phase && !PHASES.has(patch.phase)) throw new Error('无效的流程阶段。'); for (const key of ['phase', 'hops', 'submittedAt', 'oldMessage', 'probedAt', 'invalidError']) { if (Object.hasOwn(patch, key)) f[key] = patch[key]; } return { flow: publicFlow(f) }; } if (route === '/suspend') { f.paused = true; f.reason = String(body.reason || '已暂停').slice(0, 500); return { ok: true }; } if (route === '/finish' || route === '/cancel') { state.flow = null; return { ok: true }; } if (route === '/next') { if (!f.current || (f.phase === 'select' && f.current.result === 'invalid')) { const selected = draw(accounts, state.schedule, localDay()); f.current = { username: selected.account.username, attempted: false, result: null, round: selected.round, position: selected.position }; f.phase = 'logout'; // 预留账号和流程阶段同时提交,崩溃后不会再次抽取。 f.hops = 0; f.submittedAt = null; f.probedAt = null; f.invalidError = null; } const a = accounts.find(a => a.username === f.current.username); if (!a) throw new Error('本次账号已不存在。'); return { account: a, flow: publicFlow(f) }; } const current = f.current; if (!current || current.username !== body.account) throw new Error('当前账号与流程不匹配。'); const a = accounts.find(a => a.username === current.username); if (!a) throw new Error('当前账号不存在。'); if (route === '/attempt') { if (!current.attempted) { a.lastchecked = stamp(); current.attempted = true; store.put(a); } } else if (route === '/result') { if (!current.attempted || !['available', 'invalid'].includes(body.result)) throw new Error('登录结果无效。'); const error = body.result === 'invalid' ? errorRecord(body.error) : null; if (current.result && current.result !== body.result) throw new Error('不能更改已确认的结果。'); if (!current.result) { a.status = body.result; a.lastError = error; if (body.result === 'available') a.lastlogin = stamp(); current.result = body.result; store.put(a); } // 登录成功和释放换号占用在同一事务提交,之后即可直接切换下一账号。 if (body.result === 'available') state.flow = null; } else throw new Error('未知操作。'); return { ok: true }; }); }