background.js 111 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348
  1. // background.js — Service Worker: orchestration, state, tab management, message routing
  2. importScripts('data/names.js', 'hotmail-utils.js', 'content/activation-utils.js');
  3. const {
  4. buildHotmailGraphMessagesUrl,
  5. extractVerificationCodeFromMessage,
  6. filterHotmailAccountsByUsage,
  7. getLatestHotmailMessage,
  8. getHotmailGraphRequestConfig,
  9. getHotmailVerificationPollConfig,
  10. getHotmailVerificationRequestTimestamp,
  11. normalizeHotmailMailApiMessages,
  12. pickHotmailAccountForRun,
  13. pickVerificationMessage,
  14. pickVerificationMessageWithFallback,
  15. pickVerificationMessageWithTimeFallback,
  16. shouldClearHotmailCurrentSelection,
  17. } = self.HotmailUtils;
  18. const {
  19. isRecoverableStep9AuthFailure,
  20. } = self.MultiPageActivationUtils;
  21. const buildHotmailMailApiLatestUrl = buildHotmailGraphMessagesUrl;
  22. const getHotmailMailApiRequestConfig = getHotmailGraphRequestConfig;
  23. const LOG_PREFIX = '[MultiPage:bg]';
  24. const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill';
  25. const HOTMAIL_PROVIDER = 'hotmail-api';
  26. const HOTMAIL_MAILBOXES = ['INBOX', 'Junk'];
  27. const STOP_ERROR_MESSAGE = '流程已被用户停止。';
  28. const HUMAN_STEP_DELAY_MIN = 700;
  29. const HUMAN_STEP_DELAY_MAX = 2200;
  30. const STEP7_RESTART_MAX_ROUNDS = 8;
  31. initializeSessionStorageAccess();
  32. // ============================================================
  33. // 状态管理(chrome.storage.session + chrome.storage.local)
  34. // ============================================================
  35. const PERSISTED_SETTING_DEFAULTS = {
  36. vpsUrl: '', // VPS 面板地址,可手动填写。
  37. vpsPassword: '', // VPS 面板登录密码,可手动填写。
  38. customPassword: '', // 自定义账号密码;留空时由程序自动生成随机密码。
  39. autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
  40. mailProvider: '163', // 验证码邮箱来源,当前支持 163 / inbucket。
  41. inbucketHost: '', // 仅当 mailProvider 为 inbucket 时填写 Inbucket 地址,其他情况保持为空。
  42. inbucketMailbox: '', // 仅当 mailProvider 为 inbucket 时填写邮箱名,其他情况保持为空。
  43. hotmailAccounts: [],
  44. };
  45. const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
  46. const DEFAULT_STATE = {
  47. currentStep: 0, // 当前流程执行到的步骤编号。
  48. stepStatuses: {
  49. 1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending', // 运行时步骤状态映射,不要手动预填。
  50. 6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending',
  51. },
  52. oauthUrl: null, // 运行时抓取到的 OAuth 地址,不要手动预填。
  53. email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。
  54. password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。
  55. accounts: [], // 已生成账号记录:{ email, password, createdAt }。
  56. lastEmailTimestamp: null, // 最近一次获取到邮箱数据的运行时时间戳。
  57. lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。
  58. lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。
  59. localhostUrl: null, // 运行时捕获到的 localhost 回调地址,不要手动预填。
  60. flowStartTime: null, // 当前流程开始时间。
  61. tabRegistry: {}, // 程序维护的标签页注册表。
  62. sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
  63. logs: [], // 侧边栏展示的运行日志。
  64. ...PERSISTED_SETTING_DEFAULTS, // 合并 chrome.storage.local 中持久化保存的用户配置。
  65. autoRunning: false, // 当前是否处于自动运行中。
  66. autoRunPhase: 'idle', // 当前自动运行阶段。
  67. autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。
  68. autoRunTotalRuns: 1, // 自动运行计划总轮数。
  69. autoRunAttemptRun: 0, // 当前轮次的重试序号。
  70. signupVerificationRequestedAt: null,
  71. loginVerificationRequestedAt: null,
  72. currentHotmailAccountId: null,
  73. };
  74. async function getPersistedSettings() {
  75. const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS);
  76. return {
  77. ...PERSISTED_SETTING_DEFAULTS,
  78. ...stored,
  79. autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures),
  80. hotmailAccounts: normalizeHotmailAccounts(stored.hotmailAccounts),
  81. };
  82. }
  83. async function getState() {
  84. const [state, persistedSettings] = await Promise.all([
  85. chrome.storage.session.get(null),
  86. getPersistedSettings(),
  87. ]);
  88. return { ...DEFAULT_STATE, ...persistedSettings, ...state };
  89. }
  90. async function initializeSessionStorageAccess() {
  91. try {
  92. if (chrome.storage?.session?.setAccessLevel) {
  93. await chrome.storage.session.setAccessLevel({
  94. accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS',
  95. });
  96. console.log(LOG_PREFIX, 'Enabled storage.session for content scripts');
  97. }
  98. } catch (err) {
  99. console.warn(LOG_PREFIX, 'Failed to enable storage.session for content scripts:', err?.message || err);
  100. }
  101. }
  102. async function setState(updates) {
  103. console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
  104. await chrome.storage.session.set(updates);
  105. }
  106. async function setPersistentSettings(updates) {
  107. const persistedUpdates = {};
  108. for (const key of PERSISTED_SETTING_KEYS) {
  109. if (updates[key] !== undefined) {
  110. if (key === 'autoRunSkipFailures') {
  111. persistedUpdates[key] = Boolean(updates[key]);
  112. } else if (key === 'hotmailAccounts') {
  113. persistedUpdates[key] = normalizeHotmailAccounts(updates[key]);
  114. } else {
  115. persistedUpdates[key] = updates[key];
  116. }
  117. }
  118. }
  119. if (Object.keys(persistedUpdates).length > 0) {
  120. await chrome.storage.local.set(persistedUpdates);
  121. }
  122. }
  123. function broadcastDataUpdate(payload) {
  124. chrome.runtime.sendMessage({
  125. type: 'DATA_UPDATED',
  126. payload,
  127. }).catch(() => { });
  128. }
  129. async function setEmailState(email) {
  130. await setState({ email });
  131. broadcastDataUpdate({ email });
  132. if (email) {
  133. await resumeAutoRunIfWaitingForEmail();
  134. }
  135. }
  136. async function setPasswordState(password) {
  137. await setState({ password });
  138. broadcastDataUpdate({ password });
  139. }
  140. async function resetState() {
  141. console.log(LOG_PREFIX, 'Resetting all state');
  142. // Preserve settings and persistent data across resets
  143. const [prev, persistedSettings] = await Promise.all([
  144. chrome.storage.session.get([
  145. 'seenCodes',
  146. 'seenInbucketMailIds',
  147. 'accounts',
  148. 'tabRegistry',
  149. 'sourceLastUrls',
  150. ]),
  151. getPersistedSettings(),
  152. ]);
  153. await chrome.storage.session.clear();
  154. await chrome.storage.session.set({
  155. ...DEFAULT_STATE,
  156. ...persistedSettings,
  157. seenCodes: prev.seenCodes || [],
  158. seenInbucketMailIds: prev.seenInbucketMailIds || [],
  159. accounts: prev.accounts || [],
  160. tabRegistry: prev.tabRegistry || {},
  161. sourceLastUrls: prev.sourceLastUrls || {},
  162. });
  163. }
  164. /**
  165. * Generate a random password: 14 chars, mix of uppercase, lowercase, digits, symbols.
  166. */
  167. function generatePassword() {
  168. const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
  169. const lower = 'abcdefghjkmnpqrstuvwxyz';
  170. const digits = '23456789';
  171. const symbols = '!@#$%&*?';
  172. const all = upper + lower + digits + symbols;
  173. // Ensure at least one of each type
  174. let pw = '';
  175. pw += upper[Math.floor(Math.random() * upper.length)];
  176. pw += lower[Math.floor(Math.random() * lower.length)];
  177. pw += digits[Math.floor(Math.random() * digits.length)];
  178. pw += symbols[Math.floor(Math.random() * symbols.length)];
  179. // Fill remaining 10 chars
  180. for (let i = 0; i < 10; i++) {
  181. pw += all[Math.floor(Math.random() * all.length)];
  182. }
  183. // Shuffle
  184. return pw.split('').sort(() => Math.random() - 0.5).join('');
  185. }
  186. function normalizeHotmailAccount(account = {}) {
  187. const normalizedLastAuthAt = Number.isFinite(Number(account.lastAuthAt)) ? Number(account.lastAuthAt) : 0;
  188. const normalizedStatus = String(
  189. account.status
  190. || (normalizedLastAuthAt > 0 || account.accessToken ? 'authorized' : 'pending')
  191. );
  192. return {
  193. id: String(account.id || crypto.randomUUID()),
  194. email: String(account.email || '').trim(),
  195. password: String(account.password || ''),
  196. clientId: String(account.clientId || '').trim(),
  197. accessToken: String(account.accessToken || ''),
  198. refreshToken: String(account.refreshToken || ''),
  199. expiresAt: Number.isFinite(Number(account.expiresAt)) ? Number(account.expiresAt) : 0,
  200. status: normalizedStatus,
  201. enabled: account.enabled !== undefined ? Boolean(account.enabled) : true,
  202. used: Boolean(account.used),
  203. lastUsedAt: Number.isFinite(Number(account.lastUsedAt)) ? Number(account.lastUsedAt) : 0,
  204. lastAuthAt: normalizedLastAuthAt,
  205. lastError: String(account.lastError || ''),
  206. };
  207. }
  208. function normalizeHotmailAccounts(accounts) {
  209. if (!Array.isArray(accounts)) return [];
  210. const deduped = new Map();
  211. for (const account of accounts) {
  212. const normalized = normalizeHotmailAccount(account);
  213. if (!normalized.email && !normalized.id) continue;
  214. deduped.set(normalized.id, normalized);
  215. }
  216. return [...deduped.values()];
  217. }
  218. function findHotmailAccount(accounts, accountId) {
  219. return normalizeHotmailAccounts(accounts).find((account) => account.id === accountId) || null;
  220. }
  221. function isHotmailProvider(stateOrProvider) {
  222. const provider = typeof stateOrProvider === 'string'
  223. ? stateOrProvider
  224. : stateOrProvider?.mailProvider;
  225. return provider === HOTMAIL_PROVIDER;
  226. }
  227. async function syncHotmailAccounts(accounts) {
  228. const normalized = normalizeHotmailAccounts(accounts);
  229. await setPersistentSettings({ hotmailAccounts: normalized });
  230. await setState({ hotmailAccounts: normalized });
  231. broadcastDataUpdate({ hotmailAccounts: normalized });
  232. return normalized;
  233. }
  234. async function upsertHotmailAccount(input) {
  235. const state = await getState();
  236. const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
  237. const normalizedEmail = String(input?.email || '').trim().toLowerCase();
  238. const existing = input?.id
  239. ? findHotmailAccount(accounts, input.id)
  240. : accounts.find((account) => account.email.toLowerCase() === normalizedEmail) || null;
  241. const credentialsChanged = !existing
  242. || (input?.clientId !== undefined && String(input.clientId).trim() !== existing.clientId)
  243. || (input?.refreshToken !== undefined && String(input.refreshToken).trim() !== existing.refreshToken)
  244. || (input?.email !== undefined && String(input.email).trim().toLowerCase() !== existing.email.toLowerCase());
  245. const normalized = normalizeHotmailAccount({
  246. ...(existing || {}),
  247. ...(credentialsChanged ? {
  248. accessToken: '',
  249. expiresAt: 0,
  250. status: 'pending',
  251. lastAuthAt: 0,
  252. lastError: '',
  253. } : {}),
  254. ...input,
  255. id: input?.id || existing?.id || crypto.randomUUID(),
  256. });
  257. const nextAccounts = existing
  258. ? accounts.map((account) => (account.id === normalized.id ? normalized : account))
  259. : [...accounts, normalized];
  260. await syncHotmailAccounts(nextAccounts);
  261. return normalized;
  262. }
  263. async function deleteHotmailAccount(accountId) {
  264. const state = await getState();
  265. const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
  266. const nextAccounts = accounts.filter((account) => account.id !== accountId);
  267. await syncHotmailAccounts(nextAccounts);
  268. if (state.currentHotmailAccountId === accountId) {
  269. await setState({ currentHotmailAccountId: null });
  270. if (isHotmailProvider(state)) {
  271. await setEmailState(null);
  272. }
  273. broadcastDataUpdate({ currentHotmailAccountId: null });
  274. }
  275. }
  276. async function deleteHotmailAccounts(mode = 'all') {
  277. const state = await getState();
  278. const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
  279. const targets = filterHotmailAccountsByUsage(accounts, mode);
  280. const targetIds = new Set(targets.map((account) => account.id));
  281. const nextAccounts = mode === 'used'
  282. ? accounts.filter((account) => !targetIds.has(account.id))
  283. : [];
  284. await syncHotmailAccounts(nextAccounts);
  285. if (state.currentHotmailAccountId && targetIds.has(state.currentHotmailAccountId)) {
  286. await setState({ currentHotmailAccountId: null });
  287. if (isHotmailProvider(state)) {
  288. await setEmailState(null);
  289. }
  290. broadcastDataUpdate({ currentHotmailAccountId: null });
  291. }
  292. return {
  293. deletedCount: targets.length,
  294. remainingCount: nextAccounts.length,
  295. };
  296. }
  297. async function patchHotmailAccount(accountId, updates = {}) {
  298. const state = await getState();
  299. const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
  300. const account = findHotmailAccount(accounts, accountId);
  301. if (!account) {
  302. throw new Error('未找到对应的 Hotmail 账号。');
  303. }
  304. const nextAccount = normalizeHotmailAccount({
  305. ...account,
  306. ...updates,
  307. id: account.id,
  308. });
  309. await syncHotmailAccounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
  310. if (state.currentHotmailAccountId === account.id && shouldClearHotmailCurrentSelection(nextAccount)) {
  311. await setState({ currentHotmailAccountId: null });
  312. broadcastDataUpdate({ currentHotmailAccountId: null });
  313. if (isHotmailProvider(state)) {
  314. await setEmailState(null);
  315. }
  316. }
  317. return nextAccount;
  318. }
  319. async function setCurrentHotmailAccount(accountId, options = {}) {
  320. const { markUsed = false, syncEmail = true } = options;
  321. const state = await getState();
  322. const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
  323. const account = findHotmailAccount(accounts, accountId);
  324. if (!account) {
  325. throw new Error('未找到对应的 Hotmail 账号。');
  326. }
  327. if (markUsed) {
  328. account.lastUsedAt = Date.now();
  329. await syncHotmailAccounts(accounts.map((item) => (item.id === account.id ? account : item)));
  330. }
  331. await setState({ currentHotmailAccountId: account.id });
  332. broadcastDataUpdate({ currentHotmailAccountId: account.id });
  333. if (syncEmail) {
  334. await setEmailState(account.email || null);
  335. }
  336. return account;
  337. }
  338. async function ensureHotmailAccountForFlow(options = {}) {
  339. const { allowAllocate = true, markUsed = false, preferredAccountId = null } = options;
  340. const state = await getState();
  341. const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
  342. const isAccountAllocatable = (candidate) => Boolean(candidate)
  343. && candidate.status === 'authorized'
  344. && !candidate.used
  345. && Boolean(candidate.refreshToken);
  346. let account = null;
  347. if (preferredAccountId) {
  348. account = findHotmailAccount(accounts, preferredAccountId);
  349. }
  350. if (!account && state.currentHotmailAccountId) {
  351. account = findHotmailAccount(accounts, state.currentHotmailAccountId);
  352. }
  353. if ((!account || !isAccountAllocatable(account)) && allowAllocate) {
  354. account = pickHotmailAccountForRun(accounts, {});
  355. }
  356. if (!account) {
  357. throw new Error('没有可用的 Hotmail 账号。请先在侧边栏添加至少一个带刷新令牌(refresh token)的账号。');
  358. }
  359. if (!isAccountAllocatable(account)) {
  360. throw new Error(`Hotmail 账号 ${account.email || account.id} 尚未就绪,无法读取邮件。`);
  361. }
  362. return setCurrentHotmailAccount(account.id, { markUsed, syncEmail: true });
  363. }
  364. async function requestHotmailMailApiLegacy(account, mailbox = 'INBOX') {
  365. if (!account?.email) {
  366. throw new Error('Hotmail 账号缺少邮箱地址。');
  367. }
  368. if (!account?.clientId) {
  369. throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少客户端 ID。`);
  370. }
  371. if (!account?.refreshToken) {
  372. throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`);
  373. }
  374. const url = buildHotmailMailApiLatestUrl({
  375. clientId: account.clientId,
  376. email: account.email,
  377. refreshToken: account.refreshToken,
  378. mailbox,
  379. responseType: 'json',
  380. });
  381. const { timeoutMs } = getHotmailMailApiRequestConfig();
  382. const controller = new AbortController();
  383. const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
  384. let response;
  385. try {
  386. response = await fetch(url, { method: 'GET', signal: controller.signal });
  387. } catch (err) {
  388. if (err?.name === 'AbortError') {
  389. throw new Error(`Hotmail API 请求超时(>${Math.round(timeoutMs / 1000)} 秒):${mailbox}`);
  390. }
  391. throw new Error(`Hotmail API 请求失败:${err.message}`);
  392. } finally {
  393. clearTimeout(timeoutId);
  394. }
  395. const text = await response.text();
  396. let payload = {};
  397. try {
  398. payload = text ? JSON.parse(text) : {};
  399. } catch {
  400. payload = { raw: text };
  401. }
  402. if (!response.ok) {
  403. const errorText = payload?.message || payload?.error || payload?.msg || text || `HTTP ${response.status}`;
  404. throw new Error(`Hotmail API 请求失败:${errorText}`);
  405. }
  406. if (payload && payload.success === false) {
  407. const errorText = payload?.message || payload?.msg || payload?.error || '未知错误';
  408. throw new Error(`Hotmail API 返回失败:${errorText}`);
  409. }
  410. return {
  411. mailbox,
  412. payload,
  413. messages: normalizeHotmailMailApiMessages(payload?.data),
  414. nextRefreshToken: String(payload?.new_refresh_token || payload?.newRefreshToken || '').trim(),
  415. };
  416. }
  417. function applyHotmailApiResultToAccountLegacy(account, apiResult) {
  418. const nextRefreshToken = String(apiResult?.nextRefreshToken || '').trim();
  419. return {
  420. ...account,
  421. accessToken: '',
  422. expiresAt: 0,
  423. refreshToken: nextRefreshToken || account.refreshToken,
  424. status: 'authorized',
  425. lastAuthAt: Date.now(),
  426. lastError: '',
  427. };
  428. }
  429. async function fetchHotmailMailboxMessagesLegacy(account, mailboxes = HOTMAIL_MAILBOXES) {
  430. let workingAccount = normalizeHotmailAccount(account);
  431. const mailboxResults = [];
  432. for (const mailbox of mailboxes) {
  433. const result = await requestHotmailMailApiLegacy(workingAccount, mailbox);
  434. workingAccount = applyHotmailApiResultToAccountLegacy(workingAccount, result);
  435. mailboxResults.push({
  436. mailbox,
  437. count: result.messages.length,
  438. messages: result.messages.map((message) => ({ ...message, mailbox })),
  439. });
  440. }
  441. const savedAccount = await upsertHotmailAccount(workingAccount);
  442. return {
  443. account: savedAccount,
  444. mailboxResults,
  445. messages: mailboxResults.flatMap((item) => item.messages),
  446. };
  447. }
  448. function isHotmailAccessTokenUsable(account, now = Date.now()) {
  449. return Boolean(account?.accessToken)
  450. && Number(account?.expiresAt || 0) > now + 60_000;
  451. }
  452. async function refreshHotmailAccessToken(account) {
  453. if (!account?.email) {
  454. throw new Error('Hotmail 账号缺少邮箱地址。');
  455. }
  456. if (!account?.clientId) {
  457. throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少客户端 ID。`);
  458. }
  459. if (!account?.refreshToken) {
  460. throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`);
  461. }
  462. const { timeoutMs, scopes, tokenUrl } = getHotmailGraphRequestConfig();
  463. const controller = new AbortController();
  464. const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
  465. const formData = new URLSearchParams();
  466. formData.set('client_id', account.clientId);
  467. formData.set('grant_type', 'refresh_token');
  468. formData.set('refresh_token', account.refreshToken);
  469. formData.set('scope', scopes.join(' '));
  470. let response;
  471. try {
  472. response = await fetch(tokenUrl, {
  473. method: 'POST',
  474. headers: {
  475. 'Content-Type': 'application/x-www-form-urlencoded',
  476. },
  477. body: formData.toString(),
  478. signal: controller.signal,
  479. });
  480. } catch (err) {
  481. const error = new Error(
  482. err?.name === 'AbortError'
  483. ? `Hotmail 令牌刷新超时(>${Math.round(timeoutMs / 1000)} 秒)`
  484. : `Hotmail 令牌刷新失败:${err.message}`
  485. );
  486. error.code = 'HOTMAIL_TOKEN_REFRESH_FAILED';
  487. throw error;
  488. } finally {
  489. clearTimeout(timeoutId);
  490. }
  491. const text = await response.text();
  492. let payload = {};
  493. try {
  494. payload = text ? JSON.parse(text) : {};
  495. } catch {
  496. payload = { raw: text };
  497. }
  498. if (!response.ok || !payload?.access_token) {
  499. const errorText = payload?.error_description || payload?.error?.message || payload?.error || payload?.message || text || `HTTP ${response.status}`;
  500. const error = new Error(`Hotmail 令牌刷新失败:${errorText}`);
  501. error.code = 'HOTMAIL_TOKEN_REFRESH_FAILED';
  502. throw error;
  503. }
  504. const expiresInSeconds = Math.max(60, Number(payload.expires_in || payload.expiresIn || 0) || 3600);
  505. return normalizeHotmailAccount({
  506. ...account,
  507. accessToken: String(payload.access_token || ''),
  508. refreshToken: String(payload.refresh_token || '').trim() || account.refreshToken,
  509. expiresAt: Date.now() + expiresInSeconds * 1000,
  510. status: 'authorized',
  511. lastAuthAt: Date.now(),
  512. lastError: '',
  513. });
  514. }
  515. async function requestHotmailGraphMessages(account, mailbox = 'INBOX') {
  516. const { timeoutMs, pageSize, messageFields } = getHotmailGraphRequestConfig();
  517. const controller = new AbortController();
  518. const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
  519. const url = buildHotmailGraphMessagesUrl({
  520. mailbox,
  521. top: pageSize,
  522. selectFields: messageFields,
  523. });
  524. let response;
  525. try {
  526. response = await fetch(url, {
  527. method: 'GET',
  528. headers: {
  529. Accept: 'application/json',
  530. Authorization: `Bearer ${account.accessToken}`,
  531. },
  532. signal: controller.signal,
  533. });
  534. } catch (err) {
  535. const error = new Error(
  536. err?.name === 'AbortError'
  537. ? `Hotmail 邮件请求超时(>${Math.round(timeoutMs / 1000)} 秒):${mailbox}`
  538. : `Hotmail 邮件请求失败:${err.message}`
  539. );
  540. error.code = 'HOTMAIL_GRAPH_REQUEST_FAILED';
  541. throw error;
  542. } finally {
  543. clearTimeout(timeoutId);
  544. }
  545. const text = await response.text();
  546. let payload = {};
  547. try {
  548. payload = text ? JSON.parse(text) : {};
  549. } catch {
  550. payload = { raw: text };
  551. }
  552. if (!response.ok) {
  553. const errorText = payload?.error?.message || payload?.error_description || payload?.message || text || `HTTP ${response.status}`;
  554. const error = new Error(`Hotmail 邮件请求失败:${errorText}`);
  555. error.code = response.status === 401 || response.status === 403
  556. ? 'HOTMAIL_GRAPH_AUTH_FAILED'
  557. : 'HOTMAIL_GRAPH_REQUEST_FAILED';
  558. throw error;
  559. }
  560. return {
  561. mailbox,
  562. payload,
  563. messages: normalizeHotmailMailApiMessages(payload?.value),
  564. };
  565. }
  566. function buildHotmailAuthFailureAccount(account, errorMessage) {
  567. return normalizeHotmailAccount({
  568. ...account,
  569. accessToken: '',
  570. expiresAt: 0,
  571. status: 'error',
  572. lastError: String(errorMessage || ''),
  573. });
  574. }
  575. async function fetchHotmailMailboxMessages(account, mailboxes = HOTMAIL_MAILBOXES) {
  576. let workingAccount = normalizeHotmailAccount(account);
  577. const mailboxResults = [];
  578. try {
  579. if (!isHotmailAccessTokenUsable(workingAccount)) {
  580. workingAccount = await refreshHotmailAccessToken(workingAccount);
  581. }
  582. for (const mailbox of mailboxes) {
  583. let result;
  584. try {
  585. result = await requestHotmailGraphMessages(workingAccount, mailbox);
  586. } catch (err) {
  587. if (err?.code !== 'HOTMAIL_GRAPH_AUTH_FAILED') {
  588. throw err;
  589. }
  590. workingAccount = await refreshHotmailAccessToken({
  591. ...workingAccount,
  592. accessToken: '',
  593. expiresAt: 0,
  594. });
  595. result = await requestHotmailGraphMessages(workingAccount, mailbox);
  596. }
  597. mailboxResults.push({
  598. mailbox,
  599. count: result.messages.length,
  600. messages: result.messages.map((message) => ({ ...message, mailbox })),
  601. });
  602. }
  603. } catch (err) {
  604. if (err?.code === 'HOTMAIL_TOKEN_REFRESH_FAILED' || err?.code === 'HOTMAIL_GRAPH_AUTH_FAILED') {
  605. const failedAccount = buildHotmailAuthFailureAccount(workingAccount, err.message);
  606. await upsertHotmailAccount(failedAccount);
  607. }
  608. throw err;
  609. }
  610. const savedAccount = await upsertHotmailAccount(workingAccount);
  611. return {
  612. account: savedAccount,
  613. mailboxResults,
  614. messages: mailboxResults.flatMap((item) => item.messages),
  615. };
  616. }
  617. async function verifyHotmailAccount(accountId) {
  618. const state = await getState();
  619. const account = findHotmailAccount(state.hotmailAccounts, accountId);
  620. if (!account) {
  621. throw new Error('未找到需要校验的 Hotmail 账号。');
  622. }
  623. const result = await fetchHotmailMailboxMessages(account, ['INBOX']);
  624. return {
  625. account: result.account,
  626. messageCount: result.mailboxResults[0]?.count || 0,
  627. };
  628. }
  629. async function testHotmailAccountMailAccess(accountId) {
  630. const state = await getState();
  631. const account = findHotmailAccount(state.hotmailAccounts, accountId);
  632. if (!account) {
  633. throw new Error('未找到需要测试的 Hotmail 账号。');
  634. }
  635. const result = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES);
  636. const latestMessage = getLatestHotmailMessage(result.messages);
  637. const latestCode = latestMessage ? extractVerificationCodeFromMessage(latestMessage) : null;
  638. return {
  639. account: result.account,
  640. accountId: result.account.id,
  641. email: result.account.email,
  642. messageCount: result.messages.length,
  643. latestSubject: latestMessage?.subject || '',
  644. latestMailbox: latestMessage?.mailbox || '',
  645. latestCode: latestCode || '',
  646. inboxCount: result.mailboxResults.find((item) => item.mailbox === 'INBOX')?.count || 0,
  647. junkCount: result.mailboxResults.find((item) => item.mailbox === 'Junk')?.count || 0,
  648. };
  649. }
  650. async function pollHotmailVerificationCode(step, state, pollPayload = {}) {
  651. await addLog(`步骤 ${step}:正在确定 Hotmail 收信账号...`, 'info');
  652. let account = await ensureHotmailAccountForFlow({
  653. allowAllocate: true,
  654. markUsed: false,
  655. preferredAccountId: state.currentHotmailAccountId || null,
  656. });
  657. await addLog(`步骤 ${step}:当前使用 Hotmail 账号 ${account.email} 轮询收件箱。`, 'info');
  658. const maxAttempts = Number(pollPayload.maxAttempts) || 5;
  659. const intervalMs = Number(pollPayload.intervalMs) || 3000;
  660. let lastError = null;
  661. function summarizeMessagesForLog(messages) {
  662. return (messages || [])
  663. .slice()
  664. .sort((left, right) => {
  665. const leftTime = Date.parse(left.receivedDateTime || '') || 0;
  666. const rightTime = Date.parse(right.receivedDateTime || '') || 0;
  667. return rightTime - leftTime;
  668. })
  669. .slice(0, 3)
  670. .map((message) => {
  671. const receivedAt = message?.receivedDateTime || '未知时间';
  672. const sender = message?.from?.emailAddress?.address || '未知发件人';
  673. const subject = message?.subject || '(无主题)';
  674. const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80);
  675. return `[${message.mailbox || 'INBOX'}] ${receivedAt} | ${sender} | ${subject} | ${preview}`;
  676. })
  677. .join(' || ');
  678. }
  679. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  680. throwIfStopped();
  681. try {
  682. await addLog(`步骤 ${step}:正在轮询 Hotmail 邮件(${attempt}/${maxAttempts})...`, 'info');
  683. const fetchResult = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES);
  684. account = fetchResult.account;
  685. const matchResult = pickVerificationMessageWithTimeFallback(fetchResult.messages, {
  686. afterTimestamp: pollPayload.filterAfterTimestamp || 0,
  687. senderFilters: pollPayload.senderFilters || [],
  688. subjectFilters: pollPayload.subjectFilters || [],
  689. excludeCodes: pollPayload.excludeCodes || [],
  690. });
  691. const match = matchResult.match;
  692. if (match?.code) {
  693. const mailboxLabel = match.message?.mailbox || 'INBOX';
  694. if (matchResult.usedRelaxedFilters) {
  695. const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
  696. await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Hotmail ${mailboxLabel} 验证码。`, 'warn');
  697. }
  698. await addLog(`步骤 ${step}:已在 Hotmail ${mailboxLabel} 中找到验证码:${match.code}`, 'ok');
  699. return {
  700. ok: true,
  701. code: match.code,
  702. emailTimestamp: match.receivedAt || Date.now(),
  703. mailId: match.message?.id || '',
  704. };
  705. }
  706. lastError = new Error(`步骤 ${step}:暂未在 Hotmail 收件箱中找到匹配验证码(${attempt}/${maxAttempts})。`);
  707. await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
  708. const mailSummary = summarizeMessagesForLog(fetchResult.messages);
  709. if (mailSummary) {
  710. await addLog(`步骤 ${step}:最近邮件样本:${mailSummary}`, 'info');
  711. }
  712. } catch (err) {
  713. lastError = err;
  714. await addLog(`步骤 ${step}:Hotmail 收件箱轮询失败:${err.message}`, 'warn');
  715. }
  716. if (attempt < maxAttempts) {
  717. await sleepWithStop(intervalMs);
  718. }
  719. }
  720. throw lastError || new Error(`步骤 ${step}:未在 Hotmail 收件箱中找到新的匹配验证码。`);
  721. }
  722. // ============================================================
  723. // Tab Registry
  724. // ============================================================
  725. async function getTabRegistry() {
  726. const state = await getState();
  727. return state.tabRegistry || {};
  728. }
  729. async function registerTab(source, tabId) {
  730. const registry = await getTabRegistry();
  731. registry[source] = { tabId, ready: true };
  732. await setState({ tabRegistry: registry });
  733. console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
  734. }
  735. async function isTabAlive(source) {
  736. const registry = await getTabRegistry();
  737. const entry = registry[source];
  738. if (!entry) return false;
  739. try {
  740. await chrome.tabs.get(entry.tabId);
  741. return true;
  742. } catch {
  743. // Tab no longer exists — clean up registry
  744. registry[source] = null;
  745. await setState({ tabRegistry: registry });
  746. return false;
  747. }
  748. }
  749. async function getTabId(source) {
  750. const registry = await getTabRegistry();
  751. return registry[source]?.tabId || null;
  752. }
  753. function parseUrlSafely(rawUrl) {
  754. if (!rawUrl) return null;
  755. try {
  756. return new URL(rawUrl);
  757. } catch {
  758. return null;
  759. }
  760. }
  761. function isSignupPageHost(hostname = '') {
  762. return ['auth0.openai.com', 'auth.openai.com', 'accounts.openai.com'].includes(hostname);
  763. }
  764. function is163MailHost(hostname = '') {
  765. return hostname === 'mail.163.com'
  766. || hostname.endsWith('.mail.163.com')
  767. || hostname === 'webmail.vip.163.com';
  768. }
  769. function isLocalhostOAuthCallbackUrl(rawUrl) {
  770. const parsed = parseUrlSafely(rawUrl);
  771. if (!parsed) return false;
  772. if (!['http:', 'https:'].includes(parsed.protocol)) return false;
  773. if (parsed.hostname !== 'localhost') return false;
  774. if (parsed.pathname !== '/auth/callback') return false;
  775. const code = (parsed.searchParams.get('code') || '').trim();
  776. const state = (parsed.searchParams.get('state') || '').trim();
  777. return Boolean(code && state);
  778. }
  779. function buildLocalhostCleanupPrefix(rawUrl) {
  780. if (!isLocalhostOAuthCallbackUrl(rawUrl)) return '';
  781. const parsed = parseUrlSafely(rawUrl);
  782. return parsed ? `${parsed.origin}/auth` : '';
  783. }
  784. function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
  785. const candidate = parseUrlSafely(candidateUrl);
  786. if (!candidate) return false;
  787. const reference = parseUrlSafely(referenceUrl);
  788. switch (source) {
  789. case 'signup-page':
  790. return isSignupPageHost(candidate.hostname);
  791. case 'duck-mail':
  792. return candidate.hostname === 'duckduckgo.com' && candidate.pathname.startsWith('/email/');
  793. case 'qq-mail':
  794. return candidate.hostname === 'mail.qq.com' || candidate.hostname === 'wx.mail.qq.com';
  795. case 'mail-163':
  796. return is163MailHost(candidate.hostname);
  797. case 'inbucket-mail':
  798. return Boolean(reference)
  799. && candidate.origin === reference.origin
  800. && candidate.pathname.startsWith('/m/');
  801. case 'vps-panel':
  802. return Boolean(reference)
  803. && candidate.origin === reference.origin
  804. && candidate.pathname === reference.pathname;
  805. default:
  806. return false;
  807. }
  808. }
  809. async function rememberSourceLastUrl(source, url) {
  810. if (!source || !url) return;
  811. const state = await getState();
  812. const sourceLastUrls = { ...(state.sourceLastUrls || {}) };
  813. sourceLastUrls[source] = url;
  814. await setState({ sourceLastUrls });
  815. }
  816. async function closeConflictingTabsForSource(source, currentUrl, options = {}) {
  817. const { excludeTabIds = [] } = options;
  818. const excluded = new Set(excludeTabIds.filter(id => Number.isInteger(id)));
  819. const state = await getState();
  820. const lastUrl = state.sourceLastUrls?.[source];
  821. const referenceUrls = [currentUrl, lastUrl].filter(Boolean);
  822. if (!referenceUrls.length) return;
  823. const tabs = await chrome.tabs.query({});
  824. const matchedIds = tabs
  825. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  826. .filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
  827. .map(tab => tab.id);
  828. if (!matchedIds.length) return;
  829. await chrome.tabs.remove(matchedIds).catch(() => { });
  830. const registry = await getTabRegistry();
  831. if (registry[source]?.tabId && matchedIds.includes(registry[source].tabId)) {
  832. registry[source] = null;
  833. await setState({ tabRegistry: registry });
  834. }
  835. await addLog(`已关闭 ${matchedIds.length} 个旧的${getSourceLabel(source)}标签页。`, 'info');
  836. }
  837. async function closeTabsByUrlPrefix(prefix, options = {}) {
  838. if (!prefix) return 0;
  839. const { excludeTabIds = [] } = options;
  840. const excluded = new Set(excludeTabIds.filter(id => Number.isInteger(id)));
  841. const tabs = await chrome.tabs.query({});
  842. const matchedIds = tabs
  843. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  844. .filter((tab) => typeof tab.url === 'string' && tab.url.startsWith(prefix))
  845. .map((tab) => tab.id);
  846. if (!matchedIds.length) return 0;
  847. await chrome.tabs.remove(matchedIds).catch(() => { });
  848. await addLog(`已关闭 ${matchedIds.length} 个匹配 ${prefix} 的 localhost 残留标签页。`, 'info');
  849. return matchedIds.length;
  850. }
  851. async function pingContentScriptOnTab(tabId) {
  852. if (!Number.isInteger(tabId)) return null;
  853. try {
  854. return await chrome.tabs.sendMessage(tabId, {
  855. type: 'PING',
  856. source: 'background',
  857. payload: {},
  858. });
  859. } catch {
  860. return null;
  861. }
  862. }
  863. async function waitForTabUrlFamily(source, tabId, referenceUrl, options = {}) {
  864. const { timeoutMs = 15000, retryDelayMs = 400 } = options;
  865. const start = Date.now();
  866. while (Date.now() - start < timeoutMs) {
  867. try {
  868. const tab = await chrome.tabs.get(tabId);
  869. if (matchesSourceUrlFamily(source, tab.url, referenceUrl)) {
  870. return tab;
  871. }
  872. } catch {
  873. return null;
  874. }
  875. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  876. }
  877. return null;
  878. }
  879. async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
  880. const {
  881. inject = null,
  882. injectSource = null,
  883. timeoutMs = 30000,
  884. retryDelayMs = 700,
  885. logMessage = '',
  886. } = options;
  887. const start = Date.now();
  888. let lastError = null;
  889. let logged = false;
  890. while (Date.now() - start < timeoutMs) {
  891. const pong = await pingContentScriptOnTab(tabId);
  892. if (pong?.ok && (!pong.source || pong.source === source)) {
  893. await registerTab(source, tabId);
  894. return;
  895. }
  896. if (!inject || !inject.length) {
  897. throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
  898. }
  899. const registry = await getTabRegistry();
  900. if (registry[source]) {
  901. registry[source].ready = false;
  902. await setState({ tabRegistry: registry });
  903. }
  904. try {
  905. if (injectSource) {
  906. await chrome.scripting.executeScript({
  907. target: { tabId },
  908. func: (injectedSource) => {
  909. window.__MULTIPAGE_SOURCE = injectedSource;
  910. },
  911. args: [injectSource],
  912. });
  913. }
  914. await chrome.scripting.executeScript({
  915. target: { tabId },
  916. files: inject,
  917. });
  918. } catch (err) {
  919. lastError = err;
  920. }
  921. const pongAfterInject = await pingContentScriptOnTab(tabId);
  922. if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
  923. await registerTab(source, tabId);
  924. return;
  925. }
  926. if (logMessage && !logged) {
  927. await addLog(logMessage, 'warn');
  928. logged = true;
  929. }
  930. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  931. }
  932. throw lastError || new Error(`${getSourceLabel(source)} 内容脚本长时间未就绪。`);
  933. }
  934. // ============================================================
  935. // Command Queue (for content scripts not yet ready)
  936. // ============================================================
  937. const pendingCommands = new Map(); // source -> { message, resolve, reject, timer }
  938. function queueCommand(source, message, timeout = 15000) {
  939. return new Promise((resolve, reject) => {
  940. const timer = setTimeout(() => {
  941. pendingCommands.delete(source);
  942. const err = `Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`;
  943. console.error(LOG_PREFIX, err);
  944. reject(new Error(err));
  945. }, timeout);
  946. pendingCommands.set(source, { message, resolve, reject, timer });
  947. console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
  948. });
  949. }
  950. function flushCommand(source, tabId) {
  951. const pending = pendingCommands.get(source);
  952. if (pending) {
  953. clearTimeout(pending.timer);
  954. pendingCommands.delete(source);
  955. chrome.tabs.sendMessage(tabId, pending.message).then(pending.resolve).catch(pending.reject);
  956. console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
  957. }
  958. }
  959. function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
  960. for (const [source, pending] of pendingCommands.entries()) {
  961. clearTimeout(pending.timer);
  962. pending.reject(new Error(reason));
  963. pendingCommands.delete(source);
  964. console.log(LOG_PREFIX, `Cancelled queued command for ${source}`);
  965. }
  966. }
  967. // ============================================================
  968. // Reuse or create tab
  969. // ============================================================
  970. async function reuseOrCreateTab(source, url, options = {}) {
  971. const alive = await isTabAlive(source);
  972. if (alive) {
  973. const tabId = await getTabId(source);
  974. await closeConflictingTabsForSource(source, url, { excludeTabIds: [tabId] });
  975. const currentTab = await chrome.tabs.get(tabId);
  976. const sameUrl = currentTab.url === url;
  977. const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
  978. const registry = await getTabRegistry();
  979. if (sameUrl) {
  980. await chrome.tabs.update(tabId, { active: true });
  981. console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}) on same URL`);
  982. if (shouldReloadOnReuse) {
  983. if (registry[source]) registry[source].ready = false;
  984. await setState({ tabRegistry: registry });
  985. await chrome.tabs.reload(tabId);
  986. await new Promise((resolve) => {
  987. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  988. const listener = (tid, info) => {
  989. if (tid === tabId && info.status === 'complete') {
  990. chrome.tabs.onUpdated.removeListener(listener);
  991. clearTimeout(timer);
  992. resolve();
  993. }
  994. };
  995. chrome.tabs.onUpdated.addListener(listener);
  996. });
  997. }
  998. // For dynamically injected pages like the VPS panel, re-inject immediately.
  999. if (options.inject) {
  1000. if (registry[source]) registry[source].ready = false;
  1001. await setState({ tabRegistry: registry });
  1002. if (options.injectSource) {
  1003. await chrome.scripting.executeScript({
  1004. target: { tabId },
  1005. func: (injectedSource) => {
  1006. window.__MULTIPAGE_SOURCE = injectedSource;
  1007. },
  1008. args: [options.injectSource],
  1009. });
  1010. }
  1011. await chrome.scripting.executeScript({
  1012. target: { tabId },
  1013. files: options.inject,
  1014. });
  1015. await new Promise(r => setTimeout(r, 500));
  1016. }
  1017. await rememberSourceLastUrl(source, url);
  1018. return tabId;
  1019. }
  1020. // Mark as not ready BEFORE navigating — so READY signal from new page is captured correctly
  1021. if (registry[source]) registry[source].ready = false;
  1022. await setState({ tabRegistry: registry });
  1023. // Navigate existing tab to new URL
  1024. await chrome.tabs.update(tabId, { url, active: true });
  1025. console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}), navigated to ${url.slice(0, 60)}`);
  1026. // Wait for page load complete (with 30s timeout)
  1027. await new Promise((resolve) => {
  1028. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  1029. const listener = (tid, info) => {
  1030. if (tid === tabId && info.status === 'complete') {
  1031. chrome.tabs.onUpdated.removeListener(listener);
  1032. clearTimeout(timer);
  1033. resolve();
  1034. }
  1035. };
  1036. chrome.tabs.onUpdated.addListener(listener);
  1037. });
  1038. // If dynamic injection needed (VPS panel), re-inject after navigation
  1039. if (options.inject) {
  1040. if (options.injectSource) {
  1041. await chrome.scripting.executeScript({
  1042. target: { tabId },
  1043. func: (injectedSource) => {
  1044. window.__MULTIPAGE_SOURCE = injectedSource;
  1045. },
  1046. args: [options.injectSource],
  1047. });
  1048. }
  1049. await chrome.scripting.executeScript({
  1050. target: { tabId },
  1051. files: options.inject,
  1052. });
  1053. }
  1054. // Wait a bit for content script to inject and send READY
  1055. await new Promise(r => setTimeout(r, 500));
  1056. await rememberSourceLastUrl(source, url);
  1057. return tabId;
  1058. }
  1059. // Create new tab
  1060. await closeConflictingTabsForSource(source, url);
  1061. const tab = await chrome.tabs.create({ url, active: true });
  1062. console.log(LOG_PREFIX, `Created new tab ${source} (${tab.id})`);
  1063. // If dynamic injection needed (VPS panel), inject scripts after load
  1064. if (options.inject) {
  1065. await new Promise((resolve) => {
  1066. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  1067. const listener = (tabId, info) => {
  1068. if (tabId === tab.id && info.status === 'complete') {
  1069. chrome.tabs.onUpdated.removeListener(listener);
  1070. clearTimeout(timer);
  1071. resolve();
  1072. }
  1073. };
  1074. chrome.tabs.onUpdated.addListener(listener);
  1075. });
  1076. if (options.injectSource) {
  1077. await chrome.scripting.executeScript({
  1078. target: { tabId: tab.id },
  1079. func: (injectedSource) => {
  1080. window.__MULTIPAGE_SOURCE = injectedSource;
  1081. },
  1082. args: [options.injectSource],
  1083. });
  1084. }
  1085. await chrome.scripting.executeScript({
  1086. target: { tabId: tab.id },
  1087. files: options.inject,
  1088. });
  1089. }
  1090. await rememberSourceLastUrl(source, url);
  1091. return tab.id;
  1092. }
  1093. // ============================================================
  1094. // Send command to content script (with readiness check)
  1095. // ============================================================
  1096. async function sendToContentScript(source, message) {
  1097. const registry = await getTabRegistry();
  1098. const entry = registry[source];
  1099. if (!entry || !entry.ready) {
  1100. console.log(LOG_PREFIX, `${source} not ready, queuing command`);
  1101. return queueCommand(source, message);
  1102. }
  1103. // Verify tab is still alive
  1104. const alive = await isTabAlive(source);
  1105. if (!alive) {
  1106. // Tab was closed — queue the command, it will be sent when tab is reopened
  1107. console.log(LOG_PREFIX, `${source} tab was closed, queuing command`);
  1108. return queueCommand(source, message);
  1109. }
  1110. console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type);
  1111. return chrome.tabs.sendMessage(entry.tabId, message);
  1112. }
  1113. async function sendToContentScriptResilient(source, message, options = {}) {
  1114. const { timeoutMs = 30000, retryDelayMs = 600, logMessage = '' } = options;
  1115. const start = Date.now();
  1116. let lastError = null;
  1117. let logged = false;
  1118. while (Date.now() - start < timeoutMs) {
  1119. throwIfStopped();
  1120. try {
  1121. return await sendToContentScript(source, message);
  1122. } catch (err) {
  1123. if (!isRetryableContentScriptTransportError(err)) {
  1124. throw err;
  1125. }
  1126. lastError = err;
  1127. if (logMessage && !logged) {
  1128. await addLog(logMessage, 'warn');
  1129. logged = true;
  1130. }
  1131. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  1132. }
  1133. }
  1134. throw lastError || new Error(`等待 ${getSourceLabel(source)} 重新就绪超时。`);
  1135. }
  1136. async function sendToMailContentScriptResilient(mail, message, options = {}) {
  1137. const { timeoutMs = 45000, maxRecoveryAttempts = 2 } = options;
  1138. const start = Date.now();
  1139. let lastError = null;
  1140. let recoveries = 0;
  1141. let logged = false;
  1142. while (Date.now() - start < timeoutMs) {
  1143. throwIfStopped();
  1144. try {
  1145. return await sendToContentScript(mail.source, message);
  1146. } catch (err) {
  1147. if (!isRetryableContentScriptTransportError(err)) {
  1148. throw err;
  1149. }
  1150. lastError = err;
  1151. if (!logged) {
  1152. await addLog(`步骤 ${message.step}:${mail.label} 页面通信异常,正在尝试让邮箱页重新就绪...`, 'warn');
  1153. logged = true;
  1154. }
  1155. if (recoveries >= maxRecoveryAttempts) {
  1156. break;
  1157. }
  1158. recoveries += 1;
  1159. await reuseOrCreateTab(mail.source, mail.url, {
  1160. inject: mail.inject,
  1161. injectSource: mail.injectSource,
  1162. reloadIfSameUrl: true,
  1163. });
  1164. await new Promise((resolve) => setTimeout(resolve, 800));
  1165. }
  1166. }
  1167. throw lastError || new Error(`${mail.label} 页面未能重新就绪。`);
  1168. }
  1169. // ============================================================
  1170. // Logging
  1171. // ============================================================
  1172. async function addLog(message, level = 'info') {
  1173. const state = await getState();
  1174. const logs = state.logs || [];
  1175. const entry = { message, level, timestamp: Date.now() };
  1176. logs.push(entry);
  1177. // Keep last 500 logs
  1178. if (logs.length > 500) logs.splice(0, logs.length - 500);
  1179. await setState({ logs });
  1180. // Broadcast to side panel
  1181. chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => { });
  1182. }
  1183. function getSourceLabel(source) {
  1184. const labels = {
  1185. 'sidepanel': '侧边栏',
  1186. 'signup-page': '认证页',
  1187. 'vps-panel': 'CPA 面板',
  1188. 'qq-mail': 'QQ 邮箱',
  1189. 'mail-163': '163 邮箱',
  1190. 'inbucket-mail': 'Inbucket 邮箱',
  1191. 'duck-mail': 'Duck 邮箱',
  1192. 'hotmail-api': 'Hotmail(微软 Graph)',
  1193. };
  1194. return labels[source] || source || '未知来源';
  1195. }
  1196. // ============================================================
  1197. // Step Status Management
  1198. // ============================================================
  1199. async function setStepStatus(step, status) {
  1200. const state = await getState();
  1201. const statuses = { ...state.stepStatuses };
  1202. statuses[step] = status;
  1203. await setState({ stepStatuses: statuses, currentStep: step });
  1204. // Broadcast to side panel
  1205. chrome.runtime.sendMessage({
  1206. type: 'STEP_STATUS_CHANGED',
  1207. payload: { step, status },
  1208. }).catch(() => { });
  1209. }
  1210. function isStopError(error) {
  1211. const message = typeof error === 'string' ? error : error?.message;
  1212. return message === STOP_ERROR_MESSAGE;
  1213. }
  1214. function isRetryableContentScriptTransportError(error) {
  1215. const message = String(typeof error === 'string' ? error : error?.message || '');
  1216. return /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response/i.test(message);
  1217. }
  1218. function getErrorMessage(error) {
  1219. return String(typeof error === 'string' ? error : error?.message || '');
  1220. }
  1221. function isVerificationMailPollingError(error) {
  1222. const message = getErrorMessage(error);
  1223. return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s/i.test(message);
  1224. }
  1225. function isRestartCurrentAttemptError(error) {
  1226. const message = String(typeof error === 'string' ? error : error?.message || '');
  1227. return /当前邮箱已存在,需要重新开始新一轮/.test(message);
  1228. }
  1229. function isStep9RecoverableAuthError(error) {
  1230. const message = String(typeof error === 'string' ? error : error?.message || '');
  1231. return /STEP9_OAUTH_RETRY::/i.test(message)
  1232. || isRecoverableStep9AuthFailure(message);
  1233. }
  1234. function isLegacyStep9RecoverableAuthError(error) {
  1235. const message = String(typeof error === 'string' ? error : error?.message || '');
  1236. return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*Timeout waiting for OAuth callback/i.test(message);
  1237. }
  1238. function isStepDoneStatus(status) {
  1239. return status === 'completed' || status === 'manual_completed' || status === 'skipped';
  1240. }
  1241. function getFirstUnfinishedStep(statuses = {}) {
  1242. for (let step = 1; step <= 9; step++) {
  1243. if (!isStepDoneStatus(statuses[step] || 'pending')) {
  1244. return step;
  1245. }
  1246. }
  1247. return null;
  1248. }
  1249. function hasSavedProgress(statuses = {}) {
  1250. return Object.values({ ...DEFAULT_STATE.stepStatuses, ...statuses }).some((status) => status !== 'pending');
  1251. }
  1252. function getDownstreamStateResets(step) {
  1253. if (step <= 1) {
  1254. return {
  1255. oauthUrl: null,
  1256. flowStartTime: null,
  1257. password: null,
  1258. lastEmailTimestamp: null,
  1259. signupVerificationRequestedAt: null,
  1260. loginVerificationRequestedAt: null,
  1261. lastSignupCode: null,
  1262. lastLoginCode: null,
  1263. localhostUrl: null,
  1264. };
  1265. }
  1266. if (step === 2) {
  1267. return {
  1268. password: null,
  1269. lastEmailTimestamp: null,
  1270. signupVerificationRequestedAt: null,
  1271. loginVerificationRequestedAt: null,
  1272. lastSignupCode: null,
  1273. lastLoginCode: null,
  1274. localhostUrl: null,
  1275. };
  1276. }
  1277. if (step === 3 || step === 4) {
  1278. return {
  1279. lastEmailTimestamp: null,
  1280. signupVerificationRequestedAt: null,
  1281. loginVerificationRequestedAt: null,
  1282. lastSignupCode: null,
  1283. lastLoginCode: null,
  1284. localhostUrl: null,
  1285. };
  1286. }
  1287. if (step === 5 || step === 6 || step === 7) {
  1288. return {
  1289. lastLoginCode: null,
  1290. loginVerificationRequestedAt: null,
  1291. localhostUrl: null,
  1292. };
  1293. }
  1294. if (step === 8) {
  1295. return {
  1296. localhostUrl: null,
  1297. };
  1298. }
  1299. return {};
  1300. }
  1301. async function invalidateDownstreamAfterStepRestart(step, options = {}) {
  1302. const { logLabel = `步骤 ${step} 重新执行` } = options;
  1303. const state = await getState();
  1304. const statuses = { ...(state.stepStatuses || {}) };
  1305. const changedSteps = [];
  1306. for (let downstream = step + 1; downstream <= 9; downstream++) {
  1307. if (statuses[downstream] !== 'pending') {
  1308. statuses[downstream] = 'pending';
  1309. changedSteps.push(downstream);
  1310. }
  1311. }
  1312. if (changedSteps.length) {
  1313. await setState({ stepStatuses: statuses });
  1314. for (const downstream of changedSteps) {
  1315. chrome.runtime.sendMessage({
  1316. type: 'STEP_STATUS_CHANGED',
  1317. payload: { step: downstream, status: 'pending' },
  1318. }).catch(() => { });
  1319. }
  1320. await addLog(`${logLabel},已重置后续步骤状态:${changedSteps.join(', ')}`, 'warn');
  1321. }
  1322. const resets = getDownstreamStateResets(step);
  1323. if (Object.keys(resets).length) {
  1324. await setState(resets);
  1325. broadcastDataUpdate(resets);
  1326. }
  1327. }
  1328. function clearStopRequest() {
  1329. stopRequested = false;
  1330. }
  1331. function getAutoRunStatusPayload(phase, payload = {}) {
  1332. const currentRun = payload.currentRun ?? autoRunCurrentRun;
  1333. const totalRuns = payload.totalRuns ?? autoRunTotalRuns;
  1334. const attemptRun = payload.attemptRun ?? autoRunAttemptRun;
  1335. const autoRunning = phase === 'running' || phase === 'waiting_email' || phase === 'retrying';
  1336. return {
  1337. autoRunning,
  1338. autoRunPhase: phase,
  1339. autoRunCurrentRun: currentRun,
  1340. autoRunTotalRuns: totalRuns,
  1341. autoRunAttemptRun: attemptRun,
  1342. };
  1343. }
  1344. async function broadcastAutoRunStatus(phase, payload = {}) {
  1345. const statusPayload = {
  1346. phase,
  1347. currentRun: payload.currentRun ?? autoRunCurrentRun,
  1348. totalRuns: payload.totalRuns ?? autoRunTotalRuns,
  1349. attemptRun: payload.attemptRun ?? autoRunAttemptRun,
  1350. };
  1351. await setState(getAutoRunStatusPayload(phase, statusPayload));
  1352. chrome.runtime.sendMessage({
  1353. type: 'AUTO_RUN_STATUS',
  1354. payload: statusPayload,
  1355. }).catch(() => { });
  1356. }
  1357. function isAutoRunLockedState(state) {
  1358. return Boolean(state.autoRunning) && (state.autoRunPhase === 'running' || state.autoRunPhase === 'retrying');
  1359. }
  1360. function isAutoRunPausedState(state) {
  1361. return Boolean(state.autoRunning) && state.autoRunPhase === 'waiting_email';
  1362. }
  1363. async function ensureManualInteractionAllowed(actionLabel) {
  1364. const state = await getState();
  1365. if (isAutoRunLockedState(state)) {
  1366. throw new Error(`自动流程运行中,请先停止后再${actionLabel}。`);
  1367. }
  1368. if (isAutoRunPausedState(state)) {
  1369. throw new Error(`自动流程当前已暂停。请点击“继续”,或先确认接管自动流程后再${actionLabel}。`);
  1370. }
  1371. return state;
  1372. }
  1373. async function skipStep(step) {
  1374. const state = await ensureManualInteractionAllowed('跳过步骤');
  1375. if (!Number.isInteger(step) || step < 1 || step > 9) {
  1376. throw new Error(`无效步骤:${step}`);
  1377. }
  1378. const statuses = { ...(state.stepStatuses || {}) };
  1379. const currentStatus = statuses[step];
  1380. if (currentStatus === 'running') {
  1381. throw new Error(`步骤 ${step} 正在运行中,不能跳过。`);
  1382. }
  1383. if (isStepDoneStatus(currentStatus)) {
  1384. throw new Error(`步骤 ${step} 已完成,无需再跳过。`);
  1385. }
  1386. if (step > 1) {
  1387. const prevStatus = statuses[step - 1];
  1388. if (!isStepDoneStatus(prevStatus)) {
  1389. throw new Error(`请先完成步骤 ${step - 1},再跳过步骤 ${step}。`);
  1390. }
  1391. }
  1392. await setStepStatus(step, 'skipped');
  1393. await addLog(`步骤 ${step} 已跳过`, 'warn');
  1394. if (step === 1) {
  1395. const latestState = await getState();
  1396. const step2Status = latestState.stepStatuses?.[2];
  1397. if (!isStepDoneStatus(step2Status) && step2Status !== 'running') {
  1398. await setStepStatus(2, 'skipped');
  1399. await addLog('步骤 1 已跳过,步骤 2 也已同时跳过。', 'warn');
  1400. }
  1401. }
  1402. return { ok: true, step, status: 'skipped' };
  1403. }
  1404. function throwIfStopped() {
  1405. if (stopRequested) {
  1406. throw new Error(STOP_ERROR_MESSAGE);
  1407. }
  1408. }
  1409. async function sleepWithStop(ms) {
  1410. const start = Date.now();
  1411. while (Date.now() - start < ms) {
  1412. throwIfStopped();
  1413. await new Promise(r => setTimeout(r, Math.min(100, ms - (Date.now() - start))));
  1414. }
  1415. }
  1416. async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY_MAX) {
  1417. const duration = Math.floor(Math.random() * (max - min + 1)) + min;
  1418. await sleepWithStop(duration);
  1419. }
  1420. async function clickWithDebugger(tabId, rect) {
  1421. if (!tabId) {
  1422. throw new Error('未找到用于调试点击的认证页面标签页。');
  1423. }
  1424. if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) {
  1425. throw new Error('步骤 8 的调试器兜底点击需要有效的按钮坐标。');
  1426. }
  1427. const target = { tabId };
  1428. try {
  1429. await chrome.debugger.attach(target, '1.3');
  1430. } catch (err) {
  1431. throw new Error(
  1432. `步骤 8 的调试器兜底点击附加失败:${err.message}。` +
  1433. '如果认证页标签已打开 DevTools,请先关闭后重试。'
  1434. );
  1435. }
  1436. try {
  1437. const x = Math.round(rect.centerX);
  1438. const y = Math.round(rect.centerY);
  1439. await chrome.debugger.sendCommand(target, 'Page.bringToFront');
  1440. await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
  1441. type: 'mouseMoved',
  1442. x,
  1443. y,
  1444. button: 'none',
  1445. buttons: 0,
  1446. clickCount: 0,
  1447. });
  1448. await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
  1449. type: 'mousePressed',
  1450. x,
  1451. y,
  1452. button: 'left',
  1453. buttons: 1,
  1454. clickCount: 1,
  1455. });
  1456. await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
  1457. type: 'mouseReleased',
  1458. x,
  1459. y,
  1460. button: 'left',
  1461. buttons: 0,
  1462. clickCount: 1,
  1463. });
  1464. } finally {
  1465. await chrome.debugger.detach(target).catch(() => { });
  1466. }
  1467. }
  1468. async function broadcastStopToContentScripts() {
  1469. const registry = await getTabRegistry();
  1470. for (const entry of Object.values(registry)) {
  1471. if (!entry?.tabId) continue;
  1472. try {
  1473. await chrome.tabs.sendMessage(entry.tabId, {
  1474. type: 'STOP_FLOW',
  1475. source: 'background',
  1476. payload: {},
  1477. });
  1478. } catch { }
  1479. }
  1480. }
  1481. let stopRequested = false;
  1482. // ============================================================
  1483. // Message Handler (central router)
  1484. // ============================================================
  1485. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  1486. console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message);
  1487. handleMessage(message, sender).then(response => {
  1488. sendResponse(response);
  1489. }).catch(err => {
  1490. console.error(LOG_PREFIX, 'Handler error:', err);
  1491. sendResponse({ error: err.message });
  1492. });
  1493. return true; // async response
  1494. });
  1495. async function handleMessage(message, sender) {
  1496. switch (message.type) {
  1497. case 'CONTENT_SCRIPT_READY': {
  1498. const tabId = sender.tab?.id;
  1499. if (tabId && message.source) {
  1500. await registerTab(message.source, tabId);
  1501. flushCommand(message.source, tabId);
  1502. await addLog(`内容脚本已就绪:${getSourceLabel(message.source)}(标签页 ${tabId})`);
  1503. }
  1504. return { ok: true };
  1505. }
  1506. case 'LOG': {
  1507. const { message: msg, level } = message.payload;
  1508. await addLog(`[${getSourceLabel(message.source)}] ${msg}`, level);
  1509. return { ok: true };
  1510. }
  1511. case 'STEP_COMPLETE': {
  1512. if (stopRequested) {
  1513. await setStepStatus(message.step, 'stopped');
  1514. notifyStepError(message.step, STOP_ERROR_MESSAGE);
  1515. return { ok: true };
  1516. }
  1517. await setStepStatus(message.step, 'completed');
  1518. await addLog(`步骤 ${message.step} 已完成`, 'ok');
  1519. await handleStepData(message.step, message.payload);
  1520. notifyStepComplete(message.step, message.payload);
  1521. return { ok: true };
  1522. }
  1523. case 'STEP_ERROR': {
  1524. if (isStopError(message.error)) {
  1525. await setStepStatus(message.step, 'stopped');
  1526. await addLog(`步骤 ${message.step} 已被用户停止`, 'warn');
  1527. notifyStepError(message.step, message.error);
  1528. } else {
  1529. await setStepStatus(message.step, 'failed');
  1530. await addLog(`步骤 ${message.step} 失败:${message.error}`, 'error');
  1531. notifyStepError(message.step, message.error);
  1532. }
  1533. return { ok: true };
  1534. }
  1535. case 'GET_STATE': {
  1536. return await getState();
  1537. }
  1538. case 'RESET': {
  1539. clearStopRequest();
  1540. await resetState();
  1541. await addLog('流程已重置', 'info');
  1542. return { ok: true };
  1543. }
  1544. case 'EXECUTE_STEP': {
  1545. clearStopRequest();
  1546. if (message.source === 'sidepanel') {
  1547. await ensureManualInteractionAllowed('手动执行步骤');
  1548. }
  1549. const step = message.payload.step;
  1550. if (message.source === 'sidepanel') {
  1551. await invalidateDownstreamAfterStepRestart(step, { logLabel: `步骤 ${step} 重新执行` });
  1552. }
  1553. // Save email if provided (from side panel step 3)
  1554. if (message.payload.email) {
  1555. await setEmailState(message.payload.email);
  1556. }
  1557. await executeStep(step);
  1558. return { ok: true };
  1559. }
  1560. case 'AUTO_RUN': {
  1561. clearStopRequest();
  1562. const totalRuns = message.payload?.totalRuns || 1;
  1563. const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures);
  1564. const mode = message.payload?.mode === 'continue' ? 'continue' : 'restart';
  1565. await setState({ autoRunSkipFailures });
  1566. autoRunLoop(totalRuns, { autoRunSkipFailures, mode }); // fire-and-forget
  1567. return { ok: true };
  1568. }
  1569. case 'RESUME_AUTO_RUN': {
  1570. clearStopRequest();
  1571. if (message.payload.email) {
  1572. await setEmailState(message.payload.email);
  1573. }
  1574. resumeAutoRun(); // fire-and-forget
  1575. return { ok: true };
  1576. }
  1577. case 'TAKEOVER_AUTO_RUN': {
  1578. await requestStop({ logMessage: '已确认手动接管,正在停止自动流程并切换为手动控制...' });
  1579. await addLog('自动流程已切换为手动控制。', 'warn');
  1580. return { ok: true };
  1581. }
  1582. case 'SKIP_STEP': {
  1583. const step = Number(message.payload?.step);
  1584. return await skipStep(step);
  1585. }
  1586. case 'SAVE_SETTING': {
  1587. const updates = {};
  1588. if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
  1589. if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
  1590. if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
  1591. if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures);
  1592. if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
  1593. if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
  1594. if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
  1595. await setPersistentSettings(updates);
  1596. await setState(updates);
  1597. return { ok: true };
  1598. }
  1599. case 'UPSERT_HOTMAIL_ACCOUNT': {
  1600. const account = await upsertHotmailAccount(message.payload || {});
  1601. return { ok: true, account };
  1602. }
  1603. case 'DELETE_HOTMAIL_ACCOUNT': {
  1604. await deleteHotmailAccount(String(message.payload?.accountId || ''));
  1605. return { ok: true };
  1606. }
  1607. case 'DELETE_HOTMAIL_ACCOUNTS': {
  1608. const result = await deleteHotmailAccounts(String(message.payload?.mode || 'all'));
  1609. return { ok: true, ...result };
  1610. }
  1611. case 'SELECT_HOTMAIL_ACCOUNT': {
  1612. const account = await setCurrentHotmailAccount(String(message.payload?.accountId || ''), {
  1613. markUsed: false,
  1614. syncEmail: true,
  1615. });
  1616. return { ok: true, account };
  1617. }
  1618. case 'PATCH_HOTMAIL_ACCOUNT': {
  1619. const account = await patchHotmailAccount(
  1620. String(message.payload?.accountId || ''),
  1621. message.payload?.updates || {}
  1622. );
  1623. return { ok: true, account };
  1624. }
  1625. case 'VERIFY_HOTMAIL_ACCOUNT':
  1626. case 'AUTHORIZE_HOTMAIL_ACCOUNT': {
  1627. const accountId = String(message.payload?.accountId || '');
  1628. try {
  1629. const result = await verifyHotmailAccount(accountId);
  1630. await setCurrentHotmailAccount(result.account.id, { markUsed: false, syncEmail: true });
  1631. await addLog(`Hotmail 账号 ${result.account.email} 校验通过,可直接用于收信。`, 'ok');
  1632. return { ok: true, account: result.account, messageCount: result.messageCount };
  1633. } catch (err) {
  1634. const state = await getState();
  1635. const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
  1636. const target = findHotmailAccount(accounts, accountId);
  1637. if (target) {
  1638. target.status = 'error';
  1639. target.lastError = err.message;
  1640. await syncHotmailAccounts(accounts.map((item) => (item.id === target.id ? target : item)));
  1641. }
  1642. throw err;
  1643. }
  1644. }
  1645. case 'TEST_HOTMAIL_ACCOUNT': {
  1646. const result = await testHotmailAccountMailAccess(String(message.payload?.accountId || ''));
  1647. return { ok: true, ...result };
  1648. }
  1649. // Side panel data updates
  1650. case 'SAVE_EMAIL': {
  1651. const state = await getState();
  1652. if (isAutoRunLockedState(state)) {
  1653. throw new Error('自动流程运行中,当前不能手动修改邮箱。');
  1654. }
  1655. await setEmailState(message.payload.email);
  1656. await resumeAutoRun();
  1657. return { ok: true, email: message.payload.email };
  1658. }
  1659. case 'FETCH_DUCK_EMAIL': {
  1660. clearStopRequest();
  1661. const state = await getState();
  1662. if (isAutoRunLockedState(state)) {
  1663. throw new Error('自动流程运行中,当前不能手动获取 Duck 邮箱。');
  1664. }
  1665. const email = await fetchDuckEmail(message.payload || {});
  1666. await resumeAutoRun();
  1667. return { ok: true, email };
  1668. }
  1669. case 'STOP_FLOW': {
  1670. await requestStop();
  1671. return { ok: true };
  1672. }
  1673. default:
  1674. console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`);
  1675. return { error: `Unknown message type: ${message.type}` };
  1676. }
  1677. }
  1678. // ============================================================
  1679. // Step Data Handlers
  1680. // ============================================================
  1681. async function handleStepData(step, payload) {
  1682. switch (step) {
  1683. case 1:
  1684. if (payload.oauthUrl) {
  1685. await setState({ oauthUrl: payload.oauthUrl });
  1686. broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
  1687. }
  1688. break;
  1689. case 3:
  1690. if (payload.email) await setEmailState(payload.email);
  1691. if (payload.signupVerificationRequestedAt) {
  1692. await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt });
  1693. }
  1694. if (payload.loginVerificationRequestedAt) {
  1695. await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
  1696. }
  1697. break;
  1698. case 6:
  1699. if (payload.loginVerificationRequestedAt) {
  1700. await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
  1701. }
  1702. break;
  1703. case 4:
  1704. await setState({
  1705. lastEmailTimestamp: payload.emailTimestamp || null,
  1706. signupVerificationRequestedAt: null,
  1707. });
  1708. break;
  1709. case 7:
  1710. await setState({
  1711. lastEmailTimestamp: payload.emailTimestamp || null,
  1712. loginVerificationRequestedAt: null,
  1713. });
  1714. break;
  1715. case 8:
  1716. if (payload.localhostUrl) {
  1717. if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) {
  1718. throw new Error('步骤 8 返回了无效的 localhost OAuth 回调地址。');
  1719. }
  1720. await setState({ localhostUrl: payload.localhostUrl });
  1721. broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
  1722. }
  1723. break;
  1724. case 9: {
  1725. const latestState = await getState();
  1726. if (latestState.currentHotmailAccountId && isHotmailProvider(latestState)) {
  1727. await patchHotmailAccount(latestState.currentHotmailAccountId, {
  1728. used: true,
  1729. lastUsedAt: Date.now(),
  1730. });
  1731. await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok');
  1732. }
  1733. const localhostPrefix = buildLocalhostCleanupPrefix(payload.localhostUrl);
  1734. if (localhostPrefix) {
  1735. await closeTabsByUrlPrefix(localhostPrefix);
  1736. }
  1737. break;
  1738. }
  1739. }
  1740. }
  1741. // ============================================================
  1742. // Step Completion Waiting
  1743. // ============================================================
  1744. // Map of step -> { resolve, reject } for waiting on step completion
  1745. const stepWaiters = new Map();
  1746. let resumeWaiter = null;
  1747. const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000;
  1748. const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([4, 7, 8]);
  1749. function waitForStepComplete(step, timeoutMs = 120000) {
  1750. return new Promise((resolve, reject) => {
  1751. throwIfStopped();
  1752. const timer = setTimeout(() => {
  1753. stepWaiters.delete(step);
  1754. reject(new Error(`步骤 ${step} 等待超时(>${timeoutMs / 1000} 秒)`));
  1755. }, timeoutMs);
  1756. stepWaiters.set(step, {
  1757. resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); },
  1758. reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); },
  1759. });
  1760. });
  1761. }
  1762. function notifyStepComplete(step, payload) {
  1763. const waiter = stepWaiters.get(step);
  1764. if (waiter) waiter.resolve(payload);
  1765. }
  1766. function notifyStepError(step, error) {
  1767. const waiter = stepWaiters.get(step);
  1768. if (waiter) waiter.reject(new Error(error));
  1769. }
  1770. async function completeStepFromBackground(step, payload = {}) {
  1771. if (stopRequested) {
  1772. await setStepStatus(step, 'stopped');
  1773. notifyStepError(step, STOP_ERROR_MESSAGE);
  1774. return;
  1775. }
  1776. await setStepStatus(step, 'completed');
  1777. await addLog(`步骤 ${step} 已完成`, 'ok');
  1778. await handleStepData(step, payload);
  1779. notifyStepComplete(step, payload);
  1780. }
  1781. async function markRunningStepsStopped() {
  1782. const state = await getState();
  1783. const runningSteps = Object.entries(state.stepStatuses || {})
  1784. .filter(([, status]) => status === 'running')
  1785. .map(([step]) => Number(step));
  1786. for (const step of runningSteps) {
  1787. await setStepStatus(step, 'stopped');
  1788. }
  1789. }
  1790. async function requestStop(options = {}) {
  1791. const { logMessage = '已收到停止请求,正在取消当前操作...' } = options;
  1792. if (stopRequested) return;
  1793. stopRequested = true;
  1794. cancelPendingCommands();
  1795. if (webNavListener) {
  1796. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  1797. webNavListener = null;
  1798. }
  1799. await addLog(logMessage, 'warn');
  1800. await broadcastStopToContentScripts();
  1801. for (const waiter of stepWaiters.values()) {
  1802. waiter.reject(new Error(STOP_ERROR_MESSAGE));
  1803. }
  1804. stepWaiters.clear();
  1805. if (resumeWaiter) {
  1806. resumeWaiter.reject(new Error(STOP_ERROR_MESSAGE));
  1807. resumeWaiter = null;
  1808. }
  1809. await markRunningStepsStopped();
  1810. autoRunActive = false;
  1811. await broadcastAutoRunStatus('stopped', {
  1812. currentRun: autoRunCurrentRun,
  1813. totalRuns: autoRunTotalRuns,
  1814. attemptRun: autoRunAttemptRun,
  1815. });
  1816. }
  1817. // ============================================================
  1818. // Step Execution
  1819. // ============================================================
  1820. async function executeStep(step) {
  1821. console.log(LOG_PREFIX, `Executing step ${step}`);
  1822. throwIfStopped();
  1823. await setStepStatus(step, 'running');
  1824. await addLog(`步骤 ${step} 开始执行`);
  1825. await humanStepDelay();
  1826. const state = await getState();
  1827. // Set flow start time on first step
  1828. if (step === 1 && !state.flowStartTime) {
  1829. await setState({ flowStartTime: Date.now() });
  1830. }
  1831. try {
  1832. switch (step) {
  1833. case 1: await executeStep1(state); break;
  1834. case 2: await executeStep2(state); break;
  1835. case 3: await executeStep3(state); break;
  1836. case 4: await executeStep4(state); break;
  1837. case 5: await executeStep5(state); break;
  1838. case 6: await executeStep6(state); break;
  1839. case 7: await executeStep7(state); break;
  1840. case 8: await executeStep8(state); break;
  1841. case 9: await executeStep9(state); break;
  1842. default:
  1843. throw new Error(`未知步骤:${step}`);
  1844. }
  1845. } catch (err) {
  1846. if (isStopError(err)) {
  1847. await setStepStatus(step, 'stopped');
  1848. await addLog(`步骤 ${step} 已被用户停止`, 'warn');
  1849. throw err;
  1850. }
  1851. await setStepStatus(step, 'failed');
  1852. await addLog(`步骤 ${step} 失败:${err.message}`, 'error');
  1853. throw err;
  1854. }
  1855. }
  1856. /**
  1857. * Execute a step and wait for it to complete before returning.
  1858. * @param {number} step
  1859. * @param {number} delayAfter - ms to wait after completion (for page transitions)
  1860. */
  1861. async function executeStepAndWait(step, delayAfter = 2000) {
  1862. throwIfStopped();
  1863. if (AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step)) {
  1864. await addLog(`自动运行:步骤 ${step} 由后台流程负责收尾,执行函数返回后将直接进入下一步。`, 'info');
  1865. await executeStep(step);
  1866. const latestState = await getState();
  1867. await addLog(`自动运行:步骤 ${step} 已执行返回,当前状态为 ${latestState.stepStatuses?.[step] || 'pending'},准备继续后续步骤。`, 'info');
  1868. } else {
  1869. await addLog(`自动运行:步骤 ${step} 已发起,正在等待完成信号(超时 ${AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS / 1000} 秒)。`, 'info');
  1870. const completionResultPromise = waitForStepComplete(step, AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS).then(
  1871. payload => ({ ok: true, payload }),
  1872. error => ({ ok: false, error }),
  1873. );
  1874. try {
  1875. await executeStep(step);
  1876. } catch (err) {
  1877. notifyStepError(step, getErrorMessage(err));
  1878. await completionResultPromise;
  1879. throw err;
  1880. }
  1881. const completionResult = await completionResultPromise;
  1882. if (!completionResult.ok) {
  1883. throw completionResult.error;
  1884. }
  1885. await addLog(`自动运行:步骤 ${step} 已收到完成信号,准备继续后续步骤。`, 'info');
  1886. }
  1887. // Extra delay for page transitions / DOM updates
  1888. if (delayAfter > 0) {
  1889. await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
  1890. }
  1891. }
  1892. async function fetchDuckEmail(options = {}) {
  1893. throwIfStopped();
  1894. const { generateNew = true } = options;
  1895. await addLog(`Duck 邮箱:正在打开自动填充设置(${generateNew ? '生成新地址' : '复用当前地址'})...`);
  1896. await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
  1897. const result = await sendToContentScript('duck-mail', {
  1898. type: 'FETCH_DUCK_EMAIL',
  1899. source: 'background',
  1900. payload: { generateNew },
  1901. });
  1902. if (result?.error) {
  1903. throw new Error(result.error);
  1904. }
  1905. if (!result?.email) {
  1906. throw new Error('未返回 Duck 邮箱地址。');
  1907. }
  1908. await setEmailState(result.email);
  1909. await addLog(`Duck 邮箱:${result.generated ? '已生成' : '已读取'} ${result.email}`, 'ok');
  1910. return result.email;
  1911. }
  1912. // ============================================================
  1913. // Auto Run Flow
  1914. // ============================================================
  1915. let autoRunActive = false;
  1916. let autoRunCurrentRun = 0;
  1917. let autoRunTotalRuns = 1;
  1918. let autoRunAttemptRun = 0;
  1919. const DUCK_EMAIL_MAX_ATTEMPTS = 5;
  1920. const VERIFICATION_POLL_MAX_ROUNDS = 5;
  1921. const AUTO_STEP_DELAYS = {
  1922. 1: 2000,
  1923. 2: 2000,
  1924. 3: 3000,
  1925. 4: 2000,
  1926. 5: 3000,
  1927. 6: 3000,
  1928. 7: 2000,
  1929. 8: 2000,
  1930. 9: 1000,
  1931. };
  1932. async function resumeAutoRunIfWaitingForEmail(options = {}) {
  1933. const { silent = false } = options;
  1934. const state = await getState();
  1935. if (!state.email || !isAutoRunPausedState(state)) {
  1936. return false;
  1937. }
  1938. if (resumeWaiter) {
  1939. if (!silent) {
  1940. await addLog('邮箱已就绪,自动继续后续步骤...', 'info');
  1941. }
  1942. resumeWaiter.resolve();
  1943. resumeWaiter = null;
  1944. return true;
  1945. }
  1946. return false;
  1947. }
  1948. async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
  1949. const currentState = await getState();
  1950. if (isHotmailProvider(currentState)) {
  1951. const account = await ensureHotmailAccountForFlow({
  1952. allowAllocate: true,
  1953. markUsed: true,
  1954. preferredAccountId: null,
  1955. });
  1956. await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:已分配 Hotmail 账号 ${account.email}(第 ${attemptRuns} 次尝试)===`, 'ok');
  1957. return account.email;
  1958. }
  1959. if (currentState.email) {
  1960. return currentState.email;
  1961. }
  1962. let lastDuckError = null;
  1963. for (let duckAttempt = 1; duckAttempt <= DUCK_EMAIL_MAX_ATTEMPTS; duckAttempt++) {
  1964. try {
  1965. if (duckAttempt > 1) {
  1966. await addLog(`Duck 邮箱:正在进行第 ${duckAttempt}/${DUCK_EMAIL_MAX_ATTEMPTS} 次自动获取重试...`, 'warn');
  1967. }
  1968. const duckEmail = await fetchDuckEmail({ generateNew: true });
  1969. await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Duck 邮箱已就绪:${duckEmail}(第 ${attemptRuns} 次尝试,Duck 第 ${duckAttempt}/${DUCK_EMAIL_MAX_ATTEMPTS} 次获取)===`, 'ok');
  1970. return duckEmail;
  1971. } catch (err) {
  1972. lastDuckError = err;
  1973. await addLog(`Duck 邮箱自动获取失败(${duckAttempt}/${DUCK_EMAIL_MAX_ATTEMPTS}):${err.message}`, 'warn');
  1974. }
  1975. }
  1976. await addLog(`Duck 邮箱自动获取已连续失败 ${DUCK_EMAIL_MAX_ATTEMPTS} 次:${lastDuckError?.message || '未知错误'}`, 'error');
  1977. await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
  1978. await broadcastAutoRunStatus('waiting_email', {
  1979. currentRun: targetRun,
  1980. totalRuns,
  1981. attemptRun: attemptRuns,
  1982. });
  1983. await waitForResume();
  1984. const resumedState = await getState();
  1985. if (!resumedState.email) {
  1986. throw new Error('无法继续:当前没有邮箱地址。');
  1987. }
  1988. return resumedState.email;
  1989. }
  1990. async function runAutoSequenceFromStep(startStep, context = {}) {
  1991. const { targetRun, totalRuns, attemptRuns, continued = false } = context;
  1992. const maxStep9RestartAttempts = 5;
  1993. let step9RestartAttempts = 0;
  1994. if (continued) {
  1995. await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info');
  1996. } else {
  1997. await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info');
  1998. }
  1999. if (startStep <= 2) {
  2000. for (const step of [1, 2]) {
  2001. if (step < startStep) continue;
  2002. await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
  2003. }
  2004. }
  2005. if (startStep <= 3) {
  2006. await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns);
  2007. await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,注册、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
  2008. await broadcastAutoRunStatus('running', {
  2009. currentRun: targetRun,
  2010. totalRuns,
  2011. attemptRun: attemptRuns,
  2012. });
  2013. await executeStepAndWait(3, AUTO_STEP_DELAYS[3]);
  2014. } else {
  2015. await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续执行剩余流程(第 ${attemptRuns} 次尝试)===`, 'info');
  2016. }
  2017. const signupTabId = await getTabId('signup-page');
  2018. if (signupTabId) {
  2019. await chrome.tabs.update(signupTabId, { active: true });
  2020. }
  2021. let step = Math.max(startStep, 4);
  2022. while (step <= 9) {
  2023. try {
  2024. await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
  2025. step += 1;
  2026. } catch (err) {
  2027. const latestState = await getState();
  2028. const currentMail = getMailConfig(latestState);
  2029. const shouldRetryStep9 = step === 9
  2030. && (
  2031. isLegacyStep9RecoverableAuthError(err)
  2032. || (currentMail.provider === HOTMAIL_PROVIDER && isStep9RecoverableAuthError(err))
  2033. )
  2034. && step9RestartAttempts < maxStep9RestartAttempts;
  2035. if (shouldRetryStep9) {
  2036. step9RestartAttempts += 1;
  2037. await addLog(
  2038. `步骤 9:检测到 CPA 认证失败,正在回到步骤 6 重新开始授权流程(${step9RestartAttempts}/${maxStep9RestartAttempts})...`,
  2039. 'warn'
  2040. );
  2041. await invalidateDownstreamAfterStepRestart(6, {
  2042. logLabel: `步骤 9 认证失败后准备回到步骤 6 重试(${step9RestartAttempts}/${maxStep9RestartAttempts})`,
  2043. });
  2044. step = 6;
  2045. continue;
  2046. }
  2047. throw err;
  2048. }
  2049. }
  2050. }
  2051. // Outer loop: keep retrying until the target number of successful runs is reached.
  2052. async function autoRunLoop(totalRuns, options = {}) {
  2053. if (autoRunActive) {
  2054. await addLog('自动运行已在进行中', 'warn');
  2055. return;
  2056. }
  2057. clearStopRequest();
  2058. autoRunActive = true;
  2059. autoRunTotalRuns = totalRuns;
  2060. autoRunCurrentRun = 0;
  2061. autoRunAttemptRun = 0;
  2062. const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
  2063. const initialMode = options.mode === 'continue' ? 'continue' : 'restart';
  2064. const resumeCurrentRun = Number.isInteger(options.resumeCurrentRun) ? options.resumeCurrentRun : 0;
  2065. const resumeSuccessfulRuns = Number.isInteger(options.resumeSuccessfulRuns) ? options.resumeSuccessfulRuns : 0;
  2066. const resumeAttemptRunsProcessed = Number.isInteger(options.resumeAttemptRunsProcessed) ? options.resumeAttemptRunsProcessed : 0;
  2067. let maxAttempts = autoRunSkipFailures ? Math.max(totalRuns * 10, totalRuns + 20) : totalRuns;
  2068. const forcedRetryCap = Math.max(totalRuns * 10, totalRuns + 20);
  2069. let successfulRuns = Math.max(0, resumeSuccessfulRuns);
  2070. let attemptRuns = Math.max(0, resumeAttemptRunsProcessed);
  2071. let forceFreshTabsNextRun = false;
  2072. let continueCurrentOnFirstAttempt = initialMode === 'continue';
  2073. await setState({
  2074. autoRunSkipFailures,
  2075. ...getAutoRunStatusPayload('running', {
  2076. currentRun: resumeCurrentRun,
  2077. totalRuns,
  2078. attemptRun: resumeAttemptRunsProcessed,
  2079. }),
  2080. });
  2081. while (successfulRuns < totalRuns && attemptRuns < maxAttempts) {
  2082. attemptRuns += 1;
  2083. const targetRun = successfulRuns + 1;
  2084. autoRunCurrentRun = targetRun;
  2085. autoRunAttemptRun = attemptRuns;
  2086. let startStep = 1;
  2087. let useExistingProgress = false;
  2088. if (continueCurrentOnFirstAttempt) {
  2089. const currentState = await getState();
  2090. const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses);
  2091. if (resumeStep && hasSavedProgress(currentState.stepStatuses)) {
  2092. startStep = resumeStep;
  2093. useExistingProgress = true;
  2094. } else if (hasSavedProgress(currentState.stepStatuses)) {
  2095. await addLog('当前流程已全部处理,将按“重新开始”新开一轮自动运行。', 'info');
  2096. }
  2097. continueCurrentOnFirstAttempt = false;
  2098. }
  2099. if (!useExistingProgress) {
  2100. // Reset everything at the start of each fresh attempt (keep user settings).
  2101. const prevState = await getState();
  2102. const keepSettings = {
  2103. vpsUrl: prevState.vpsUrl,
  2104. vpsPassword: prevState.vpsPassword,
  2105. customPassword: prevState.customPassword,
  2106. autoRunSkipFailures: prevState.autoRunSkipFailures,
  2107. mailProvider: prevState.mailProvider,
  2108. inbucketHost: prevState.inbucketHost,
  2109. inbucketMailbox: prevState.inbucketMailbox,
  2110. ...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }),
  2111. ...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
  2112. };
  2113. await resetState();
  2114. await setState(keepSettings);
  2115. chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => { });
  2116. await sleepWithStop(500);
  2117. } else {
  2118. await setState({
  2119. autoRunSkipFailures,
  2120. ...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }),
  2121. });
  2122. }
  2123. if (forceFreshTabsNextRun) {
  2124. await addLog(`兜底模式:上一轮已放弃,当前开始第 ${attemptRuns} 次尝试,将使用新线程继续补足第 ${targetRun}/${totalRuns} 轮。`, 'warn');
  2125. forceFreshTabsNextRun = false;
  2126. }
  2127. try {
  2128. throwIfStopped();
  2129. await broadcastAutoRunStatus('running', {
  2130. currentRun: targetRun,
  2131. totalRuns,
  2132. attemptRun: attemptRuns,
  2133. });
  2134. await runAutoSequenceFromStep(startStep, {
  2135. targetRun,
  2136. totalRuns,
  2137. attemptRuns,
  2138. continued: useExistingProgress,
  2139. });
  2140. successfulRuns += 1;
  2141. autoRunCurrentRun = successfulRuns;
  2142. await addLog(`=== 目标 ${successfulRuns}/${totalRuns} 轮已完成(第 ${attemptRuns} 次尝试成功)===`, 'ok');
  2143. continue;
  2144. } catch (err) {
  2145. if (isStopError(err)) {
  2146. await addLog(`目标 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
  2147. await broadcastAutoRunStatus('stopped', {
  2148. currentRun: targetRun,
  2149. totalRuns,
  2150. attemptRun: attemptRuns,
  2151. });
  2152. break;
  2153. }
  2154. if (isRestartCurrentAttemptError(err)) {
  2155. await addLog(`目标 ${targetRun}/${totalRuns} 轮检测到当前邮箱已存在,当前线程已放弃,将重新开始新一轮。`, 'warn');
  2156. cancelPendingCommands('当前线程因邮箱已存在而放弃。');
  2157. await broadcastStopToContentScripts();
  2158. await broadcastAutoRunStatus('retrying', {
  2159. currentRun: targetRun,
  2160. totalRuns,
  2161. attemptRun: attemptRuns,
  2162. });
  2163. forceFreshTabsNextRun = true;
  2164. maxAttempts = Math.max(maxAttempts, Math.min(forcedRetryCap, attemptRuns + 1));
  2165. continue;
  2166. }
  2167. if (!autoRunSkipFailures) {
  2168. await addLog(`目标 ${targetRun}/${totalRuns} 轮失败:${err.message}`, 'error');
  2169. await broadcastAutoRunStatus('stopped', {
  2170. currentRun: targetRun,
  2171. totalRuns,
  2172. attemptRun: attemptRuns,
  2173. });
  2174. break;
  2175. }
  2176. await addLog(`目标 ${targetRun}/${totalRuns} 轮的第 ${attemptRuns} 次尝试失败:${err.message}`, 'error');
  2177. await addLog('兜底开关已开启:将放弃当前线程,重新开一轮继续补足目标次数。', 'warn');
  2178. cancelPendingCommands('当前尝试已放弃。');
  2179. await broadcastStopToContentScripts();
  2180. await broadcastAutoRunStatus('retrying', {
  2181. currentRun: targetRun,
  2182. totalRuns,
  2183. attemptRun: attemptRuns,
  2184. });
  2185. forceFreshTabsNextRun = true;
  2186. }
  2187. }
  2188. if (!stopRequested && autoRunSkipFailures && successfulRuns < totalRuns && attemptRuns >= maxAttempts) {
  2189. await addLog(`已达到安全重试上限(${attemptRuns} 次尝试),当前仅完成 ${successfulRuns}/${totalRuns} 轮。`, 'error');
  2190. await broadcastAutoRunStatus('stopped', {
  2191. currentRun: successfulRuns,
  2192. totalRuns: autoRunTotalRuns,
  2193. attemptRun: attemptRuns,
  2194. });
  2195. } else if (stopRequested) {
  2196. await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
  2197. await broadcastAutoRunStatus('stopped', {
  2198. currentRun: successfulRuns,
  2199. totalRuns: autoRunTotalRuns,
  2200. attemptRun: attemptRuns,
  2201. });
  2202. } else if (successfulRuns >= autoRunTotalRuns) {
  2203. await addLog(`=== 全部 ${autoRunTotalRuns} 轮均已成功完成,共尝试 ${attemptRuns} 次 ===`, 'ok');
  2204. await broadcastAutoRunStatus('complete', {
  2205. currentRun: successfulRuns,
  2206. totalRuns: autoRunTotalRuns,
  2207. attemptRun: attemptRuns,
  2208. });
  2209. } else {
  2210. await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
  2211. await broadcastAutoRunStatus('stopped', {
  2212. currentRun: successfulRuns,
  2213. totalRuns: autoRunTotalRuns,
  2214. attemptRun: attemptRuns,
  2215. });
  2216. }
  2217. autoRunActive = false;
  2218. autoRunAttemptRun = attemptRuns;
  2219. await setState(getAutoRunStatusPayload(stopRequested ? 'stopped' : (successfulRuns >= autoRunTotalRuns ? 'complete' : 'stopped'), {
  2220. currentRun: successfulRuns,
  2221. totalRuns: autoRunTotalRuns,
  2222. attemptRun: attemptRuns,
  2223. }));
  2224. clearStopRequest();
  2225. }
  2226. async function waitForResume() {
  2227. throwIfStopped();
  2228. const state = await getState();
  2229. if (state.email) {
  2230. await addLog('邮箱已就绪,自动继续后续步骤...', 'info');
  2231. return;
  2232. }
  2233. return new Promise((resolve, reject) => {
  2234. resumeWaiter = { resolve, reject };
  2235. });
  2236. }
  2237. async function resumeAutoRun() {
  2238. throwIfStopped();
  2239. const state = await getState();
  2240. if (!state.email) {
  2241. await addLog('无法继续:当前没有邮箱地址,请先在侧边栏填写邮箱。', 'error');
  2242. return false;
  2243. }
  2244. const resumedInMemory = await resumeAutoRunIfWaitingForEmail({ silent: true });
  2245. if (resumedInMemory) {
  2246. return true;
  2247. }
  2248. if (!isAutoRunPausedState(state)) {
  2249. return false;
  2250. }
  2251. if (autoRunActive) {
  2252. return false;
  2253. }
  2254. const totalRuns = state.autoRunTotalRuns || 1;
  2255. const currentRun = state.autoRunCurrentRun || 1;
  2256. const attemptRun = state.autoRunAttemptRun || 1;
  2257. const successfulRuns = Math.max(0, currentRun - 1);
  2258. await addLog('检测到自动流程暂停上下文已丢失,正在从当前进度恢复自动运行...', 'warn');
  2259. autoRunLoop(totalRuns, {
  2260. autoRunSkipFailures: Boolean(state.autoRunSkipFailures),
  2261. mode: 'continue',
  2262. resumeCurrentRun: currentRun,
  2263. resumeSuccessfulRuns: successfulRuns,
  2264. resumeAttemptRunsProcessed: Math.max(0, attemptRun - 1),
  2265. });
  2266. return true;
  2267. }
  2268. // ============================================================
  2269. // Step 1: Get OAuth Link (via vps-panel.js)
  2270. // ============================================================
  2271. async function executeStep1(state) {
  2272. if (!state.vpsUrl) {
  2273. throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
  2274. }
  2275. await addLog('步骤 1:正在打开 CPA 面板...');
  2276. const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
  2277. await closeConflictingTabsForSource('vps-panel', state.vpsUrl);
  2278. const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true });
  2279. const tabId = tab.id;
  2280. await rememberSourceLastUrl('vps-panel', state.vpsUrl);
  2281. await addLog('步骤 1:CPA 面板已打开,正在等待页面进入目标地址...');
  2282. const matchedTab = await waitForTabUrlFamily('vps-panel', tabId, state.vpsUrl, {
  2283. timeoutMs: 15000,
  2284. retryDelayMs: 400,
  2285. });
  2286. if (!matchedTab) {
  2287. await addLog('步骤 1:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...', 'warn');
  2288. }
  2289. await ensureContentScriptReadyOnTab('vps-panel', tabId, {
  2290. inject: injectFiles,
  2291. timeoutMs: 45000,
  2292. retryDelayMs: 900,
  2293. logMessage: '步骤 1:CPA 面板仍在加载,正在重试连接内容脚本...',
  2294. });
  2295. const result = await sendToContentScriptResilient('vps-panel', {
  2296. type: 'EXECUTE_STEP',
  2297. step: 1,
  2298. source: 'background',
  2299. payload: { vpsPassword: state.vpsPassword },
  2300. }, {
  2301. timeoutMs: 30000,
  2302. retryDelayMs: 700,
  2303. logMessage: '步骤 1:CPA 面板通信未就绪,正在等待页面恢复...',
  2304. });
  2305. if (result?.error) {
  2306. throw new Error(result.error);
  2307. }
  2308. }
  2309. // ============================================================
  2310. // Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register)
  2311. // ============================================================
  2312. async function executeStep2(state) {
  2313. if (!state.oauthUrl) {
  2314. throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
  2315. }
  2316. await addLog('步骤 2:正在打开认证链接...');
  2317. await reuseOrCreateTab('signup-page', state.oauthUrl);
  2318. await sendToContentScript('signup-page', {
  2319. type: 'EXECUTE_STEP',
  2320. step: 2,
  2321. source: 'background',
  2322. payload: {},
  2323. });
  2324. }
  2325. // ============================================================
  2326. // Step 3: Fill Email & Password (via signup-page.js)
  2327. // ============================================================
  2328. async function executeStep3(state) {
  2329. let resolvedEmail = state.email;
  2330. if (isHotmailProvider(state)) {
  2331. const account = await ensureHotmailAccountForFlow({
  2332. allowAllocate: true,
  2333. markUsed: true,
  2334. preferredAccountId: state.currentHotmailAccountId || null,
  2335. });
  2336. resolvedEmail = account.email;
  2337. }
  2338. if (!resolvedEmail) {
  2339. throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。');
  2340. }
  2341. const password = state.customPassword || generatePassword();
  2342. if (resolvedEmail !== state.email) {
  2343. await setEmailState(resolvedEmail);
  2344. }
  2345. await setPasswordState(password);
  2346. // Save account record
  2347. const accounts = state.accounts || [];
  2348. accounts.push({ email: resolvedEmail, password, createdAt: new Date().toISOString() });
  2349. await setState({ accounts });
  2350. await addLog(
  2351. `步骤 3:正在填写邮箱 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
  2352. );
  2353. await sendToContentScript('signup-page', {
  2354. type: 'EXECUTE_STEP',
  2355. step: 3,
  2356. source: 'background',
  2357. payload: { email: resolvedEmail, password },
  2358. });
  2359. }
  2360. // ============================================================
  2361. // Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js)
  2362. // ============================================================
  2363. function getMailConfig(state) {
  2364. const provider = state.mailProvider || 'qq';
  2365. if (provider === HOTMAIL_PROVIDER) {
  2366. return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(微软 Graph)' };
  2367. }
  2368. if (provider === '163') {
  2369. return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' };
  2370. }
  2371. if (provider === '163-vip') {
  2372. return { source: 'mail-163', url: 'https://webmail.vip.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 VIP 邮箱' };
  2373. }
  2374. if (provider === 'inbucket') {
  2375. const host = normalizeInbucketOrigin(state.inbucketHost);
  2376. const mailbox = (state.inbucketMailbox || '').trim();
  2377. if (!host) {
  2378. return { error: 'Inbucket 主机地址为空或无效。' };
  2379. }
  2380. if (!mailbox) {
  2381. return { error: 'Inbucket 邮箱名称为空。' };
  2382. }
  2383. return {
  2384. source: 'inbucket-mail',
  2385. url: `${host}/m/${encodeURIComponent(mailbox)}/`,
  2386. label: `Inbucket 邮箱(${mailbox})`,
  2387. navigateOnReuse: true,
  2388. inject: ['content/activation-utils.js', 'content/utils.js', 'content/inbucket-mail.js'],
  2389. injectSource: 'inbucket-mail',
  2390. };
  2391. }
  2392. return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' };
  2393. }
  2394. function normalizeInbucketOrigin(rawValue) {
  2395. const value = (rawValue || '').trim();
  2396. if (!value) return '';
  2397. const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
  2398. try {
  2399. const parsed = new URL(candidate);
  2400. return parsed.origin;
  2401. } catch {
  2402. return '';
  2403. }
  2404. }
  2405. function getVerificationCodeStateKey(step) {
  2406. return step === 4 ? 'lastSignupCode' : 'lastLoginCode';
  2407. }
  2408. function getVerificationCodeLabel(step) {
  2409. return step === 4 ? '注册' : '登录';
  2410. }
  2411. function getVerificationPollPayload(step, state, overrides = {}) {
  2412. if (step === 4) {
  2413. return {
  2414. filterAfterTimestamp: getHotmailVerificationRequestTimestamp(4, state),
  2415. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
  2416. subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm'],
  2417. targetEmail: state.email,
  2418. maxAttempts: 5,
  2419. intervalMs: 3000,
  2420. ...overrides,
  2421. };
  2422. }
  2423. return {
  2424. filterAfterTimestamp: getHotmailVerificationRequestTimestamp(7, state),
  2425. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
  2426. subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm', 'login'],
  2427. targetEmail: state.email,
  2428. maxAttempts: 5,
  2429. intervalMs: 3000,
  2430. ...overrides,
  2431. };
  2432. }
  2433. async function requestVerificationCodeResend(step) {
  2434. const signupTabId = await getTabId('signup-page');
  2435. if (!signupTabId) {
  2436. throw new Error('认证页面标签页已关闭,无法重新请求验证码。');
  2437. }
  2438. await chrome.tabs.update(signupTabId, { active: true });
  2439. await addLog(`步骤 ${step}:正在请求新的${getVerificationCodeLabel(step)}验证码...`, 'warn');
  2440. const result = await sendToContentScript('signup-page', {
  2441. type: 'RESEND_VERIFICATION_CODE',
  2442. step,
  2443. source: 'background',
  2444. payload: {},
  2445. });
  2446. if (result && result.error) {
  2447. throw new Error(result.error);
  2448. }
  2449. return Date.now();
  2450. }
  2451. async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
  2452. if (mail.provider === HOTMAIL_PROVIDER) {
  2453. const hotmailPollConfig = getHotmailVerificationPollConfig(step);
  2454. return pollHotmailVerificationCode(step, state, {
  2455. ...getVerificationPollPayload(step, state),
  2456. ...hotmailPollConfig,
  2457. ...pollOverrides,
  2458. });
  2459. }
  2460. const stateKey = getVerificationCodeStateKey(step);
  2461. const rejectedCodes = new Set();
  2462. if (state[stateKey]) {
  2463. rejectedCodes.add(state[stateKey]);
  2464. }
  2465. for (const code of (pollOverrides.excludeCodes || [])) {
  2466. if (code) rejectedCodes.add(code);
  2467. }
  2468. let lastError = null;
  2469. const filterAfterTimestamp = pollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
  2470. const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS;
  2471. for (let round = 1; round <= maxRounds; round++) {
  2472. if (round > 1) {
  2473. await requestVerificationCodeResend(step);
  2474. }
  2475. const payload = getVerificationPollPayload(step, state, {
  2476. ...pollOverrides,
  2477. filterAfterTimestamp,
  2478. excludeCodes: [...rejectedCodes],
  2479. });
  2480. try {
  2481. const result = await sendToMailContentScriptResilient(
  2482. mail,
  2483. {
  2484. type: 'POLL_EMAIL',
  2485. step,
  2486. source: 'background',
  2487. payload,
  2488. },
  2489. {
  2490. timeoutMs: 45000,
  2491. maxRecoveryAttempts: 2,
  2492. }
  2493. );
  2494. if (result && result.error) {
  2495. throw new Error(result.error);
  2496. }
  2497. if (!result || !result.code) {
  2498. throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
  2499. }
  2500. if (rejectedCodes.has(result.code)) {
  2501. throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
  2502. }
  2503. return result;
  2504. } catch (err) {
  2505. lastError = err;
  2506. await addLog(`步骤 ${step}:${err.message}`, 'warn');
  2507. if (round < maxRounds) {
  2508. await addLog(`步骤 ${step}:将重新发送验证码后重试(${round + 1}/${maxRounds})...`, 'warn');
  2509. }
  2510. }
  2511. }
  2512. throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
  2513. }
  2514. async function submitVerificationCode(step, code) {
  2515. const signupTabId = await getTabId('signup-page');
  2516. if (!signupTabId) {
  2517. throw new Error('认证页面标签页已关闭,无法填写验证码。');
  2518. }
  2519. await chrome.tabs.update(signupTabId, { active: true });
  2520. const result = await sendToContentScript('signup-page', {
  2521. type: 'FILL_CODE',
  2522. step,
  2523. source: 'background',
  2524. payload: { code },
  2525. });
  2526. if (result && result.error) {
  2527. throw new Error(result.error);
  2528. }
  2529. return result || {};
  2530. }
  2531. async function resolveVerificationStep(step, state, mail, options = {}) {
  2532. const stateKey = getVerificationCodeStateKey(step);
  2533. const rejectedCodes = new Set();
  2534. const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER
  2535. ? getHotmailVerificationPollConfig(step)
  2536. : null;
  2537. const ignorePersistedLastCode = Boolean(hotmailPollConfig?.ignorePersistedLastCode);
  2538. if (state[stateKey] && !ignorePersistedLastCode) {
  2539. rejectedCodes.add(state[stateKey]);
  2540. }
  2541. const nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null;
  2542. const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined
  2543. ? Boolean(options.requestFreshCodeFirst)
  2544. : (hotmailPollConfig?.requestFreshCodeFirst ?? false);
  2545. const maxSubmitAttempts = 3;
  2546. if (requestFreshCodeFirst) {
  2547. try {
  2548. await requestVerificationCodeResend(step);
  2549. await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn');
  2550. } catch (err) {
  2551. await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn');
  2552. }
  2553. }
  2554. if (mail.provider === HOTMAIL_PROVIDER) {
  2555. const initialDelayMs = Number(options.initialDelayMs ?? hotmailPollConfig.initialDelayMs) || 0;
  2556. if (initialDelayMs > 0) {
  2557. await addLog(`步骤 ${step}:等待 ${Math.round(initialDelayMs / 1000)} 秒,让 Hotmail 验证码邮件先到达...`, 'info');
  2558. await sleepWithStop(initialDelayMs);
  2559. }
  2560. }
  2561. for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
  2562. const result = await pollFreshVerificationCode(step, state, mail, {
  2563. excludeCodes: [...rejectedCodes],
  2564. filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
  2565. });
  2566. await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
  2567. const submitResult = await submitVerificationCode(step, result.code);
  2568. if (submitResult.invalidCode) {
  2569. rejectedCodes.add(result.code);
  2570. await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn');
  2571. if (attempt >= maxSubmitAttempts) {
  2572. throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
  2573. }
  2574. await requestVerificationCodeResend(step);
  2575. await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn');
  2576. continue;
  2577. }
  2578. await setState({
  2579. lastEmailTimestamp: result.emailTimestamp,
  2580. [stateKey]: result.code,
  2581. });
  2582. await completeStepFromBackground(step, {
  2583. emailTimestamp: result.emailTimestamp,
  2584. code: result.code,
  2585. });
  2586. return;
  2587. }
  2588. }
  2589. async function executeStep4(state) {
  2590. const mail = getMailConfig(state);
  2591. if (mail.error) throw new Error(mail.error);
  2592. const stepStartedAt = Date.now();
  2593. const signupTabId = await getTabId('signup-page');
  2594. if (!signupTabId) {
  2595. throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
  2596. }
  2597. await chrome.tabs.update(signupTabId, { active: true });
  2598. await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
  2599. const prepareResult = await sendToContentScriptResilient(
  2600. 'signup-page',
  2601. {
  2602. type: 'PREPARE_SIGNUP_VERIFICATION',
  2603. step: 4,
  2604. source: 'background',
  2605. payload: { password: state.password || state.customPassword || '' },
  2606. },
  2607. {
  2608. timeoutMs: 30000,
  2609. retryDelayMs: 700,
  2610. logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...',
  2611. }
  2612. );
  2613. if (prepareResult && prepareResult.error) {
  2614. throw new Error(prepareResult.error);
  2615. }
  2616. if (prepareResult?.verificationRequestedAt) {
  2617. await setState({ loginVerificationRequestedAt: prepareResult.verificationRequestedAt });
  2618. }
  2619. if (prepareResult?.alreadyVerified) {
  2620. await completeStepFromBackground(4, {});
  2621. return;
  2622. }
  2623. if (mail.provider === HOTMAIL_PROVIDER) {
  2624. await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
  2625. } else {
  2626. await addLog(`步骤 4:正在打开${mail.label}...`);
  2627. // For mail tabs, only create if not alive — don't navigate (preserves login session)
  2628. const alive = await isTabAlive(mail.source);
  2629. if (alive) {
  2630. if (mail.navigateOnReuse) {
  2631. await reuseOrCreateTab(mail.source, mail.url, {
  2632. inject: mail.inject,
  2633. injectSource: mail.injectSource,
  2634. });
  2635. } else {
  2636. const tabId = await getTabId(mail.source);
  2637. await chrome.tabs.update(tabId, { active: true });
  2638. }
  2639. } else {
  2640. await reuseOrCreateTab(mail.source, mail.url, {
  2641. inject: mail.inject,
  2642. injectSource: mail.injectSource,
  2643. });
  2644. }
  2645. }
  2646. await resolveVerificationStep(4, state, mail, {
  2647. filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
  2648. requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
  2649. });
  2650. return;
  2651. }
  2652. // ============================================================
  2653. // Step 5: Fill Name & Birthday (via signup-page.js)
  2654. // ============================================================
  2655. async function executeStep5(state) {
  2656. const { firstName, lastName } = generateRandomName();
  2657. const { year, month, day } = generateRandomBirthday();
  2658. await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
  2659. await sendToContentScript('signup-page', {
  2660. type: 'EXECUTE_STEP',
  2661. step: 5,
  2662. source: 'background',
  2663. payload: { firstName, lastName, year, month, day },
  2664. });
  2665. }
  2666. // ============================================================
  2667. // Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login)
  2668. // ============================================================
  2669. async function refreshOAuthUrlBeforeStep6(state) {
  2670. if (!state.vpsUrl) {
  2671. throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
  2672. }
  2673. await addLog('步骤 6:正在刷新登录用的 CPA OAuth 链接...');
  2674. const waitForFreshOAuth = waitForStepComplete(1, 120000);
  2675. await executeStep1(state);
  2676. await waitForFreshOAuth;
  2677. const latestState = await getState();
  2678. if (!latestState.oauthUrl) {
  2679. throw new Error('刷新 OAuth 链接后仍未拿到可用链接。');
  2680. }
  2681. return latestState.oauthUrl;
  2682. }
  2683. async function executeStep6(state) {
  2684. if (!state.email) {
  2685. throw new Error('缺少邮箱地址,请先完成步骤 3。');
  2686. }
  2687. const oauthUrl = await refreshOAuthUrlBeforeStep6(state);
  2688. await addLog('步骤 6:正在打开最新 OAuth 链接并登录...');
  2689. // Reuse the signup-page tab — navigate it to the OAuth URL
  2690. await reuseOrCreateTab('signup-page', oauthUrl);
  2691. // signup-page.js will inject (same auth.openai.com domain) and handle login
  2692. await sendToContentScript('signup-page', {
  2693. type: 'EXECUTE_STEP',
  2694. step: 6,
  2695. source: 'background',
  2696. payload: { email: state.email, password: state.password },
  2697. });
  2698. }
  2699. // ============================================================
  2700. // Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js)
  2701. // ============================================================
  2702. async function runStep7Attempt(state) {
  2703. const mail = getMailConfig(state);
  2704. if (mail.error) throw new Error(mail.error);
  2705. const stepStartedAt = Date.now();
  2706. const authTabId = await getTabId('signup-page');
  2707. if (authTabId) {
  2708. await chrome.tabs.update(authTabId, { active: true });
  2709. } else {
  2710. if (!state.oauthUrl) {
  2711. throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
  2712. }
  2713. await reuseOrCreateTab('signup-page', state.oauthUrl);
  2714. }
  2715. await addLog('步骤 7:正在准备认证页,必要时切换到一次性验证码登录...');
  2716. const prepareResult = await sendToContentScript('signup-page', {
  2717. type: 'PREPARE_LOGIN_CODE',
  2718. step: 7,
  2719. source: 'background',
  2720. payload: {},
  2721. });
  2722. if (prepareResult && prepareResult.error) {
  2723. throw new Error(prepareResult.error);
  2724. }
  2725. if (mail.provider === HOTMAIL_PROVIDER) {
  2726. await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`);
  2727. } else {
  2728. await addLog(`步骤 7:正在打开${mail.label}...`);
  2729. const alive = await isTabAlive(mail.source);
  2730. if (alive) {
  2731. if (mail.navigateOnReuse) {
  2732. await reuseOrCreateTab(mail.source, mail.url, {
  2733. inject: mail.inject,
  2734. injectSource: mail.injectSource,
  2735. });
  2736. } else {
  2737. const tabId = await getTabId(mail.source);
  2738. await chrome.tabs.update(tabId, { active: true });
  2739. }
  2740. } else {
  2741. await reuseOrCreateTab(mail.source, mail.url, {
  2742. inject: mail.inject,
  2743. injectSource: mail.injectSource,
  2744. });
  2745. }
  2746. }
  2747. await resolveVerificationStep(7, state, mail, {
  2748. filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
  2749. requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
  2750. });
  2751. }
  2752. async function rerunStep6ForStep7Recovery() {
  2753. const currentState = await getState();
  2754. const waitForStep6 = waitForStepComplete(6, 120000);
  2755. await addLog('步骤 7:正在回到步骤 6,重新发起登录验证码流程...', 'warn');
  2756. await executeStep6(currentState);
  2757. await waitForStep6;
  2758. await sleepWithStop(3000);
  2759. }
  2760. async function executeStep7(state) {
  2761. let lastError = null;
  2762. for (let round = 1; round <= STEP7_RESTART_MAX_ROUNDS; round++) {
  2763. const currentState = round === 1 ? state : await getState();
  2764. try {
  2765. if (round > 1) {
  2766. await addLog(`步骤 7:正在进行第 ${round}/${STEP7_RESTART_MAX_ROUNDS} 轮登录验证码恢复尝试。`, 'warn');
  2767. }
  2768. await runStep7Attempt(currentState);
  2769. return;
  2770. } catch (err) {
  2771. lastError = err;
  2772. if (!isVerificationMailPollingError(err)) {
  2773. throw err;
  2774. }
  2775. if (round >= STEP7_RESTART_MAX_ROUNDS) {
  2776. break;
  2777. }
  2778. await addLog(`步骤 7:检测到邮箱轮询类失败,准备从步骤 6 重新开始(${round + 1}/${STEP7_RESTART_MAX_ROUNDS})...`, 'warn');
  2779. await rerunStep6ForStep7Recovery();
  2780. }
  2781. }
  2782. throw lastError || new Error(`步骤 7:登录验证码流程在 ${STEP7_RESTART_MAX_ROUNDS} 轮后仍未成功。`);
  2783. }
  2784. // ============================================================
  2785. // Step 8: 完成 OAuth(自动点击 + localhost 回调监听)
  2786. // ============================================================
  2787. let webNavListener = null;
  2788. async function executeStep8(state) {
  2789. if (!state.oauthUrl) {
  2790. throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
  2791. }
  2792. await addLog('步骤 8:正在监听 localhost 回调地址...');
  2793. // 只在当前步骤内注册 webNavigation 监听
  2794. return new Promise((resolve, reject) => {
  2795. let resolved = false;
  2796. let signupTabId = null;
  2797. const cleanupListener = () => {
  2798. if (webNavListener) {
  2799. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  2800. webNavListener = null;
  2801. }
  2802. };
  2803. const timeout = setTimeout(() => {
  2804. cleanupListener();
  2805. reject(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。'));
  2806. }, 120000);
  2807. webNavListener = (details) => {
  2808. if (resolved || !signupTabId) return;
  2809. if (details.tabId !== signupTabId) return;
  2810. if (details.frameId !== 0) return;
  2811. if (isLocalhostOAuthCallbackUrl(details.url)) {
  2812. console.log(LOG_PREFIX, `已捕获 localhost OAuth 回调:${details.url}`);
  2813. resolved = true;
  2814. cleanupListener();
  2815. clearTimeout(timeout);
  2816. addLog(`步骤 8:已捕获 localhost 地址:${details.url}`, 'ok').then(() => {
  2817. return completeStepFromBackground(8, { localhostUrl: details.url });
  2818. }).then(() => {
  2819. resolve();
  2820. }).catch((err) => {
  2821. reject(err);
  2822. });
  2823. }
  2824. };
  2825. // 步骤 7 之后,认证页会进入 OAuth 同意页,出现“继续”按钮。
  2826. // 这里先在页面内定位按钮,再通过 debugger 输入事件发起点击。
  2827. (async () => {
  2828. try {
  2829. signupTabId = await getTabId('signup-page');
  2830. if (signupTabId) {
  2831. await chrome.tabs.update(signupTabId, { active: true });
  2832. await addLog('步骤 8:已切回认证页,正在准备调试器点击...');
  2833. } else {
  2834. signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
  2835. await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...');
  2836. }
  2837. chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
  2838. const clickResult = await sendToContentScript('signup-page', {
  2839. type: 'STEP8_FIND_AND_CLICK',
  2840. source: 'background',
  2841. payload: {},
  2842. });
  2843. if (clickResult?.error) {
  2844. throw new Error(clickResult.error);
  2845. }
  2846. if (!resolved) {
  2847. await clickWithDebugger(signupTabId, clickResult?.rect);
  2848. await addLog('步骤 8:已发送调试器点击,正在等待跳转...');
  2849. }
  2850. } catch (err) {
  2851. clearTimeout(timeout);
  2852. cleanupListener();
  2853. reject(err);
  2854. }
  2855. })();
  2856. });
  2857. }
  2858. // ============================================================
  2859. // Step 9: CPA 回调验证(通过 vps-panel.js)
  2860. // ============================================================
  2861. async function executeStep9(state) {
  2862. if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
  2863. throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。');
  2864. }
  2865. if (!state.localhostUrl) {
  2866. throw new Error('缺少 localhost 回调地址,请先完成步骤 8。');
  2867. }
  2868. if (!state.vpsUrl) {
  2869. throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
  2870. }
  2871. await addLog('步骤 9:正在打开 CPA 面板...');
  2872. const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
  2873. let tabId = await getTabId('vps-panel');
  2874. const alive = tabId && await isTabAlive('vps-panel');
  2875. if (!alive) {
  2876. tabId = await reuseOrCreateTab('vps-panel', state.vpsUrl, {
  2877. inject: injectFiles,
  2878. reloadIfSameUrl: true,
  2879. });
  2880. } else {
  2881. await closeConflictingTabsForSource('vps-panel', state.vpsUrl, { excludeTabIds: [tabId] });
  2882. await chrome.tabs.update(tabId, { active: true });
  2883. await rememberSourceLastUrl('vps-panel', state.vpsUrl);
  2884. }
  2885. await ensureContentScriptReadyOnTab('vps-panel', tabId, {
  2886. inject: injectFiles,
  2887. timeoutMs: 45000,
  2888. retryDelayMs: 900,
  2889. logMessage: '姝ラ 9锛欳PA 闈㈡澘浠嶅湪鍔犺浇锛屾鍦ㄩ噸璇曡繛鎺ュ唴瀹硅剼鏈?..',
  2890. });
  2891. await addLog('步骤 9:正在填写回调地址...');
  2892. const result = await sendToContentScriptResilient('vps-panel', {
  2893. type: 'EXECUTE_STEP',
  2894. step: 9,
  2895. source: 'background',
  2896. payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
  2897. }, {
  2898. timeoutMs: 30000,
  2899. retryDelayMs: 700,
  2900. logMessage: '步骤 9:CPA 面板通信未就绪,正在等待页面恢复...',
  2901. });
  2902. if (result?.error) {
  2903. throw new Error(result.error);
  2904. }
  2905. }
  2906. // ============================================================
  2907. // Open Side Panel on extension icon click
  2908. // ============================================================
  2909. chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });