168 lines
9.0 KiB
JavaScript
168 lines
9.0 KiB
JavaScript
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 };
|
|
});
|
|
}
|
|
|