import GuJumpgate snapshot from GitHub
This commit is contained in:
@@ -0,0 +1,777 @@
|
||||
(function attachBackgroundAccountRunHistory(root, factory) {
|
||||
root.MultiPageBackgroundAccountRunHistory = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundAccountRunHistoryModule() {
|
||||
function createAccountRunHistoryHelpers(deps = {}) {
|
||||
const {
|
||||
ACCOUNT_RUN_HISTORY_STORAGE_KEY = 'accountRunHistory',
|
||||
addLog,
|
||||
buildLocalHelperEndpoint,
|
||||
chrome,
|
||||
getErrorMessage,
|
||||
getNodeIdByStepForState = null,
|
||||
getNodeTitleForState = null,
|
||||
getState,
|
||||
normalizeAccountRunHistoryHelperBaseUrl,
|
||||
} = deps;
|
||||
|
||||
function normalizeTimestamp(value) {
|
||||
const timestamp = Date.parse(String(value || ''));
|
||||
return Number.isFinite(timestamp) ? timestamp : 0;
|
||||
}
|
||||
|
||||
function normalizeRetryCount(value) {
|
||||
const count = Math.floor(Number(value) || 0);
|
||||
return count > 0 ? count : 0;
|
||||
}
|
||||
|
||||
function normalizeFinalStatus(status = '') {
|
||||
const normalized = String(status || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
if (normalized === 'success') {
|
||||
return 'success';
|
||||
}
|
||||
if (normalized === 'running' || /_running$/.test(normalized) || /^node:[^:]+:running$/.test(normalized)) {
|
||||
return 'running';
|
||||
}
|
||||
if (normalized === 'failed' || /_failed$/.test(normalized) || /^node:[^:]+:failed$/.test(normalized)) {
|
||||
return 'failed';
|
||||
}
|
||||
if (normalized === 'stopped' || /_stopped$/.test(normalized) || /^node:[^:]+:stopped$/.test(normalized)) {
|
||||
return 'stopped';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeNodeId(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function extractRecordNodeFromStatus(status = '') {
|
||||
const match = String(status || '').trim().match(/^node:([^:]+):(?:running|failed|stopped)$/i);
|
||||
return match ? normalizeNodeId(match[1]) : '';
|
||||
}
|
||||
|
||||
function extractRecordStepFromStatus(status = '') {
|
||||
const normalizedStatus = String(status || '').trim().toLowerCase();
|
||||
const statusMatch = normalizedStatus.match(/^step(\d+)_(?:failed|stopped)$/);
|
||||
if (statusMatch) {
|
||||
const step = Number(statusMatch[1]);
|
||||
return Number.isInteger(step) && step > 0 ? step : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractRecordStepFromDetailPrefix(detail = '') {
|
||||
const text = String(detail || '').trim();
|
||||
const detailMatch = text.match(/^(?:Step\s+(\d+)|步骤\s*(\d+))\s*(?::|:|失败|停止|已|\b)/i);
|
||||
if (!detailMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const step = Number(detailMatch[1] || detailMatch[2]);
|
||||
return Number.isInteger(step) && step > 0 ? step : null;
|
||||
}
|
||||
|
||||
function extractAnyRecordStepFromDetail(detail = '') {
|
||||
const text = String(detail || '').trim();
|
||||
const detailMatch = text.match(/(?:Step\s+(\d+)|步骤\s*(\d+))/i);
|
||||
if (!detailMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const step = Number(detailMatch[1] || detailMatch[2]);
|
||||
return Number.isInteger(step) && step > 0 ? step : null;
|
||||
}
|
||||
|
||||
function extractRecordStep(status = '', detail = '') {
|
||||
return extractRecordStepFromStatus(status) || extractRecordStepFromDetailPrefix(detail);
|
||||
}
|
||||
|
||||
function parseFailureLabelStep(label = '') {
|
||||
const match = String(label || '').trim().match(/^步骤\s*(\d+)\s*(?:失败|停止)$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const step = Number(match[1]);
|
||||
return Number.isInteger(step) && step > 0 ? step : null;
|
||||
}
|
||||
|
||||
function parseFailureLabelNode(label = '') {
|
||||
const match = String(label || '').trim().match(/^节点\s*([A-Za-z0-9_.:-]+)\s*(?:失败|停止)$/);
|
||||
return match ? normalizeNodeId(match[1]) : '';
|
||||
}
|
||||
|
||||
function shouldIgnorePersistedFailedStepCandidate(candidate, failureDetail = '') {
|
||||
if (!Number.isInteger(candidate) || candidate <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const text = String(failureDetail || '').trim();
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const leadingStep = extractRecordStepFromDetailPrefix(text);
|
||||
if (Number.isInteger(leadingStep) && leadingStep > 0) {
|
||||
return leadingStep !== candidate;
|
||||
}
|
||||
|
||||
const incidentalStep = extractAnyRecordStepFromDetail(text);
|
||||
return incidentalStep === candidate;
|
||||
}
|
||||
|
||||
function resolveNormalizedFailedStep(record = {}, failureDetail = '') {
|
||||
const explicitStatusStep = extractRecordStepFromStatus(record.finalStatus || record.status || '');
|
||||
if (Number.isInteger(explicitStatusStep) && explicitStatusStep > 0) {
|
||||
return explicitStatusStep;
|
||||
}
|
||||
|
||||
const detailStep = extractRecordStepFromDetailPrefix(failureDetail);
|
||||
if (Number.isInteger(detailStep) && detailStep > 0) {
|
||||
return detailStep;
|
||||
}
|
||||
|
||||
const failedStepCandidate = Number(record.failedStep);
|
||||
if (Number.isInteger(failedStepCandidate)
|
||||
&& failedStepCandidate > 0
|
||||
&& !shouldIgnorePersistedFailedStepCandidate(failedStepCandidate, failureDetail)) {
|
||||
return failedStepCandidate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveNormalizedFailedNodeId(record = {}, status = '', failedStep = null) {
|
||||
const explicitNodeId = normalizeNodeId(record.failedNodeId || record.failedNode || record.nodeId);
|
||||
if (explicitNodeId) {
|
||||
return explicitNodeId;
|
||||
}
|
||||
const statusNodeId = extractRecordNodeFromStatus(status || record.finalStatus || record.status || '');
|
||||
if (statusNodeId) {
|
||||
return statusNodeId;
|
||||
}
|
||||
if (Number.isInteger(failedStep) && failedStep > 0 && typeof getNodeIdByStepForState === 'function') {
|
||||
return normalizeNodeId(getNodeIdByStepForState(failedStep, record));
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function resolveFailureLabel(finalStatus, rawFailureLabel = '', computedFailureLabel = '', failedNodeId = '', failedStep = null) {
|
||||
const rawLabel = String(rawFailureLabel || '').trim();
|
||||
if (finalStatus === 'stopped') {
|
||||
return computedFailureLabel;
|
||||
}
|
||||
if (finalStatus !== 'failed') {
|
||||
return rawLabel || computedFailureLabel;
|
||||
}
|
||||
|
||||
const rawNodeId = parseFailureLabelNode(rawLabel);
|
||||
if (rawNodeId) {
|
||||
return rawNodeId === failedNodeId ? rawLabel : computedFailureLabel;
|
||||
}
|
||||
|
||||
const rawStep = parseFailureLabelStep(rawLabel);
|
||||
if (Number.isInteger(rawStep) && rawStep > 0) {
|
||||
return rawStep === failedStep ? rawLabel : computedFailureLabel;
|
||||
}
|
||||
|
||||
return rawLabel || computedFailureLabel;
|
||||
}
|
||||
|
||||
function isPhoneVerificationFailure(detail = '') {
|
||||
const text = String(detail || '').trim();
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /add[_\s-]?phone/i.test(text)
|
||||
|| /手机号(?:验证|页面|页)|手机(?:号)?页面|出现手机号验证/.test(text)
|
||||
|| /进入了手机号页面/.test(text);
|
||||
}
|
||||
|
||||
function getNodeDisplayName(nodeId, state = {}) {
|
||||
const normalizedNodeId = normalizeNodeId(nodeId);
|
||||
if (!normalizedNodeId) {
|
||||
return '';
|
||||
}
|
||||
if (typeof getNodeTitleForState === 'function') {
|
||||
const title = String(getNodeTitleForState(normalizedNodeId, state) || '').trim();
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
return normalizedNodeId;
|
||||
}
|
||||
|
||||
function buildFailureLabel(finalStatus, failedNodeId = '', failedStep = null, failureDetail = '', state = {}) {
|
||||
if (finalStatus === 'success') {
|
||||
return '流程完成';
|
||||
}
|
||||
if (finalStatus === 'running') {
|
||||
return '正在运行';
|
||||
}
|
||||
if (finalStatus === 'stopped') {
|
||||
if (failedNodeId) {
|
||||
return `节点 ${getNodeDisplayName(failedNodeId, state)} 停止`;
|
||||
}
|
||||
if (Number.isInteger(failedStep) && failedStep > 0) {
|
||||
return `步骤 ${failedStep} 停止`;
|
||||
}
|
||||
return '流程已停止';
|
||||
}
|
||||
if (finalStatus !== 'failed') {
|
||||
return '无';
|
||||
}
|
||||
if (isPhoneVerificationFailure(failureDetail)) {
|
||||
return '出现手机号验证';
|
||||
}
|
||||
if (failedNodeId) {
|
||||
return `节点 ${getNodeDisplayName(failedNodeId, state)} 失败`;
|
||||
}
|
||||
if (Number.isInteger(failedStep) && failedStep > 0) {
|
||||
return `步骤 ${failedStep} 失败`;
|
||||
}
|
||||
return '流程失败';
|
||||
}
|
||||
|
||||
function normalizeAccountIdentifierType(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
|
||||
}
|
||||
|
||||
function normalizeAccountIdentifierValue(value = '', identifierType = 'email') {
|
||||
const normalizedValue = String(value || '').trim();
|
||||
if (!normalizedValue) {
|
||||
return '';
|
||||
}
|
||||
return normalizeAccountIdentifierType(identifierType) === 'phone'
|
||||
? normalizedValue
|
||||
: normalizedValue.toLowerCase();
|
||||
}
|
||||
|
||||
function getActivationPhoneNumber(activation = null) {
|
||||
if (!activation || typeof activation !== 'object' || Array.isArray(activation)) {
|
||||
return '';
|
||||
}
|
||||
return String(
|
||||
activation.phoneNumber
|
||||
?? activation.number
|
||||
?? activation.phone
|
||||
?? ''
|
||||
).trim();
|
||||
}
|
||||
|
||||
function resolveStatePhoneNumber(state = {}) {
|
||||
const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase();
|
||||
const accountIdentifierPhone = identifierType === 'phone'
|
||||
? String(state?.accountIdentifier || '').trim()
|
||||
: '';
|
||||
|
||||
return String(
|
||||
state?.phoneNumber
|
||||
|| state?.signupPhoneNumber
|
||||
|| accountIdentifierPhone
|
||||
|| getActivationPhoneNumber(state?.signupPhoneCompletedActivation)
|
||||
|| getActivationPhoneNumber(state?.signupPhoneActivation)
|
||||
|| getActivationPhoneNumber(state?.currentPhoneActivation)
|
||||
|| ''
|
||||
).trim();
|
||||
}
|
||||
|
||||
function normalizePhoneRecordKey(value = '') {
|
||||
const rawValue = String(value || '').trim();
|
||||
const digits = rawValue.replace(/\D+/g, '');
|
||||
return digits || rawValue.toLowerCase();
|
||||
}
|
||||
|
||||
function resolveRecordIdentity(record = {}) {
|
||||
const rawEmail = String(record.email || '').trim().toLowerCase();
|
||||
const rawPhoneNumber = String(record.phoneNumber ?? record.phone ?? record.number ?? '').trim();
|
||||
const rawIdentifierType = String(record.accountIdentifierType || '').trim().toLowerCase();
|
||||
const inferredIdentifierType = rawIdentifierType === 'phone'
|
||||
? 'phone'
|
||||
: (rawIdentifierType === 'email'
|
||||
? 'email'
|
||||
: ((!rawEmail && rawPhoneNumber) ? 'phone' : 'email'));
|
||||
const rawAccountIdentifier = String(
|
||||
record.accountIdentifier
|
||||
|| (inferredIdentifierType === 'phone' ? rawPhoneNumber : rawEmail)
|
||||
|| ''
|
||||
).trim();
|
||||
const accountIdentifierType = rawAccountIdentifier
|
||||
? normalizeAccountIdentifierType(inferredIdentifierType)
|
||||
: (rawEmail ? 'email' : (rawPhoneNumber ? 'phone' : ''));
|
||||
const accountIdentifier = normalizeAccountIdentifierValue(
|
||||
rawAccountIdentifier || (accountIdentifierType === 'phone' ? rawPhoneNumber : rawEmail),
|
||||
accountIdentifierType || inferredIdentifierType
|
||||
);
|
||||
const email = rawEmail || (accountIdentifierType === 'email' ? accountIdentifier : '');
|
||||
const phoneNumber = rawPhoneNumber || (accountIdentifierType === 'phone' ? accountIdentifier : '');
|
||||
|
||||
return {
|
||||
email,
|
||||
phoneNumber,
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRecordId(identifier = '', identifierType = 'email') {
|
||||
const normalizedIdentifierType = normalizeAccountIdentifierType(identifierType);
|
||||
const normalizedIdentifier = normalizeAccountIdentifierValue(identifier, normalizedIdentifierType);
|
||||
if (!normalizedIdentifier) {
|
||||
return '';
|
||||
}
|
||||
if (normalizedIdentifierType === 'phone' && /^phone:/i.test(normalizedIdentifier)) {
|
||||
return normalizedIdentifier.toLowerCase();
|
||||
}
|
||||
return normalizedIdentifierType === 'phone'
|
||||
? `phone:${normalizedIdentifier.toLowerCase()}`
|
||||
: normalizedIdentifier;
|
||||
}
|
||||
|
||||
function normalizeSource(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual';
|
||||
}
|
||||
|
||||
function normalizeAutoRunContext(context) {
|
||||
if (!context || typeof context !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentRun = Math.max(0, Math.floor(Number(context.currentRun) || 0));
|
||||
const totalRuns = Math.max(0, Math.floor(Number(context.totalRuns) || 0));
|
||||
const attemptRun = Math.max(0, Math.floor(Number(context.attemptRun) || 0));
|
||||
|
||||
if (!currentRun && !totalRuns && !attemptRun) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
currentRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
};
|
||||
}
|
||||
|
||||
function buildAutoRunContextFromState(state = {}) {
|
||||
return normalizeAutoRunContext({
|
||||
currentRun: state.autoRunCurrentRun,
|
||||
totalRuns: state.autoRunTotalRuns,
|
||||
attemptRun: state.autoRunAttemptRun,
|
||||
});
|
||||
}
|
||||
|
||||
function getRetryCountFromState(state = {}) {
|
||||
if (!Boolean(state.autoRunning)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const attemptRun = Math.max(0, Math.floor(Number(state.autoRunAttemptRun) || 0));
|
||||
return attemptRun > 1 ? attemptRun - 1 : 0;
|
||||
}
|
||||
|
||||
function normalizeAccountRunHistoryRecord(record) {
|
||||
if (!record || typeof record !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const identity = resolveRecordIdentity(record);
|
||||
const email = identity.email;
|
||||
const phoneNumber = identity.phoneNumber;
|
||||
const accountIdentifierType = identity.accountIdentifierType;
|
||||
const accountIdentifier = identity.accountIdentifier;
|
||||
const password = String(record.password ?? '').trim();
|
||||
const finalStatus = normalizeFinalStatus(record.finalStatus || record.status || '');
|
||||
|
||||
if (!accountIdentifier || !finalStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const finishedAt = String(record.finishedAt || record.recordedAt || '').trim();
|
||||
const failureDetail = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? String(record.failureDetail || record.reason || '').trim()
|
||||
: '';
|
||||
const failedStep = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? resolveNormalizedFailedStep(record, failureDetail)
|
||||
: null;
|
||||
const failedNodeId = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? resolveNormalizedFailedNodeId(record, record.finalStatus || record.status || '', failedStep)
|
||||
: '';
|
||||
const autoRunContext = normalizeAutoRunContext(record.autoRunContext);
|
||||
const retryCount = normalizeRetryCount(
|
||||
record.retryCount !== undefined
|
||||
? record.retryCount
|
||||
: ((autoRunContext?.attemptRun || 0) > 1 ? autoRunContext.attemptRun - 1 : 0)
|
||||
);
|
||||
const source = normalizeSource(record.source || (autoRunContext ? 'auto' : 'manual'));
|
||||
const computedFailureLabel = buildFailureLabel(finalStatus, failedNodeId, failedStep, failureDetail, record);
|
||||
const rawFailureLabel = String(record.failureLabel || '').trim();
|
||||
const flowId = String(record.flowId || record.activeFlowId || '').trim();
|
||||
const runId = String(record.runId || record.activeRunId || '').trim();
|
||||
|
||||
return {
|
||||
recordId: String(record.recordId || '').trim() || buildRecordId(accountIdentifier, accountIdentifierType),
|
||||
flowId,
|
||||
runId,
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
email,
|
||||
phoneNumber,
|
||||
password,
|
||||
finalStatus,
|
||||
finishedAt,
|
||||
retryCount,
|
||||
failureLabel: resolveFailureLabel(finalStatus, rawFailureLabel, computedFailureLabel, failedNodeId, failedStep),
|
||||
failureDetail,
|
||||
failedNodeId,
|
||||
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
|
||||
source,
|
||||
autoRunContext: source === 'auto' ? autoRunContext : null,
|
||||
plusModeEnabled: Boolean(record.plusModeEnabled),
|
||||
contributionMode: Boolean(record.contributionMode),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAccountRunHistory(records) {
|
||||
if (!Array.isArray(records)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return records
|
||||
.map((item) => normalizeAccountRunHistoryRecord(item))
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => normalizeTimestamp(right.finishedAt) - normalizeTimestamp(left.finishedAt));
|
||||
}
|
||||
|
||||
async function getPersistedAccountRunHistory() {
|
||||
try {
|
||||
const stored = await chrome.storage.local.get(ACCOUNT_RUN_HISTORY_STORAGE_KEY);
|
||||
return normalizeAccountRunHistory(stored[ACCOUNT_RUN_HISTORY_STORAGE_KEY]);
|
||||
} catch (err) {
|
||||
console.warn('[MultiPage:account-run-history] Failed to read account run history:', err?.message || err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function setPersistedAccountRunHistory(records) {
|
||||
const normalizedHistory = normalizeAccountRunHistory(records);
|
||||
await chrome.storage.local.set({
|
||||
[ACCOUNT_RUN_HISTORY_STORAGE_KEY]: normalizedHistory,
|
||||
});
|
||||
return normalizedHistory;
|
||||
}
|
||||
|
||||
function buildAccountRunHistoryRecord(state = {}, status = '', reason = '') {
|
||||
const identity = resolveRecordIdentity({
|
||||
accountIdentifierType: state.accountIdentifierType,
|
||||
accountIdentifier: state.accountIdentifier,
|
||||
email: state.email,
|
||||
phoneNumber: resolveStatePhoneNumber(state),
|
||||
});
|
||||
const email = identity.email;
|
||||
const phoneNumber = identity.phoneNumber;
|
||||
const accountIdentifierType = identity.accountIdentifierType;
|
||||
const accountIdentifier = identity.accountIdentifier;
|
||||
const password = String(state.password || state.customPassword || '').trim();
|
||||
const finalStatus = normalizeFinalStatus(status);
|
||||
|
||||
if (!accountIdentifier || !finalStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const failureDetail = finalStatus === 'failed' || finalStatus === 'stopped' ? String(reason || '').trim() : '';
|
||||
const failedStep = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? extractRecordStep(status, failureDetail)
|
||||
: null;
|
||||
const statusNodeId = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? extractRecordNodeFromStatus(status)
|
||||
: '';
|
||||
const stateNodeId = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? normalizeNodeId(state.currentNodeId)
|
||||
: '';
|
||||
const failedNodeIdFromStep = !statusNodeId
|
||||
&& !stateNodeId
|
||||
&& Number.isInteger(failedStep)
|
||||
&& failedStep > 0
|
||||
&& typeof getNodeIdByStepForState === 'function'
|
||||
? normalizeNodeId(getNodeIdByStepForState(failedStep, state))
|
||||
: '';
|
||||
const failedNodeId = statusNodeId || stateNodeId || failedNodeIdFromStep;
|
||||
const source = Boolean(state.autoRunning) ? 'auto' : 'manual';
|
||||
const autoRunContext = source === 'auto' ? buildAutoRunContextFromState(state) : null;
|
||||
const retryCount = source === 'auto' ? getRetryCountFromState(state) : 0;
|
||||
const finishedAt = new Date().toISOString();
|
||||
const flowId = String(state.flowId || state.activeFlowId || '').trim();
|
||||
const runId = String(state.runId || state.activeRunId || '').trim();
|
||||
|
||||
return {
|
||||
recordId: buildRecordId(accountIdentifier, accountIdentifierType),
|
||||
flowId,
|
||||
runId,
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
email,
|
||||
phoneNumber,
|
||||
password,
|
||||
finalStatus,
|
||||
finishedAt,
|
||||
retryCount,
|
||||
failureLabel: buildFailureLabel(finalStatus, failedNodeId, failedStep, failureDetail, state),
|
||||
failureDetail,
|
||||
failedNodeId,
|
||||
failedStep: statusNodeId ? null : (Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null),
|
||||
source,
|
||||
autoRunContext,
|
||||
plusModeEnabled: Boolean(state.plusModeEnabled),
|
||||
contributionMode: Boolean(state.contributionMode),
|
||||
};
|
||||
}
|
||||
|
||||
function upsertAccountRunHistoryRecord(history, record) {
|
||||
const normalizedHistory = normalizeAccountRunHistory(history);
|
||||
if (!record) {
|
||||
return normalizedHistory;
|
||||
}
|
||||
|
||||
const recordId = String(record.recordId || '').trim();
|
||||
const emailKey = String(record.email || '').trim().toLowerCase();
|
||||
const phoneKey = normalizePhoneRecordKey(record.phoneNumber);
|
||||
const identifierKey = buildRecordId(
|
||||
record.accountIdentifier || record.email || record.phoneNumber,
|
||||
record.accountIdentifierType || (phoneKey && !emailKey ? 'phone' : 'email')
|
||||
);
|
||||
const nextHistory = normalizedHistory.filter((item) => {
|
||||
const itemRecordId = String(item.recordId || '').trim();
|
||||
const itemEmailKey = String(item.email || '').trim().toLowerCase();
|
||||
const itemPhoneKey = normalizePhoneRecordKey(item.phoneNumber);
|
||||
const itemIdentifierKey = buildRecordId(
|
||||
item.accountIdentifier || item.email || item.phoneNumber,
|
||||
item.accountIdentifierType || (itemPhoneKey && !itemEmailKey ? 'phone' : 'email')
|
||||
);
|
||||
return itemRecordId !== recordId
|
||||
&& itemIdentifierKey !== identifierKey
|
||||
&& (!emailKey || itemEmailKey !== emailKey)
|
||||
&& (!phoneKey || itemPhoneKey !== phoneKey);
|
||||
});
|
||||
|
||||
nextHistory.unshift(record);
|
||||
return normalizeAccountRunHistory(nextHistory);
|
||||
}
|
||||
|
||||
function deleteAccountRunHistoryEntries(history, recordIds = []) {
|
||||
const normalizedHistory = normalizeAccountRunHistory(history);
|
||||
const normalizedIds = Array.isArray(recordIds)
|
||||
? recordIds
|
||||
.map((value) => String(value || '').trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (!normalizedIds.length) {
|
||||
return {
|
||||
deletedCount: 0,
|
||||
nextHistory: normalizedHistory,
|
||||
};
|
||||
}
|
||||
|
||||
const selectedIds = new Set(normalizedIds);
|
||||
const nextHistory = normalizedHistory.filter((record) => !selectedIds.has(buildRecordId(
|
||||
record.recordId || record.accountIdentifier || record.email || record.phoneNumber,
|
||||
String(record.recordId || '').startsWith('phone:') || String(record.accountIdentifierType || '').trim().toLowerCase() === 'phone'
|
||||
? 'phone'
|
||||
: 'email'
|
||||
)));
|
||||
|
||||
return {
|
||||
deletedCount: normalizedHistory.length - nextHistory.length,
|
||||
nextHistory: normalizeAccountRunHistory(nextHistory),
|
||||
};
|
||||
}
|
||||
|
||||
async function appendAccountRunHistoryRecord(status, stateOverride = null, reason = '') {
|
||||
const state = stateOverride || await getState();
|
||||
const record = buildAccountRunHistoryRecord(state, status, reason);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const history = await getPersistedAccountRunHistory();
|
||||
const nextHistory = upsertAccountRunHistoryRecord(history, record);
|
||||
await setPersistedAccountRunHistory(nextHistory);
|
||||
return record;
|
||||
}
|
||||
|
||||
function summarizeAccountRunHistory(records = []) {
|
||||
return normalizeAccountRunHistory(records).reduce((summary, record) => {
|
||||
summary.total += 1;
|
||||
if (record.finalStatus === 'success') {
|
||||
summary.success += 1;
|
||||
} else if (record.finalStatus === 'running') {
|
||||
summary.running += 1;
|
||||
} else if (record.finalStatus === 'failed') {
|
||||
summary.failed += 1;
|
||||
} else if (record.finalStatus === 'stopped') {
|
||||
summary.stopped += 1;
|
||||
}
|
||||
summary.retryTotal += normalizeRetryCount(record.retryCount);
|
||||
return summary;
|
||||
}, {
|
||||
total: 0,
|
||||
success: 0,
|
||||
running: 0,
|
||||
failed: 0,
|
||||
stopped: 0,
|
||||
retryTotal: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function buildAccountRunHistorySnapshotPayload(records = []) {
|
||||
const normalizedHistory = normalizeAccountRunHistory(records);
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
summary: summarizeAccountRunHistory(normalizedHistory),
|
||||
records: normalizedHistory,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldSyncAccountRunHistorySnapshot(state = {}) {
|
||||
if (Boolean(state.contributionMode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const helperBaseUrl = normalizeAccountRunHistoryHelperBaseUrl(state.accountRunHistoryHelperBaseUrl);
|
||||
return Boolean(helperBaseUrl);
|
||||
}
|
||||
|
||||
function shouldAppendAccountRunTextFile(state = {}) {
|
||||
return shouldSyncAccountRunHistorySnapshot(state);
|
||||
}
|
||||
|
||||
async function syncAccountRunHistorySnapshot(records, stateOverride = null) {
|
||||
const state = stateOverride || await getState();
|
||||
if (!shouldSyncAccountRunHistorySnapshot(state)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const helperBaseUrl = normalizeAccountRunHistoryHelperBaseUrl(state.accountRunHistoryHelperBaseUrl);
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(buildLocalHelperEndpoint(helperBaseUrl, '/sync-account-run-records'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify(buildAccountRunHistorySnapshotPayload(records)),
|
||||
});
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let payload = null;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (err) {
|
||||
throw new Error(`账号记录快照同步失败:本地 helper 返回了无法解析的响应(${getErrorMessage(err)})`);
|
||||
}
|
||||
|
||||
if (!response.ok || payload?.ok === false) {
|
||||
throw new Error(`账号记录快照同步失败:${payload?.error || `HTTP ${response.status}`}`);
|
||||
}
|
||||
|
||||
return payload?.filePath || '';
|
||||
}
|
||||
|
||||
async function appendAccountRunRecord(status, stateOverride = null, reason = '') {
|
||||
const state = stateOverride || await getState();
|
||||
const record = await appendAccountRunHistoryRecord(status, state, reason);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const history = await getPersistedAccountRunHistory();
|
||||
const filePath = await syncAccountRunHistorySnapshot(history, state);
|
||||
if (filePath) {
|
||||
await addLog(`账号记录快照已同步到本地:${filePath}`, 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
await addLog(getErrorMessage(err), 'warn');
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async function clearAccountRunHistory(stateOverride = null) {
|
||||
const state = stateOverride || await getState();
|
||||
const history = await getPersistedAccountRunHistory();
|
||||
await setPersistedAccountRunHistory([]);
|
||||
|
||||
try {
|
||||
const filePath = await syncAccountRunHistorySnapshot([], state);
|
||||
if (filePath) {
|
||||
await addLog(`账号记录快照已同步到本地:${filePath}`, 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
await addLog(getErrorMessage(err), 'warn');
|
||||
}
|
||||
|
||||
return {
|
||||
clearedCount: history.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function deleteAccountRunHistoryRecords(recordIds = [], stateOverride = null) {
|
||||
const state = stateOverride || await getState();
|
||||
const history = await getPersistedAccountRunHistory();
|
||||
const { deletedCount, nextHistory } = deleteAccountRunHistoryEntries(history, recordIds);
|
||||
|
||||
if (!deletedCount) {
|
||||
return {
|
||||
deletedCount: 0,
|
||||
remainingCount: history.length,
|
||||
};
|
||||
}
|
||||
|
||||
await setPersistedAccountRunHistory(nextHistory);
|
||||
|
||||
try {
|
||||
const filePath = await syncAccountRunHistorySnapshot(nextHistory, state);
|
||||
if (filePath) {
|
||||
await addLog(`账号记录快照已同步到本地:${filePath}`, 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
await addLog(getErrorMessage(err), 'warn');
|
||||
}
|
||||
|
||||
return {
|
||||
deletedCount,
|
||||
remainingCount: nextHistory.length,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
appendAccountRunRecord,
|
||||
appendAccountRunHistoryRecord,
|
||||
buildAccountRunHistoryRecord,
|
||||
buildAccountRunHistorySnapshotPayload,
|
||||
clearAccountRunHistory,
|
||||
deleteAccountRunHistoryRecords,
|
||||
getPersistedAccountRunHistory,
|
||||
normalizeAccountRunHistory,
|
||||
normalizeAccountRunHistoryRecord,
|
||||
normalizeFinalStatus,
|
||||
setPersistedAccountRunHistory,
|
||||
shouldAppendAccountRunTextFile,
|
||||
shouldSyncAccountRunHistorySnapshot,
|
||||
summarizeAccountRunHistory,
|
||||
syncAccountRunHistorySnapshot,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createAccountRunHistoryHelpers,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,336 @@
|
||||
(function cloudMailProviderModule(root, factory) {
|
||||
root.MultiPageBackgroundCloudMailProvider = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createCloudMailProviderModule() {
|
||||
function createCloudMailProvider(deps = {}) {
|
||||
const {
|
||||
addLog = async () => {},
|
||||
buildCloudMailHeaders,
|
||||
CLOUD_MAIL_DEFAULT_PAGE_SIZE = 20,
|
||||
CLOUD_MAIL_GENERATOR = 'cloudmail',
|
||||
CLOUD_MAIL_PROVIDER = 'cloudmail',
|
||||
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||
getCloudMailTokenFromResponse,
|
||||
getState = async () => ({}),
|
||||
joinCloudMailUrl,
|
||||
normalizeCloudMailAddress,
|
||||
normalizeCloudMailBaseUrl,
|
||||
normalizeCloudMailDomain,
|
||||
normalizeCloudMailDomains,
|
||||
normalizeCloudMailMailApiMessages,
|
||||
persistRegistrationEmailState = null,
|
||||
pickVerificationMessageWithTimeFallback,
|
||||
setEmailState = async () => {},
|
||||
setPersistentSettings = async () => {},
|
||||
sleepWithStop = async () => {},
|
||||
throwIfStopped = () => {},
|
||||
} = deps;
|
||||
|
||||
async function persistResolvedEmailState(state = null, email, options = {}) {
|
||||
if (typeof persistRegistrationEmailState === 'function') {
|
||||
await persistRegistrationEmailState(state, email, options);
|
||||
return;
|
||||
}
|
||||
await setEmailState(email, options);
|
||||
}
|
||||
|
||||
function getCloudMailConfig(state = {}) {
|
||||
return {
|
||||
baseUrl: normalizeCloudMailBaseUrl(state.cloudMailBaseUrl),
|
||||
adminEmail: String(state.cloudMailAdminEmail || '').trim(),
|
||||
adminPassword: String(state.cloudMailAdminPassword || ''),
|
||||
token: String(state.cloudMailToken || '').trim(),
|
||||
receiveMailbox: normalizeCloudMailReceiveMailbox(state.cloudMailReceiveMailbox),
|
||||
domain: normalizeCloudMailDomain(state.cloudMailDomain),
|
||||
domains: normalizeCloudMailDomains(state.cloudMailDomains),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCloudMailReceiveMailbox(value = '') {
|
||||
const normalized = normalizeCloudMailAddress(value);
|
||||
if (!normalized) return '';
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) ? normalized : '';
|
||||
}
|
||||
|
||||
function resolveCloudMailPollTargetEmail(state = {}, pollPayload = {}, config = getCloudMailConfig(state)) {
|
||||
const configuredReceiveMailbox = normalizeCloudMailReceiveMailbox(config.receiveMailbox);
|
||||
const mailProvider = String(state?.mailProvider || '').trim().toLowerCase();
|
||||
const emailGenerator = String(state?.emailGenerator || '').trim().toLowerCase();
|
||||
const shouldPreferConfiguredReceiveMailbox = mailProvider === CLOUD_MAIL_PROVIDER
|
||||
&& emailGenerator !== CLOUD_MAIL_GENERATOR;
|
||||
if (shouldPreferConfiguredReceiveMailbox && configuredReceiveMailbox) {
|
||||
return configuredReceiveMailbox;
|
||||
}
|
||||
|
||||
const requestedTarget = normalizeCloudMailReceiveMailbox(pollPayload.targetEmail);
|
||||
if (requestedTarget) {
|
||||
return requestedTarget;
|
||||
}
|
||||
|
||||
return normalizeCloudMailReceiveMailbox(state.email);
|
||||
}
|
||||
|
||||
function ensureCloudMailConfig(state, options = {}) {
|
||||
const { requireToken = false, requireCredentials = false, requireDomain = false } = options;
|
||||
const config = getCloudMailConfig(state);
|
||||
if (!config.baseUrl) {
|
||||
throw new Error('Cloud Mail 服务地址为空或格式无效。');
|
||||
}
|
||||
if (requireCredentials && (!config.adminEmail || !config.adminPassword)) {
|
||||
throw new Error('Cloud Mail 缺少管理员邮箱或密码。');
|
||||
}
|
||||
if (requireToken && !config.token) {
|
||||
throw new Error('Cloud Mail 尚未获取到身份令牌,请先生成 Token。');
|
||||
}
|
||||
if (requireDomain && !config.domain) {
|
||||
throw new Error('Cloud Mail 域名为空或格式无效。');
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async function requestCloudMailJson(config, path, options = {}) {
|
||||
if (!fetchImpl) {
|
||||
throw new Error('Cloud Mail 当前运行环境不支持 fetch。');
|
||||
}
|
||||
const {
|
||||
method = 'POST',
|
||||
payload,
|
||||
timeoutMs = 20000,
|
||||
requireToken = true,
|
||||
} = options;
|
||||
const url = joinCloudMailUrl(config.baseUrl, path);
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
|
||||
let response;
|
||||
try {
|
||||
response = await fetchImpl(url, {
|
||||
method,
|
||||
headers: buildCloudMailHeaders(config, {
|
||||
json: payload !== undefined,
|
||||
token: requireToken ? undefined : '',
|
||||
}),
|
||||
body: payload !== undefined ? JSON.stringify(payload) : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = err?.name === 'AbortError'
|
||||
? `Cloud Mail 请求超时(>${Math.round(timeoutMs / 1000)} 秒)`
|
||||
: `Cloud Mail 请求失败:${err.message}`;
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
const text = await response.text();
|
||||
let parsed;
|
||||
try {
|
||||
parsed = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
parsed = text;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const payloadError = typeof parsed === 'object' && parsed
|
||||
? (parsed.message || parsed.error || parsed.msg)
|
||||
: '';
|
||||
throw new Error(`Cloud Mail 请求失败:${payloadError || text || `HTTP ${response.status}`}`);
|
||||
}
|
||||
if (parsed && typeof parsed === 'object' && 'code' in parsed && Number(parsed.code) !== 200) {
|
||||
throw new Error(`Cloud Mail 业务错误:${parsed.message || parsed.msg || `code=${parsed.code}`}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function ensureCloudMailToken(state, options = {}) {
|
||||
const { forceRefresh = false } = options;
|
||||
const latestState = state || await getState();
|
||||
const config = ensureCloudMailConfig(latestState, { requireCredentials: true });
|
||||
if (!forceRefresh && config.token) {
|
||||
return { config, token: config.token };
|
||||
}
|
||||
const loginConfig = { ...config, token: '' };
|
||||
const result = await requestCloudMailJson(loginConfig, '/api/public/genToken', {
|
||||
method: 'POST',
|
||||
payload: { email: config.adminEmail, password: config.adminPassword },
|
||||
requireToken: false,
|
||||
});
|
||||
const token = getCloudMailTokenFromResponse(result);
|
||||
if (!token) {
|
||||
throw new Error('Cloud Mail 未返回可用 Token。');
|
||||
}
|
||||
await setPersistentSettings({ cloudMailToken: token });
|
||||
return { config: { ...config, token }, token };
|
||||
}
|
||||
|
||||
function generateCloudMailAliasLocalPart() {
|
||||
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const digits = '0123456789';
|
||||
const chars = [];
|
||||
for (let i = 0; i < 6; i++) chars.push(letters[Math.floor(Math.random() * letters.length)]);
|
||||
for (let i = 0; i < 4; i++) chars.push(digits[Math.floor(Math.random() * digits.length)]);
|
||||
for (let i = chars.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
async function fetchCloudMailAddress(state, options = {}) {
|
||||
throwIfStopped();
|
||||
const latestState = state || await getState();
|
||||
const { config } = await ensureCloudMailToken(latestState);
|
||||
const ensuredConfig = ensureCloudMailConfig({ ...latestState, cloudMailToken: config.token }, {
|
||||
requireToken: true,
|
||||
requireDomain: true,
|
||||
});
|
||||
const requestedLocal = String(options.localPart || options.name || '').trim().toLowerCase()
|
||||
|| generateCloudMailAliasLocalPart();
|
||||
const address = `${requestedLocal}@${ensuredConfig.domain}`.toLowerCase();
|
||||
const payload = { list: [{ email: address }] };
|
||||
try {
|
||||
await requestCloudMailJson(ensuredConfig, '/api/public/addUser', { method: 'POST', payload });
|
||||
} catch (err) {
|
||||
if (/token|unauthor|401/i.test(String(err?.message || ''))) {
|
||||
const refreshed = await ensureCloudMailToken(latestState, { forceRefresh: true });
|
||||
await requestCloudMailJson(refreshed.config, '/api/public/addUser', { method: 'POST', payload });
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
await persistResolvedEmailState(latestState, address, {
|
||||
source: 'generated:cloudmail',
|
||||
preserveAccountIdentity: Boolean(options?.preserveAccountIdentity),
|
||||
});
|
||||
await addLog(`Cloud Mail:已生成 ${address}`, 'ok');
|
||||
return address;
|
||||
}
|
||||
|
||||
function summarizeCloudMailMessagesForLog(messages) {
|
||||
return (messages || [])
|
||||
.slice()
|
||||
.sort((left, right) => {
|
||||
const leftTime = Date.parse(left.receivedDateTime || '') || 0;
|
||||
const rightTime = Date.parse(right.receivedDateTime || '') || 0;
|
||||
return rightTime - leftTime;
|
||||
})
|
||||
.slice(0, 3)
|
||||
.map((message) => {
|
||||
const receivedAt = message?.receivedDateTime || '未知时间';
|
||||
const sender = message?.from?.emailAddress?.address || '未知发件人';
|
||||
const subject = message?.subject || '(无主题)';
|
||||
const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
||||
const address = message?.address || '未知地址';
|
||||
return `[${address}] ${receivedAt} | ${sender} | ${subject} | ${preview}`;
|
||||
})
|
||||
.join(' || ');
|
||||
}
|
||||
|
||||
async function listCloudMailMessages(state, options = {}) {
|
||||
const latestState = state || await getState();
|
||||
const { config } = await ensureCloudMailToken(latestState);
|
||||
const address = normalizeCloudMailAddress(options.address);
|
||||
const pageSize = Number(options.limit) || CLOUD_MAIL_DEFAULT_PAGE_SIZE;
|
||||
const pageNum = Number(options.page) || 1;
|
||||
const request = async (currentConfig) => requestCloudMailJson(currentConfig, '/api/public/emailList', {
|
||||
method: 'POST',
|
||||
payload: {
|
||||
toEmail: address || undefined,
|
||||
type: 0,
|
||||
isDel: 0,
|
||||
timeSort: 'desc',
|
||||
num: pageNum,
|
||||
size: pageSize,
|
||||
},
|
||||
});
|
||||
let payload;
|
||||
try {
|
||||
payload = await request(config);
|
||||
} catch (err) {
|
||||
if (/token|unauthor|401/i.test(String(err?.message || ''))) {
|
||||
const refreshed = await ensureCloudMailToken(latestState, { forceRefresh: true });
|
||||
payload = await request(refreshed.config);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const messages = normalizeCloudMailMailApiMessages(payload).filter((message) => {
|
||||
if (!address) return true;
|
||||
return !message.address || normalizeCloudMailAddress(message.address) === address;
|
||||
});
|
||||
return { config, messages };
|
||||
}
|
||||
|
||||
async function pollCloudMailVerificationCode(step, state, pollPayload = {}) {
|
||||
const latestState = state || await getState();
|
||||
const config = ensureCloudMailConfig(latestState, { requireCredentials: true });
|
||||
const targetEmail = resolveCloudMailPollTargetEmail(latestState, pollPayload, config);
|
||||
const registrationEmail = normalizeCloudMailReceiveMailbox(latestState.email);
|
||||
if (!targetEmail) {
|
||||
throw new Error('Cloud Mail 轮询前缺少目标邮箱地址,请先填写注册邮箱或"邮件接收"邮箱。');
|
||||
}
|
||||
if (registrationEmail && registrationEmail !== targetEmail) {
|
||||
await addLog(`步骤 ${step}:正在轮询 Cloud Mail 收件邮箱(${targetEmail}),注册邮箱为 ${registrationEmail}...`, 'info');
|
||||
} else {
|
||||
await addLog(`步骤 ${step}:正在轮询 Cloud Mail 邮件(${targetEmail})...`, 'info');
|
||||
}
|
||||
const maxAttempts = Number(pollPayload.maxAttempts) || 5;
|
||||
const intervalMs = Number(pollPayload.intervalMs) || 3000;
|
||||
let lastError = null;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
throwIfStopped();
|
||||
try {
|
||||
const { messages } = await listCloudMailMessages(latestState, {
|
||||
address: targetEmail,
|
||||
limit: pollPayload.limit || CLOUD_MAIL_DEFAULT_PAGE_SIZE,
|
||||
page: pollPayload.page || 1,
|
||||
});
|
||||
const matchResult = pickVerificationMessageWithTimeFallback(messages, {
|
||||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||||
senderFilters: pollPayload.senderFilters || [],
|
||||
subjectFilters: pollPayload.subjectFilters || [],
|
||||
excludeCodes: pollPayload.excludeCodes || [],
|
||||
});
|
||||
const match = matchResult.match;
|
||||
if (match?.code) {
|
||||
if (matchResult.usedRelaxedFilters) {
|
||||
const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
|
||||
await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Cloud Mail 验证码。`, 'warn');
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
code: match.code,
|
||||
emailTimestamp: match.receivedAt || Date.now(),
|
||||
mailId: match.message?.id || '',
|
||||
};
|
||||
}
|
||||
lastError = new Error(`步骤 ${step}:暂未在 Cloud Mail 中找到匹配验证码(${attempt}/${maxAttempts})。`);
|
||||
await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
|
||||
const sample = summarizeCloudMailMessagesForLog(messages);
|
||||
if (sample) {
|
||||
await addLog(`步骤 ${step}:最近邮件样本:${sample}`, 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
await addLog(`步骤 ${step}:Cloud Mail 轮询失败:${err.message}`, 'warn');
|
||||
}
|
||||
if (attempt < maxAttempts) {
|
||||
await sleepWithStop(intervalMs);
|
||||
}
|
||||
}
|
||||
throw lastError || new Error(`步骤 ${step}:未在 Cloud Mail 中找到新的匹配验证码。`);
|
||||
}
|
||||
|
||||
return {
|
||||
ensureCloudMailConfig,
|
||||
ensureCloudMailToken,
|
||||
fetchCloudMailAddress,
|
||||
getCloudMailConfig,
|
||||
listCloudMailMessages,
|
||||
normalizeCloudMailReceiveMailbox,
|
||||
pollCloudMailVerificationCode,
|
||||
requestCloudMailJson,
|
||||
resolveCloudMailPollTargetEmail,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createCloudMailProvider,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,762 @@
|
||||
(function attachBackgroundContributionOAuth(root, factory) {
|
||||
root.MultiPageBackgroundContributionOAuth = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundContributionOAuthModule() {
|
||||
const API_BASE_URL = '';
|
||||
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
|
||||
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']);
|
||||
const CALLBACK_FINAL_STATUSES = new Set(['submitted']);
|
||||
const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']);
|
||||
|
||||
const RUNTIME_DEFAULTS = {
|
||||
contributionMode: false,
|
||||
contributionModeExpected: false,
|
||||
contributionSource: 'sub2api',
|
||||
contributionTargetGroupName: 'codex号池',
|
||||
contributionNickname: '',
|
||||
contributionQq: '',
|
||||
contributionSessionId: '',
|
||||
contributionAuthUrl: '',
|
||||
contributionAuthState: '',
|
||||
contributionCallbackUrl: '',
|
||||
contributionStatus: '',
|
||||
contributionStatusMessage: '',
|
||||
contributionLastPollAt: 0,
|
||||
contributionCallbackStatus: 'idle',
|
||||
contributionCallbackMessage: '',
|
||||
contributionAuthOpenedAt: 0,
|
||||
contributionAuthTabId: 0,
|
||||
};
|
||||
|
||||
const RUNTIME_KEYS = Object.keys(RUNTIME_DEFAULTS);
|
||||
|
||||
function createContributionOAuthManager(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
broadcastDataUpdate,
|
||||
chrome,
|
||||
closeLocalhostCallbackTabs,
|
||||
createAutomationTab = null,
|
||||
getState,
|
||||
setState,
|
||||
} = deps;
|
||||
|
||||
let listenersBound = false;
|
||||
const pendingCallbackSubmissions = new Map();
|
||||
const pendingCapturedCallbacks = new Map();
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value, fallback = 0) {
|
||||
const numeric = Math.floor(Number(value) || 0);
|
||||
return numeric > 0 ? numeric : fallback;
|
||||
}
|
||||
|
||||
function normalizeContributionStatus(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'started':
|
||||
return 'started';
|
||||
case 'waiting':
|
||||
case 'wait':
|
||||
return 'waiting';
|
||||
case 'processing':
|
||||
return 'processing';
|
||||
case 'auto_approved':
|
||||
case 'approved':
|
||||
return 'auto_approved';
|
||||
case 'auto_rejected':
|
||||
case 'rejected':
|
||||
return 'auto_rejected';
|
||||
case 'manual_review_required':
|
||||
case 'manual_review':
|
||||
return 'manual_review_required';
|
||||
case 'expired':
|
||||
case 'timeout':
|
||||
return 'expired';
|
||||
case 'error':
|
||||
case 'failed':
|
||||
return 'error';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeContributionCallbackStatus(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'idle':
|
||||
return 'idle';
|
||||
case 'waiting':
|
||||
case 'pending':
|
||||
return 'waiting';
|
||||
case 'captured':
|
||||
return 'captured';
|
||||
case 'submitting':
|
||||
case 'processing':
|
||||
return 'submitting';
|
||||
case 'submitted':
|
||||
case 'success':
|
||||
case 'done':
|
||||
return 'submitted';
|
||||
case 'failed':
|
||||
case 'error':
|
||||
return 'failed';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isContributionFinalStatus(status = '') {
|
||||
return FINAL_STATUSES.has(normalizeContributionStatus(status));
|
||||
}
|
||||
|
||||
function getStatusLabel(status = '') {
|
||||
switch (normalizeContributionStatus(status)) {
|
||||
case 'started':
|
||||
return '已生成登录地址';
|
||||
case 'waiting':
|
||||
return '等待提交回调';
|
||||
case 'processing':
|
||||
return '已提交回调,等待 CPA 确认';
|
||||
case 'auto_approved':
|
||||
return '贡献成功,CPA 已确认';
|
||||
case 'auto_rejected':
|
||||
return '贡献未通过确认';
|
||||
case 'manual_review_required':
|
||||
return '已提交,等待人工处理';
|
||||
case 'expired':
|
||||
return '贡献会话已超时';
|
||||
case 'error':
|
||||
return '贡献流程失败';
|
||||
default:
|
||||
return '等待开始贡献';
|
||||
}
|
||||
}
|
||||
|
||||
function getCallbackLabel(status = '') {
|
||||
switch (normalizeContributionCallbackStatus(status)) {
|
||||
case 'waiting':
|
||||
case 'idle':
|
||||
return '等待回调';
|
||||
case 'captured':
|
||||
return '已捕获回调地址';
|
||||
case 'submitting':
|
||||
return '正在提交回调';
|
||||
case 'submitted':
|
||||
return '已提交回调';
|
||||
case 'failed':
|
||||
return '回调提交失败';
|
||||
default:
|
||||
return '等待回调';
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapPayload(payload) {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||||
return { ...payload.data, ...payload };
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function getErrorMessage(payload, responseStatus = 500) {
|
||||
const details = [
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.error,
|
||||
payload?.reason,
|
||||
]
|
||||
.map((item) => normalizeString(item))
|
||||
.find(Boolean);
|
||||
|
||||
if (details) {
|
||||
return details;
|
||||
}
|
||||
|
||||
return `贡献服务请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
async function fetchContributionJson(endpoint, options = {}) {
|
||||
if (!API_BASE_URL) {
|
||||
throw new Error('贡献服务未配置,当前构建已禁用默认贡献接口。');
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 15000));
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
||||
},
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let rawPayload = {};
|
||||
try {
|
||||
rawPayload = await response.json();
|
||||
} catch {
|
||||
rawPayload = {};
|
||||
}
|
||||
|
||||
const payload = unwrapPayload(rawPayload);
|
||||
if (!response.ok || payload.ok === false) {
|
||||
const error = new Error(getErrorMessage(payload, response.status));
|
||||
error.payload = payload;
|
||||
error.responseStatus = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('贡献服务请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function pickContributionState(state = {}) {
|
||||
const picked = {};
|
||||
for (const key of RUNTIME_KEYS) {
|
||||
picked[key] = state[key] !== undefined ? state[key] : RUNTIME_DEFAULTS[key];
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
async function applyRuntimeUpdates(updates = {}) {
|
||||
if (!updates || typeof updates !== 'object' || Array.isArray(updates) || Object.keys(updates).length === 0) {
|
||||
return getState();
|
||||
}
|
||||
|
||||
await setState(updates);
|
||||
broadcastDataUpdate(updates);
|
||||
return getState();
|
||||
}
|
||||
|
||||
function extractAuthStateFromUrl(authUrl = '') {
|
||||
try {
|
||||
return new URL(authUrl).searchParams.get('state') || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function buildNickname(state = {}, preferredNickname = '') {
|
||||
const nickname = normalizeString(preferredNickname)
|
||||
|| normalizeString(state.contributionNickname);
|
||||
return nickname || '';
|
||||
}
|
||||
|
||||
function buildContributionQq(state = {}, preferredQq = '') {
|
||||
const qq = normalizeString(preferredQq) || normalizeString(state.contributionQq);
|
||||
return qq;
|
||||
}
|
||||
|
||||
function isPlusModeState(state = {}) {
|
||||
return Boolean(state?.plusModeEnabled);
|
||||
}
|
||||
|
||||
function normalizeContributionModeSource(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
return normalized === 'sub2api' ? 'sub2api' : 'cpa';
|
||||
}
|
||||
|
||||
function resolveContributionModeRoutingState(state = {}) {
|
||||
const currentStatus = normalizeString(state?.contributionStatus).toLowerCase();
|
||||
const currentSource = normalizeContributionModeSource(state?.contributionSource);
|
||||
const hasActiveSession = Boolean(
|
||||
normalizeString(state?.contributionSessionId)
|
||||
&& currentStatus
|
||||
&& !FINAL_STATUSES.has(currentStatus)
|
||||
);
|
||||
|
||||
if (hasActiveSession) {
|
||||
return {
|
||||
source: currentSource,
|
||||
targetGroupName: currentSource === 'sub2api'
|
||||
? (normalizeString(state?.contributionTargetGroupName) || 'codex号池')
|
||||
: '',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
source: 'sub2api',
|
||||
targetGroupName: isPlusModeState(state)
|
||||
? 'openai-plus'
|
||||
: (normalizeString(state?.contributionTargetGroupName) || 'codex号池'),
|
||||
};
|
||||
}
|
||||
|
||||
function buildStatusMessage(status, payload = {}) {
|
||||
const label = getStatusLabel(status);
|
||||
const details = [
|
||||
payload.status_message,
|
||||
payload.statusMessage,
|
||||
payload.message,
|
||||
payload.detail,
|
||||
payload.reason,
|
||||
]
|
||||
.map((item) => normalizeString(item))
|
||||
.find(Boolean);
|
||||
|
||||
if (!details || details === label) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return `${label}:${details}`;
|
||||
}
|
||||
|
||||
function buildCallbackMessage(status, payload = {}) {
|
||||
const label = getCallbackLabel(status);
|
||||
const details = [
|
||||
payload.callback_message,
|
||||
payload.callbackMessage,
|
||||
payload.message,
|
||||
payload.detail,
|
||||
payload.reason,
|
||||
]
|
||||
.map((item) => normalizeString(item))
|
||||
.find(Boolean);
|
||||
|
||||
if (!details || details === label) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return `${label}:${details}`;
|
||||
}
|
||||
|
||||
function deriveCallbackState(payload = {}, state = {}) {
|
||||
const existingStatus = normalizeContributionCallbackStatus(state.contributionCallbackStatus);
|
||||
const callbackUrl = normalizeString(
|
||||
payload.callback_url
|
||||
|| payload.callbackUrl
|
||||
|| state.contributionCallbackUrl
|
||||
);
|
||||
const explicitStatus = normalizeContributionCallbackStatus(
|
||||
payload.callback_status
|
||||
|| payload.callbackStatus
|
||||
);
|
||||
|
||||
if (explicitStatus) {
|
||||
return {
|
||||
status: explicitStatus,
|
||||
message: buildCallbackMessage(explicitStatus, payload),
|
||||
callbackUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (payload.callback_submitted === true || payload.callbackSubmitted === true) {
|
||||
return {
|
||||
status: 'submitted',
|
||||
message: buildCallbackMessage('submitted', payload),
|
||||
callbackUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (callbackUrl) {
|
||||
return {
|
||||
status: CALLBACK_FINAL_STATUSES.has(existingStatus) ? existingStatus : 'captured',
|
||||
message: buildCallbackMessage(CALLBACK_FINAL_STATUSES.has(existingStatus) ? existingStatus : 'captured', payload),
|
||||
callbackUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (CALLBACK_FINAL_STATUSES.has(existingStatus) || existingStatus === 'failed') {
|
||||
return {
|
||||
status: existingStatus,
|
||||
message: normalizeString(state.contributionCallbackMessage) || buildCallbackMessage(existingStatus),
|
||||
callbackUrl: normalizeString(state.contributionCallbackUrl),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'waiting',
|
||||
message: buildCallbackMessage('waiting', payload),
|
||||
callbackUrl: '',
|
||||
};
|
||||
}
|
||||
|
||||
function isContributionCallbackUrl(rawUrl, state = {}) {
|
||||
const urlText = normalizeString(rawUrl);
|
||||
if (!urlText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(urlText);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const errorText = normalizeString(parsed.searchParams.get('error'))
|
||||
|| normalizeString(parsed.searchParams.get('error_description'));
|
||||
const authState = normalizeString(parsed.searchParams.get('state'));
|
||||
if ((!code && !errorText) || !authState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hostLooksLocal = ['localhost', '127.0.0.1'].includes(parsed.hostname);
|
||||
const pathLooksLikeCallback = /callback/i.test(parsed.pathname || '');
|
||||
if (!hostLooksLocal && !pathLooksLikeCallback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedState = normalizeString(state.contributionAuthState);
|
||||
return !expectedState || expectedState === authState;
|
||||
}
|
||||
|
||||
async function openContributionAuthUrl(authUrl, options = {}) {
|
||||
const normalizedUrl = normalizeString(authUrl);
|
||||
if (!normalizedUrl) {
|
||||
throw new Error('贡献服务未返回有效的登录地址。');
|
||||
}
|
||||
|
||||
const currentState = options.stateOverride || await getState();
|
||||
const preferredTabId = normalizePositiveInteger(options.tabId || currentState.contributionAuthTabId, 0);
|
||||
let tab = null;
|
||||
|
||||
if (preferredTabId) {
|
||||
tab = await chrome.tabs.update(preferredTabId, {
|
||||
url: normalizedUrl,
|
||||
active: true,
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
if (!tab) {
|
||||
tab = typeof createAutomationTab === 'function'
|
||||
? await createAutomationTab({ url: normalizedUrl, active: true })
|
||||
: await chrome.tabs.create({ url: normalizedUrl, active: true });
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates({
|
||||
contributionAuthUrl: normalizedUrl,
|
||||
contributionAuthOpenedAt: Date.now(),
|
||||
contributionAuthTabId: normalizePositiveInteger(tab?.id, 0),
|
||||
});
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
async function fetchContributionResult(sessionId) {
|
||||
try {
|
||||
return await fetchContributionJson(`/result?session_id=${encodeURIComponent(sessionId)}`);
|
||||
} catch (error) {
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`贡献模式:获取最终结果失败:${error.message}`, 'warn');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitContributionCallback(callbackUrl, options = {}) {
|
||||
const currentState = options.stateOverride || await getState();
|
||||
const sessionId = normalizeString(currentState.contributionSessionId);
|
||||
const normalizedUrl = normalizeString(callbackUrl);
|
||||
|
||||
if (!sessionId || !normalizedUrl) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
|
||||
if (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting') {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const dedupeKey = `${sessionId}::${normalizedUrl}`;
|
||||
if (pendingCallbackSubmissions.has(dedupeKey)) {
|
||||
return pendingCallbackSubmissions.get(dedupeKey);
|
||||
}
|
||||
|
||||
const task = (async () => {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'submitting',
|
||||
contributionCallbackMessage: buildCallbackMessage('submitting'),
|
||||
});
|
||||
|
||||
try {
|
||||
const payload = await fetchContributionJson('/submit-callback', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
callback_url: normalizedUrl,
|
||||
},
|
||||
});
|
||||
|
||||
const nextStatus = 'submitted';
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: nextStatus,
|
||||
contributionCallbackMessage: buildCallbackMessage(nextStatus, payload),
|
||||
});
|
||||
|
||||
if (typeof closeLocalhostCallbackTabs === 'function') {
|
||||
await closeLocalhostCallbackTabs(normalizedUrl).catch(() => {});
|
||||
}
|
||||
|
||||
return await pollContributionStatus({ reason: options.reason || 'submit_callback' });
|
||||
} catch (error) {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'failed',
|
||||
contributionCallbackMessage: `回调提交失败:${error.message}`,
|
||||
});
|
||||
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`贡献模式:回调提交失败:${error.message}`, 'warn');
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
pendingCallbackSubmissions.delete(dedupeKey);
|
||||
}
|
||||
})();
|
||||
|
||||
pendingCallbackSubmissions.set(dedupeKey, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
async function handleCapturedCallback(rawUrl, metadata = {}) {
|
||||
const currentState = await getState();
|
||||
if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) {
|
||||
return currentState;
|
||||
}
|
||||
if (!isContributionCallbackUrl(rawUrl, currentState)) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const normalizedUrl = normalizeString(rawUrl);
|
||||
const callbackDedupeKey = `${normalizeString(currentState.contributionSessionId)}::${normalizedUrl}`;
|
||||
if (pendingCapturedCallbacks.has(callbackDedupeKey)) {
|
||||
return pendingCapturedCallbacks.get(callbackDedupeKey);
|
||||
}
|
||||
if (pendingCallbackSubmissions.has(callbackDedupeKey)) {
|
||||
return pendingCallbackSubmissions.get(callbackDedupeKey);
|
||||
}
|
||||
const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
|
||||
if (
|
||||
normalizedUrl
|
||||
&& normalizeString(currentState.contributionCallbackUrl) === normalizedUrl
|
||||
&& (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting')
|
||||
) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const task = (async () => {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'captured',
|
||||
contributionCallbackMessage: buildCallbackMessage('captured'),
|
||||
});
|
||||
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`贡献模式:已捕获回调地址(${metadata.source || 'unknown'})。`, 'info');
|
||||
}
|
||||
|
||||
try {
|
||||
return await submitContributionCallback(normalizedUrl, {
|
||||
reason: metadata.source || 'navigation',
|
||||
stateOverride: await getState(),
|
||||
});
|
||||
} catch {
|
||||
return getState();
|
||||
} finally {
|
||||
pendingCapturedCallbacks.delete(callbackDedupeKey);
|
||||
}
|
||||
})();
|
||||
|
||||
pendingCapturedCallbacks.set(callbackDedupeKey, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
async function pollContributionStatus(options = {}) {
|
||||
const currentState = options.stateOverride || await getState();
|
||||
const sessionId = normalizeString(currentState.contributionSessionId);
|
||||
if (!sessionId) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const payload = await fetchContributionJson(`/status?session_id=${encodeURIComponent(sessionId)}`);
|
||||
const nextStatus = normalizeContributionStatus(payload.status || payload.state || payload.phase) || currentState.contributionStatus || 'waiting';
|
||||
let finalPayload = null;
|
||||
|
||||
if (isContributionFinalStatus(nextStatus)) {
|
||||
finalPayload = await fetchContributionResult(sessionId);
|
||||
}
|
||||
|
||||
const mergedPayload = finalPayload ? { ...payload, ...finalPayload } : payload;
|
||||
const normalizedStatus = normalizeContributionStatus(mergedPayload.status || mergedPayload.state || mergedPayload.phase) || nextStatus;
|
||||
const callbackState = deriveCallbackState(mergedPayload, currentState);
|
||||
const updates = {
|
||||
contributionLastPollAt: Date.now(),
|
||||
contributionStatus: normalizedStatus,
|
||||
contributionStatusMessage: buildStatusMessage(normalizedStatus, mergedPayload),
|
||||
contributionCallbackUrl: callbackState.callbackUrl,
|
||||
contributionCallbackStatus: callbackState.status,
|
||||
contributionCallbackMessage: callbackState.message,
|
||||
};
|
||||
|
||||
const authUrl = normalizeString(mergedPayload.auth_url || mergedPayload.authUrl);
|
||||
if (authUrl) {
|
||||
updates.contributionAuthUrl = authUrl;
|
||||
}
|
||||
|
||||
const authState = normalizeString(mergedPayload.state || mergedPayload.auth_state || mergedPayload.authState)
|
||||
|| (authUrl ? extractAuthStateFromUrl(authUrl) : '');
|
||||
if (authState) {
|
||||
updates.contributionAuthState = authState;
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates(updates);
|
||||
const nextState = await getState();
|
||||
|
||||
if (
|
||||
normalizeString(nextState.contributionCallbackUrl)
|
||||
&& CALLBACK_WAITING_STATUSES.has(normalizeContributionCallbackStatus(nextState.contributionCallbackStatus))
|
||||
) {
|
||||
try {
|
||||
return await submitContributionCallback(nextState.contributionCallbackUrl, {
|
||||
reason: options.reason || 'status_poll',
|
||||
stateOverride: nextState,
|
||||
});
|
||||
} catch {
|
||||
return getState();
|
||||
}
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
async function startContributionFlow(options = {}) {
|
||||
const currentState = options.stateOverride || await getState();
|
||||
const shouldOpenAuthTab = options.openAuthTab !== false;
|
||||
if (!currentState.contributionMode) {
|
||||
throw new Error('请先进入贡献模式。');
|
||||
}
|
||||
|
||||
const currentSessionId = normalizeString(currentState.contributionSessionId);
|
||||
const currentStatus = normalizeContributionStatus(currentState.contributionStatus);
|
||||
if (currentSessionId && ACTIVE_STATUSES.has(currentStatus)) {
|
||||
if (normalizeString(currentState.contributionAuthUrl)) {
|
||||
if (shouldOpenAuthTab) {
|
||||
await openContributionAuthUrl(currentState.contributionAuthUrl, {
|
||||
stateOverride: currentState,
|
||||
}).catch(() => null);
|
||||
}
|
||||
}
|
||||
return pollContributionStatus({ reason: 'resume_existing' });
|
||||
}
|
||||
|
||||
const routingState = resolveContributionModeRoutingState(currentState);
|
||||
const payload = await fetchContributionJson('/start', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
nickname: buildNickname(currentState, options.nickname),
|
||||
qq: buildContributionQq(currentState, options.qq),
|
||||
email: normalizeString(currentState.email),
|
||||
source: routingState.source,
|
||||
target_group_name: routingState.targetGroupName,
|
||||
channel: 'codex-extension',
|
||||
},
|
||||
});
|
||||
|
||||
const sessionId = normalizeString(payload.session_id || payload.sessionId);
|
||||
const authUrl = normalizeString(payload.auth_url || payload.authUrl);
|
||||
const authState = normalizeString(payload.state || payload.auth_state || payload.authState) || extractAuthStateFromUrl(authUrl);
|
||||
if (!sessionId || !authUrl) {
|
||||
throw new Error('贡献服务未返回有效的 session_id 或 auth_url。');
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates({
|
||||
contributionSessionId: sessionId,
|
||||
contributionAuthUrl: authUrl,
|
||||
contributionAuthState: authState,
|
||||
contributionCallbackUrl: '',
|
||||
contributionStatus: normalizeContributionStatus(payload.status) || 'started',
|
||||
contributionStatusMessage: buildStatusMessage(normalizeContributionStatus(payload.status) || 'started', payload),
|
||||
contributionLastPollAt: 0,
|
||||
contributionCallbackStatus: 'waiting',
|
||||
contributionCallbackMessage: buildCallbackMessage('waiting'),
|
||||
contributionAuthOpenedAt: 0,
|
||||
contributionAuthTabId: 0,
|
||||
});
|
||||
|
||||
if (shouldOpenAuthTab) {
|
||||
await openContributionAuthUrl(authUrl);
|
||||
}
|
||||
return pollContributionStatus({ reason: 'after_start' });
|
||||
}
|
||||
|
||||
function onNavigationEvent(details = {}, source) {
|
||||
if (details?.frameId !== undefined && Number(details.frameId) !== 0) {
|
||||
return;
|
||||
}
|
||||
handleCapturedCallback(details?.url || '', {
|
||||
source,
|
||||
tabId: normalizePositiveInteger(details?.tabId, 0),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function onTabUpdated(tabId, changeInfo, tab) {
|
||||
const candidateUrl = normalizeString(changeInfo?.url || tab?.url);
|
||||
if (!candidateUrl) {
|
||||
return;
|
||||
}
|
||||
handleCapturedCallback(candidateUrl, {
|
||||
source: 'tabs.onUpdated',
|
||||
tabId: normalizePositiveInteger(tabId, 0),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function ensureCallbackListeners() {
|
||||
if (listenersBound) {
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.webNavigation.onCommitted.addListener((details) => {
|
||||
onNavigationEvent(details, 'webNavigation.onCommitted');
|
||||
});
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
|
||||
onNavigationEvent(details, 'webNavigation.onHistoryStateUpdated');
|
||||
});
|
||||
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
||||
listenersBound = true;
|
||||
}
|
||||
|
||||
return {
|
||||
ensureCallbackListeners,
|
||||
handleCapturedCallback,
|
||||
isContributionCallbackUrl,
|
||||
isContributionFinalStatus,
|
||||
pollContributionStatus,
|
||||
startContributionFlow,
|
||||
submitContributionCallback,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ACTIVE_STATUSES,
|
||||
FINAL_STATUSES,
|
||||
RUNTIME_DEFAULTS,
|
||||
RUNTIME_KEYS,
|
||||
createContributionOAuthManager,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,376 @@
|
||||
(function attachGeneratedEmailHelpers(root, factory) {
|
||||
root.MultiPageGeneratedEmailHelpers = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createGeneratedEmailHelpersModule() {
|
||||
function createGeneratedEmailHelpers(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
buildGeneratedAliasEmail,
|
||||
buildCloudflareTempEmailHeaders,
|
||||
CLOUDFLARE_TEMP_EMAIL_GENERATOR,
|
||||
CUSTOM_EMAIL_POOL_GENERATOR,
|
||||
DUCK_AUTOFILL_URL,
|
||||
fetch,
|
||||
fetchIcloudHideMyEmail,
|
||||
getCloudflareTempEmailAddressFromResponse,
|
||||
getCloudflareTempEmailConfig,
|
||||
getCustomEmailPoolEmail,
|
||||
getRegistrationEmailBaseline,
|
||||
getState,
|
||||
ensureMail2925AccountForFlow,
|
||||
joinCloudflareTempEmailUrl,
|
||||
normalizeCloudflareDomain,
|
||||
normalizeCloudflareTempEmailAddress,
|
||||
normalizeEmailGenerator,
|
||||
isGeneratedAliasProvider,
|
||||
persistRegistrationEmailState = null,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScript,
|
||||
setEmailState,
|
||||
throwIfStopped,
|
||||
} = deps;
|
||||
|
||||
async function persistResolvedEmailState(state = null, email, options = {}) {
|
||||
if (typeof persistRegistrationEmailState === 'function') {
|
||||
await persistRegistrationEmailState(state, email, options);
|
||||
return;
|
||||
}
|
||||
await setEmailState(email, options);
|
||||
}
|
||||
|
||||
function generateCloudflareAliasLocalPart() {
|
||||
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const digits = '0123456789';
|
||||
const chars = [];
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
chars.push(letters[Math.floor(Math.random() * letters.length)]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
chars.push(digits[Math.floor(Math.random() * digits.length)]);
|
||||
}
|
||||
|
||||
for (let i = chars.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
async function fetchCloudflareEmail(state, options = {}) {
|
||||
throwIfStopped();
|
||||
const latestState = state || await getState();
|
||||
const domain = normalizeCloudflareDomain(latestState.cloudflareDomain);
|
||||
if (!domain) {
|
||||
throw new Error('Cloudflare 域名为空或格式无效。');
|
||||
}
|
||||
|
||||
const localPart = String(options.localPart || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
|
||||
const aliasEmail = `${localPart}@${domain}`;
|
||||
|
||||
await persistResolvedEmailState(latestState, aliasEmail, {
|
||||
source: 'generated:cloudflare',
|
||||
preserveAccountIdentity: Boolean(options?.preserveAccountIdentity),
|
||||
});
|
||||
await addLog(`Cloudflare 邮箱:已生成 ${aliasEmail}`, 'ok');
|
||||
return aliasEmail;
|
||||
}
|
||||
|
||||
function ensureCloudflareTempEmailConfig(state, options = {}) {
|
||||
const {
|
||||
requireAdminAuth = false,
|
||||
requireDomain = false,
|
||||
} = options;
|
||||
const config = getCloudflareTempEmailConfig(state);
|
||||
if (!config.baseUrl) {
|
||||
throw new Error('Cloudflare Temp Email 服务地址为空或格式无效。');
|
||||
}
|
||||
if (requireAdminAuth && !config.adminAuth) {
|
||||
throw new Error('Cloudflare Temp Email 缺少 Admin Auth。');
|
||||
}
|
||||
if (requireDomain && !config.domain) {
|
||||
throw new Error('Cloudflare Temp Email 域名为空或格式无效。');
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async function requestCloudflareTempEmailJson(config, path, options = {}) {
|
||||
const {
|
||||
method = 'GET',
|
||||
payload,
|
||||
searchParams,
|
||||
timeoutMs = 20000,
|
||||
} = options;
|
||||
|
||||
const url = new URL(joinCloudflareTempEmailUrl(config.baseUrl, path));
|
||||
if (searchParams && typeof searchParams === 'object') {
|
||||
for (const [key, value] of Object.entries(searchParams)) {
|
||||
if (value === undefined || value === null || value === '') continue;
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url.toString(), {
|
||||
method,
|
||||
headers: buildCloudflareTempEmailHeaders(config, {
|
||||
json: payload !== undefined,
|
||||
}),
|
||||
body: payload !== undefined ? JSON.stringify(payload) : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = err?.name === 'AbortError'
|
||||
? `Cloudflare Temp Email 请求超时(>${Math.round(timeoutMs / 1000)} 秒)`
|
||||
: `Cloudflare Temp Email 请求失败:${err.message}`;
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
let parsed;
|
||||
try {
|
||||
parsed = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
parsed = text;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const payloadError = typeof parsed === 'object' && parsed
|
||||
? (parsed.message || parsed.error || parsed.msg)
|
||||
: '';
|
||||
throw new Error(`Cloudflare Temp Email 请求失败:${payloadError || text || `HTTP ${response.status}`}`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function fetchCloudflareTempEmailAddress(state, options = {}) {
|
||||
throwIfStopped();
|
||||
const latestState = state || await getState();
|
||||
const config = ensureCloudflareTempEmailConfig(latestState, {
|
||||
requireAdminAuth: true,
|
||||
requireDomain: true,
|
||||
});
|
||||
const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
|
||||
const payload = {
|
||||
enablePrefix: true,
|
||||
enableRandomSubdomain: Boolean(config.useRandomSubdomain),
|
||||
name: requestedName,
|
||||
domain: config.domain,
|
||||
};
|
||||
const result = await requestCloudflareTempEmailJson(config, '/admin/new_address', {
|
||||
method: 'POST',
|
||||
payload,
|
||||
});
|
||||
const address = normalizeCloudflareTempEmailAddress(getCloudflareTempEmailAddressFromResponse(result));
|
||||
if (!address) {
|
||||
throw new Error('Cloudflare Temp Email 未返回可用邮箱地址。');
|
||||
}
|
||||
|
||||
await persistResolvedEmailState(latestState, address, {
|
||||
source: 'generated:cloudflare-temp-email',
|
||||
preserveAccountIdentity: Boolean(options?.preserveAccountIdentity),
|
||||
});
|
||||
await addLog(`Cloudflare Temp Email:已生成 ${address}`, 'ok');
|
||||
return address;
|
||||
}
|
||||
|
||||
function normalizeEmailForComparison(value) {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function fetchDuckEmail(options = {}) {
|
||||
throwIfStopped();
|
||||
const {
|
||||
generateNew = true,
|
||||
baselineEmail = '',
|
||||
state = null,
|
||||
} = options;
|
||||
|
||||
await addLog(`Duck 邮箱:正在打开自动填充设置(${generateNew ? '生成新地址' : '复用当前地址'})...`);
|
||||
await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
|
||||
|
||||
const result = await sendToContentScript('duck-mail', {
|
||||
type: 'FETCH_DUCK_EMAIL',
|
||||
source: 'background',
|
||||
payload: {
|
||||
generateNew,
|
||||
baselineEmail: normalizeEmailForComparison(baselineEmail),
|
||||
},
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
if (!result?.email) {
|
||||
throw new Error('未返回 Duck 邮箱地址。');
|
||||
}
|
||||
|
||||
await persistResolvedEmailState(state, result.email, {
|
||||
source: 'generated:duck',
|
||||
preserveAccountIdentity: Boolean(options?.preserveAccountIdentity),
|
||||
});
|
||||
await addLog(`Duck 邮箱:${result.generated ? '已生成' : '已读取'} ${result.email}`, 'ok');
|
||||
return result.email;
|
||||
}
|
||||
|
||||
async function fetchCustomEmailPoolEmail(state, options = {}) {
|
||||
throwIfStopped();
|
||||
const latestState = state || await getState();
|
||||
const requestedIndex = Math.max(0, Math.floor(Number(options.poolIndex) || 0));
|
||||
const email = String(getCustomEmailPoolEmail?.(latestState, requestedIndex + 1) || '').trim().toLowerCase();
|
||||
if (!email) {
|
||||
throw new Error(
|
||||
requestedIndex > 0
|
||||
? `自定义邮箱池第 ${requestedIndex + 1} 个邮箱不存在,请检查邮箱池配置。`
|
||||
: '自定义邮箱池为空,请先至少填写 1 个邮箱。'
|
||||
);
|
||||
}
|
||||
|
||||
await persistResolvedEmailState(latestState, email, {
|
||||
source: 'generated:custom-pool',
|
||||
preserveAccountIdentity: Boolean(options?.preserveAccountIdentity),
|
||||
});
|
||||
await addLog(`自定义邮箱池:已取用 ${email}`, 'ok');
|
||||
return email;
|
||||
}
|
||||
|
||||
async function fetchManagedAliasEmail(state, options = {}) {
|
||||
throwIfStopped();
|
||||
const provider = String(options.mailProvider || state?.mailProvider || '').trim().toLowerCase();
|
||||
let mergedState = {
|
||||
...(state || {}),
|
||||
mailProvider: provider,
|
||||
};
|
||||
if (options.mail2925Mode !== undefined) {
|
||||
mergedState.mail2925Mode = String(options.mail2925Mode || '').trim();
|
||||
}
|
||||
if (options.gmailBaseEmail !== undefined) {
|
||||
mergedState.gmailBaseEmail = String(options.gmailBaseEmail || '').trim();
|
||||
}
|
||||
if (options.mail2925BaseEmail !== undefined) {
|
||||
mergedState.mail2925BaseEmail = String(options.mail2925BaseEmail || '').trim();
|
||||
}
|
||||
if (
|
||||
provider === '2925'
|
||||
&& Boolean(mergedState.mail2925UseAccountPool)
|
||||
&& typeof ensureMail2925AccountForFlow === 'function'
|
||||
) {
|
||||
const account = await ensureMail2925AccountForFlow({
|
||||
allowAllocate: true,
|
||||
preferredAccountId: mergedState.currentMail2925AccountId || null,
|
||||
});
|
||||
const latestState = await getState();
|
||||
mergedState = {
|
||||
...latestState,
|
||||
...mergedState,
|
||||
currentMail2925AccountId: account.id,
|
||||
};
|
||||
}
|
||||
|
||||
const email = buildGeneratedAliasEmail(mergedState);
|
||||
await persistResolvedEmailState(mergedState, email, {
|
||||
source: `generated:${provider || 'alias'}`,
|
||||
preserveAccountIdentity: Boolean(options?.preserveAccountIdentity),
|
||||
});
|
||||
await addLog(`${provider === 'gmail' ? 'Gmail +tag' : '2925'}:已生成 ${email}`, 'ok');
|
||||
return email;
|
||||
}
|
||||
|
||||
async function fetchGeneratedEmail(state, options = {}) {
|
||||
const currentState = state || await getState();
|
||||
const provider = String(options.mailProvider || currentState.mailProvider || '').trim().toLowerCase();
|
||||
const mail2925Mode = options.mail2925Mode !== undefined
|
||||
? options.mail2925Mode
|
||||
: currentState.mail2925Mode;
|
||||
const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator);
|
||||
const mergedState = {
|
||||
...currentState,
|
||||
mailProvider: provider || currentState.mailProvider,
|
||||
mail2925Mode,
|
||||
emailGenerator: generator,
|
||||
};
|
||||
if (options.gmailBaseEmail !== undefined) {
|
||||
mergedState.gmailBaseEmail = String(options.gmailBaseEmail || '').trim();
|
||||
}
|
||||
if (options.mail2925BaseEmail !== undefined) {
|
||||
mergedState.mail2925BaseEmail = String(options.mail2925BaseEmail || '').trim();
|
||||
}
|
||||
if (options.customEmailPool !== undefined) {
|
||||
mergedState.customEmailPool = options.customEmailPool;
|
||||
}
|
||||
if (generator === 'custom') {
|
||||
throw new Error('当前邮箱生成方式为自定义邮箱,请直接填写注册邮箱。');
|
||||
}
|
||||
if (generator === CUSTOM_EMAIL_POOL_GENERATOR) {
|
||||
return fetchCustomEmailPoolEmail(mergedState, options);
|
||||
}
|
||||
const shouldUseManagedAlias = typeof isGeneratedAliasProvider === 'function'
|
||||
? isGeneratedAliasProvider(mergedState, mail2925Mode)
|
||||
: false;
|
||||
if (shouldUseManagedAlias) {
|
||||
return fetchManagedAliasEmail(mergedState, options);
|
||||
}
|
||||
if (generator === 'icloud') {
|
||||
const stateFetchMode = String(mergedState.icloudFetchMode || '').trim().toLowerCase();
|
||||
const icloudOptions = {
|
||||
generateNew: Boolean(options.generateNew) || stateFetchMode === 'always_new',
|
||||
preserveAccountIdentity: Boolean(options?.preserveAccountIdentity),
|
||||
source: 'generated:icloud',
|
||||
state: mergedState,
|
||||
};
|
||||
if (mergedState.icloudHostPreference !== undefined) {
|
||||
icloudOptions.hostPreference = mergedState.icloudHostPreference;
|
||||
}
|
||||
if (mergedState.preferredIcloudHost !== undefined) {
|
||||
icloudOptions.preferredHost = mergedState.preferredIcloudHost;
|
||||
}
|
||||
return fetchIcloudHideMyEmail(icloudOptions);
|
||||
}
|
||||
if (generator === 'cloudflare') {
|
||||
return fetchCloudflareEmail(mergedState, options);
|
||||
}
|
||||
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) {
|
||||
return fetchCloudflareTempEmailAddress(mergedState, options);
|
||||
}
|
||||
const resolvedDuckBaselineEmail = typeof getRegistrationEmailBaseline === 'function'
|
||||
? getRegistrationEmailBaseline(mergedState, {
|
||||
preferredEmail: options.currentEmail,
|
||||
fallbackEmail: options.baselineEmail,
|
||||
})
|
||||
: String(
|
||||
options.currentEmail
|
||||
|| options.baselineEmail
|
||||
|| mergedState.email
|
||||
|| ''
|
||||
).trim();
|
||||
return fetchDuckEmail({
|
||||
...options,
|
||||
state: mergedState,
|
||||
baselineEmail: resolvedDuckBaselineEmail,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ensureCloudflareTempEmailConfig,
|
||||
fetchCloudflareEmail,
|
||||
fetchCustomEmailPoolEmail,
|
||||
fetchCloudflareTempEmailAddress,
|
||||
fetchDuckEmail,
|
||||
fetchGeneratedEmail,
|
||||
generateCloudflareAliasLocalPart,
|
||||
requestCloudflareTempEmailJson,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createGeneratedEmailHelpers,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,110 @@
|
||||
// background/ip-proxy-provider-711proxy.js — 711Proxy 参数与账号规则
|
||||
(function register711ProxyProvider(root) {
|
||||
function normalizeCountryCode(value = '') {
|
||||
const raw = String(value || '').trim().toUpperCase().replace(/[^A-Z]/g, '');
|
||||
return /^[A-Z]{2}$/.test(raw) ? raw : '';
|
||||
}
|
||||
|
||||
function normalize711SessionId(value = '') {
|
||||
return String(value || '').trim().replace(/[^A-Za-z0-9_-]/g, '').slice(0, 64);
|
||||
}
|
||||
|
||||
function normalize711SessionMinutes(value = '') {
|
||||
const raw = String(value ?? '').trim();
|
||||
if (!raw) return '';
|
||||
const numeric = Number.parseInt(raw, 10);
|
||||
if (!Number.isInteger(numeric)) return '';
|
||||
return String(Math.max(1, Math.min(180, numeric)));
|
||||
}
|
||||
|
||||
function apply711SessionToUsername(username = '', options = {}) {
|
||||
const text = String(username || '').trim();
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const sessionId = normalize711SessionId(options?.sessionId || '');
|
||||
const sessTime = normalize711SessionMinutes(options?.sessTime || '');
|
||||
let next = text;
|
||||
|
||||
if (sessionId) {
|
||||
if (/(?:^|[-_])session[-_:][A-Za-z0-9_-]+?(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/i.test(next)) {
|
||||
next = next.replace(
|
||||
/((?:^|[-_])session[-_:])([A-Za-z0-9_-]+?)(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/i,
|
||||
`$1${sessionId}`
|
||||
);
|
||||
} else {
|
||||
next = `${next}-session-${sessionId}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (sessTime) {
|
||||
if (/(?:^|[-_])sessTime[-_:]?\d+\b/i.test(next)) {
|
||||
next = next.replace(/((?:^|[-_])sessTime[-_:]?)(\d+)\b/i, `$1${sessTime}`);
|
||||
} else {
|
||||
next = `${next}-sessTime-${sessTime}`;
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function apply711RegionToUsername(username = '', regionCode = '') {
|
||||
const text = String(username || '').trim();
|
||||
const normalizedRegion = normalizeCountryCode(regionCode);
|
||||
if (!text || !normalizedRegion) {
|
||||
return text;
|
||||
}
|
||||
if (/(?:^|[-_])region[-_:]?[A-Za-z]{2}\b/i.test(text)) {
|
||||
return text.replace(/((?:^|[-_])region[-_:]?)([A-Za-z]{2})\b/i, `$1${normalizedRegion}`);
|
||||
}
|
||||
return `${text}-region-${normalizedRegion}`;
|
||||
}
|
||||
|
||||
function transform711ProxyAccountEntry(entry = {}, context = {}) {
|
||||
const state = context?.state || {};
|
||||
const hasAccountList = Boolean(context?.hasAccountList);
|
||||
const nextEntry = { ...entry };
|
||||
const username = String(nextEntry.username || '').trim();
|
||||
if (!username) {
|
||||
return nextEntry;
|
||||
}
|
||||
|
||||
const configuredRegion = normalizeCountryCode(state?.ipProxyRegion || '');
|
||||
if (!hasAccountList && configuredRegion) {
|
||||
nextEntry.username = apply711RegionToUsername(username, configuredRegion);
|
||||
if (!String(nextEntry.region || '').trim()) {
|
||||
nextEntry.region = configuredRegion;
|
||||
}
|
||||
}
|
||||
|
||||
// 账号列表模式按每行原样生效,不叠加固定账号区的 session/sessTime。
|
||||
if (hasAccountList) {
|
||||
return nextEntry;
|
||||
}
|
||||
|
||||
const sessionId = normalize711SessionId(state?.ipProxyAccountSessionPrefix || '');
|
||||
const sessTime = normalize711SessionMinutes(state?.ipProxyAccountLifeMinutes || '');
|
||||
if (!sessionId && !sessTime) {
|
||||
return nextEntry;
|
||||
}
|
||||
|
||||
nextEntry.username = apply711SessionToUsername(nextEntry.username, {
|
||||
sessionId,
|
||||
sessTime,
|
||||
});
|
||||
return nextEntry;
|
||||
}
|
||||
|
||||
root.transformIpProxyAccountEntryByProvider = function transformIpProxyAccountEntryByProvider(
|
||||
provider = '',
|
||||
entry = {},
|
||||
context = {}
|
||||
) {
|
||||
const normalizedProvider = String(provider || '').trim().toLowerCase();
|
||||
if (normalizedProvider === '711proxy') {
|
||||
return transform711ProxyAccountEntry(entry, context);
|
||||
}
|
||||
return entry;
|
||||
};
|
||||
})(typeof self !== 'undefined' ? self : globalThis);
|
||||
@@ -0,0 +1,463 @@
|
||||
(function attachBackgroundLocalCliProxyApi(root, factory) {
|
||||
root.MultiPageBackgroundLocalCliProxyApi = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundLocalCliProxyApiModule() {
|
||||
const AUTH_URL = 'https://auth.openai.com/oauth/authorize';
|
||||
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
||||
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
||||
const REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
||||
const DEFAULT_RELATIVE_AUTH_DIR = '.cli-proxy-api';
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function ensureTextEncoder() {
|
||||
if (typeof TextEncoder === 'function') {
|
||||
return new TextEncoder();
|
||||
}
|
||||
throw new Error('当前环境缺少 TextEncoder,无法生成 PKCE。');
|
||||
}
|
||||
|
||||
function getCryptoLike(explicitCrypto = null) {
|
||||
const candidate = explicitCrypto || globalThis.crypto || null;
|
||||
if (candidate && typeof candidate.getRandomValues === 'function' && candidate.subtle) {
|
||||
return candidate;
|
||||
}
|
||||
throw new Error('当前环境缺少 Web Crypto,无法执行本地 OAuth 模块。');
|
||||
}
|
||||
|
||||
function getFetchLike(explicitFetch = null) {
|
||||
const candidate = explicitFetch || globalThis.fetch || null;
|
||||
if (typeof candidate === 'function') {
|
||||
return candidate.bind(globalThis);
|
||||
}
|
||||
throw new Error('当前环境缺少 fetch,无法交换 OAuth token。');
|
||||
}
|
||||
|
||||
function getSessionConverter(explicitConverter = null) {
|
||||
const candidate = explicitConverter
|
||||
|| globalThis.MultiPageSessionToJsonConverter
|
||||
|| (typeof self !== 'undefined' ? self.MultiPageSessionToJsonConverter : null)
|
||||
|| null;
|
||||
if (candidate && typeof candidate.convertSessionJson === 'function') {
|
||||
return candidate;
|
||||
}
|
||||
throw new Error('session-to-json 转换模块未加载,无法生成本地 auth json。');
|
||||
}
|
||||
|
||||
function bytesToBase64Url(bytes) {
|
||||
if (!(bytes instanceof Uint8Array)) {
|
||||
throw new Error('bytesToBase64Url 需要 Uint8Array 输入。');
|
||||
}
|
||||
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
return Buffer.from(bytes).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
let binary = '';
|
||||
for (let index = 0; index < bytes.length; index += 0x8000) {
|
||||
binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
|
||||
}
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
async function sha256Bytes(value, cryptoLike) {
|
||||
const encoder = ensureTextEncoder();
|
||||
const digest = await cryptoLike.subtle.digest('SHA-256', encoder.encode(String(value || '')));
|
||||
return new Uint8Array(digest);
|
||||
}
|
||||
|
||||
async function sha256Hex(value, cryptoLike) {
|
||||
const bytes = await sha256Bytes(value, cryptoLike);
|
||||
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
async function generateRandomState(options = {}) {
|
||||
const cryptoLike = getCryptoLike(options.crypto);
|
||||
const bytes = new Uint8Array(16);
|
||||
cryptoLike.getRandomValues(bytes);
|
||||
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
async function generatePkceCodes(options = {}) {
|
||||
const cryptoLike = getCryptoLike(options.crypto);
|
||||
const bytes = new Uint8Array(96);
|
||||
cryptoLike.getRandomValues(bytes);
|
||||
const codeVerifier = bytesToBase64Url(bytes);
|
||||
const codeChallenge = bytesToBase64Url(await sha256Bytes(codeVerifier, cryptoLike));
|
||||
return {
|
||||
codeVerifier,
|
||||
codeChallenge,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePlanTypeForFilename(planType = '') {
|
||||
const parts = normalizeString(planType)
|
||||
.split(/[^A-Za-z0-9]+/g)
|
||||
.map((part) => part.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
return parts.join('-');
|
||||
}
|
||||
|
||||
function resolvePathSeparator(basePath = '') {
|
||||
return String(basePath || '').includes('\\') ? '\\' : '/';
|
||||
}
|
||||
|
||||
function sanitizeRelativeDir(input = DEFAULT_RELATIVE_AUTH_DIR) {
|
||||
const normalized = normalizeString(input || DEFAULT_RELATIVE_AUTH_DIR);
|
||||
if (!normalized) {
|
||||
return DEFAULT_RELATIVE_AUTH_DIR;
|
||||
}
|
||||
|
||||
const segments = normalized
|
||||
.replace(/\\/g, '/')
|
||||
.split('/')
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (segments.some((segment) => segment === '.' || segment === '..')) {
|
||||
throw new Error('relativeAuthDir 不能包含 . 或 .. 路径段。');
|
||||
}
|
||||
|
||||
return segments.join('/');
|
||||
}
|
||||
|
||||
function joinPath(basePath, ...parts) {
|
||||
const separator = resolvePathSeparator(basePath);
|
||||
const normalizedBase = normalizeString(basePath).replace(/[\\/]+$/g, '');
|
||||
const resultParts = [normalizedBase];
|
||||
|
||||
for (const rawPart of parts) {
|
||||
const normalized = String(rawPart || '')
|
||||
.replace(/\\/g, '/')
|
||||
.split('/')
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
if (normalized.length) {
|
||||
resultParts.push(normalized.join(separator));
|
||||
}
|
||||
}
|
||||
|
||||
return resultParts.join(separator);
|
||||
}
|
||||
|
||||
function buildAuthUrl({
|
||||
state,
|
||||
codeChallenge,
|
||||
clientId = CLIENT_ID,
|
||||
redirectUri = REDIRECT_URI,
|
||||
} = {}) {
|
||||
const normalizedState = normalizeString(state);
|
||||
const normalizedChallenge = normalizeString(codeChallenge);
|
||||
if (!normalizedState) {
|
||||
throw new Error('生成 OAuth 地址失败:缺少 state。');
|
||||
}
|
||||
if (!normalizedChallenge) {
|
||||
throw new Error('生成 OAuth 地址失败:缺少 PKCE code_challenge。');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('client_id', normalizeString(clientId) || CLIENT_ID);
|
||||
params.set('response_type', 'code');
|
||||
params.set('redirect_uri', normalizeString(redirectUri) || REDIRECT_URI);
|
||||
params.set('scope', 'openid email profile offline_access');
|
||||
params.set('state', normalizedState);
|
||||
params.set('code_challenge', normalizedChallenge);
|
||||
params.set('code_challenge_method', 'S256');
|
||||
params.set('prompt', 'login');
|
||||
params.set('id_token_add_organizations', 'true');
|
||||
params.set('codex_cli_simplified_flow', 'true');
|
||||
return `${AUTH_URL}?${params.toString()}`;
|
||||
}
|
||||
|
||||
function parseOAuthCallback(callbackUrl, expectedState = '') {
|
||||
const rawInput = normalizeString(callbackUrl);
|
||||
if (!rawInput) {
|
||||
throw new Error('缺少 OAuth callback 地址。');
|
||||
}
|
||||
|
||||
let candidate = rawInput;
|
||||
if (!candidate.includes('://')) {
|
||||
if (candidate.startsWith('?')) {
|
||||
candidate = `http://localhost${candidate}`;
|
||||
} else if (candidate.includes('=') && !candidate.includes('/')) {
|
||||
candidate = `http://localhost/?${candidate}`;
|
||||
} else {
|
||||
candidate = `http://${candidate}`;
|
||||
}
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(candidate);
|
||||
} catch {
|
||||
throw new Error('OAuth callback 地址格式无效。');
|
||||
}
|
||||
|
||||
const query = parsed.searchParams;
|
||||
let code = normalizeString(query.get('code'));
|
||||
let state = normalizeString(query.get('state'));
|
||||
let error = normalizeString(query.get('error'));
|
||||
let errorDescription = normalizeString(query.get('error_description'));
|
||||
|
||||
if (parsed.hash) {
|
||||
const fragment = new URLSearchParams(parsed.hash.replace(/^#/, ''));
|
||||
code = code || normalizeString(fragment.get('code'));
|
||||
state = state || normalizeString(fragment.get('state'));
|
||||
error = error || normalizeString(fragment.get('error'));
|
||||
errorDescription = errorDescription || normalizeString(fragment.get('error_description'));
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw new Error(errorDescription ? `OAuth 回调失败:${errorDescription}` : `OAuth 回调失败:${error}`);
|
||||
}
|
||||
if (!code) {
|
||||
throw new Error('OAuth callback 缺少 code。');
|
||||
}
|
||||
|
||||
const normalizedExpectedState = normalizeString(expectedState);
|
||||
if (normalizedExpectedState && state !== normalizedExpectedState) {
|
||||
throw new Error(`OAuth state 不匹配:expected ${normalizedExpectedState}, got ${state || '(empty)'}`);
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
state,
|
||||
callbackUrl: parsed.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function buildCredentialFileName(authJson, options = {}) {
|
||||
const cryptoLike = getCryptoLike(options.crypto);
|
||||
const email = normalizeString(authJson?.email);
|
||||
if (!email) {
|
||||
throw new Error('生成本地 auth 文件名失败:json 中缺少 email。');
|
||||
}
|
||||
|
||||
const planType = normalizePlanTypeForFilename(authJson?.plan_type || authJson?.chatgpt_plan_type);
|
||||
const accountId = normalizeString(authJson?.account_id || authJson?.chatgpt_account_id);
|
||||
const hashAccountId = accountId ? (await sha256Hex(accountId, cryptoLike)).slice(0, 8) : '';
|
||||
|
||||
if (!planType) {
|
||||
return `codex-${email}.json`;
|
||||
}
|
||||
if (planType === 'team') {
|
||||
return `codex-${hashAccountId}-${email}-${planType}.json`;
|
||||
}
|
||||
return `codex-${email}-${planType}.json`;
|
||||
}
|
||||
|
||||
function createLocalCliProxyApi(deps = {}) {
|
||||
const fetchLike = getFetchLike(deps.fetch);
|
||||
const cryptoLike = getCryptoLike(deps.crypto);
|
||||
const sessionConverter = getSessionConverter(deps.sessionToJsonConverter);
|
||||
const ensureDirectory = typeof deps.ensureDirectory === 'function' ? deps.ensureDirectory : null;
|
||||
const writeTextFile = typeof deps.writeTextFile === 'function' ? deps.writeTextFile : null;
|
||||
|
||||
async function createAuthorizationRequest(options = {}) {
|
||||
const oauthState = normalizeString(options.state) || await generateRandomState({ crypto: cryptoLike });
|
||||
const pkceCodes = options.pkceCodes?.codeVerifier && options.pkceCodes?.codeChallenge
|
||||
? {
|
||||
codeVerifier: normalizeString(options.pkceCodes.codeVerifier),
|
||||
codeChallenge: normalizeString(options.pkceCodes.codeChallenge),
|
||||
}
|
||||
: await generatePkceCodes({ crypto: cryptoLike });
|
||||
|
||||
return {
|
||||
oauthState,
|
||||
redirectUri: normalizeString(options.redirectUri) || REDIRECT_URI,
|
||||
pkceCodes,
|
||||
oauthUrl: buildAuthUrl({
|
||||
state: oauthState,
|
||||
codeChallenge: pkceCodes.codeChallenge,
|
||||
clientId: options.clientId || CLIENT_ID,
|
||||
redirectUri: options.redirectUri || REDIRECT_URI,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function exchangeCodeForTokens(options = {}) {
|
||||
const code = normalizeString(options.code);
|
||||
const redirectUri = normalizeString(options.redirectUri) || REDIRECT_URI;
|
||||
const codeVerifier = normalizeString(options.pkceCodes?.codeVerifier);
|
||||
if (!code) {
|
||||
throw new Error('token 交换失败:缺少 OAuth code。');
|
||||
}
|
||||
if (!codeVerifier) {
|
||||
throw new Error('token 交换失败:缺少 PKCE code_verifier。');
|
||||
}
|
||||
|
||||
const form = new URLSearchParams();
|
||||
form.set('grant_type', 'authorization_code');
|
||||
form.set('client_id', normalizeString(options.clientId) || CLIENT_ID);
|
||||
form.set('code', code);
|
||||
form.set('redirect_uri', redirectUri);
|
||||
form.set('code_verifier', codeVerifier);
|
||||
|
||||
const response = await fetchLike(TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: form.toString(),
|
||||
});
|
||||
|
||||
const rawText = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`token exchange failed with status ${response.status}: ${rawText}`);
|
||||
}
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = JSON.parse(rawText || '{}');
|
||||
} catch {
|
||||
throw new Error('token 交换失败:返回内容不是合法 JSON。');
|
||||
}
|
||||
|
||||
const accessToken = normalizeString(payload.access_token);
|
||||
const refreshToken = normalizeString(payload.refresh_token);
|
||||
const idToken = normalizeString(payload.id_token);
|
||||
const expiresIn = Number(payload.expires_in);
|
||||
if (!accessToken) {
|
||||
throw new Error('token 交换失败:缺少 access_token。');
|
||||
}
|
||||
|
||||
const expiresAt = Number.isFinite(expiresIn) && expiresIn > 0
|
||||
? new Date(Date.now() + expiresIn * 1000).toISOString()
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
idToken,
|
||||
tokenType: normalizeString(payload.token_type),
|
||||
expiresIn: Number.isFinite(expiresIn) ? expiresIn : undefined,
|
||||
expiresAt,
|
||||
rawPayload: payload,
|
||||
};
|
||||
}
|
||||
|
||||
async function buildAuthJsonArtifact(options = {}) {
|
||||
const accessToken = normalizeString(options.accessToken || options.access_token);
|
||||
if (!accessToken) {
|
||||
throw new Error('生成本地 auth json 失败:缺少 accessToken。');
|
||||
}
|
||||
|
||||
const sourceSession = options.session && typeof options.session === 'object' && !Array.isArray(options.session)
|
||||
? options.session
|
||||
: {};
|
||||
const sessionRecord = {
|
||||
...sourceSession,
|
||||
type: 'codex',
|
||||
accessToken,
|
||||
refreshToken: normalizeString(options.refreshToken || options.refresh_token || sourceSession.refreshToken || sourceSession.refresh_token),
|
||||
idToken: normalizeString(options.idToken || options.id_token || sourceSession.idToken || sourceSession.id_token),
|
||||
sessionToken: normalizeString(options.sessionToken || options.session_token || sourceSession.sessionToken || sourceSession.session_token),
|
||||
expiresAt: options.expiresAt || options.expires_at || sourceSession.expiresAt || sourceSession.expires,
|
||||
email: options.email || sourceSession.email || sourceSession.user?.email,
|
||||
account_id: options.accountId || options.account_id || sourceSession.account?.id,
|
||||
user_id: options.userId || options.user_id || sourceSession.user?.id,
|
||||
plan_type: options.planType || options.plan_type || sourceSession.account?.planType || sourceSession.account?.plan_type,
|
||||
};
|
||||
|
||||
const converted = sessionConverter.convertSessionJson(sessionRecord, {
|
||||
lastRefresh: options.lastRefresh,
|
||||
now: options.now || new Date(),
|
||||
sourceName: normalizeString(options.sourceName) || 'CLIProxyAPI Local OAuth',
|
||||
});
|
||||
|
||||
const authJson = converted.output;
|
||||
const fileName = await buildCredentialFileName(authJson, { crypto: cryptoLike });
|
||||
const pluginDir = normalizeString(options.pluginDir);
|
||||
if (!pluginDir) {
|
||||
throw new Error('生成本地 auth json 失败:缺少 pluginDir。');
|
||||
}
|
||||
|
||||
const relativeAuthDir = sanitizeRelativeDir(options.relativeAuthDir || DEFAULT_RELATIVE_AUTH_DIR);
|
||||
const directoryPath = joinPath(pluginDir, relativeAuthDir);
|
||||
const filePath = joinPath(directoryPath, fileName);
|
||||
const jsonText = `${JSON.stringify(authJson, null, 2)}\n`;
|
||||
|
||||
return {
|
||||
provider: 'codex',
|
||||
fileName,
|
||||
directoryPath,
|
||||
filePath,
|
||||
relativeAuthDir,
|
||||
authJson,
|
||||
jsonText,
|
||||
warnings: Array.isArray(converted.warnings) ? converted.warnings.slice() : [],
|
||||
};
|
||||
}
|
||||
|
||||
async function saveAuthJsonArtifact(artifact) {
|
||||
if (!artifact || typeof artifact !== 'object') {
|
||||
throw new Error('保存本地 auth json 失败:artifact 无效。');
|
||||
}
|
||||
if (!writeTextFile) {
|
||||
throw new Error('保存本地 auth json 失败:未提供 writeTextFile。');
|
||||
}
|
||||
if (!normalizeString(artifact.filePath)) {
|
||||
throw new Error('保存本地 auth json 失败:缺少 filePath。');
|
||||
}
|
||||
|
||||
if (ensureDirectory) {
|
||||
await ensureDirectory(artifact.directoryPath);
|
||||
}
|
||||
await writeTextFile(artifact.filePath, artifact.jsonText);
|
||||
|
||||
return {
|
||||
...artifact,
|
||||
saved: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function exchangeCallbackToAuthArtifact(options = {}) {
|
||||
const callback = parseOAuthCallback(options.callbackUrl, options.expectedState);
|
||||
const tokenBundle = await exchangeCodeForTokens({
|
||||
code: callback.code,
|
||||
pkceCodes: options.pkceCodes,
|
||||
redirectUri: options.redirectUri,
|
||||
clientId: options.clientId,
|
||||
});
|
||||
|
||||
return buildAuthJsonArtifact({
|
||||
...tokenBundle,
|
||||
pluginDir: options.pluginDir,
|
||||
relativeAuthDir: options.relativeAuthDir,
|
||||
sourceName: options.sourceName,
|
||||
now: options.now,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
AUTH_URL,
|
||||
TOKEN_URL,
|
||||
CLIENT_ID,
|
||||
REDIRECT_URI,
|
||||
DEFAULT_RELATIVE_AUTH_DIR,
|
||||
buildAuthJsonArtifact,
|
||||
createAuthorizationRequest,
|
||||
exchangeCallbackToAuthArtifact,
|
||||
exchangeCodeForTokens,
|
||||
parseOAuthCallback,
|
||||
saveAuthJsonArtifact,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
AUTH_URL,
|
||||
TOKEN_URL,
|
||||
CLIENT_ID,
|
||||
REDIRECT_URI,
|
||||
DEFAULT_RELATIVE_AUTH_DIR,
|
||||
buildAuthUrl,
|
||||
buildCredentialFileName,
|
||||
createLocalCliProxyApi,
|
||||
generatePkceCodes,
|
||||
generateRandomState,
|
||||
normalizePlanTypeForFilename,
|
||||
parseOAuthCallback,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,252 @@
|
||||
(function attachBackgroundLoggingStatus(root, factory) {
|
||||
root.MultiPageBackgroundLoggingStatus = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundLoggingStatusModule() {
|
||||
function createLoggingStatus(deps = {}) {
|
||||
const {
|
||||
chrome,
|
||||
DEFAULT_STATE,
|
||||
getStepIdByNodeIdForState,
|
||||
getState,
|
||||
isRecoverableStep9AuthFailure,
|
||||
LOG_PREFIX,
|
||||
setState,
|
||||
sourceRegistry = null,
|
||||
STOP_ERROR_MESSAGE,
|
||||
} = deps;
|
||||
|
||||
function getSourceLabel(source) {
|
||||
if (sourceRegistry?.getSourceLabel) {
|
||||
return sourceRegistry.getSourceLabel(source);
|
||||
}
|
||||
const labels = {
|
||||
'openai-auth': '认证页',
|
||||
'gmail-mail': 'Gmail 邮箱',
|
||||
'sidepanel': '侧边栏',
|
||||
'signup-page': '认证页',
|
||||
'vps-panel': 'CPA 面板',
|
||||
'sub2api-panel': 'SUB2API 后台',
|
||||
'codex2api-panel': 'Codex2API 后台',
|
||||
'qq-mail': 'QQ 邮箱',
|
||||
'mail-163': '163 邮箱',
|
||||
'mail-2925': '2925 邮箱',
|
||||
'inbucket-mail': 'Inbucket 邮箱',
|
||||
'duck-mail': 'Duck 邮箱',
|
||||
'hotmail-api': 'Hotmail(API对接/本地助手)',
|
||||
'luckmail-api': 'LuckMail(API 购邮)',
|
||||
'cloudflare-temp-email': 'Cloudflare Temp Email',
|
||||
'cloudmail': 'Cloud Mail',
|
||||
'plus-checkout': 'Plus Checkout',
|
||||
'paypal-flow': 'PayPal 授权页',
|
||||
'gopay-flow': 'GoPay 授权页',
|
||||
'unknown-source': '未知来源',
|
||||
};
|
||||
return labels[source] || source || '未知来源';
|
||||
}
|
||||
|
||||
function normalizeLogStep(value) {
|
||||
const step = Math.floor(Number(value) || 0);
|
||||
return step > 0 ? step : null;
|
||||
}
|
||||
|
||||
function buildLogEntry(message, level = 'info', options = {}) {
|
||||
const normalizedOptions = options && typeof options === 'object' ? options : {};
|
||||
const step = normalizeLogStep(normalizedOptions.step);
|
||||
const stepKey = String(normalizedOptions.stepKey || '').trim();
|
||||
const nodeId = String(normalizedOptions.nodeId || normalizedOptions.nodeKey || stepKey || '').trim();
|
||||
return {
|
||||
message: String(message || ''),
|
||||
level,
|
||||
timestamp: Date.now(),
|
||||
step,
|
||||
stepKey,
|
||||
nodeId,
|
||||
};
|
||||
}
|
||||
|
||||
async function addLog(message, level = 'info', options = {}) {
|
||||
const state = await getState();
|
||||
const logs = state.logs || [];
|
||||
const entry = buildLogEntry(message, level, options);
|
||||
logs.push(entry);
|
||||
if (logs.length > 500) logs.splice(0, logs.length - 500);
|
||||
await setState({ logs });
|
||||
chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => { });
|
||||
}
|
||||
|
||||
async function setNodeStatus(nodeId, status) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
if (!normalizedNodeId) {
|
||||
throw new Error('setNodeStatus 缺少 nodeId。');
|
||||
}
|
||||
const state = await getState();
|
||||
const nodeStatuses = { ...(state.nodeStatuses || {}) };
|
||||
nodeStatuses[normalizedNodeId] = status;
|
||||
await setState({
|
||||
nodeStatuses,
|
||||
currentNodeId: normalizedNodeId,
|
||||
});
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'NODE_STATUS_CHANGED',
|
||||
payload: { nodeId: normalizedNodeId, status },
|
||||
}).catch(() => { });
|
||||
}
|
||||
|
||||
function getErrorMessage(error) {
|
||||
return String(typeof error === 'string' ? error : error?.message || '')
|
||||
.replace(/^GPC_TASK_ENDED::/i, '')
|
||||
.replace(/^AUTO_RUN_STEP_IDLE_RESTART::/i, '');
|
||||
}
|
||||
|
||||
function isVerificationMailPollingError(error) {
|
||||
const message = getErrorMessage(error);
|
||||
return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|内容脚本\s+\d+(?:\.\d+)?\s*秒内未响应|did not respond in \d+s|405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/(?:email|phone)-verification/i.test(message);
|
||||
}
|
||||
|
||||
function isAddPhoneAuthFailure(error) {
|
||||
const message = getErrorMessage(error);
|
||||
if (/\u624b\u673a\u53f7\u8f93\u5165\u6a21\u5f0f|phone\s+entry/i.test(message)) {
|
||||
return false;
|
||||
}
|
||||
return /https:\/\/auth\.openai\.com\/(?:add-phone|phone-verification)(?:[/?#]|$)|\badd-phone\b|phone-verification|\u6dfb\u52a0\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u624b\u673a\u9a8c\u8bc1\u7801\u9875|\u624b\u673a\u9a8c\u8bc1\u9875|\u8fdb\u5165\u624b\u673a\u53f7\u9875\u9762|\u624b\u673a\u53f7\u9875|\u624b\u673a\u53f7\u9875\u9762|phone\s+number|telephone/i.test(message);
|
||||
}
|
||||
|
||||
function getLoginAuthStateLabel(state) {
|
||||
switch (state) {
|
||||
case 'verification_page':
|
||||
return '登录验证码页';
|
||||
case 'password_page':
|
||||
return '密码页';
|
||||
case 'email_page':
|
||||
return '邮箱输入页';
|
||||
case 'login_timeout_error_page':
|
||||
return '登录超时报错页';
|
||||
case 'oauth_consent_page':
|
||||
return 'OAuth 授权页';
|
||||
case 'add_phone_page':
|
||||
return '手机号页';
|
||||
case 'add_email_page':
|
||||
return '添加邮箱页';
|
||||
case 'phone_verification_page':
|
||||
return '手机验证码页';
|
||||
default:
|
||||
return '未知页面';
|
||||
}
|
||||
}
|
||||
|
||||
function isRestartCurrentAttemptError(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||
return /当前邮箱已存在,需要重新开始新一轮|SIGNUP_PHONE_PASSWORD_MISMATCH::/i.test(message);
|
||||
}
|
||||
|
||||
function isSignupUserAlreadyExistsFailure(error) {
|
||||
const message = getErrorMessage(error);
|
||||
return /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(message);
|
||||
}
|
||||
|
||||
function isStep9RecoverableAuthError(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||
return /STEP9_OAUTH_RETRY::/i.test(message)
|
||||
|| isRecoverableStep9AuthFailure(message);
|
||||
}
|
||||
|
||||
function isLegacyStep9RecoverableAuthError(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||
return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(message);
|
||||
}
|
||||
|
||||
function isStepDoneStatus(status) {
|
||||
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
||||
}
|
||||
|
||||
function getFirstUnfinishedStep(statuses = {}) {
|
||||
const nodeStatuses = statuses && typeof statuses === 'object' ? statuses : {};
|
||||
const nodeIds = Object.keys(DEFAULT_STATE.nodeStatuses || {});
|
||||
for (const nodeId of nodeIds) {
|
||||
if (!isStepDoneStatus(nodeStatuses[nodeId] || 'pending')) {
|
||||
return typeof getStepIdByNodeIdForState === 'function'
|
||||
? getStepIdByNodeIdForState(nodeId, {})
|
||||
: null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasSavedProgress(statuses = {}) {
|
||||
return Object.values({ ...DEFAULT_STATE.nodeStatuses, ...statuses }).some((status) => status !== 'pending');
|
||||
}
|
||||
|
||||
function getRunningSteps(statuses = {}) {
|
||||
return Object.entries({ ...DEFAULT_STATE.nodeStatuses, ...statuses })
|
||||
.filter(([, status]) => status === 'running')
|
||||
.map(([nodeId]) => (typeof getStepIdByNodeIdForState === 'function' ? getStepIdByNodeIdForState(nodeId, {}) : null))
|
||||
.filter((step) => Number.isInteger(step) && step > 0)
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function getFirstUnfinishedNode(statuses = {}) {
|
||||
const nodeIds = Object.keys(DEFAULT_STATE.nodeStatuses || {});
|
||||
for (const nodeId of nodeIds) {
|
||||
if (!isStepDoneStatus(statuses[nodeId] || 'pending')) {
|
||||
return nodeId;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function hasSavedNodeProgress(statuses = {}) {
|
||||
return Object.values({ ...DEFAULT_STATE.nodeStatuses, ...statuses }).some((status) => status !== 'pending');
|
||||
}
|
||||
|
||||
function getRunningNodes(statuses = {}) {
|
||||
return Object.entries({ ...DEFAULT_STATE.nodeStatuses, ...statuses })
|
||||
.filter(([, status]) => status === 'running')
|
||||
.map(([nodeId]) => nodeId);
|
||||
}
|
||||
|
||||
function getAutoRunStatusPayload(phase, payload = {}) {
|
||||
return {
|
||||
autoRunning: phase === 'scheduled'
|
||||
|| phase === 'running'
|
||||
|| phase === 'waiting_step'
|
||||
|| phase === 'waiting_email'
|
||||
|| phase === 'retrying'
|
||||
|| phase === 'waiting_interval',
|
||||
autoRunPhase: phase,
|
||||
autoRunCurrentRun: payload.currentRun ?? 0,
|
||||
autoRunTotalRuns: payload.totalRuns ?? 1,
|
||||
autoRunAttemptRun: payload.attemptRun ?? 0,
|
||||
autoRunSessionId: Math.max(0, Math.floor(Number(payload.sessionId ?? payload.autoRunSessionId) || 0)),
|
||||
scheduledAutoRunAt: Number.isFinite(Number(payload.scheduledAt)) ? Number(payload.scheduledAt) : null,
|
||||
autoRunCountdownAt: Number.isFinite(Number(payload.countdownAt)) ? Number(payload.countdownAt) : null,
|
||||
autoRunCountdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''),
|
||||
autoRunCountdownNote: payload.countdownNote === undefined ? '' : String(payload.countdownNote || ''),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
addLog,
|
||||
getAutoRunStatusPayload,
|
||||
getFirstUnfinishedNode,
|
||||
isAddPhoneAuthFailure,
|
||||
getErrorMessage,
|
||||
getFirstUnfinishedStep,
|
||||
getLoginAuthStateLabel,
|
||||
getRunningNodes,
|
||||
getRunningSteps,
|
||||
getSourceLabel,
|
||||
hasSavedNodeProgress,
|
||||
hasSavedProgress,
|
||||
isLegacyStep9RecoverableAuthError,
|
||||
isRestartCurrentAttemptError,
|
||||
isSignupUserAlreadyExistsFailure,
|
||||
isStep9RecoverableAuthError,
|
||||
isStepDoneStatus,
|
||||
isVerificationMailPollingError,
|
||||
setNodeStatus,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createLoggingStatus,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,771 @@
|
||||
(function attachBackgroundMail2925Session(root, factory) {
|
||||
root.MultiPageBackgroundMail2925Session = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundMail2925SessionModule() {
|
||||
function createMail2925SessionManager(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
broadcastDataUpdate,
|
||||
chrome,
|
||||
ensureContentScriptReadyOnTab,
|
||||
findMail2925Account,
|
||||
getMail2925AccountStatus,
|
||||
getState,
|
||||
isAutoRunLockedState,
|
||||
isMail2925AccountAvailable,
|
||||
MAIL2925_LIMIT_COOLDOWN_MS,
|
||||
normalizeMail2925Account,
|
||||
normalizeMail2925Accounts,
|
||||
pickMail2925AccountForRun,
|
||||
requestStop,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScriptResilient,
|
||||
sendToMailContentScriptResilient,
|
||||
setPersistentSettings,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
upsertMail2925AccountInList,
|
||||
waitForTabComplete,
|
||||
waitForTabUrlMatch,
|
||||
} = deps;
|
||||
|
||||
const MAIL2925_SOURCE = 'mail-2925';
|
||||
const MAIL2925_URL = 'https://2925.com/#/mailList';
|
||||
const MAIL2925_LOGIN_URL = 'https://2925.com/login/';
|
||||
const MAIL2925_INJECT = ['content/utils.js', 'content/operation-delay.js', 'content/mail-2925.js'];
|
||||
const MAIL2925_INJECT_SOURCE = 'mail-2925';
|
||||
const MAIL2925_COOKIE_DOMAINS = [
|
||||
'2925.com',
|
||||
'www.2925.com',
|
||||
];
|
||||
const MAIL2925_COOKIE_ORIGINS = [
|
||||
'https://2925.com',
|
||||
'https://www.2925.com',
|
||||
];
|
||||
const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::';
|
||||
const MAIL2925_THREAD_TERMINATED_ERROR_PREFIX = 'MAIL2925_THREAD_TERMINATED::';
|
||||
const MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS = 15000;
|
||||
const MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS = 120000;
|
||||
const MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS = 120000;
|
||||
|
||||
function getMail2925MailConfig() {
|
||||
return {
|
||||
provider: '2925',
|
||||
source: MAIL2925_SOURCE,
|
||||
url: MAIL2925_URL,
|
||||
label: '2925 邮箱',
|
||||
inject: MAIL2925_INJECT,
|
||||
injectSource: MAIL2925_INJECT_SOURCE,
|
||||
};
|
||||
}
|
||||
|
||||
function getErrorMessage(error) {
|
||||
return String(typeof error === 'string' ? error : error?.message || '');
|
||||
}
|
||||
|
||||
function buildMail2925ThreadTerminatedError(message) {
|
||||
return new Error(`${MAIL2925_THREAD_TERMINATED_ERROR_PREFIX}${String(message || '').trim()}`);
|
||||
}
|
||||
|
||||
async function stopAutoRunForMail2925LoginFailure(errorMessage = '') {
|
||||
if (typeof requestStop !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const state = await getState();
|
||||
const autoRunning = typeof isAutoRunLockedState === 'function'
|
||||
? isAutoRunLockedState(state)
|
||||
: Boolean(state?.autoRunning);
|
||||
if (!autoRunning) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await requestStop({
|
||||
logMessage: errorMessage || '2925 登录失败,已按手动停止逻辑暂停自动流程。',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function isMail2925LimitReachedError(error) {
|
||||
const message = getErrorMessage(error);
|
||||
return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX)
|
||||
|| message.includes('子邮箱已达上限')
|
||||
|| message.includes('已达上限邮箱');
|
||||
}
|
||||
|
||||
function isMail2925ThreadTerminatedError(error) {
|
||||
return getErrorMessage(error).startsWith(MAIL2925_THREAD_TERMINATED_ERROR_PREFIX);
|
||||
}
|
||||
|
||||
function isRetryableMail2925TransportError(error) {
|
||||
const message = getErrorMessage(error).toLowerCase();
|
||||
return message.includes('receiving end does not exist')
|
||||
|| message.includes('message port closed')
|
||||
|| message.includes('content script on')
|
||||
|| message.includes('did not respond');
|
||||
}
|
||||
|
||||
async function syncMail2925Accounts(accounts) {
|
||||
const normalized = normalizeMail2925Accounts(accounts);
|
||||
await setPersistentSettings({ mail2925Accounts: normalized });
|
||||
await setState({ mail2925Accounts: normalized });
|
||||
broadcastDataUpdate({ mail2925Accounts: normalized });
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function upsertMail2925Account(input = {}) {
|
||||
const state = await getState();
|
||||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||||
const normalizedEmail = String(input?.email || '').trim().toLowerCase();
|
||||
const existing = input?.id
|
||||
? findMail2925Account(accounts, input.id)
|
||||
: accounts.find((account) => account.email === normalizedEmail) || null;
|
||||
const credentialsChanged = !existing
|
||||
|| (input?.email !== undefined && normalizedEmail !== existing.email)
|
||||
|| (input?.password !== undefined && String(input.password || '') !== existing.password);
|
||||
const normalized = normalizeMail2925Account({
|
||||
...(existing || {}),
|
||||
...(credentialsChanged ? { lastError: '' } : {}),
|
||||
...input,
|
||||
id: input?.id || existing?.id || crypto.randomUUID(),
|
||||
});
|
||||
|
||||
const nextAccounts = existing
|
||||
? accounts.map((account) => (account.id === normalized.id ? normalized : account))
|
||||
: [...accounts, normalized];
|
||||
|
||||
await syncMail2925Accounts(nextAccounts);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getCurrentMail2925Account(state = {}) {
|
||||
return findMail2925Account(state.mail2925Accounts, state.currentMail2925AccountId) || null;
|
||||
}
|
||||
|
||||
async function getMail2925CurrentTabUrl() {
|
||||
try {
|
||||
const state = await getState();
|
||||
const tabId = Number(state?.tabRegistry?.[MAIL2925_SOURCE]?.tabId || 0);
|
||||
if (!Number.isInteger(tabId) || tabId <= 0 || typeof chrome.tabs?.get !== 'function') {
|
||||
return '';
|
||||
}
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
return String(tab?.url || '').trim();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function getMail2925TabUrlById(tabId) {
|
||||
try {
|
||||
if (!Number.isInteger(Number(tabId)) || Number(tabId) <= 0 || typeof chrome.tabs?.get !== 'function') {
|
||||
return '';
|
||||
}
|
||||
const tab = await chrome.tabs.get(Number(tabId));
|
||||
return String(tab?.url || '').trim();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isMail2925LoginUrl(rawUrl = '') {
|
||||
try {
|
||||
const parsed = new URL(String(rawUrl || ''));
|
||||
return (parsed.hostname === '2925.com' || parsed.hostname === 'www.2925.com')
|
||||
&& /^\/login\/?$/.test(parsed.pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMailboxEmail(value = '') {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function setCurrentMail2925Account(accountId, options = {}) {
|
||||
const { logMessage = '', updateLastUsedAt = false } = options;
|
||||
const state = await getState();
|
||||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||||
const account = findMail2925Account(accounts, accountId);
|
||||
if (!account) {
|
||||
throw new Error('未找到对应的 2925 账号。');
|
||||
}
|
||||
|
||||
let nextAccount = account;
|
||||
if (updateLastUsedAt) {
|
||||
nextAccount = normalizeMail2925Account({
|
||||
...account,
|
||||
lastUsedAt: Date.now(),
|
||||
});
|
||||
await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
|
||||
}
|
||||
|
||||
await setPersistentSettings({ currentMail2925AccountId: nextAccount.id });
|
||||
await setState({ currentMail2925AccountId: nextAccount.id });
|
||||
broadcastDataUpdate({ currentMail2925AccountId: nextAccount.id });
|
||||
if (logMessage) {
|
||||
await addLog(logMessage, 'ok');
|
||||
}
|
||||
return nextAccount;
|
||||
}
|
||||
|
||||
async function patchMail2925Account(accountId, updates = {}) {
|
||||
const state = await getState();
|
||||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||||
const account = findMail2925Account(accounts, accountId);
|
||||
if (!account) {
|
||||
throw new Error('未找到对应的 2925 账号。');
|
||||
}
|
||||
|
||||
const nextAccount = normalizeMail2925Account({
|
||||
...account,
|
||||
...updates,
|
||||
id: account.id,
|
||||
});
|
||||
await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
|
||||
|
||||
if (state.currentMail2925AccountId === account.id && nextAccount.enabled === false) {
|
||||
await setPersistentSettings({ currentMail2925AccountId: '' });
|
||||
await setState({ currentMail2925AccountId: null });
|
||||
broadcastDataUpdate({ currentMail2925AccountId: null });
|
||||
}
|
||||
|
||||
return nextAccount;
|
||||
}
|
||||
|
||||
async function deleteMail2925Account(accountId) {
|
||||
const state = await getState();
|
||||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||||
const nextAccounts = accounts.filter((account) => account.id !== accountId);
|
||||
await syncMail2925Accounts(nextAccounts);
|
||||
|
||||
if (state.currentMail2925AccountId === accountId) {
|
||||
await setPersistentSettings({ currentMail2925AccountId: '' });
|
||||
await setState({ currentMail2925AccountId: null });
|
||||
broadcastDataUpdate({ currentMail2925AccountId: null });
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMail2925Accounts(mode = 'all') {
|
||||
const state = await getState();
|
||||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||||
const nextAccounts = mode === 'all'
|
||||
? []
|
||||
: accounts.filter((account) => getMail2925AccountStatus(account) !== String(mode || '').trim());
|
||||
const deletedCount = Math.max(0, accounts.length - nextAccounts.length);
|
||||
await syncMail2925Accounts(nextAccounts);
|
||||
|
||||
if (state.currentMail2925AccountId && !findMail2925Account(nextAccounts, state.currentMail2925AccountId)) {
|
||||
await setPersistentSettings({ currentMail2925AccountId: '' });
|
||||
await setState({ currentMail2925AccountId: null });
|
||||
broadcastDataUpdate({ currentMail2925AccountId: null });
|
||||
}
|
||||
|
||||
return {
|
||||
deletedCount,
|
||||
remainingCount: nextAccounts.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureMail2925AccountForFlow(options = {}) {
|
||||
const {
|
||||
allowAllocate = true,
|
||||
preferredAccountId = null,
|
||||
excludeIds = [],
|
||||
markUsed = false,
|
||||
} = options;
|
||||
const state = await getState();
|
||||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||||
const now = Date.now();
|
||||
|
||||
let account = null;
|
||||
if (preferredAccountId) {
|
||||
account = findMail2925Account(accounts, preferredAccountId);
|
||||
}
|
||||
if (!account && state.currentMail2925AccountId) {
|
||||
account = findMail2925Account(accounts, state.currentMail2925AccountId);
|
||||
}
|
||||
if ((!account || !isMail2925AccountAvailable(account, now)) && allowAllocate) {
|
||||
account = pickMail2925AccountForRun(accounts, {
|
||||
excludeIds,
|
||||
now,
|
||||
});
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
throw new Error('没有可用的 2925 账号。请先在侧边栏添加至少一个带密码的 2925 账号。');
|
||||
}
|
||||
if (!account.password) {
|
||||
throw new Error(`2925 账号 ${account.email || account.id} 缺少密码,无法自动登录。`);
|
||||
}
|
||||
if (!isMail2925AccountAvailable(account, now)) {
|
||||
const disabledUntil = Number(account.disabledUntil || 0);
|
||||
if (disabledUntil > now) {
|
||||
throw new Error(`2925 账号 ${account.email || account.id} 当前处于冷却期,将在 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })} 后恢复。`);
|
||||
}
|
||||
throw new Error(`2925 账号 ${account.email || account.id} 当前不可用。`);
|
||||
}
|
||||
|
||||
return setCurrentMail2925Account(account.id, { updateLastUsedAt: markUsed });
|
||||
}
|
||||
|
||||
function normalizeCookieDomainForMatch(domain) {
|
||||
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
|
||||
}
|
||||
|
||||
function shouldClearMail2925Cookie(cookie) {
|
||||
const domain = normalizeCookieDomainForMatch(cookie?.domain);
|
||||
if (!domain) return false;
|
||||
return MAIL2925_COOKIE_DOMAINS.some((target) => (
|
||||
domain === target || domain.endsWith(`.${target}`)
|
||||
));
|
||||
}
|
||||
|
||||
function buildCookieRemovalUrl(cookie) {
|
||||
const host = normalizeCookieDomainForMatch(cookie?.domain);
|
||||
const path = String(cookie?.path || '/').startsWith('/')
|
||||
? String(cookie?.path || '/')
|
||||
: `/${String(cookie?.path || '')}`;
|
||||
return `https://${host}${path}`;
|
||||
}
|
||||
|
||||
async function collectMail2925Cookies() {
|
||||
if (!chrome.cookies?.getAll) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stores = chrome.cookies.getAllCookieStores
|
||||
? await chrome.cookies.getAllCookieStores()
|
||||
: [{ id: undefined }];
|
||||
const cookies = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const store of stores) {
|
||||
const storeId = store?.id;
|
||||
const batch = await chrome.cookies.getAll(storeId ? { storeId } : {});
|
||||
for (const cookie of batch || []) {
|
||||
if (!shouldClearMail2925Cookie(cookie)) continue;
|
||||
const key = [
|
||||
cookie.storeId || storeId || '',
|
||||
cookie.domain || '',
|
||||
cookie.path || '',
|
||||
cookie.name || '',
|
||||
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||||
].join('|');
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
cookies.push(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
async function removeMail2925Cookie(cookie) {
|
||||
const details = {
|
||||
url: buildCookieRemovalUrl(cookie),
|
||||
name: cookie.name,
|
||||
};
|
||||
|
||||
if (cookie.storeId) {
|
||||
details.storeId = cookie.storeId;
|
||||
}
|
||||
if (cookie.partitionKey) {
|
||||
details.partitionKey = cookie.partitionKey;
|
||||
}
|
||||
|
||||
try {
|
||||
return Boolean(await chrome.cookies.remove(details));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function clearMail2925SessionCookies() {
|
||||
if (!chrome.cookies?.getAll || !chrome.cookies?.remove) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const cookies = await collectMail2925Cookies();
|
||||
let removedCount = 0;
|
||||
for (const cookie of cookies) {
|
||||
throwIfStopped();
|
||||
if (await removeMail2925Cookie(cookie)) {
|
||||
removedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (chrome.browsingData?.removeCookies) {
|
||||
try {
|
||||
await chrome.browsingData.removeCookies({
|
||||
since: 0,
|
||||
origins: MAIL2925_COOKIE_ORIGINS,
|
||||
});
|
||||
} catch (_) {
|
||||
// Best effort cleanup only.
|
||||
}
|
||||
}
|
||||
|
||||
return removedCount;
|
||||
}
|
||||
|
||||
async function recoverMail2925LoginPageAfterTransportError(tabId) {
|
||||
const numericTabId = Number(tabId);
|
||||
if (!Number.isInteger(numericTabId) || numericTabId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUrl = (await getMail2925TabUrlById(numericTabId)) || await getMail2925CurrentTabUrl();
|
||||
await addLog(
|
||||
`2925:登录提交后页面发生跳转或重载,正在等待当前标签页恢复后继续确认登录态。当前地址:${currentUrl || 'unknown'}`,
|
||||
'warn'
|
||||
);
|
||||
|
||||
if (typeof waitForTabComplete === 'function') {
|
||||
const completedTab = await waitForTabComplete(numericTabId, {
|
||||
timeoutMs: MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS,
|
||||
retryDelayMs: 300,
|
||||
});
|
||||
await addLog(
|
||||
`2925:登录跳转等待结束,当前标签地址:${String(completedTab?.url || '').trim() || 'unknown'}`,
|
||||
completedTab?.url ? 'info' : 'warn'
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof ensureContentScriptReadyOnTab === 'function') {
|
||||
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, numericTabId, {
|
||||
inject: MAIL2925_INJECT,
|
||||
injectSource: MAIL2925_INJECT_SOURCE,
|
||||
timeoutMs: MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS,
|
||||
retryDelayMs: 800,
|
||||
logMessage: '步骤 0:2925 登录后页面仍在跳转,正在等待邮箱页重新就绪...',
|
||||
});
|
||||
}
|
||||
|
||||
const recoveredUrl = (await getMail2925TabUrlById(numericTabId)) || await getMail2925CurrentTabUrl();
|
||||
await addLog(`2925:登录跳转恢复后当前标签地址:${recoveredUrl || 'unknown'}`, 'info');
|
||||
}
|
||||
|
||||
async function ensureMail2925MailboxSession(options = {}) {
|
||||
const {
|
||||
accountId = null,
|
||||
forceRelogin = false,
|
||||
actionLabel = '确保 2925 邮箱登录态',
|
||||
allowLoginWhenOnLoginPage = true,
|
||||
expectedMailboxEmail = '',
|
||||
} = options;
|
||||
|
||||
const normalizedExpectedMailboxEmail = normalizeMailboxEmail(expectedMailboxEmail);
|
||||
|
||||
let account = null;
|
||||
if (forceRelogin || (allowLoginWhenOnLoginPage && normalizedExpectedMailboxEmail)) {
|
||||
account = await ensureMail2925AccountForFlow({
|
||||
allowAllocate: true,
|
||||
preferredAccountId: accountId,
|
||||
});
|
||||
}
|
||||
|
||||
const sendLoginMessage = typeof sendToContentScriptResilient === 'function'
|
||||
? sendToContentScriptResilient
|
||||
: async (source, message, runtimeOptions = {}) => sendToMailContentScriptResilient(
|
||||
getMail2925MailConfig(),
|
||||
message,
|
||||
{
|
||||
timeoutMs: runtimeOptions.timeoutMs,
|
||||
responseTimeoutMs: runtimeOptions.responseTimeoutMs,
|
||||
maxRecoveryAttempts: 0,
|
||||
}
|
||||
);
|
||||
|
||||
const buildSuccessPayload = () => ({
|
||||
account,
|
||||
mail: getMail2925MailConfig(),
|
||||
result: {
|
||||
loggedIn: true,
|
||||
currentView: 'mailbox',
|
||||
usedExistingSession: true,
|
||||
},
|
||||
});
|
||||
|
||||
const failMailboxSession = async (message) => {
|
||||
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
|
||||
if (stopped) {
|
||||
throw new Error('流程已被用户停止。');
|
||||
}
|
||||
throw new Error(message);
|
||||
};
|
||||
|
||||
if (forceRelogin) {
|
||||
const removedCount = await clearMail2925SessionCookies();
|
||||
await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info');
|
||||
if (typeof sleepWithStop === 'function') {
|
||||
await addLog('2925:清理 cookie 后等待 3 秒,再打开登录页...', 'info');
|
||||
await sleepWithStop(3000);
|
||||
}
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL;
|
||||
await addLog(
|
||||
forceRelogin
|
||||
? `2925:准备打开登录页 ${MAIL2925_LOGIN_URL}(强制重登录)`
|
||||
: `2925:准备打开邮箱页 ${MAIL2925_URL}(登录页自动登录=${allowLoginWhenOnLoginPage ? '开启' : '关闭'})`,
|
||||
'info'
|
||||
);
|
||||
const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, {
|
||||
inject: MAIL2925_INJECT,
|
||||
injectSource: MAIL2925_INJECT_SOURCE,
|
||||
});
|
||||
|
||||
let openedUrl = await getMail2925TabUrlById(tabId);
|
||||
if (!openedUrl) {
|
||||
openedUrl = await getMail2925CurrentTabUrl();
|
||||
}
|
||||
await addLog(`2925:打开页后当前标签地址:${openedUrl || 'unknown'}`, 'info');
|
||||
|
||||
if (forceRelogin && typeof waitForTabUrlMatch === 'function') {
|
||||
const matchedLoginTab = await waitForTabUrlMatch(
|
||||
tabId,
|
||||
(url) => isMail2925LoginUrl(url),
|
||||
{ timeoutMs: 15000, retryDelayMs: 300 }
|
||||
);
|
||||
await addLog(`2925:等待最终落到登录页结果:${matchedLoginTab?.url || '超时'}`, matchedLoginTab ? 'info' : 'warn');
|
||||
if (matchedLoginTab?.url) {
|
||||
openedUrl = String(matchedLoginTab.url || '').trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof ensureContentScriptReadyOnTab === 'function') {
|
||||
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, {
|
||||
inject: MAIL2925_INJECT,
|
||||
injectSource: MAIL2925_INJECT_SOURCE,
|
||||
timeoutMs: 20000,
|
||||
retryDelayMs: 800,
|
||||
logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...',
|
||||
});
|
||||
}
|
||||
|
||||
if (!forceRelogin && !isMail2925LoginUrl(openedUrl) && !normalizedExpectedMailboxEmail) {
|
||||
await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info');
|
||||
return buildSuccessPayload();
|
||||
}
|
||||
|
||||
if (!forceRelogin && isMail2925LoginUrl(openedUrl) && !allowLoginWhenOnLoginPage) {
|
||||
await failMailboxSession(`2925:${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`);
|
||||
}
|
||||
|
||||
if (!account && (forceRelogin || allowLoginWhenOnLoginPage)) {
|
||||
account = await ensureMail2925AccountForFlow({
|
||||
allowAllocate: true,
|
||||
preferredAccountId: accountId,
|
||||
});
|
||||
}
|
||||
|
||||
if (forceRelogin && typeof sleepWithStop === 'function') {
|
||||
await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info');
|
||||
await sleepWithStop(3000);
|
||||
}
|
||||
|
||||
let result;
|
||||
const sendEnsureSessionRequest = async () => {
|
||||
const beforeSendUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
|
||||
await addLog(`2925:发送 ENSURE_MAIL2925_SESSION 前当前地址:${beforeSendUrl || 'unknown'}`, 'info');
|
||||
return sendLoginMessage(
|
||||
MAIL2925_SOURCE,
|
||||
{
|
||||
type: 'ENSURE_MAIL2925_SESSION',
|
||||
step: 0,
|
||||
source: 'background',
|
||||
payload: {
|
||||
email: account?.email || '',
|
||||
password: account?.password || '',
|
||||
forceLogin: forceRelogin,
|
||||
allowLoginWhenOnLoginPage,
|
||||
},
|
||||
},
|
||||
{
|
||||
timeoutMs: MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS,
|
||||
retryDelayMs: 800,
|
||||
responseTimeoutMs: MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS,
|
||||
logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...',
|
||||
}
|
||||
);
|
||||
};
|
||||
try {
|
||||
result = await sendEnsureSessionRequest();
|
||||
} catch (err) {
|
||||
if (isRetryableMail2925TransportError(err)) {
|
||||
try {
|
||||
await recoverMail2925LoginPageAfterTransportError(tabId);
|
||||
await addLog('2925:页面恢复完成,正在重新确认登录态...', 'info');
|
||||
result = await sendEnsureSessionRequest();
|
||||
} catch (recoveryErr) {
|
||||
err = recoveryErr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '登录结果确认超时'})。`;
|
||||
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
|
||||
if (stopped) {
|
||||
throw new Error('流程已被用户停止。');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (result?.error) {
|
||||
await failMailboxSession(`2925:${actionLabel}失败(${result.error})。`);
|
||||
}
|
||||
if (result?.limitReached) {
|
||||
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '子邮箱已达上限邮箱'}`);
|
||||
}
|
||||
const actualMailboxEmail = normalizeMailboxEmail(result?.mailboxEmail || '');
|
||||
if (normalizedExpectedMailboxEmail && actualMailboxEmail && actualMailboxEmail !== normalizedExpectedMailboxEmail) {
|
||||
if (allowLoginWhenOnLoginPage) {
|
||||
await addLog(
|
||||
`2925:当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,准备登出当前账号并登录目标账号。`,
|
||||
'warn'
|
||||
);
|
||||
return ensureMail2925MailboxSession({
|
||||
accountId: account?.id || accountId || null,
|
||||
forceRelogin: true,
|
||||
allowLoginWhenOnLoginPage: true,
|
||||
expectedMailboxEmail: normalizedExpectedMailboxEmail,
|
||||
actionLabel,
|
||||
});
|
||||
}
|
||||
await failMailboxSession(
|
||||
`2925:${actionLabel}失败,当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,且当前未启用 2925 账号池。`
|
||||
);
|
||||
}
|
||||
if (normalizedExpectedMailboxEmail && !actualMailboxEmail && result?.currentView === 'mailbox') {
|
||||
await addLog('2925:未能识别当前邮箱页顶部邮箱地址,已跳过邮箱一致性校验。', 'warn');
|
||||
}
|
||||
if (!result?.loggedIn) {
|
||||
await failMailboxSession(`2925:${actionLabel}失败,登录后仍未进入收件箱。`);
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
await addLog('2925:未触发自动登录,继续复用当前已登录会话。', 'info');
|
||||
return {
|
||||
account: null,
|
||||
mail: getMail2925MailConfig(),
|
||||
result: {
|
||||
...result,
|
||||
usedExistingSession: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await patchMail2925Account(account.id, {
|
||||
lastLoginAt: Date.now(),
|
||||
lastError: '',
|
||||
});
|
||||
await setState({ currentMail2925AccountId: account.id });
|
||||
broadcastDataUpdate({ currentMail2925AccountId: account.id });
|
||||
|
||||
const finalUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
|
||||
await addLog(`2925:登录态确认成功,当前地址=${finalUrl || 'unknown'}`, 'ok');
|
||||
|
||||
return {
|
||||
account: await ensureMail2925AccountForFlow({
|
||||
allowAllocate: false,
|
||||
preferredAccountId: account.id,
|
||||
}),
|
||||
mail: getMail2925MailConfig(),
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleMail2925LimitReachedError(step, error) {
|
||||
const reason = getErrorMessage(error).replace(MAIL2925_LIMIT_ERROR_PREFIX, '').trim()
|
||||
|| '子邮箱已达上限邮箱';
|
||||
const state = await getState();
|
||||
const currentAccount = getCurrentMail2925Account(state);
|
||||
const poolEnabled = Boolean(state?.mail2925UseAccountPool);
|
||||
|
||||
if (!poolEnabled) {
|
||||
if (typeof requestStop === 'function') {
|
||||
await requestStop({
|
||||
logMessage: `步骤 ${step}:2925 检测到“${reason}”,当前未启用账号池,已按手动停止逻辑暂停自动流程。`,
|
||||
});
|
||||
}
|
||||
return new Error('流程已被用户停止。');
|
||||
}
|
||||
|
||||
if (!currentAccount) {
|
||||
if (typeof requestStop === 'function') {
|
||||
await requestStop({
|
||||
logMessage: `步骤 ${step}:2925 检测到“${reason}”,但当前没有可识别的账号可供切换。`,
|
||||
});
|
||||
}
|
||||
return new Error('流程已被用户停止。');
|
||||
}
|
||||
|
||||
const disabledUntil = Date.now() + Math.max(1, Number(MAIL2925_LIMIT_COOLDOWN_MS) || (24 * 60 * 60 * 1000));
|
||||
await patchMail2925Account(currentAccount.id, {
|
||||
lastLimitAt: Date.now(),
|
||||
disabledUntil,
|
||||
lastError: reason,
|
||||
});
|
||||
await addLog(
|
||||
`步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,已禁用到 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })}。`,
|
||||
'warn'
|
||||
);
|
||||
|
||||
const nextState = await getState();
|
||||
const nextAccounts = normalizeMail2925Accounts(nextState.mail2925Accounts);
|
||||
const nextAccount = pickMail2925AccountForRun(nextAccounts, {
|
||||
excludeIds: [currentAccount.id],
|
||||
});
|
||||
|
||||
if (!nextAccount) {
|
||||
await setPersistentSettings({ currentMail2925AccountId: '' });
|
||||
await setState({ currentMail2925AccountId: null });
|
||||
broadcastDataUpdate({ currentMail2925AccountId: null });
|
||||
if (typeof requestStop === 'function') {
|
||||
await requestStop({
|
||||
logMessage: `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,但当前没有可切换的下一个账号。`,
|
||||
});
|
||||
}
|
||||
return new Error('流程已被用户停止。');
|
||||
}
|
||||
|
||||
await setCurrentMail2925Account(nextAccount.id);
|
||||
await ensureMail2925MailboxSession({
|
||||
accountId: nextAccount.id,
|
||||
forceRelogin: true,
|
||||
allowLoginWhenOnLoginPage: true,
|
||||
actionLabel: `步骤 ${step}:切换 2925 账号`,
|
||||
});
|
||||
await addLog(`步骤 ${step}:2925 已切换到下一个账号 ${nextAccount.email}。`, 'warn');
|
||||
return buildMail2925ThreadTerminatedError(
|
||||
`步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,已切换到 ${nextAccount.email},当前尝试结束,等待下一轮重试。`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
MAIL2925_LIMIT_ERROR_PREFIX,
|
||||
MAIL2925_THREAD_TERMINATED_ERROR_PREFIX,
|
||||
clearMail2925SessionCookies,
|
||||
deleteMail2925Account,
|
||||
deleteMail2925Accounts,
|
||||
ensureMail2925AccountForFlow,
|
||||
ensureMail2925MailboxSession,
|
||||
getCurrentMail2925Account,
|
||||
getMail2925MailConfig,
|
||||
handleMail2925LimitReachedError,
|
||||
isMail2925LimitReachedError,
|
||||
isMail2925ThreadTerminatedError,
|
||||
patchMail2925Account,
|
||||
setCurrentMail2925Account,
|
||||
syncMail2925Accounts,
|
||||
upsertMail2925Account,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createMail2925SessionManager,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
(function attachBackgroundMailRuleRegistry(root, factory) {
|
||||
root.MultiPageBackgroundMailRuleRegistry = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundMailRuleRegistryModule() {
|
||||
function createMailRuleRegistry(deps = {}) {
|
||||
const {
|
||||
defaultFlowId = 'openai',
|
||||
flowBuilders = {},
|
||||
} = deps;
|
||||
|
||||
function resolveFlowId(state = {}) {
|
||||
const activeFlowId = String(state?.activeFlowId || '').trim();
|
||||
return activeFlowId || String(defaultFlowId || '').trim() || 'openai';
|
||||
}
|
||||
|
||||
function getFlowBuilder(flowId) {
|
||||
const normalizedFlowId = String(flowId || '').trim();
|
||||
return normalizedFlowId ? flowBuilders[normalizedFlowId] || null : null;
|
||||
}
|
||||
|
||||
function getVerificationMailRule(step, state = {}) {
|
||||
const flowId = resolveFlowId(state);
|
||||
const flowBuilder = getFlowBuilder(flowId);
|
||||
if (!flowBuilder || typeof flowBuilder.getRuleDefinition !== 'function') {
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件规则定义。`);
|
||||
}
|
||||
return flowBuilder.getRuleDefinition(step, state);
|
||||
}
|
||||
|
||||
function getVerificationMailRuleForNode(nodeId, state = {}) {
|
||||
const flowId = resolveFlowId(state);
|
||||
const flowBuilder = getFlowBuilder(flowId);
|
||||
if (!flowBuilder) {
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件规则定义。`);
|
||||
}
|
||||
if (typeof flowBuilder.getRuleDefinitionForNode === 'function') {
|
||||
return flowBuilder.getRuleDefinitionForNode(nodeId, state);
|
||||
}
|
||||
if (typeof flowBuilder.getRuleDefinition === 'function') {
|
||||
return flowBuilder.getRuleDefinition({ nodeId }, state);
|
||||
}
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件规则定义。`);
|
||||
}
|
||||
|
||||
function buildVerificationPollPayload(step, state = {}, overrides = {}) {
|
||||
const flowId = resolveFlowId(state);
|
||||
const flowBuilder = getFlowBuilder(flowId);
|
||||
if (!flowBuilder || typeof flowBuilder.buildVerificationPollPayload !== 'function') {
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件轮询规则构造器。`);
|
||||
}
|
||||
return flowBuilder.buildVerificationPollPayload(step, state, overrides);
|
||||
}
|
||||
|
||||
function buildVerificationPollPayloadForNode(nodeId, state = {}, overrides = {}) {
|
||||
const flowId = resolveFlowId(state);
|
||||
const flowBuilder = getFlowBuilder(flowId);
|
||||
if (!flowBuilder) {
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件轮询规则构造器。`);
|
||||
}
|
||||
if (typeof flowBuilder.buildVerificationPollPayloadForNode === 'function') {
|
||||
return flowBuilder.buildVerificationPollPayloadForNode(nodeId, state, overrides);
|
||||
}
|
||||
if (typeof flowBuilder.buildVerificationPollPayload === 'function') {
|
||||
return flowBuilder.buildVerificationPollPayload({ nodeId }, state, overrides);
|
||||
}
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件轮询规则构造器。`);
|
||||
}
|
||||
|
||||
return {
|
||||
buildVerificationPollPayload,
|
||||
buildVerificationPollPayloadForNode,
|
||||
getVerificationMailRule,
|
||||
getVerificationMailRuleForNode,
|
||||
resolveFlowId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createMailRuleRegistry,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,228 @@
|
||||
(function attachBackgroundNavigationUtils(root, factory) {
|
||||
root.MultiPageBackgroundNavigationUtils = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundNavigationUtilsModule() {
|
||||
function createNavigationUtils(deps = {}) {
|
||||
const {
|
||||
DEFAULT_CODEX2API_URL,
|
||||
DEFAULT_SUB2API_URL,
|
||||
normalizeLocalCpaStep9Mode,
|
||||
sourceRegistry = null,
|
||||
} = deps;
|
||||
|
||||
function parseUrlSafely(rawUrl) {
|
||||
if (!rawUrl) return null;
|
||||
try {
|
||||
return new URL(rawUrl);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSub2ApiUrl(rawUrl) {
|
||||
const input = (rawUrl || '').trim() || DEFAULT_SUB2API_URL;
|
||||
if (!input) return '';
|
||||
const withProtocol = /^https?:\/\//i.test(input) ? input : `https://${input}`;
|
||||
const parsed = new URL(withProtocol);
|
||||
if (!parsed.pathname || parsed.pathname === '/') {
|
||||
parsed.pathname = '/admin/accounts';
|
||||
}
|
||||
parsed.hash = '';
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function normalizeCodex2ApiUrl(rawUrl) {
|
||||
const input = (rawUrl || '').trim() || DEFAULT_CODEX2API_URL;
|
||||
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
|
||||
const parsed = new URL(withProtocol);
|
||||
if (!parsed.pathname || parsed.pathname === '/' || parsed.pathname === '/admin') {
|
||||
parsed.pathname = '/admin/accounts';
|
||||
}
|
||||
parsed.hash = '';
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function getPanelMode(state = {}) {
|
||||
if (state.panelMode === 'local-cpa-json') {
|
||||
return 'local-cpa-json';
|
||||
}
|
||||
if (state.panelMode === 'local-cpa-json-no-rt') {
|
||||
return 'local-cpa-json-no-rt';
|
||||
}
|
||||
if (state.panelMode === 'sub2api') {
|
||||
return 'sub2api';
|
||||
}
|
||||
if (state.panelMode === 'codex2api') {
|
||||
return 'codex2api';
|
||||
}
|
||||
return 'cpa';
|
||||
}
|
||||
|
||||
function getPanelModeLabel(modeOrState) {
|
||||
const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState);
|
||||
if (mode === 'local-cpa-json') {
|
||||
return '本地CPA JSON 有RT';
|
||||
}
|
||||
if (mode === 'local-cpa-json-no-rt') {
|
||||
return '本地CPA JSON 无RT';
|
||||
}
|
||||
if (mode === 'sub2api') {
|
||||
return 'SUB2API';
|
||||
}
|
||||
if (mode === 'codex2api') {
|
||||
return 'Codex2API';
|
||||
}
|
||||
return 'CPA';
|
||||
}
|
||||
|
||||
function isSignupPageHost(hostname = '') {
|
||||
return ['auth0.openai.com', 'auth.openai.com', 'accounts.openai.com'].includes(hostname);
|
||||
}
|
||||
|
||||
function isSignupEntryHost(hostname = '') {
|
||||
return ['chatgpt.com', 'www.chatgpt.com', 'chat.openai.com'].includes(hostname);
|
||||
}
|
||||
|
||||
function isSignupPasswordPageUrl(rawUrl) {
|
||||
const parsed = parseUrlSafely(rawUrl);
|
||||
if (!parsed) return false;
|
||||
return isSignupPageHost(parsed.hostname)
|
||||
&& /\/(?:create-account|log-in)\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
}
|
||||
|
||||
function isSignupEmailVerificationPageUrl(rawUrl) {
|
||||
const parsed = parseUrlSafely(rawUrl);
|
||||
if (!parsed) return false;
|
||||
return isSignupPageHost(parsed.hostname)
|
||||
&& /\/email-verification(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
}
|
||||
|
||||
function is163MailHost(hostname = '') {
|
||||
return hostname === 'mail.163.com'
|
||||
|| hostname.endsWith('.mail.163.com')
|
||||
|| hostname === 'mail.126.com'
|
||||
|| hostname.endsWith('.mail.126.com')
|
||||
|| hostname === 'webmail.vip.163.com';
|
||||
}
|
||||
|
||||
function isLocalhostOAuthCallbackUrl(rawUrl) {
|
||||
const parsed = parseUrlSafely(rawUrl);
|
||||
if (!parsed) return false;
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) return false;
|
||||
if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) return false;
|
||||
if (!['/auth/callback', '/codex/callback'].includes(parsed.pathname)) return false;
|
||||
|
||||
const code = (parsed.searchParams.get('code') || '').trim();
|
||||
const state = (parsed.searchParams.get('state') || '').trim();
|
||||
return Boolean(code && state);
|
||||
}
|
||||
|
||||
function isLocalCpaUrl(rawUrl) {
|
||||
const parsed = parseUrlSafely(rawUrl);
|
||||
if (!parsed) return false;
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) return false;
|
||||
return ['localhost', '127.0.0.1'].includes(parsed.hostname);
|
||||
}
|
||||
|
||||
function shouldBypassStep9ForLocalCpa(state) {
|
||||
return normalizeLocalCpaStep9Mode(state?.localCpaStep9Mode) === 'bypass'
|
||||
&& Boolean(state?.localhostUrl)
|
||||
&& isLocalCpaUrl(state?.vpsUrl);
|
||||
}
|
||||
|
||||
function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
|
||||
if (sourceRegistry?.matchesSourceUrlFamily) {
|
||||
return sourceRegistry.matchesSourceUrlFamily(source, candidateUrl, referenceUrl);
|
||||
}
|
||||
const candidate = parseUrlSafely(candidateUrl);
|
||||
if (!candidate) return false;
|
||||
|
||||
const reference = parseUrlSafely(referenceUrl);
|
||||
|
||||
switch (source) {
|
||||
case 'openai-auth':
|
||||
case 'signup-page':
|
||||
return isSignupPageHost(candidate.hostname) || isSignupEntryHost(candidate.hostname);
|
||||
case 'duck-mail':
|
||||
return candidate.hostname === 'duckduckgo.com' && candidate.pathname.startsWith('/email/');
|
||||
case 'qq-mail':
|
||||
return candidate.hostname === 'mail.qq.com' || candidate.hostname === 'wx.mail.qq.com';
|
||||
case 'mail-163':
|
||||
return is163MailHost(candidate.hostname);
|
||||
case 'gmail-mail':
|
||||
return candidate.hostname === 'mail.google.com';
|
||||
case 'icloud-mail':
|
||||
return candidate.hostname === 'www.icloud.com'
|
||||
|| candidate.hostname === 'www.icloud.com.cn';
|
||||
case 'inbucket-mail':
|
||||
return Boolean(reference)
|
||||
&& candidate.origin === reference.origin
|
||||
&& candidate.pathname.startsWith('/m/');
|
||||
case 'mail-2925':
|
||||
return candidate.hostname === '2925.com' || candidate.hostname === 'www.2925.com';
|
||||
case 'vps-panel':
|
||||
return Boolean(reference)
|
||||
&& candidate.origin === reference.origin
|
||||
&& candidate.pathname === reference.pathname;
|
||||
case 'sub2api-panel':
|
||||
return Boolean(reference)
|
||||
&& candidate.origin === reference.origin
|
||||
&& (
|
||||
candidate.pathname.startsWith('/admin/accounts')
|
||||
|| candidate.pathname.startsWith('/login')
|
||||
|| candidate.pathname === '/'
|
||||
);
|
||||
case 'codex2api-panel':
|
||||
return Boolean(reference)
|
||||
&& candidate.origin === reference.origin
|
||||
&& (
|
||||
candidate.pathname.startsWith('/admin/accounts')
|
||||
|| candidate.pathname === '/admin'
|
||||
|| candidate.pathname === '/'
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
|
||||
if (!Number.isInteger(signupTabId) || !details) return '';
|
||||
if (details.tabId !== signupTabId) return '';
|
||||
if (details.frameId !== 0) return '';
|
||||
return isLocalhostOAuthCallbackUrl(details.url) ? details.url : '';
|
||||
}
|
||||
|
||||
function getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId) {
|
||||
if (!Number.isInteger(signupTabId) || tabId !== signupTabId) return '';
|
||||
const candidates = [changeInfo?.url, tab?.url];
|
||||
for (const candidate of candidates) {
|
||||
if (isLocalhostOAuthCallbackUrl(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return {
|
||||
getPanelMode,
|
||||
getPanelModeLabel,
|
||||
getStep8CallbackUrlFromNavigation,
|
||||
getStep8CallbackUrlFromTabUpdate,
|
||||
is163MailHost,
|
||||
isLocalCpaUrl,
|
||||
isLocalhostOAuthCallbackUrl,
|
||||
isSignupEmailVerificationPageUrl,
|
||||
isSignupEntryHost,
|
||||
isSignupPageHost,
|
||||
isSignupPasswordPageUrl,
|
||||
matchesSourceUrlFamily,
|
||||
normalizeCodex2ApiUrl,
|
||||
normalizeSub2ApiUrl,
|
||||
parseUrlSafely,
|
||||
shouldBypassStep9ForLocalCpa,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createNavigationUtils,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,344 @@
|
||||
(function attachBackgroundPanelBridge(root, factory) {
|
||||
root.MultiPageBackgroundPanelBridge = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPanelBridgeModule() {
|
||||
function createPanelBridge(deps = {}) {
|
||||
const {
|
||||
chrome,
|
||||
addLog,
|
||||
createLocalCliProxyApi = null,
|
||||
closeConflictingTabsForSource,
|
||||
createAutomationTab = null,
|
||||
ensureContentScriptReadyOnTab,
|
||||
getPanelMode,
|
||||
normalizeCodex2ApiUrl,
|
||||
normalizeSub2ApiUrl,
|
||||
rememberSourceLastUrl,
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
waitForTabUrlFamily,
|
||||
DEFAULT_SUB2API_GROUP_NAME,
|
||||
SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||
} = deps;
|
||||
|
||||
let sub2ApiApi = null;
|
||||
let localCliProxyApi = null;
|
||||
|
||||
function getSub2ApiApi() {
|
||||
if (sub2ApiApi) {
|
||||
return sub2ApiApi;
|
||||
}
|
||||
const factory = deps.createSub2ApiApi
|
||||
|| self.MultiPageBackgroundSub2ApiApi?.createSub2ApiApi;
|
||||
if (typeof factory !== 'function') {
|
||||
throw new Error('SUB2API 直连接口模块未加载,无法生成 OAuth 链接。');
|
||||
}
|
||||
sub2ApiApi = factory({
|
||||
addLog,
|
||||
normalizeSub2ApiUrl,
|
||||
DEFAULT_SUB2API_GROUP_NAME,
|
||||
});
|
||||
return sub2ApiApi;
|
||||
}
|
||||
|
||||
function normalizeAdminKey(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function getLocalCliProxyApi() {
|
||||
if (localCliProxyApi) {
|
||||
return localCliProxyApi;
|
||||
}
|
||||
const factory = createLocalCliProxyApi
|
||||
|| self.MultiPageBackgroundLocalCliProxyApi?.createLocalCliProxyApi;
|
||||
if (typeof factory !== 'function') {
|
||||
throw new Error('本地 CPA JSON 有RT 模块未加载,无法生成 OAuth 链接。');
|
||||
}
|
||||
localCliProxyApi = factory({
|
||||
crypto: globalThis.crypto,
|
||||
fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||
sessionToJsonConverter: self.MultiPageSessionToJsonConverter,
|
||||
});
|
||||
return localCliProxyApi;
|
||||
}
|
||||
|
||||
function extractStateFromAuthUrl(authUrl = '') {
|
||||
try {
|
||||
return new URL(authUrl).searchParams.get('state') || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getCodex2ApiErrorMessage(payload, responseStatus = 500) {
|
||||
const candidates = [
|
||||
payload?.error,
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.reason,
|
||||
];
|
||||
const message = candidates
|
||||
.map((value) => String(value || '').trim())
|
||||
.find(Boolean);
|
||||
return message || `Codex2API 请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
function deriveCpaManagementOrigin(vpsUrl) {
|
||||
const normalizedUrl = String(vpsUrl || '').trim();
|
||||
if (!normalizedUrl) {
|
||||
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(normalizedUrl);
|
||||
} catch {
|
||||
throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
|
||||
}
|
||||
return parsed.origin;
|
||||
}
|
||||
|
||||
function getCpaApiErrorMessage(payload, responseStatus = 500) {
|
||||
const candidates = [
|
||||
payload?.error,
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.reason,
|
||||
];
|
||||
const message = candidates
|
||||
.map((value) => String(value || '').trim())
|
||||
.find(Boolean);
|
||||
return message || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
async function fetchCpaManagementJson(origin, path, options = {}) {
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const managementKey = String(options.managementKey || '').trim();
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (managementKey) {
|
||||
headers.Authorization = `Bearer ${managementKey}`;
|
||||
headers['X-Management-Key'] = managementKey;
|
||||
}
|
||||
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method: options.method || 'POST',
|
||||
headers,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getCpaApiErrorMessage(payload, response.status));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('CPA 管理接口请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCodex2ApiJson(origin, path, options = {}) {
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method: options.method || 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Key': normalizeAdminKey(options.adminKey),
|
||||
},
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getCodex2ApiErrorMessage(payload, response.status));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('Codex2API 请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestOAuthUrlFromPanel(state, options = {}) {
|
||||
if (getPanelMode(state) === 'local-cpa-json') {
|
||||
return requestLocalCpaJsonOAuthUrl(state, options);
|
||||
}
|
||||
if (getPanelMode(state) === 'codex2api') {
|
||||
return requestCodex2ApiOAuthUrl(state, options);
|
||||
}
|
||||
if (getPanelMode(state) === 'sub2api') {
|
||||
return requestSub2ApiOAuthUrl(state, options);
|
||||
}
|
||||
return requestCpaOAuthUrl(state, options);
|
||||
}
|
||||
|
||||
async function requestCpaOAuthUrl(state, options = {}) {
|
||||
const { logLabel = 'OAuth 刷新' } = options;
|
||||
if (!state.vpsUrl) {
|
||||
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
||||
}
|
||||
const managementKey = String(state.vpsPassword || '').trim();
|
||||
if (!managementKey) {
|
||||
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const origin = deriveCpaManagementOrigin(state.vpsUrl);
|
||||
|
||||
await addLog(`${logLabel}:正在通过 CPA 管理接口获取 OAuth 授权链接...`);
|
||||
const result = await fetchCpaManagementJson(origin, '/v0/management/codex-auth-url', {
|
||||
method: 'GET',
|
||||
managementKey,
|
||||
});
|
||||
|
||||
const oauthUrl = String(
|
||||
result?.url
|
||||
|| result?.auth_url
|
||||
|| result?.authUrl
|
||||
|| result?.data?.url
|
||||
|| result?.data?.auth_url
|
||||
|| result?.data?.authUrl
|
||||
|| ''
|
||||
).trim();
|
||||
const oauthState = String(
|
||||
result?.state
|
||||
|| result?.auth_state
|
||||
|| result?.authState
|
||||
|| result?.data?.state
|
||||
|| result?.data?.auth_state
|
||||
|| result?.data?.authState
|
||||
|| ''
|
||||
).trim()
|
||||
|| extractStateFromAuthUrl(oauthUrl);
|
||||
|
||||
if (!oauthUrl || !oauthUrl.startsWith('http')) {
|
||||
throw new Error('CPA 管理接口未返回有效的 auth_url。');
|
||||
}
|
||||
|
||||
return {
|
||||
oauthUrl,
|
||||
cpaOAuthState: oauthState || null,
|
||||
cpaManagementOrigin: origin,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestLocalCpaJsonOAuthUrl(state, options = {}) {
|
||||
const { logLabel = 'OAuth 刷新' } = options;
|
||||
if (!String(state?.localCpaJsonPluginDir || '').trim()) {
|
||||
throw new Error('尚未配置本地插件目录,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await addLog(`${logLabel}:正在按本地 CPA JSON 有RT 规则生成 OAuth 授权链接...`);
|
||||
const api = getLocalCliProxyApi();
|
||||
const result = await api.createAuthorizationRequest();
|
||||
|
||||
return {
|
||||
oauthUrl: result.oauthUrl,
|
||||
localCpaJsonOAuthState: result.oauthState || null,
|
||||
localCpaJsonPkceCodes: result.pkceCodes || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestCodex2ApiOAuthUrl(state, options = {}) {
|
||||
const { logLabel = 'OAuth 刷新' } = options;
|
||||
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
|
||||
const adminKey = normalizeAdminKey(state.codex2apiAdminKey);
|
||||
|
||||
if (!adminKey) {
|
||||
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const origin = new URL(codex2apiUrl).origin;
|
||||
await addLog(`${logLabel}:正在通过 Codex2API 协议生成 OAuth 授权链接...`);
|
||||
|
||||
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/generate-auth-url', {
|
||||
adminKey,
|
||||
method: 'POST',
|
||||
body: {},
|
||||
});
|
||||
|
||||
const oauthUrl = String(result?.auth_url || result?.authUrl || '').trim();
|
||||
const sessionId = String(result?.session_id || result?.sessionId || '').trim();
|
||||
const oauthState = extractStateFromAuthUrl(oauthUrl);
|
||||
|
||||
if (!oauthUrl || !sessionId) {
|
||||
throw new Error('Codex2API 未返回有效的 auth_url 或 session_id。');
|
||||
}
|
||||
|
||||
return {
|
||||
oauthUrl,
|
||||
codex2apiSessionId: sessionId,
|
||||
codex2apiOAuthState: oauthState || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestSub2ApiOAuthUrl(state, options = {}) {
|
||||
const { logLabel = 'OAuth 刷新' } = options;
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
}
|
||||
if (!state.sub2apiEmail) {
|
||||
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
||||
}
|
||||
if (!state.sub2apiPassword) {
|
||||
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const api = getSub2ApiApi();
|
||||
return api.generateOpenAiAuthUrl({
|
||||
...state,
|
||||
sub2apiUrl,
|
||||
}, {
|
||||
logLabel,
|
||||
timeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
requestOAuthUrlFromPanel,
|
||||
requestLocalCpaJsonOAuthUrl,
|
||||
requestCodex2ApiOAuthUrl,
|
||||
requestCpaOAuthUrl,
|
||||
requestSub2ApiOAuthUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createPanelBridge,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
(function attachBackgroundPayPalAccountStore(root, factory) {
|
||||
root.MultiPageBackgroundPayPalAccountStore = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPayPalAccountStoreModule() {
|
||||
function createPayPalAccountStore(deps = {}) {
|
||||
const {
|
||||
broadcastDataUpdate,
|
||||
findPayPalAccount,
|
||||
getState,
|
||||
normalizePayPalAccount,
|
||||
normalizePayPalAccounts,
|
||||
setPersistentSettings,
|
||||
setState,
|
||||
upsertPayPalAccountInList,
|
||||
} = deps;
|
||||
|
||||
async function syncSelectedPayPalAccountState(account = null) {
|
||||
const updates = account
|
||||
? {
|
||||
currentPayPalAccountId: account.id,
|
||||
paypalEmail: String(account.email || '').trim(),
|
||||
paypalPassword: String(account.password || ''),
|
||||
}
|
||||
: {
|
||||
currentPayPalAccountId: '',
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
};
|
||||
|
||||
await setPersistentSettings(updates);
|
||||
await setState({
|
||||
currentPayPalAccountId: updates.currentPayPalAccountId || null,
|
||||
paypalEmail: updates.paypalEmail,
|
||||
paypalPassword: updates.paypalPassword,
|
||||
});
|
||||
broadcastDataUpdate({
|
||||
currentPayPalAccountId: updates.currentPayPalAccountId || null,
|
||||
paypalEmail: updates.paypalEmail,
|
||||
paypalPassword: updates.paypalPassword,
|
||||
});
|
||||
}
|
||||
|
||||
async function syncPayPalAccounts(accounts) {
|
||||
const normalized = normalizePayPalAccounts(accounts);
|
||||
await setPersistentSettings({ paypalAccounts: normalized });
|
||||
await setState({ paypalAccounts: normalized });
|
||||
broadcastDataUpdate({ paypalAccounts: normalized });
|
||||
|
||||
const state = await getState();
|
||||
if (state.currentPayPalAccountId && !findPayPalAccount(normalized, state.currentPayPalAccountId)) {
|
||||
await syncSelectedPayPalAccountState(null);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getCurrentPayPalAccount(state = {}) {
|
||||
return findPayPalAccount(state.paypalAccounts, state.currentPayPalAccountId) || null;
|
||||
}
|
||||
|
||||
async function upsertPayPalAccount(input = {}) {
|
||||
const state = await getState();
|
||||
const accounts = normalizePayPalAccounts(state.paypalAccounts);
|
||||
const normalizedEmail = String(input?.email || '').trim().toLowerCase();
|
||||
const existing = input?.id
|
||||
? findPayPalAccount(accounts, input.id)
|
||||
: accounts.find((account) => account.email === normalizedEmail) || null;
|
||||
const normalized = normalizePayPalAccount({
|
||||
...(existing || {}),
|
||||
...input,
|
||||
id: input?.id || existing?.id || crypto.randomUUID(),
|
||||
createdAt: existing?.createdAt || Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
||||
const nextAccounts = typeof upsertPayPalAccountInList === 'function'
|
||||
? upsertPayPalAccountInList(accounts, normalized)
|
||||
: accounts.concat(normalized);
|
||||
|
||||
await syncPayPalAccounts(nextAccounts);
|
||||
|
||||
if (state.currentPayPalAccountId === normalized.id) {
|
||||
await syncSelectedPayPalAccountState(normalized);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function setCurrentPayPalAccount(accountId) {
|
||||
const state = await getState();
|
||||
const accounts = normalizePayPalAccounts(state.paypalAccounts);
|
||||
const account = findPayPalAccount(accounts, accountId);
|
||||
if (!account) {
|
||||
throw new Error('未找到对应的 PayPal 账号。');
|
||||
}
|
||||
|
||||
await syncSelectedPayPalAccountState(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentPayPalAccount,
|
||||
setCurrentPayPalAccount,
|
||||
syncPayPalAccounts,
|
||||
upsertPayPalAccount,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createPayPalAccountStore,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,146 @@
|
||||
(function attachBackgroundPlusSuccessSessionUpload(root, factory) {
|
||||
root.MultiPageBackgroundPlusSuccessSessionUpload = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusSuccessSessionUploadModule() {
|
||||
const PAYMENTS_SUCCESS_URL_PATTERN = /^https:\/\/(?:chatgpt\.com|www\.chatgpt\.com|chat\.openai\.com)\/(?:backend-api\/)?payments\/success(?:[/?#]|$)/i;
|
||||
|
||||
function createPlusSuccessSessionUploadManager(deps = {}) {
|
||||
const {
|
||||
addLog: rawAddLog = async () => {},
|
||||
completeNodeFromBackground = null,
|
||||
failNodeFromBackground = null,
|
||||
getState = async () => ({}),
|
||||
setState = async () => {},
|
||||
delay = (ms = 0) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0))),
|
||||
} = deps;
|
||||
|
||||
const activeTabIds = new Set();
|
||||
|
||||
function addLog(message, level = 'info', options = {}) {
|
||||
return rawAddLog(message, level, {
|
||||
step: 6,
|
||||
stepKey: 'plus-checkout-create',
|
||||
...(options && typeof options === 'object' ? options : {}),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function isPaymentsSuccessUrl(url = '') {
|
||||
return PAYMENTS_SUCCESS_URL_PATTERN.test(String(url || ''));
|
||||
}
|
||||
|
||||
function normalizeOauthDelaySeconds(value = 0) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(3600, Math.max(0, Math.floor(numeric)));
|
||||
}
|
||||
|
||||
function isHostedCheckoutSuccessWaitActive(state = {}, tabId = null) {
|
||||
if (normalizeString(state?.plusPaymentMethod).toLowerCase() !== 'paypal') {
|
||||
return false;
|
||||
}
|
||||
if (state?.plusHostedCheckoutIsFinalStep === false) {
|
||||
return false;
|
||||
}
|
||||
const nodeStatus = normalizeString(state?.nodeStatuses?.['plus-checkout-create']).toLowerCase();
|
||||
if (nodeStatus && nodeStatus !== 'running' && nodeStatus !== 'pending') {
|
||||
return false;
|
||||
}
|
||||
const checkoutTabId = Number(state?.plusCheckoutTabId);
|
||||
if (!Number.isInteger(checkoutTabId) || checkoutTabId <= 0) {
|
||||
return false;
|
||||
}
|
||||
return tabId === null || checkoutTabId === Number(tabId);
|
||||
}
|
||||
|
||||
async function processPaymentsSuccessTab(tabId, successUrl = '') {
|
||||
const numericTabId = Number(tabId);
|
||||
if (!Number.isInteger(numericTabId) || activeTabIds.has(numericTabId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialState = await getState();
|
||||
if (!isHostedCheckoutSuccessWaitActive(initialState, numericTabId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
activeTabIds.add(numericTabId);
|
||||
try {
|
||||
const latestState = await getState();
|
||||
if (!isHostedCheckoutSuccessWaitActive(latestState, numericTabId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedSuccessUrl = normalizeString(successUrl);
|
||||
await setState({
|
||||
plusReturnUrl: normalizedSuccessUrl,
|
||||
});
|
||||
await addLog('步骤 6:检测到 ChatGPT 支付成功页,准备继续 OAuth 流程。', 'ok');
|
||||
|
||||
const oauthDelaySeconds = normalizeOauthDelaySeconds(latestState?.plusHostedCheckoutOauthDelaySeconds);
|
||||
if (oauthDelaySeconds > 0) {
|
||||
await addLog(`步骤 6:已按设置等待 ${oauthDelaySeconds} 秒,之后再进入 OAuth 登录。`, 'info');
|
||||
await delay(oauthDelaySeconds * 1000);
|
||||
const delayedState = await getState();
|
||||
if (!isHostedCheckoutSuccessWaitActive(delayedState, numericTabId)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof completeNodeFromBackground === 'function') {
|
||||
await completeNodeFromBackground('plus-checkout-create', {
|
||||
plusReturnUrl: normalizedSuccessUrl,
|
||||
plusHostedCheckoutCompleted: true,
|
||||
plusHostedCheckoutOauthDelaySeconds: oauthDelaySeconds,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
completed: true,
|
||||
plusReturnUrl: normalizedSuccessUrl,
|
||||
oauthDelaySeconds,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = normalizeString(error?.message) || 'unknown error';
|
||||
await addLog(`支付成功页收尾失败:${message}`, 'error');
|
||||
if (typeof failNodeFromBackground === 'function') {
|
||||
await failNodeFromBackground('plus-checkout-create', message);
|
||||
return {
|
||||
completed: false,
|
||||
failed: true,
|
||||
message,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
activeTabIds.delete(numericTabId);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTabUpdated(tabId, changeInfo = {}, tab = {}) {
|
||||
if (changeInfo?.status !== 'complete') {
|
||||
return null;
|
||||
}
|
||||
const nextUrl = normalizeString(changeInfo?.url || tab?.url);
|
||||
if (!isPaymentsSuccessUrl(nextUrl)) {
|
||||
return null;
|
||||
}
|
||||
return processPaymentsSuccessTab(Number(tabId), nextUrl);
|
||||
}
|
||||
|
||||
return {
|
||||
isPaymentsSuccessUrl,
|
||||
isHostedCheckoutSuccessWaitActive,
|
||||
processPaymentsSuccessTab,
|
||||
handleTabUpdated,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createPlusSuccessSessionUploadManager,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
(function attachBackgroundRegistrationEmailState(root, factory) {
|
||||
root.MultiPageRegistrationEmailState = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundRegistrationEmailStateModule() {
|
||||
const DEFAULT_REGISTRATION_EMAIL_STATE = Object.freeze({
|
||||
current: '',
|
||||
previous: '',
|
||||
source: '',
|
||||
updatedAt: 0,
|
||||
});
|
||||
|
||||
function createRegistrationEmailStateHelpers() {
|
||||
function normalizeEmailValue(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function cloneDefaultRegistrationEmailState() {
|
||||
return {
|
||||
current: DEFAULT_REGISTRATION_EMAIL_STATE.current,
|
||||
previous: DEFAULT_REGISTRATION_EMAIL_STATE.previous,
|
||||
source: DEFAULT_REGISTRATION_EMAIL_STATE.source,
|
||||
updatedAt: DEFAULT_REGISTRATION_EMAIL_STATE.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRegistrationEmailState(value, fallbackEmail = '') {
|
||||
const fallback = normalizeEmailValue(fallbackEmail);
|
||||
const candidate = value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value
|
||||
: null;
|
||||
const current = normalizeEmailValue(candidate?.current || fallback);
|
||||
const previous = normalizeEmailValue(candidate?.previous || current || fallback);
|
||||
const source = String(candidate?.source || '').trim();
|
||||
const updatedAt = Number(candidate?.updatedAt) || 0;
|
||||
return {
|
||||
current,
|
||||
previous,
|
||||
source,
|
||||
updatedAt: updatedAt > 0 ? updatedAt : 0,
|
||||
};
|
||||
}
|
||||
|
||||
function getRegistrationEmailState(state = {}) {
|
||||
return normalizeRegistrationEmailState(state?.registrationEmailState, state?.email);
|
||||
}
|
||||
|
||||
function buildRegistrationEmailStateUpdates(state = {}, options = {}) {
|
||||
const currentState = getRegistrationEmailState(state);
|
||||
const currentEmail = normalizeEmailValue(options.currentEmail);
|
||||
const preservePrevious = Boolean(options.preservePrevious);
|
||||
const source = String(options.source || '').trim();
|
||||
const nextState = cloneDefaultRegistrationEmailState();
|
||||
|
||||
nextState.current = currentEmail;
|
||||
nextState.previous = currentEmail || (preservePrevious ? currentState.previous : '');
|
||||
nextState.source = currentEmail
|
||||
? (source || currentState.source)
|
||||
: (preservePrevious ? currentState.source : '');
|
||||
nextState.updatedAt = currentEmail || (preservePrevious && currentState.previous)
|
||||
? Date.now()
|
||||
: 0;
|
||||
|
||||
return {
|
||||
email: currentEmail || null,
|
||||
registrationEmailState: nextState,
|
||||
};
|
||||
}
|
||||
|
||||
function getPreservedPhoneIdentity(state = {}) {
|
||||
const accountIdentifierType = String(state?.accountIdentifierType || '').trim().toLowerCase();
|
||||
const signupPhoneNumber = normalizeEmailValue(
|
||||
state?.signupPhoneNumber
|
||||
|| (accountIdentifierType === 'phone' ? state?.accountIdentifier : '')
|
||||
|| state?.signupPhoneCompletedActivation?.phoneNumber
|
||||
|| state?.signupPhoneActivation?.phoneNumber
|
||||
|| ''
|
||||
);
|
||||
if (accountIdentifierType !== 'phone' && !signupPhoneNumber) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountIdentifierType: 'phone',
|
||||
accountIdentifier: signupPhoneNumber || normalizeEmailValue(state?.accountIdentifier),
|
||||
signupPhoneNumber,
|
||||
signupPhoneActivation: state?.signupPhoneActivation || null,
|
||||
signupPhoneCompletedActivation: state?.signupPhoneCompletedActivation || null,
|
||||
signupPhoneVerificationRequestedAt: state?.signupPhoneVerificationRequestedAt ?? null,
|
||||
signupPhoneVerificationPurpose: String(state?.signupPhoneVerificationPurpose || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildFlowRegistrationEmailStateUpdates(state = {}, options = {}) {
|
||||
const registrationEmailUpdates = buildRegistrationEmailStateUpdates(state, options);
|
||||
if (!Boolean(options?.preserveAccountIdentity)) {
|
||||
return registrationEmailUpdates;
|
||||
}
|
||||
const preservedPhoneIdentity = getPreservedPhoneIdentity(state);
|
||||
if (!preservedPhoneIdentity) {
|
||||
return registrationEmailUpdates;
|
||||
}
|
||||
return {
|
||||
...registrationEmailUpdates,
|
||||
phoneNumber: '',
|
||||
...preservedPhoneIdentity,
|
||||
};
|
||||
}
|
||||
|
||||
function getRegistrationEmailBaseline(state = {}, options = {}) {
|
||||
const currentState = getRegistrationEmailState(state);
|
||||
const preferredEmail = normalizeEmailValue(options.preferredEmail);
|
||||
const fallbackEmail = normalizeEmailValue(options.fallbackEmail);
|
||||
return preferredEmail || currentState.current || currentState.previous || fallbackEmail || '';
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_REGISTRATION_EMAIL_STATE: cloneDefaultRegistrationEmailState(),
|
||||
buildFlowRegistrationEmailStateUpdates,
|
||||
buildRegistrationEmailStateUpdates,
|
||||
getRegistrationEmailBaseline,
|
||||
getRegistrationEmailState,
|
||||
getPreservedPhoneIdentity,
|
||||
normalizeRegistrationEmailState,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createRegistrationEmailStateHelpers,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,443 @@
|
||||
(function attachBackgroundRuntimeState(root, factory) {
|
||||
root.MultiPageBackgroundRuntimeState = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundRuntimeStateModule() {
|
||||
function createRuntimeStateHelpers(deps = {}) {
|
||||
const {
|
||||
DEFAULT_ACTIVE_FLOW_ID = 'openai',
|
||||
defaultNodeStatuses = {},
|
||||
} = deps;
|
||||
|
||||
const RUNTIME_SHARED_FIELDS = Object.freeze([
|
||||
'automationWindowId',
|
||||
'tabRegistry',
|
||||
'sourceLastUrls',
|
||||
'flowStartTime',
|
||||
]);
|
||||
const RUNTIME_PROXY_FIELDS = Object.freeze([
|
||||
'ipProxyApiPool',
|
||||
'ipProxyApiCurrentIndex',
|
||||
'ipProxyApiCurrent',
|
||||
'ipProxyAccountPool',
|
||||
'ipProxyAccountCurrentIndex',
|
||||
'ipProxyAccountCurrent',
|
||||
'ipProxyPool',
|
||||
'ipProxyCurrentIndex',
|
||||
'ipProxyCurrent',
|
||||
]);
|
||||
const OPENAI_FLOW_FIELD_GROUPS = Object.freeze({
|
||||
auth: Object.freeze([
|
||||
'oauthUrl',
|
||||
'localhostUrl',
|
||||
]),
|
||||
platformBinding: Object.freeze([
|
||||
'cpaOAuthState',
|
||||
'cpaManagementOrigin',
|
||||
'sub2apiSessionId',
|
||||
'sub2apiOAuthState',
|
||||
'sub2apiGroupId',
|
||||
'sub2apiDraftName',
|
||||
'sub2apiProxyId',
|
||||
'sub2apiGroupIds',
|
||||
'codex2apiSessionId',
|
||||
'codex2apiOAuthState',
|
||||
]),
|
||||
plus: Object.freeze([
|
||||
'plusCheckoutTabId',
|
||||
'plusCheckoutUrl',
|
||||
'plusCheckoutCountry',
|
||||
'plusCheckoutCurrency',
|
||||
'plusCheckoutSource',
|
||||
'plusBillingCountryText',
|
||||
'plusBillingAddress',
|
||||
'plusPaypalApprovedAt',
|
||||
'plusGoPayApprovedAt',
|
||||
'plusReturnUrl',
|
||||
'plusAuthUploadStatus',
|
||||
'plusAuthUploadFilename',
|
||||
'plusAuthUploadAt',
|
||||
'plusAuthUploadMessage',
|
||||
'plusAuthUploadSignature',
|
||||
'plusAuthUploadUrl',
|
||||
'plusManualConfirmationPending',
|
||||
'plusManualConfirmationRequestId',
|
||||
'plusManualConfirmationStep',
|
||||
'plusManualConfirmationMethod',
|
||||
'plusManualConfirmationTitle',
|
||||
'plusManualConfirmationMessage',
|
||||
'gopayHelperReferenceId',
|
||||
'gopayHelperGoPayGuid',
|
||||
'gopayHelperRedirectUrl',
|
||||
'gopayHelperNextAction',
|
||||
'gopayHelperFlowId',
|
||||
'gopayHelperChallengeId',
|
||||
'gopayHelperStartPayload',
|
||||
'gopayHelperTaskId',
|
||||
'gopayHelperTaskStatus',
|
||||
'gopayHelperStatusText',
|
||||
'gopayHelperRemoteStage',
|
||||
'gopayHelperApiWaitingFor',
|
||||
'gopayHelperApiInputDeadlineAt',
|
||||
'gopayHelperApiInputWaitSeconds',
|
||||
'gopayHelperLastInputError',
|
||||
'gopayHelperOtpInvalidCount',
|
||||
'gopayHelperFailureStage',
|
||||
'gopayHelperFailureDetail',
|
||||
'gopayHelperTaskPayload',
|
||||
'gopayHelperOrderCreatedAt',
|
||||
'gopayHelperTaskProgressSignature',
|
||||
'gopayHelperTaskProgressAt',
|
||||
'gopayHelperTaskProgressTaskId',
|
||||
'gopayHelperPinPayload',
|
||||
'gopayHelperResolvedOtp',
|
||||
'gopayHelperOtpRequestId',
|
||||
'gopayHelperOtpReferenceId',
|
||||
]),
|
||||
phoneVerification: Object.freeze([
|
||||
'currentPhoneActivation',
|
||||
'phoneNumber',
|
||||
'currentPhoneVerificationCode',
|
||||
'currentPhoneVerificationCountdownEndsAt',
|
||||
'currentPhoneVerificationCountdownWindowIndex',
|
||||
'currentPhoneVerificationCountdownWindowTotal',
|
||||
'reusablePhoneActivation',
|
||||
'freeReusablePhoneActivation',
|
||||
'phoneReusableActivationPool',
|
||||
'signupPhoneNumber',
|
||||
'signupPhoneActivation',
|
||||
'signupPhoneCompletedActivation',
|
||||
'signupPhoneVerificationRequestedAt',
|
||||
'signupPhoneVerificationPurpose',
|
||||
]),
|
||||
luckmail: Object.freeze([
|
||||
'currentLuckmailPurchase',
|
||||
'currentLuckmailMailCursor',
|
||||
]),
|
||||
identity: Object.freeze([
|
||||
'resolvedSignupMethod',
|
||||
'accountIdentifierType',
|
||||
'accountIdentifier',
|
||||
'registrationEmailState',
|
||||
'email',
|
||||
'password',
|
||||
'lastEmailTimestamp',
|
||||
'lastSignupCode',
|
||||
'lastLoginCode',
|
||||
'step8VerificationTargetEmail',
|
||||
]),
|
||||
});
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => cloneValue(item));
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizePlainObject(value) {
|
||||
return isPlainObject(value) ? value : {};
|
||||
}
|
||||
|
||||
function normalizeFlowId(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized || DEFAULT_ACTIVE_FLOW_ID;
|
||||
}
|
||||
|
||||
function normalizeRunId(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function normalizeNodeStatus(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return 'pending';
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildDefaultNodeStatuses() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(normalizePlainObject(defaultNodeStatuses)).map(([key, value]) => [
|
||||
String(key),
|
||||
normalizeNodeStatus(value),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeNodeStatuses(value) {
|
||||
const base = buildDefaultNodeStatuses();
|
||||
if (!isPlainObject(value)) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const next = { ...base };
|
||||
for (const [key, status] of Object.entries(value)) {
|
||||
const nodeId = String(key || '').trim();
|
||||
if (!nodeId) continue;
|
||||
next[nodeId] = normalizeNodeStatus(status);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function pickDefinedFields(state = {}, fields = []) {
|
||||
const next = {};
|
||||
for (const field of fields) {
|
||||
if (Object.prototype.hasOwnProperty.call(state, field)) {
|
||||
next[field] = cloneValue(state[field]);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildSharedState(baseValue = {}, state = {}) {
|
||||
return {
|
||||
...cloneValue(normalizePlainObject(baseValue)),
|
||||
...pickDefinedFields(state, RUNTIME_SHARED_FIELDS),
|
||||
};
|
||||
}
|
||||
|
||||
function buildServiceState(baseValue = {}, state = {}) {
|
||||
const base = cloneValue(normalizePlainObject(baseValue));
|
||||
return {
|
||||
...base,
|
||||
proxy: {
|
||||
...cloneValue(normalizePlainObject(base.proxy)),
|
||||
...pickDefinedFields(state, RUNTIME_PROXY_FIELDS),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function flattenOpenAiFlowState(flowState = {}) {
|
||||
const openaiState = normalizePlainObject(flowState.openai);
|
||||
const next = {};
|
||||
for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) {
|
||||
const group = normalizePlainObject(openaiState[groupKey]);
|
||||
for (const field of fields) {
|
||||
if (Object.prototype.hasOwnProperty.call(group, field)) {
|
||||
next[field] = cloneValue(group[field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildOpenAiFlowState(baseValue = {}, state = {}) {
|
||||
const baseFlowState = cloneValue(normalizePlainObject(baseValue));
|
||||
const baseOpenAi = cloneValue(normalizePlainObject(baseFlowState.openai));
|
||||
const openaiState = {
|
||||
...baseOpenAi,
|
||||
};
|
||||
|
||||
for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) {
|
||||
openaiState[groupKey] = {
|
||||
...cloneValue(normalizePlainObject(baseOpenAi[groupKey])),
|
||||
...pickDefinedFields(state, fields),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseFlowState,
|
||||
openai: openaiState,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRuntimeStateDefault() {
|
||||
return {
|
||||
flowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
runId: '',
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
activeRunId: '',
|
||||
currentNodeId: '',
|
||||
nodeStatuses: {},
|
||||
sharedState: {},
|
||||
serviceState: {
|
||||
proxy: {},
|
||||
},
|
||||
flowState: {
|
||||
openai: {
|
||||
auth: {},
|
||||
platformBinding: {},
|
||||
plus: {},
|
||||
phoneVerification: {},
|
||||
luckmail: {},
|
||||
identity: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ensureRuntimeState(state = {}) {
|
||||
const baseRuntimeState = {
|
||||
...buildRuntimeStateDefault(),
|
||||
...cloneValue(normalizePlainObject(state.runtimeState)),
|
||||
};
|
||||
const activeFlowId = normalizeFlowId(
|
||||
Object.prototype.hasOwnProperty.call(state, 'flowId')
|
||||
? state.flowId
|
||||
: Object.prototype.hasOwnProperty.call(state, 'activeFlowId')
|
||||
? state.activeFlowId
|
||||
: Object.prototype.hasOwnProperty.call(baseRuntimeState, 'flowId')
|
||||
? baseRuntimeState.flowId
|
||||
: baseRuntimeState.activeFlowId
|
||||
);
|
||||
const currentNodeId = String(
|
||||
Object.prototype.hasOwnProperty.call(state, 'currentNodeId')
|
||||
? state.currentNodeId
|
||||
: baseRuntimeState.currentNodeId
|
||||
).trim();
|
||||
const nodeStatuses = normalizeNodeStatuses(
|
||||
Object.prototype.hasOwnProperty.call(state, 'nodeStatuses')
|
||||
? state.nodeStatuses
|
||||
: baseRuntimeState.nodeStatuses
|
||||
);
|
||||
|
||||
return {
|
||||
...baseRuntimeState,
|
||||
flowId: activeFlowId,
|
||||
activeFlowId,
|
||||
runId: normalizeRunId(
|
||||
Object.prototype.hasOwnProperty.call(state, 'runId')
|
||||
? state.runId
|
||||
: Object.prototype.hasOwnProperty.call(state, 'activeRunId')
|
||||
? state.activeRunId
|
||||
: Object.prototype.hasOwnProperty.call(baseRuntimeState, 'runId')
|
||||
? baseRuntimeState.runId
|
||||
: baseRuntimeState.activeRunId
|
||||
),
|
||||
activeRunId: normalizeRunId(
|
||||
Object.prototype.hasOwnProperty.call(state, 'runId')
|
||||
? state.runId
|
||||
: Object.prototype.hasOwnProperty.call(state, 'activeRunId')
|
||||
? state.activeRunId
|
||||
: Object.prototype.hasOwnProperty.call(baseRuntimeState, 'runId')
|
||||
? baseRuntimeState.runId
|
||||
: baseRuntimeState.activeRunId
|
||||
),
|
||||
currentNodeId,
|
||||
nodeStatuses,
|
||||
sharedState: buildSharedState(baseRuntimeState.sharedState, state),
|
||||
serviceState: buildServiceState(baseRuntimeState.serviceState, state),
|
||||
flowState: buildOpenAiFlowState(baseRuntimeState.flowState, state),
|
||||
};
|
||||
}
|
||||
|
||||
function buildFlattenedUpdates(updates = {}) {
|
||||
const ignoredKeys = new Set(['current' + 'Step', 'step' + 'Statuses', 'legacy' + 'StepCompat']);
|
||||
const next = {};
|
||||
for (const [key, value] of Object.entries(updates || {})) {
|
||||
if (!ignoredKeys.has(key)) {
|
||||
next[key] = value;
|
||||
}
|
||||
}
|
||||
const runtimeState = normalizePlainObject(updates.runtimeState);
|
||||
const sharedState = normalizePlainObject(updates.sharedState);
|
||||
const serviceState = normalizePlainObject(updates.serviceState);
|
||||
const flowState = normalizePlainObject(updates.flowState);
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeFlowId')) {
|
||||
next.activeFlowId = runtimeState.activeFlowId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'flowId')) {
|
||||
next.flowId = runtimeState.flowId;
|
||||
next.activeFlowId = runtimeState.flowId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeRunId')) {
|
||||
next.activeRunId = runtimeState.activeRunId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'runId')) {
|
||||
next.runId = runtimeState.runId;
|
||||
next.activeRunId = runtimeState.runId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'currentNodeId')) {
|
||||
next.currentNodeId = runtimeState.currentNodeId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'nodeStatuses')) {
|
||||
next.nodeStatuses = cloneValue(runtimeState.nodeStatuses);
|
||||
}
|
||||
Object.assign(next, pickDefinedFields(sharedState, RUNTIME_SHARED_FIELDS));
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'sharedState')) {
|
||||
Object.assign(
|
||||
next,
|
||||
pickDefinedFields(normalizePlainObject(runtimeState.sharedState), RUNTIME_SHARED_FIELDS)
|
||||
);
|
||||
}
|
||||
|
||||
const serviceProxy = normalizePlainObject(serviceState.proxy);
|
||||
Object.assign(next, pickDefinedFields(serviceProxy, RUNTIME_PROXY_FIELDS));
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'serviceState')) {
|
||||
const runtimeServiceState = normalizePlainObject(runtimeState.serviceState);
|
||||
Object.assign(
|
||||
next,
|
||||
pickDefinedFields(normalizePlainObject(runtimeServiceState.proxy), RUNTIME_PROXY_FIELDS)
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(next, flattenOpenAiFlowState(flowState));
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'flowState')) {
|
||||
Object.assign(next, flattenOpenAiFlowState(normalizePlainObject(runtimeState.flowState)));
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildStateView(state = {}) {
|
||||
const runtimeState = ensureRuntimeState(state);
|
||||
return {
|
||||
...state,
|
||||
flowId: runtimeState.flowId,
|
||||
runId: runtimeState.runId,
|
||||
activeFlowId: runtimeState.activeFlowId,
|
||||
activeRunId: runtimeState.activeRunId,
|
||||
currentNodeId: runtimeState.currentNodeId,
|
||||
nodeStatuses: cloneValue(runtimeState.nodeStatuses),
|
||||
flowState: cloneValue(runtimeState.flowState),
|
||||
sharedState: cloneValue(runtimeState.sharedState),
|
||||
serviceState: cloneValue(runtimeState.serviceState),
|
||||
runtimeState,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSessionStatePatch(currentState = {}, updates = {}) {
|
||||
const flattenedUpdates = buildFlattenedUpdates(updates);
|
||||
const nextState = {
|
||||
...currentState,
|
||||
...flattenedUpdates,
|
||||
};
|
||||
const runtimeState = ensureRuntimeState(nextState);
|
||||
|
||||
return {
|
||||
...flattenedUpdates,
|
||||
flowId: runtimeState.flowId,
|
||||
runId: runtimeState.runId,
|
||||
activeFlowId: runtimeState.activeFlowId,
|
||||
activeRunId: runtimeState.activeRunId,
|
||||
currentNodeId: runtimeState.currentNodeId,
|
||||
nodeStatuses: cloneValue(runtimeState.nodeStatuses),
|
||||
runtimeState,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_ACTIVE_FLOW_ID,
|
||||
OPENAI_FLOW_FIELD_GROUPS,
|
||||
RUNTIME_PROXY_FIELDS,
|
||||
RUNTIME_SHARED_FIELDS,
|
||||
buildDefaultRuntimeState: buildRuntimeStateDefault,
|
||||
buildSessionStatePatch,
|
||||
buildStateView,
|
||||
ensureRuntimeState,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createRuntimeStateHelpers,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,401 @@
|
||||
(function attachSignupFlowHelpers(root, factory) {
|
||||
root.MultiPageSignupFlowHelpers = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createSignupFlowHelpersModule() {
|
||||
function createSignupFlowHelpers(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
buildGeneratedAliasEmail,
|
||||
chrome,
|
||||
ensureContentScriptReadyOnTab,
|
||||
ensureHotmailAccountForFlow,
|
||||
ensureMail2925AccountForFlow,
|
||||
ensureLuckmailPurchaseForFlow,
|
||||
fetchGeneratedEmail,
|
||||
isGeneratedAliasProvider,
|
||||
isReusableGeneratedAliasEmail,
|
||||
isHotmailProvider,
|
||||
isRetryableContentScriptTransportError = () => false,
|
||||
isLuckmailProvider,
|
||||
isSignupEmailVerificationPageUrl,
|
||||
isSignupPasswordPageUrl,
|
||||
isSignupPhoneVerificationPageUrl = null,
|
||||
isSignupProfilePageUrl = null,
|
||||
persistRegistrationEmailState = null,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScriptResilient,
|
||||
setEmailState,
|
||||
setState,
|
||||
SIGNUP_ENTRY_URL,
|
||||
SIGNUP_PAGE_INJECT_FILES,
|
||||
waitForTabStableComplete = null,
|
||||
waitForTabUrlMatch,
|
||||
} = deps;
|
||||
|
||||
async function waitForSignupEntryTabToSettle(tabId, step = 1) {
|
||||
if (step !== 2 || !Number.isInteger(tabId) || typeof waitForTabStableComplete !== 'function') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Do not request window focus here. The automation tab is already
|
||||
// locked to the selected Chrome window; raising that window would
|
||||
// interrupt the user's active workspace.
|
||||
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(
|
||||
`步骤 ${step}:注册页已打开,正在等待页面加载完成并额外稳定 3 秒...`,
|
||||
'info',
|
||||
{ step, stepKey: 'signup-entry' }
|
||||
);
|
||||
}
|
||||
|
||||
return waitForTabStableComplete(tabId, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
stableMs: 3000,
|
||||
initialDelayMs: 300,
|
||||
});
|
||||
}
|
||||
|
||||
async function openSignupEntryTab(step = 1) {
|
||||
const tabId = await reuseOrCreateTab('signup-page', SIGNUP_ENTRY_URL, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
});
|
||||
|
||||
await waitForSignupEntryTabToSettle(tabId, step);
|
||||
|
||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `步骤 ${step}:ChatGPT 官网仍在加载,正在重试连接内容脚本...`,
|
||||
});
|
||||
|
||||
return tabId;
|
||||
}
|
||||
|
||||
async function ensureSignupEntryPageReady(step = 1) {
|
||||
const tabId = await openSignupEntryTab(step);
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'ENSURE_SIGNUP_ENTRY_READY',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
}, {
|
||||
timeoutMs: 20000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${step}:官网注册入口正在切换,等待页面恢复...`,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return { tabId, result: result || {} };
|
||||
}
|
||||
|
||||
function parseUrlSafely(rawUrl) {
|
||||
if (!rawUrl) return null;
|
||||
try {
|
||||
return new URL(rawUrl);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackSignupPhoneVerificationPageUrl(rawUrl) {
|
||||
const parsed = parseUrlSafely(rawUrl);
|
||||
if (!parsed) return false;
|
||||
return /\/phone-verification(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
}
|
||||
|
||||
function fallbackSignupProfilePageUrl(rawUrl) {
|
||||
const parsed = parseUrlSafely(rawUrl);
|
||||
if (!parsed) return false;
|
||||
return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile|about-you)(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
}
|
||||
|
||||
function resolveSignupPostIdentityState(rawUrl) {
|
||||
if (isSignupPasswordPageUrl(rawUrl)) {
|
||||
return 'password_page';
|
||||
}
|
||||
if (isSignupEmailVerificationPageUrl(rawUrl)) {
|
||||
return 'verification_page';
|
||||
}
|
||||
const isPhoneVerificationUrl = typeof isSignupPhoneVerificationPageUrl === 'function'
|
||||
? isSignupPhoneVerificationPageUrl(rawUrl)
|
||||
: fallbackSignupPhoneVerificationPageUrl(rawUrl);
|
||||
if (isPhoneVerificationUrl) {
|
||||
return 'phone_verification_page';
|
||||
}
|
||||
const isProfileUrl = typeof isSignupProfilePageUrl === 'function'
|
||||
? isSignupProfilePageUrl(rawUrl)
|
||||
: fallbackSignupProfilePageUrl(rawUrl);
|
||||
if (isProfileUrl) {
|
||||
return 'profile_page';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function ensureSignupPostIdentityPageReadyInTab(tabId, step = 2, options = {}) {
|
||||
const { skipUrlWait = false } = options;
|
||||
let landingUrl = '';
|
||||
let landingState = '';
|
||||
|
||||
if (!skipUrlWait) {
|
||||
const matchedTab = await waitForTabUrlMatch(tabId, (url) => Boolean(resolveSignupPostIdentityState(url)), {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
});
|
||||
if (!matchedTab) {
|
||||
throw new Error('等待注册身份提交后的页面跳转超时,请检查页面是否仍停留在输入页。');
|
||||
}
|
||||
|
||||
landingUrl = matchedTab.url || '';
|
||||
landingState = resolveSignupPostIdentityState(landingUrl);
|
||||
}
|
||||
|
||||
if (!landingState) {
|
||||
try {
|
||||
const currentTab = await chrome.tabs.get(tabId);
|
||||
landingUrl = landingUrl || currentTab?.url || '';
|
||||
landingState = resolveSignupPostIdentityState(landingUrl);
|
||||
} catch {
|
||||
landingUrl = landingUrl || '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!landingState) {
|
||||
throw new Error(`注册身份提交后未能识别当前页面,既不是密码页、验证码页,也不是资料页。URL: ${landingUrl || 'unknown'}`);
|
||||
}
|
||||
|
||||
if (landingState !== 'password_page' && typeof waitForTabStableComplete === 'function') {
|
||||
const stableTab = await waitForTabStableComplete(tabId, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
stableMs: 800,
|
||||
initialDelayMs: 300,
|
||||
});
|
||||
if (stableTab?.url) {
|
||||
const stableState = resolveSignupPostIdentityState(stableTab.url);
|
||||
if (stableState) {
|
||||
landingUrl = stableTab.url;
|
||||
landingState = stableState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: landingState === 'password_page'
|
||||
? `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`
|
||||
: `步骤 ${step}:注册后续页面仍在加载,正在等待页面恢复...`,
|
||||
});
|
||||
|
||||
if (landingState !== 'password_page') {
|
||||
return {
|
||||
ready: true,
|
||||
state: landingState,
|
||||
url: landingUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'ENSURE_SIGNUP_PASSWORD_PAGE_READY',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
}, {
|
||||
timeoutMs: 20000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${step}:认证页正在切换,等待密码页重新就绪...`,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return {
|
||||
...(result || {}),
|
||||
ready: true,
|
||||
state: landingState,
|
||||
url: landingUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) {
|
||||
return ensureSignupPostIdentityPageReadyInTab(tabId, step, options);
|
||||
}
|
||||
|
||||
async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
|
||||
const result = await ensureSignupPostEmailPageReadyInTab(tabId, step, options);
|
||||
if (result.state !== 'password_page') {
|
||||
throw new Error(`当前页面不是密码页,实际落地为 ${result.state || 'unknown'}。URL: ${result.url || 'unknown'}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function finalizeSignupPasswordSubmitInTab(tabId, password = '', step = 3) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error(`认证页面标签页已关闭,无法完成步骤 ${step} 的提交后确认。`);
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `步骤 ${step}:认证页仍在切换,正在等待页面恢复后继续确认提交流程...`,
|
||||
});
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'PREPARE_SIGNUP_VERIFICATION',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: {
|
||||
password: password || '',
|
||||
prepareSource: 'step3_finalize',
|
||||
prepareLogLabel: '步骤 3 收尾',
|
||||
},
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isRetryableContentScriptTransportError(error)) {
|
||||
const message = `步骤 ${step}:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。`;
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(message, 'warn');
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result || {};
|
||||
}
|
||||
|
||||
function getPreservedPhoneIdentityForEmailResolution(state = {}, options = {}) {
|
||||
if (!Boolean(options?.preserveAccountIdentity)) {
|
||||
return null;
|
||||
}
|
||||
const accountIdentifierType = String(state?.accountIdentifierType || '').trim().toLowerCase();
|
||||
const signupPhoneNumber = String(
|
||||
state?.signupPhoneNumber
|
||||
|| (accountIdentifierType === 'phone' ? state?.accountIdentifier : '')
|
||||
|| state?.signupPhoneCompletedActivation?.phoneNumber
|
||||
|| state?.signupPhoneActivation?.phoneNumber
|
||||
|| ''
|
||||
).trim();
|
||||
if (accountIdentifierType !== 'phone' && !signupPhoneNumber) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountIdentifierType: 'phone',
|
||||
accountIdentifier: signupPhoneNumber || String(state?.accountIdentifier || '').trim(),
|
||||
signupPhoneNumber,
|
||||
signupPhoneActivation: state?.signupPhoneActivation || null,
|
||||
signupPhoneCompletedActivation: state?.signupPhoneCompletedActivation || null,
|
||||
signupPhoneVerificationRequestedAt: state?.signupPhoneVerificationRequestedAt ?? null,
|
||||
signupPhoneVerificationPurpose: state?.signupPhoneVerificationPurpose || '',
|
||||
};
|
||||
}
|
||||
|
||||
async function persistResolvedSignupEmail(resolvedEmail, state = {}, options = {}) {
|
||||
if (resolvedEmail === state.email && !options?.preserveAccountIdentity) {
|
||||
return;
|
||||
}
|
||||
const generatedEmailAlreadyPersisted = Boolean(options?.generatedEmailAlreadyPersisted);
|
||||
if (typeof persistRegistrationEmailState === 'function') {
|
||||
if (!generatedEmailAlreadyPersisted) {
|
||||
await persistRegistrationEmailState(state, resolvedEmail, {
|
||||
source: 'flow',
|
||||
preserveAccountIdentity: Boolean(options?.preserveAccountIdentity),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const preservedPhoneIdentity = getPreservedPhoneIdentityForEmailResolution(state, options);
|
||||
if (preservedPhoneIdentity && typeof setState === 'function') {
|
||||
if (!generatedEmailAlreadyPersisted && resolvedEmail !== state.email) {
|
||||
await setEmailState(resolvedEmail, { source: 'flow' });
|
||||
}
|
||||
await setState(preservedPhoneIdentity);
|
||||
return;
|
||||
}
|
||||
if (resolvedEmail !== state.email) {
|
||||
await setEmailState(resolvedEmail);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveSignupEmailForFlow(state, options = {}) {
|
||||
let resolvedEmail = state.email;
|
||||
let generatedEmailAlreadyPersisted = false;
|
||||
if (isHotmailProvider(state)) {
|
||||
const account = await ensureHotmailAccountForFlow({
|
||||
allowAllocate: true,
|
||||
markUsed: true,
|
||||
preferredAccountId: state.currentHotmailAccountId || null,
|
||||
});
|
||||
resolvedEmail = account.registrationAliasEmail || account.email;
|
||||
} else if (isLuckmailProvider(state)) {
|
||||
const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true });
|
||||
resolvedEmail = purchase.email_address;
|
||||
} else if (isGeneratedAliasProvider(state)) {
|
||||
if (Boolean(state?.mail2925UseAccountPool)
|
||||
&& String(state?.mailProvider || '').trim().toLowerCase() === '2925'
|
||||
&& typeof ensureMail2925AccountForFlow === 'function') {
|
||||
await ensureMail2925AccountForFlow({
|
||||
allowAllocate: true,
|
||||
preferredAccountId: state.currentMail2925AccountId || null,
|
||||
markUsed: true,
|
||||
});
|
||||
}
|
||||
if (!isReusableGeneratedAliasEmail?.(state, resolvedEmail)) {
|
||||
resolvedEmail = buildGeneratedAliasEmail(state);
|
||||
}
|
||||
} else if (!resolvedEmail && typeof fetchGeneratedEmail === 'function') {
|
||||
resolvedEmail = await fetchGeneratedEmail(state, options);
|
||||
generatedEmailAlreadyPersisted = true;
|
||||
}
|
||||
|
||||
if (!resolvedEmail) {
|
||||
throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。');
|
||||
}
|
||||
|
||||
if (!generatedEmailAlreadyPersisted || options?.preserveAccountIdentity) {
|
||||
await persistResolvedSignupEmail(resolvedEmail, state, {
|
||||
...options,
|
||||
generatedEmailAlreadyPersisted,
|
||||
});
|
||||
}
|
||||
|
||||
return resolvedEmail;
|
||||
}
|
||||
|
||||
return {
|
||||
ensureSignupEntryPageReady,
|
||||
ensureSignupPostIdentityPageReadyInTab,
|
||||
ensureSignupPostEmailPageReadyInTab,
|
||||
finalizeSignupPasswordSubmitInTab,
|
||||
ensureSignupPasswordPageReadyInTab,
|
||||
openSignupEntryTab,
|
||||
resolveSignupEmailForFlow,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createSignupFlowHelpers,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,352 @@
|
||||
(function attachBackgroundStep9(root, factory) {
|
||||
root.MultiPageBackgroundStep9 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep9Module() {
|
||||
function createStep9Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
cleanupStep8NavigationListeners,
|
||||
clickWithDebugger,
|
||||
completeNodeFromBackground,
|
||||
ensureStep8SignupPageReady,
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getStep8CallbackUrlFromNavigation,
|
||||
getStep8CallbackUrlFromTabUpdate,
|
||||
getStep8EffectLabel,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
prepareStep8DebuggerClick,
|
||||
recoverOAuthLocalhostTimeout,
|
||||
reloadStep8ConsentPage,
|
||||
reuseOrCreateTab,
|
||||
sleepWithStop,
|
||||
STEP8_CLICK_RETRY_DELAY_MS,
|
||||
STEP8_MAX_ROUNDS,
|
||||
STEP8_READY_WAIT_TIMEOUT_MS,
|
||||
STEP8_STRATEGIES,
|
||||
throwIfStep8SettledOrStopped,
|
||||
triggerStep8ContentStrategy,
|
||||
waitForStep8ClickEffect,
|
||||
waitForStep8Ready,
|
||||
setWebNavListener,
|
||||
setWebNavCommittedListener,
|
||||
setStep8PendingReject,
|
||||
setStep8TabUpdatedListener,
|
||||
shouldDeferStep9CallbackTimeout,
|
||||
} = deps;
|
||||
|
||||
const LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS = 240000;
|
||||
const CALLBACK_TIMEOUT_CHECK_INTERVAL_MS = 1000;
|
||||
|
||||
function getVisibleStep(state, fallback = 9) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep > 0 ? visibleStep : fallback;
|
||||
}
|
||||
|
||||
function getAuthLoginStepForVisibleStep(visibleStep) {
|
||||
return visibleStep >= 12 ? 10 : 7;
|
||||
}
|
||||
|
||||
function addStepLog(step, message, level = 'info') {
|
||||
return addLog(message, level, { step, stepKey: 'confirm-oauth' });
|
||||
}
|
||||
|
||||
async function executeStep9(state) {
|
||||
const visibleStep = getVisibleStep(state, 9);
|
||||
let activeState = state;
|
||||
|
||||
if (!activeState.oauthUrl) {
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`);
|
||||
}
|
||||
|
||||
await addStepLog(visibleStep, '正在监听 localhost 回调地址...');
|
||||
|
||||
let callbackTimeoutMs = LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS;
|
||||
let timeoutRecoveryAttempted = false;
|
||||
while (true) {
|
||||
try {
|
||||
callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS, {
|
||||
step: visibleStep,
|
||||
actionLabel: 'OAuth localhost 回调',
|
||||
oauthUrl: activeState?.oauthUrl || '',
|
||||
})
|
||||
: LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS;
|
||||
break;
|
||||
} catch (error) {
|
||||
if (timeoutRecoveryAttempted || typeof recoverOAuthLocalhostTimeout !== 'function') {
|
||||
throw error;
|
||||
}
|
||||
const recoveredState = await recoverOAuthLocalhostTimeout({
|
||||
error,
|
||||
state: activeState,
|
||||
visibleStep,
|
||||
});
|
||||
if (!recoveredState) {
|
||||
throw error;
|
||||
}
|
||||
activeState = recoveredState;
|
||||
timeoutRecoveryAttempted = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolved = false;
|
||||
let signupTabId = null;
|
||||
const callbackWaitStartedAt = Date.now();
|
||||
let timeoutCheckTimer = null;
|
||||
let timeoutDeferredLogged = false;
|
||||
|
||||
const cleanupListener = () => {
|
||||
if (timeoutCheckTimer) {
|
||||
clearTimeout(timeoutCheckTimer);
|
||||
timeoutCheckTimer = null;
|
||||
}
|
||||
cleanupStep8NavigationListeners();
|
||||
setStep8PendingReject(null);
|
||||
};
|
||||
|
||||
const rejectStep9 = (error) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
cleanupListener();
|
||||
reject(error);
|
||||
};
|
||||
|
||||
const finalizeStep9Callback = (callbackUrl) => {
|
||||
if (resolved || !callbackUrl) return;
|
||||
|
||||
resolved = true;
|
||||
cleanupListener();
|
||||
|
||||
addStepLog(visibleStep, `已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
|
||||
return completeNodeFromBackground(state?.nodeId || 'confirm-oauth', { localhostUrl: callbackUrl });
|
||||
}).then(() => {
|
||||
resolve();
|
||||
}).catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
const isCallbackTimeoutDeferred = async (elapsedMs) => {
|
||||
if (typeof shouldDeferStep9CallbackTimeout !== 'function') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const deferred = await shouldDeferStep9CallbackTimeout({
|
||||
tabId: signupTabId,
|
||||
visibleStep,
|
||||
elapsedMs,
|
||||
oauthUrl: activeState?.oauthUrl || '',
|
||||
});
|
||||
if (deferred && !timeoutDeferredLogged) {
|
||||
timeoutDeferredLogged = true;
|
||||
await addStepLog(
|
||||
visibleStep,
|
||||
'检测到认证页仍在安全验证/授权跳转中,暂停本地回调超时判定,继续等待 localhost 回调...',
|
||||
'info'
|
||||
);
|
||||
}
|
||||
return Boolean(deferred);
|
||||
} catch (error) {
|
||||
await addStepLog(
|
||||
visibleStep,
|
||||
`复核认证页跳转状态失败(${error?.message || error}),继续按原超时规则等待回调。`,
|
||||
'warn'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkCallbackTimeout = async () => {
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
const elapsedMs = Date.now() - callbackWaitStartedAt;
|
||||
if (await isCallbackTimeoutDeferred(elapsedMs)) {
|
||||
timeoutCheckTimer = setTimeout(checkCallbackTimeout, CALLBACK_TIMEOUT_CHECK_INTERVAL_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (elapsedMs >= LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS) {
|
||||
rejectStep9(new Error(`${Math.round(LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof getOAuthFlowRemainingMs === 'function') {
|
||||
try {
|
||||
await getOAuthFlowRemainingMs({
|
||||
step: visibleStep,
|
||||
actionLabel: 'OAuth localhost 回调',
|
||||
oauthUrl: activeState?.oauthUrl || '',
|
||||
});
|
||||
} catch (error) {
|
||||
rejectStep9(error);
|
||||
return;
|
||||
}
|
||||
} else if (elapsedMs >= callbackTimeoutMs) {
|
||||
rejectStep9(new Error(`${Math.round(callbackTimeoutMs / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutCheckTimer = setTimeout(checkCallbackTimeout, CALLBACK_TIMEOUT_CHECK_INTERVAL_MS);
|
||||
};
|
||||
|
||||
timeoutCheckTimer = setTimeout(
|
||||
checkCallbackTimeout,
|
||||
Math.min(CALLBACK_TIMEOUT_CHECK_INTERVAL_MS, Math.max(1, callbackTimeoutMs))
|
||||
);
|
||||
|
||||
setStep8PendingReject((error) => {
|
||||
rejectStep9(error);
|
||||
});
|
||||
|
||||
setWebNavListener((details) => {
|
||||
const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId);
|
||||
finalizeStep9Callback(callbackUrl);
|
||||
});
|
||||
|
||||
setWebNavCommittedListener((details) => {
|
||||
const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId);
|
||||
finalizeStep9Callback(callbackUrl);
|
||||
});
|
||||
|
||||
setStep8TabUpdatedListener((tabId, changeInfo, tab) => {
|
||||
const callbackUrl = getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId);
|
||||
finalizeStep9Callback(callbackUrl);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
signupTabId = await getTabId('signup-page');
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
|
||||
if (signupTabId && await isTabAlive('signup-page')) {
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await addStepLog(visibleStep, '已切回认证页,正在准备调试器点击...');
|
||||
} else {
|
||||
signupTabId = await reuseOrCreateTab('signup-page', activeState.oauthUrl);
|
||||
await addStepLog(visibleStep, '已重新打开认证页,正在准备调试器点击...');
|
||||
}
|
||||
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(deps.getWebNavListener());
|
||||
chrome.webNavigation.onCommitted.addListener(deps.getWebNavCommittedListener());
|
||||
chrome.tabs.onUpdated.addListener(deps.getStep8TabUpdatedListener());
|
||||
await ensureStep8SignupPageReady(signupTabId, {
|
||||
timeoutMs: typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(15000, {
|
||||
step: visibleStep,
|
||||
actionLabel: '等待 OAuth 同意页内容脚本就绪',
|
||||
})
|
||||
: 15000,
|
||||
visibleStep,
|
||||
logStepKey: 'confirm-oauth',
|
||||
logMessage: '认证页内容脚本尚未就绪,正在等待页面恢复...',
|
||||
});
|
||||
|
||||
for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) {
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
const pageState = await waitForStep8Ready(
|
||||
signupTabId,
|
||||
typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(STEP8_READY_WAIT_TIMEOUT_MS, {
|
||||
step: visibleStep,
|
||||
actionLabel: '等待 OAuth 同意页出现',
|
||||
})
|
||||
: STEP8_READY_WAIT_TIMEOUT_MS,
|
||||
{ visibleStep }
|
||||
);
|
||||
if (!pageState?.consentReady) {
|
||||
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)];
|
||||
|
||||
await addStepLog(visibleStep, `第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`);
|
||||
|
||||
if (strategy.mode === 'debugger') {
|
||||
const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(15000, {
|
||||
step: visibleStep,
|
||||
actionLabel: '定位 OAuth 同意页继续按钮',
|
||||
})
|
||||
: 15000;
|
||||
const clickTarget = await prepareStep8DebuggerClick(signupTabId, {
|
||||
timeoutMs: clickActionTimeoutMs,
|
||||
responseTimeoutMs: clickActionTimeoutMs,
|
||||
visibleStep,
|
||||
});
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
await clickWithDebugger(signupTabId, clickTarget?.rect, { visibleStep });
|
||||
} else {
|
||||
const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(15000, {
|
||||
step: visibleStep,
|
||||
actionLabel: '点击 OAuth 同意页继续按钮',
|
||||
})
|
||||
: 15000;
|
||||
await triggerStep8ContentStrategy(signupTabId, strategy.strategy, {
|
||||
timeoutMs: clickActionTimeoutMs,
|
||||
responseTimeoutMs: clickActionTimeoutMs,
|
||||
visibleStep,
|
||||
});
|
||||
}
|
||||
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effect = await waitForStep8ClickEffect(
|
||||
signupTabId,
|
||||
pageState.url,
|
||||
typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(15000, {
|
||||
step: visibleStep,
|
||||
actionLabel: '等待 OAuth 同意页点击生效',
|
||||
})
|
||||
: 15000,
|
||||
{ visibleStep }
|
||||
);
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (effect.progressed) {
|
||||
await addStepLog(visibleStep, `检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info');
|
||||
break;
|
||||
}
|
||||
|
||||
if (round >= STEP8_MAX_ROUNDS) {
|
||||
throw new Error(`步骤 ${visibleStep}:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
|
||||
}
|
||||
|
||||
await addStepLog(visibleStep, `${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn');
|
||||
await reloadStep8ConsentPage(
|
||||
signupTabId,
|
||||
typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(30000, {
|
||||
step: visibleStep,
|
||||
actionLabel: '刷新 OAuth 同意页',
|
||||
})
|
||||
: 30000,
|
||||
{ visibleStep }
|
||||
);
|
||||
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
|
||||
}
|
||||
} catch (err) {
|
||||
rejectStep9(err);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
return { executeStep9 };
|
||||
}
|
||||
|
||||
return { createStep9Executor };
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,959 @@
|
||||
(function attachBackgroundStep8(root, factory) {
|
||||
root.MultiPageBackgroundStep8 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() {
|
||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||||
|
||||
function createStep8Executor(deps = {}) {
|
||||
const {
|
||||
addLog: rawAddLog = async () => {},
|
||||
chrome,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
CLOUD_MAIL_PROVIDER = 'cloudmail',
|
||||
completeNodeFromBackground,
|
||||
confirmCustomVerificationStepBypass,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession,
|
||||
ensureStep8VerificationPageReady,
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getMailConfig,
|
||||
getState,
|
||||
getTabId,
|
||||
HOTMAIL_PROVIDER,
|
||||
isTabAlive,
|
||||
isVerificationMailPollingError,
|
||||
LUCKMAIL_PROVIDER,
|
||||
resolveSignupEmailForFlow,
|
||||
resolveVerificationStep,
|
||||
rerunStep7ForStep8Recovery,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScriptResilient,
|
||||
persistRegistrationEmailState = null,
|
||||
phoneVerificationHelpers = null,
|
||||
setState,
|
||||
shouldUseCustomRegistrationEmail,
|
||||
sleepWithStop,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
||||
throwIfStopped,
|
||||
} = deps;
|
||||
let activeFetchLoginCodeStep = null;
|
||||
let activeFetchLoginCodeStepKey = 'fetch-login-code';
|
||||
|
||||
function normalizeLogStep(value) {
|
||||
const step = Math.floor(Number(value) || 0);
|
||||
return step > 0 ? step : null;
|
||||
}
|
||||
|
||||
function normalizeStepLogMessage(message) {
|
||||
return String(message || '')
|
||||
.replace(/^步骤\s*\d+\s*[::]\s*/, '')
|
||||
.replace(/^Step\s+\d+\s*[::]\s*/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function addLog(message, level = 'info', options = {}) {
|
||||
const normalizedOptions = options && typeof options === 'object' ? { ...options } : {};
|
||||
const step = normalizeLogStep(normalizedOptions.step || normalizedOptions.visibleStep)
|
||||
|| normalizeLogStep(activeFetchLoginCodeStep);
|
||||
if (step) {
|
||||
normalizedOptions.step = step;
|
||||
if (!normalizedOptions.stepKey) {
|
||||
normalizedOptions.stepKey = activeFetchLoginCodeStepKey || 'fetch-login-code';
|
||||
}
|
||||
}
|
||||
delete normalizedOptions.visibleStep;
|
||||
return rawAddLog(normalizeStepLogMessage(message), level, normalizedOptions);
|
||||
}
|
||||
|
||||
function getVisibleStep(state, fallback = 8) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep > 0 ? visibleStep : fallback;
|
||||
}
|
||||
|
||||
function normalizeSignupMethod(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
|
||||
}
|
||||
|
||||
function normalizeIdentifierType(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'phone' || normalized === 'email' ? normalized : '';
|
||||
}
|
||||
|
||||
function isPhoneLoginCodeMode(state = {}) {
|
||||
if (normalizeIdentifierType(state?.accountIdentifierType) === 'phone') {
|
||||
return true;
|
||||
}
|
||||
return normalizeSignupMethod(state?.resolvedSignupMethod || state?.signupMethod) === 'phone'
|
||||
&& Boolean(
|
||||
String(state?.signupPhoneNumber || '').trim()
|
||||
|| String(state?.signupPhoneCompletedActivation?.phoneNumber || '').trim()
|
||||
|| String(state?.signupPhoneActivation?.phoneNumber || '').trim()
|
||||
);
|
||||
}
|
||||
|
||||
function getAuthLoginStepForVisibleStep(visibleStep) {
|
||||
return visibleStep >= 11 ? 10 : 7;
|
||||
}
|
||||
|
||||
async function getStep8ReadyTimeoutMs(actionLabel, expectedOauthUrl = '', visibleStep = 8) {
|
||||
if (typeof getOAuthFlowStepTimeoutMs !== 'function') {
|
||||
return 15000;
|
||||
}
|
||||
|
||||
return getOAuthFlowStepTimeoutMs(15000, {
|
||||
step: visibleStep,
|
||||
actionLabel,
|
||||
oauthUrl: expectedOauthUrl,
|
||||
});
|
||||
}
|
||||
|
||||
function getStep8RemainingTimeResolver(expectedOauthUrl = '', visibleStep = 8) {
|
||||
if (typeof getOAuthFlowRemainingMs !== 'function') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return async (details = {}) => getOAuthFlowRemainingMs({
|
||||
step: visibleStep,
|
||||
actionLabel: details.actionLabel || '登录验证码流程',
|
||||
oauthUrl: expectedOauthUrl,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeStep8VerificationTargetEmail(value) {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function resolveBoundEmailLoginTarget(state = {}, visibleStep = 0) {
|
||||
const email = String(
|
||||
state?.step8VerificationTargetEmail
|
||||
|| state?.email
|
||||
|| state?.registrationEmailState?.current
|
||||
|| ''
|
||||
).trim();
|
||||
if (!email) {
|
||||
throw new Error(`步骤 ${visibleStep || 0}:缺少绑定邮箱,无法使用邮箱模式重新发起 OAuth 登录。`);
|
||||
}
|
||||
return email;
|
||||
}
|
||||
|
||||
function buildBoundEmailLoginState(state = {}, visibleStep = 0) {
|
||||
const email = resolveBoundEmailLoginTarget(state, visibleStep);
|
||||
return {
|
||||
...state,
|
||||
forceLoginIdentifierType: 'email',
|
||||
forceEmailLogin: true,
|
||||
signupMethod: 'email',
|
||||
resolvedSignupMethod: 'email',
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: email,
|
||||
email,
|
||||
step8VerificationTargetEmail: normalizeStep8VerificationTargetEmail(email),
|
||||
};
|
||||
}
|
||||
|
||||
async function getLoginAuthStateFromContent(visibleStep, options = {}) {
|
||||
if (typeof sendToContentScriptResilient !== 'function') {
|
||||
return {};
|
||||
}
|
||||
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 15000);
|
||||
const result = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
{
|
||||
type: 'GET_LOGIN_AUTH_STATE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
},
|
||||
{
|
||||
timeoutMs,
|
||||
responseTimeoutMs: timeoutMs,
|
||||
retryDelayMs: 600,
|
||||
logMessage: options.logMessage || `步骤 ${visibleStep}:认证页正在切换,等待页面重新就绪...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: options.logStepKey || activeFetchLoginCodeStepKey || 'fetch-login-code',
|
||||
}
|
||||
);
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function submitAddEmailIfNeeded(state, visibleStep, initialPageState = null) {
|
||||
if (typeof resolveSignupEmailForFlow !== 'function' || typeof sendToContentScriptResilient !== 'function') {
|
||||
return { state, pageState: initialPageState };
|
||||
}
|
||||
|
||||
const pageState = initialPageState?.state
|
||||
? initialPageState
|
||||
: await getLoginAuthStateFromContent(visibleStep, {
|
||||
timeoutMs: 15000,
|
||||
logMessage: `步骤 ${visibleStep}:正在确认是否已进入添加邮箱页...`,
|
||||
});
|
||||
if (pageState?.state !== 'add_email_page') {
|
||||
return { state, pageState };
|
||||
}
|
||||
|
||||
const latestState = typeof getState === 'function' ? await getState() : state;
|
||||
const resolvedEmail = await resolveSignupEmailForFlow(latestState, {
|
||||
preserveAccountIdentity: true,
|
||||
});
|
||||
await addLog(`步骤 ${visibleStep}:检测到添加邮箱页,正在添加邮箱 ${resolvedEmail} 并进入邮箱验证码页...`);
|
||||
|
||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(60000, {
|
||||
step: visibleStep,
|
||||
actionLabel: '添加邮箱并进入验证码页',
|
||||
oauthUrl: latestState?.oauthUrl || state?.oauthUrl || '',
|
||||
})
|
||||
: 60000;
|
||||
const result = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
{
|
||||
type: 'SUBMIT_ADD_EMAIL',
|
||||
source: 'background',
|
||||
payload: { email: resolvedEmail },
|
||||
},
|
||||
{
|
||||
timeoutMs,
|
||||
responseTimeoutMs: timeoutMs,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${visibleStep}:添加邮箱页面正在切换,等待邮箱验证码页就绪...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: activeFetchLoginCodeStepKey || 'fetch-login-code',
|
||||
}
|
||||
);
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
const displayedEmail = normalizeStep8VerificationTargetEmail(result?.displayedEmail || resolvedEmail);
|
||||
let persistedState = latestState;
|
||||
if (typeof persistRegistrationEmailState === 'function') {
|
||||
await persistRegistrationEmailState(latestState, resolvedEmail, {
|
||||
source: activeFetchLoginCodeStepKey === 'bind-email' ? 'bind_email' : 'step8_add_email',
|
||||
preserveAccountIdentity: true,
|
||||
});
|
||||
persistedState = typeof getState === 'function' ? await getState() : latestState;
|
||||
} else {
|
||||
await setState({
|
||||
email: resolvedEmail,
|
||||
step8VerificationTargetEmail: displayedEmail,
|
||||
});
|
||||
persistedState = {
|
||||
...latestState,
|
||||
email: resolvedEmail,
|
||||
step8VerificationTargetEmail: displayedEmail,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state: {
|
||||
...persistedState,
|
||||
email: resolvedEmail,
|
||||
step8VerificationTargetEmail: displayedEmail,
|
||||
},
|
||||
pageState: {
|
||||
state: result?.directOAuthConsentPage ? 'oauth_consent_page' : 'verification_page',
|
||||
displayedEmail,
|
||||
url: result?.url || pageState?.url || '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, options = {}) {
|
||||
await setState({
|
||||
step8VerificationTargetEmail: '',
|
||||
loginVerificationRequestedAt: null,
|
||||
});
|
||||
const fromRecovery = Boolean(options.fromRecovery);
|
||||
const stepKey = options.stepKey || activeFetchLoginCodeStepKey || 'fetch-login-code';
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:当前认证页已进入 OAuth 授权页${fromRecovery ? '(轮询失败后复核)' : ''},跳过登录验证码拉取并继续后续流程。`,
|
||||
'warn',
|
||||
{ step: visibleStep, stepKey }
|
||||
);
|
||||
if (typeof completeNodeFromBackground === 'function') {
|
||||
await completeNodeFromBackground(options.nodeId || 'fetch-login-code', {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function completeStep8WhenDeferredToPostLoginPhone(visibleStep, pageState = {}, options = {}) {
|
||||
await setState({
|
||||
step8VerificationTargetEmail: '',
|
||||
loginVerificationRequestedAt: null,
|
||||
});
|
||||
const stepKey = options.stepKey || activeFetchLoginCodeStepKey || 'fetch-login-code';
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:当前认证页已进入手机号验证流程,跳过登录邮箱验证码,交给后续“手机号验证”步骤处理。`,
|
||||
'warn',
|
||||
{ step: visibleStep, stepKey }
|
||||
);
|
||||
if (typeof completeNodeFromBackground === 'function') {
|
||||
await completeNodeFromBackground(options.nodeId || 'fetch-login-code', {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
addPhonePage: pageState?.state === 'add_phone_page' || Boolean(pageState?.addPhonePage),
|
||||
phoneVerificationPage: pageState?.state === 'phone_verification_page' || Boolean(pageState?.phoneVerificationPage),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function completeStep8WhenDeferredToBindEmail(visibleStep, options = {}) {
|
||||
await setState({
|
||||
step8VerificationTargetEmail: '',
|
||||
loginVerificationRequestedAt: null,
|
||||
});
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:当前认证页已进入添加邮箱页,跳过登录短信验证码,交给后续“绑定邮箱”步骤处理。`,
|
||||
'warn'
|
||||
);
|
||||
if (typeof completeNodeFromBackground === 'function') {
|
||||
await completeNodeFromBackground(options.nodeId || 'fetch-login-code', {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
addEmailPage: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isStep8AddPhoneStateError(error) {
|
||||
const message = String(error?.message || error || '');
|
||||
return /add-phone|手机号页面|手机号验证页|phone[\s-_]verification|phone\s+number/i.test(message);
|
||||
}
|
||||
|
||||
async function recoverStep8PollingFailure(currentState, visibleStep) {
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
try {
|
||||
const pageState = await ensureStep8VerificationPageReady({
|
||||
visibleStep,
|
||||
authLoginStep,
|
||||
allowPhoneVerificationPage: true,
|
||||
allowAddEmailPage: true,
|
||||
timeoutMs: await getStep8ReadyTimeoutMs(
|
||||
'登录验证码轮询异常后复核认证页状态',
|
||||
currentState?.oauthUrl || '',
|
||||
visibleStep
|
||||
),
|
||||
});
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true, nodeId: currentState?.nodeId });
|
||||
return { outcome: 'completed' };
|
||||
}
|
||||
if (pageState?.state === 'verification_page' || pageState?.state === 'phone_verification_page' || pageState?.state === 'add_email_page') {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在当前登录后续页面,先在当前链路重试,不回到步骤 ${authLoginStep}。`,
|
||||
'warn'
|
||||
);
|
||||
return { outcome: 'retry_without_step7' };
|
||||
}
|
||||
} catch (inspectError) {
|
||||
if (isStep8RestartStep7Error(inspectError)) {
|
||||
return { outcome: 'restart_step7', error: inspectError };
|
||||
}
|
||||
if (isStep8AddPhoneStateError(inspectError)) {
|
||||
throw inspectError;
|
||||
}
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:轮询失败后复核认证页状态异常:${inspectError?.message || inspectError},将回到步骤 ${authLoginStep} 重试。`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
return { outcome: 'restart_step7' };
|
||||
}
|
||||
|
||||
function getExpectedMail2925MailboxEmail(state = {}) {
|
||||
if (Boolean(state?.mail2925UseAccountPool)) {
|
||||
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
|
||||
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
|
||||
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
|
||||
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
|
||||
if (accountEmail) {
|
||||
return accountEmail;
|
||||
}
|
||||
}
|
||||
|
||||
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function focusOrOpenMailTab(mail) {
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
if (mail.navigateOnReuse) {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = await getTabId(mail.source);
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
return;
|
||||
}
|
||||
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
}
|
||||
|
||||
function getStep8ResendIntervalMs(state = {}) {
|
||||
const mail = getMailConfig(state);
|
||||
if (mail?.provider === LUCKMAIL_PROVIDER) {
|
||||
return 15000;
|
||||
}
|
||||
if (mail?.provider === HOTMAIL_PROVIDER || mail?.provider === '2925') {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Number(STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS) || 0);
|
||||
}
|
||||
|
||||
async function executeLoginPhoneCodeStep(state, signupTabId, visibleStep) {
|
||||
if (!Number.isInteger(signupTabId)) {
|
||||
throw new Error(`步骤 ${visibleStep}:认证页面标签页已关闭,无法继续手机号登录验证码流程。`);
|
||||
}
|
||||
if (typeof phoneVerificationHelpers?.completeLoginPhoneVerificationFlow !== 'function') {
|
||||
throw new Error(`步骤 ${visibleStep}:手机号登录验证码流程不可用,接码模块尚未初始化。`);
|
||||
}
|
||||
|
||||
const result = await phoneVerificationHelpers.completeLoginPhoneVerificationFlow(signupTabId, {
|
||||
state,
|
||||
visibleStep,
|
||||
});
|
||||
|
||||
await completeNodeFromBackground(state?.nodeId || 'fetch-login-code', {
|
||||
phoneVerification: true,
|
||||
loginPhoneVerification: true,
|
||||
code: result?.code || '',
|
||||
});
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function ensureAuthTabForPostLoginStep(state, visibleStep) {
|
||||
const authTabId = await getTabId('signup-page');
|
||||
if (authTabId) {
|
||||
await chrome.tabs.update(authTabId, { active: true });
|
||||
return authTabId;
|
||||
}
|
||||
if (!state?.oauthUrl) {
|
||||
throw new Error(`步骤 ${visibleStep}:缺少登录用 OAuth 链接,请先完成刷新 OAuth 并登录。`);
|
||||
}
|
||||
return reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||
}
|
||||
|
||||
async function completePostLoginPhoneVerificationSkippedOnOauth(visibleStep, options = {}) {
|
||||
const stepKey = options.stepKey || 'post-login-phone-verification';
|
||||
await addLog(`步骤 ${visibleStep}:当前认证页已进入 OAuth 授权页,跳过手机号验证步骤。`, 'warn', {
|
||||
step: visibleStep,
|
||||
stepKey,
|
||||
});
|
||||
if (typeof completeNodeFromBackground === 'function') {
|
||||
await completeNodeFromBackground(options.nodeId || 'post-login-phone-verification', {
|
||||
directOAuthConsentPage: true,
|
||||
phoneVerification: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function executePostLoginPhoneVerification(state, runtime = {}) {
|
||||
const visibleStep = getVisibleStep(state, 9);
|
||||
activeFetchLoginCodeStep = visibleStep;
|
||||
activeFetchLoginCodeStepKey = runtime.stepKey || 'post-login-phone-verification';
|
||||
const authTabId = await ensureAuthTabForPostLoginStep(state, visibleStep);
|
||||
const pageState = await getLoginAuthStateFromContent(visibleStep, {
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认手机号验证页或 OAuth 授权页已就绪', state?.oauthUrl || '', visibleStep),
|
||||
logMessage: `步骤 ${visibleStep}:正在确认是否需要手机号验证...`,
|
||||
logStepKey: activeFetchLoginCodeStepKey,
|
||||
});
|
||||
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completePostLoginPhoneVerificationSkippedOnOauth(visibleStep, {
|
||||
nodeId: state?.nodeId || runtime.fallbackNodeId,
|
||||
stepKey: activeFetchLoginCodeStepKey,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (pageState?.state !== 'add_phone_page' && pageState?.state !== 'phone_verification_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:手机号验证步骤只处理添加手机号页或手机验证码页,当前状态:${pageState?.state || 'unknown'}。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
if (!state?.phoneVerificationEnabled) {
|
||||
throw new Error(`步骤 ${visibleStep}:检测到需要手机号验证,但手机接码未开启。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
if (typeof phoneVerificationHelpers?.completePhoneVerificationFlow !== 'function') {
|
||||
throw new Error(`步骤 ${visibleStep}:手机号验证流程不可用,接码模块尚未初始化。`);
|
||||
}
|
||||
|
||||
const result = await phoneVerificationHelpers.completePhoneVerificationFlow(authTabId, pageState, {
|
||||
step: visibleStep,
|
||||
visibleStep,
|
||||
});
|
||||
if (typeof completeNodeFromBackground === 'function') {
|
||||
await completeNodeFromBackground(state?.nodeId || runtime.fallbackNodeId || 'post-login-phone-verification', {
|
||||
phoneVerification: true,
|
||||
postLoginPhoneVerification: true,
|
||||
code: result?.code || '',
|
||||
});
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function executeBindEmail(state) {
|
||||
const visibleStep = getVisibleStep(state, 9);
|
||||
activeFetchLoginCodeStep = visibleStep;
|
||||
activeFetchLoginCodeStepKey = 'bind-email';
|
||||
await ensureAuthTabForPostLoginStep(state, visibleStep);
|
||||
const pageState = await getLoginAuthStateFromContent(visibleStep, {
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认添加邮箱页或 OAuth 授权页已就绪', state?.oauthUrl || '', visibleStep),
|
||||
logMessage: `步骤 ${visibleStep}:正在确认是否需要绑定邮箱...`,
|
||||
});
|
||||
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await addLog(`步骤 ${visibleStep}:当前认证页已进入 OAuth 授权页,跳过绑定邮箱步骤。`, 'warn', {
|
||||
step: visibleStep,
|
||||
stepKey: 'bind-email',
|
||||
});
|
||||
if (typeof completeNodeFromBackground === 'function') {
|
||||
await completeNodeFromBackground(state?.nodeId || 'bind-email', {
|
||||
directOAuthConsentPage: true,
|
||||
bindEmailSubmitted: false,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageState?.state !== 'add_email_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:绑定邮箱步骤只处理添加邮箱页,当前状态:${pageState?.state || 'unknown'}。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
|
||||
const addEmailPreparation = await submitAddEmailIfNeeded(state, visibleStep, pageState);
|
||||
const preparedState = addEmailPreparation?.state || state;
|
||||
const nextPageState = addEmailPreparation?.pageState || pageState;
|
||||
if (nextPageState?.state !== 'verification_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:绑定邮箱提交后必须进入邮箱验证码页,当前状态:${nextPageState?.state || 'unknown'}。URL: ${nextPageState?.url || ''}`.trim());
|
||||
}
|
||||
|
||||
if (typeof completeNodeFromBackground === 'function') {
|
||||
await completeNodeFromBackground(state?.nodeId || 'bind-email', {
|
||||
bindEmailSubmitted: true,
|
||||
email: preparedState?.email || '',
|
||||
step8VerificationTargetEmail: preparedState?.step8VerificationTargetEmail || nextPageState?.displayedEmail || '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function pollEmailVerificationCode(preparedState, pageState, visibleStep, runtime = {}) {
|
||||
let latestResendAt = Math.max(
|
||||
0,
|
||||
Number(runtime?.stickyLastResendAt) || 0,
|
||||
Number(preparedState?.loginVerificationRequestedAt) || 0
|
||||
);
|
||||
const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
|
||||
? runtime.onResendRequestedAt
|
||||
: null;
|
||||
const mail = getMailConfig(preparedState);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: stepStartedAt;
|
||||
const verificationSessionKey = `${visibleStep}:${stepStartedAt}`;
|
||||
const shouldCompareVerificationEmail = mail.provider !== '2925';
|
||||
const displayedVerificationEmail = shouldCompareVerificationEmail
|
||||
? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail)
|
||||
: '';
|
||||
const fixedTargetEmail = shouldCompareVerificationEmail
|
||||
? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(preparedState?.step8VerificationTargetEmail || preparedState?.email))
|
||||
: '';
|
||||
|
||||
await setState({
|
||||
step8VerificationTargetEmail: displayedVerificationEmail || '',
|
||||
});
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:邮箱验证码页面已就绪,开始获取验证码。`, 'info');
|
||||
if (shouldCompareVerificationEmail && displayedVerificationEmail) {
|
||||
await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info');
|
||||
}
|
||||
|
||||
if (shouldUseCustomRegistrationEmail(preparedState)) {
|
||||
await confirmCustomVerificationStepBypass(8, {
|
||||
completionStep: visibleStep,
|
||||
promptStep: visibleStep,
|
||||
});
|
||||
return { lastResendAt: latestResendAt };
|
||||
}
|
||||
|
||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
||||
await addLog(`步骤 ${visibleStep}:正在确认 iCloud 邮箱登录态...`, 'info');
|
||||
await ensureIcloudMailSession({
|
||||
state: preparedState,
|
||||
step: 8,
|
||||
actionLabel: `步骤 ${visibleStep}:确认 iCloud 邮箱登录态`,
|
||||
});
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
if (
|
||||
mail.provider === HOTMAIL_PROVIDER
|
||||
|| mail.provider === LUCKMAIL_PROVIDER
|
||||
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|
||||
|| mail.provider === CLOUD_MAIL_PROVIDER
|
||||
) {
|
||||
await addLog(`步骤 ${visibleStep}:正在通过 ${mail.label} 轮询验证码...`);
|
||||
} else {
|
||||
await addLog(`步骤 ${visibleStep}:正在打开${mail.label}...`);
|
||||
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
|
||||
await ensureMail2925MailboxSession({
|
||||
accountId: preparedState.currentMail2925AccountId || null,
|
||||
forceRelogin: false,
|
||||
allowLoginWhenOnLoginPage: Boolean(preparedState?.mail2925UseAccountPool),
|
||||
expectedMailboxEmail: getExpectedMail2925MailboxEmail(preparedState),
|
||||
actionLabel: `Step ${visibleStep}: ensure 2925 mailbox session`,
|
||||
});
|
||||
} else {
|
||||
await focusOrOpenMailTab(mail);
|
||||
}
|
||||
if (mail.provider === '2925') {
|
||||
await addLog(`步骤 ${visibleStep}:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
await resolveVerificationStep(8, {
|
||||
...preparedState,
|
||||
step8VerificationTargetEmail: displayedVerificationEmail || '',
|
||||
}, mail, {
|
||||
completionStep: visibleStep,
|
||||
filterAfterTimestamp: verificationFilterAfterTimestamp,
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
getRemainingTimeMs: getStep8RemainingTimeResolver(preparedState?.oauthUrl || '', visibleStep),
|
||||
requestFreshCodeFirst: false,
|
||||
lastResendAt: latestResendAt,
|
||||
onResendRequestedAt: async (requestedAt) => {
|
||||
const numericRequestedAt = Number(requestedAt) || 0;
|
||||
if (numericRequestedAt > 0) {
|
||||
latestResendAt = Math.max(latestResendAt, numericRequestedAt);
|
||||
}
|
||||
if (notifyResendRequestedAt) {
|
||||
await notifyResendRequestedAt(latestResendAt);
|
||||
}
|
||||
},
|
||||
targetEmail: fixedTargetEmail,
|
||||
maxResendRequests: mail.provider === '2925' ? 2 : undefined,
|
||||
initialPollMaxAttempts: mail.provider === '2925' ? 5 : undefined,
|
||||
pollAttemptPlan: mail.provider === '2925' ? [2, 3, 15] : undefined,
|
||||
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
|
||||
? 15000
|
||||
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
? 0
|
||||
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
|
||||
});
|
||||
return {
|
||||
lastResendAt: latestResendAt,
|
||||
};
|
||||
}
|
||||
|
||||
async function completeFetchBindEmailCodeSkippedOnOauth(visibleStep, options = {}) {
|
||||
await addLog(`步骤 ${visibleStep}:当前认证页已进入 OAuth 授权页,跳过绑定邮箱验证码步骤。`, 'warn', {
|
||||
step: visibleStep,
|
||||
stepKey: 'fetch-bind-email-code',
|
||||
});
|
||||
if (typeof completeNodeFromBackground === 'function') {
|
||||
await completeNodeFromBackground(options.nodeId || 'fetch-bind-email-code', {
|
||||
directOAuthConsentPage: true,
|
||||
bindEmailCodeSkipped: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function executeFetchBindEmailCode(state) {
|
||||
const visibleStep = getVisibleStep(state, 10);
|
||||
activeFetchLoginCodeStep = visibleStep;
|
||||
activeFetchLoginCodeStepKey = 'fetch-bind-email-code';
|
||||
await ensureAuthTabForPostLoginStep(state, visibleStep);
|
||||
const pageState = await getLoginAuthStateFromContent(visibleStep, {
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认绑定邮箱验证码页已就绪', state?.oauthUrl || '', visibleStep),
|
||||
logMessage: `步骤 ${visibleStep}:正在确认绑定邮箱验证码页...`,
|
||||
});
|
||||
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
if (state?.bindEmailSubmitted) {
|
||||
throw new Error(`步骤 ${visibleStep}:绑定邮箱提交后不应直接进入 OAuth 授权页,必须先完成邮箱验证码。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
await completeFetchBindEmailCodeSkippedOnOauth(visibleStep, { nodeId: state?.nodeId });
|
||||
return;
|
||||
}
|
||||
if (pageState?.state !== 'verification_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:获取绑定邮箱验证码步骤只处理邮箱验证码页,当前状态:${pageState?.state || 'unknown'}。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
if (!state?.bindEmailSubmitted) {
|
||||
throw new Error(`步骤 ${visibleStep}:尚未完成绑定邮箱提交,不能直接获取绑定邮箱验证码。`);
|
||||
}
|
||||
|
||||
return pollEmailVerificationCode(state, pageState, visibleStep, {
|
||||
stickyLastResendAt: Number(state?.loginVerificationRequestedAt) || 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeBoundEmailLoginCode(state) {
|
||||
const visibleStep = getVisibleStep(state, 11);
|
||||
activeFetchLoginCodeStep = visibleStep;
|
||||
activeFetchLoginCodeStepKey = 'fetch-bound-email-login-code';
|
||||
const preparedState = buildBoundEmailLoginState(state, visibleStep);
|
||||
const authTabId = await getTabId('signup-page');
|
||||
|
||||
if (authTabId) {
|
||||
await chrome.tabs.update(authTabId, { active: true });
|
||||
} else {
|
||||
if (!preparedState.oauthUrl) {
|
||||
throw new Error(`步骤 ${visibleStep}:缺少登录用 OAuth 链接,请先完成绑定邮箱后刷新 OAuth 并登录。`);
|
||||
}
|
||||
await reuseOrCreateTab('signup-page', preparedState.oauthUrl);
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
const pageState = await ensureStep8VerificationPageReady({
|
||||
visibleStep,
|
||||
authLoginStep: Math.max(1, visibleStep - 1),
|
||||
allowPhoneVerificationPage: true,
|
||||
allowAddEmailPage: false,
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认绑定邮箱登录验证码页已就绪', preparedState?.oauthUrl || '', visibleStep),
|
||||
});
|
||||
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, {
|
||||
nodeId: state?.nodeId || 'fetch-bound-email-login-code',
|
||||
stepKey: 'fetch-bound-email-login-code',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (pageState?.state === 'add_phone_page' || pageState?.state === 'phone_verification_page') {
|
||||
await completeStep8WhenDeferredToPostLoginPhone(visibleStep, pageState, {
|
||||
nodeId: state?.nodeId || 'fetch-bound-email-login-code',
|
||||
stepKey: 'fetch-bound-email-login-code',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (pageState?.state === 'add_email_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:绑定邮箱后邮箱模式登录不应再进入添加邮箱页。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
if (pageState?.state !== 'verification_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:绑定邮箱后获取登录验证码只处理邮箱登录验证码页,当前状态:${pageState?.state || 'unknown'}。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
|
||||
return pollEmailVerificationCode(preparedState, pageState, visibleStep, {
|
||||
stickyLastResendAt: Number(preparedState?.loginVerificationRequestedAt) || 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeBoundEmailPostLoginPhoneVerification(state) {
|
||||
return executePostLoginPhoneVerification(state, {
|
||||
stepKey: 'post-bound-email-phone-verification',
|
||||
fallbackNodeId: 'post-bound-email-phone-verification',
|
||||
});
|
||||
}
|
||||
|
||||
async function runStep8Attempt(state, runtime = {}) {
|
||||
const visibleStep = getVisibleStep(state, 8);
|
||||
activeFetchLoginCodeStep = visibleStep;
|
||||
activeFetchLoginCodeStepKey = 'fetch-login-code';
|
||||
const authTabId = await getTabId('signup-page');
|
||||
|
||||
if (authTabId) {
|
||||
await chrome.tabs.update(authTabId, { active: true });
|
||||
} else {
|
||||
if (!state.oauthUrl) {
|
||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`);
|
||||
}
|
||||
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
let pageState = await ensureStep8VerificationPageReady({
|
||||
visibleStep,
|
||||
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
|
||||
allowPhoneVerificationPage: true,
|
||||
allowAddEmailPage: true,
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
|
||||
});
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { nodeId: state?.nodeId });
|
||||
return;
|
||||
}
|
||||
const phoneLoginCodeMode = isPhoneLoginCodeMode(state);
|
||||
if (phoneLoginCodeMode) {
|
||||
if (pageState?.state === 'phone_verification_page') {
|
||||
return executeLoginPhoneCodeStep(state, authTabId, visibleStep);
|
||||
}
|
||||
if (pageState?.state === 'add_email_page') {
|
||||
await completeStep8WhenDeferredToBindEmail(visibleStep, { nodeId: state?.nodeId });
|
||||
return;
|
||||
}
|
||||
if (pageState?.state === 'verification_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:手机号注册模式只允许处理手机登录验证码,当前进入了普通邮箱登录验证码页,不会回落到邮箱 provider。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
if (pageState?.state === 'add_phone_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:手机号注册模式不应进入添加手机号页。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
throw new Error(`步骤 ${visibleStep}:手机号注册模式登录验证码步骤进入了不允许的页面:${pageState?.state || 'unknown'}。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
|
||||
if (pageState?.state === 'add_phone_page' || pageState?.state === 'phone_verification_page') {
|
||||
await completeStep8WhenDeferredToPostLoginPhone(visibleStep, pageState, { nodeId: state?.nodeId });
|
||||
return;
|
||||
}
|
||||
if (pageState?.state === 'add_email_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:邮箱注册模式不应进入添加邮箱页。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
|
||||
return pollEmailVerificationCode(state, pageState, visibleStep, runtime);
|
||||
}
|
||||
|
||||
function isStep8RestartStep7Error(error) {
|
||||
const message = String(error?.message || error || '');
|
||||
return /STEP8_RESTART_STEP7::/i.test(message);
|
||||
}
|
||||
|
||||
async function executeStep8(state) {
|
||||
let currentState = state;
|
||||
let mailPollingAttempt = 1;
|
||||
let lastMailPollingError = null;
|
||||
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||
let retryWithoutStep7Streak = 0;
|
||||
const maxRetryWithoutStep7Streak = 3;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const result = await runStep8Attempt(currentState, {
|
||||
stickyLastResendAt,
|
||||
onResendRequestedAt: async (requestedAt) => {
|
||||
const numericRequestedAt = Number(requestedAt) || 0;
|
||||
if (numericRequestedAt > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, numericRequestedAt);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (Number(result?.lastResendAt) > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, Number(result.lastResendAt) || 0);
|
||||
}
|
||||
retryWithoutStep7Streak = 0;
|
||||
return;
|
||||
} catch (err) {
|
||||
const visibleStep = getVisibleStep(currentState, 8);
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
let currentError = err;
|
||||
let retryWithoutStep7 = false;
|
||||
|
||||
const isMailPollingError = isVerificationMailPollingError(err);
|
||||
if (isMailPollingError && !isStep8RestartStep7Error(err)) {
|
||||
const recovery = await recoverStep8PollingFailure(currentState, visibleStep);
|
||||
if (recovery?.outcome === 'completed') {
|
||||
return;
|
||||
}
|
||||
if (recovery?.outcome === 'retry_without_step7') {
|
||||
retryWithoutStep7 = true;
|
||||
}
|
||||
if (recovery?.error) {
|
||||
currentError = recovery.error;
|
||||
}
|
||||
}
|
||||
if (!isVerificationMailPollingError(currentError) && !isStep8RestartStep7Error(currentError)) {
|
||||
throw currentError;
|
||||
}
|
||||
|
||||
lastMailPollingError = currentError;
|
||||
if (mailPollingAttempt >= STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS) {
|
||||
break;
|
||||
}
|
||||
|
||||
mailPollingAttempt += 1;
|
||||
if (retryWithoutStep7) {
|
||||
retryWithoutStep7Streak += 1;
|
||||
if (retryWithoutStep7Streak > maxRetryWithoutStep7Streak) {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:邮箱通信异常在当前链路已连续重试 ${retryWithoutStep7Streak} 次,改为回到步骤 ${authLoginStep} 重新发起授权链路,避免空轮询循环。`,
|
||||
'warn'
|
||||
);
|
||||
await rerunStep7ForStep8Recovery({
|
||||
logMessage: `邮箱通信异常持续未恢复,正在回到步骤 ${authLoginStep} 重新发起登录流程...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: 'fetch-login-code',
|
||||
});
|
||||
currentState = await getState();
|
||||
retryWithoutStep7Streak = 0;
|
||||
continue;
|
||||
}
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}(连续同链路重试 ${retryWithoutStep7Streak}/${maxRetryWithoutStep7Streak})。`,
|
||||
'warn'
|
||||
);
|
||||
const latestState = await getState();
|
||||
const latestStateResendAt = Number(latestState?.loginVerificationRequestedAt) || 0;
|
||||
if (latestStateResendAt > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, latestStateResendAt);
|
||||
}
|
||||
currentState = latestState;
|
||||
if (stickyLastResendAt > 0 && (!latestStateResendAt || latestStateResendAt < stickyLastResendAt)) {
|
||||
currentState = {
|
||||
...latestState,
|
||||
loginVerificationRequestedAt: stickyLastResendAt,
|
||||
};
|
||||
}
|
||||
const resendIntervalMs = getStep8ResendIntervalMs(currentState);
|
||||
const remainingBeforeRetryMs = stickyLastResendAt > 0 && resendIntervalMs > 0
|
||||
? Math.max(0, resendIntervalMs - (Date.now() - stickyLastResendAt))
|
||||
: 0;
|
||||
if (remainingBeforeRetryMs > 0 && typeof sleepWithStop === 'function') {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:上轮已触发重发验证码,为避免重复重发,先等待 ${Math.ceil(remainingBeforeRetryMs / 1000)} 秒后继续当前链路重试。`,
|
||||
'info'
|
||||
);
|
||||
await sleepWithStop(Math.min(remainingBeforeRetryMs, 3000));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
retryWithoutStep7Streak = 0;
|
||||
await addLog(
|
||||
isStep8RestartStep7Error(currentError)
|
||||
? `步骤 ${visibleStep}:检测到认证页进入重试/超时报错状态,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`
|
||||
: `步骤 ${visibleStep}:检测到邮箱轮询类失败,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`,
|
||||
'warn'
|
||||
);
|
||||
await rerunStep7ForStep8Recovery({
|
||||
logMessage: isStep8RestartStep7Error(currentError)
|
||||
? `认证页进入重试/超时报错状态,正在回到步骤 ${authLoginStep} 重新发起登录流程...`
|
||||
: `正在回到步骤 ${authLoginStep},重新发起登录验证码流程...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: 'fetch-login-code',
|
||||
});
|
||||
currentState = await getState();
|
||||
}
|
||||
}
|
||||
|
||||
const visibleStep = getVisibleStep(currentState, 8);
|
||||
if (lastMailPollingError) {
|
||||
throw new Error(
|
||||
`步骤 ${visibleStep}:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`步骤 ${visibleStep}:登录验证码流程未成功完成。`);
|
||||
}
|
||||
|
||||
return {
|
||||
executeStep8,
|
||||
executePostLoginPhoneVerification,
|
||||
executeBindEmail,
|
||||
executeFetchBindEmailCode,
|
||||
executeBoundEmailLoginCode,
|
||||
executeBoundEmailPostLoginPhoneVerification,
|
||||
};
|
||||
}
|
||||
|
||||
return { createStep8Executor };
|
||||
});
|
||||
@@ -0,0 +1,303 @@
|
||||
(function attachBackgroundStep4(root, factory) {
|
||||
root.MultiPageBackgroundStep4 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep4Module() {
|
||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||||
|
||||
function createStep4Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeNodeFromBackground,
|
||||
confirmCustomVerificationStepBypass,
|
||||
generateRandomBirthday,
|
||||
generateRandomName,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession,
|
||||
getMailConfig,
|
||||
getTabId,
|
||||
HOTMAIL_PROVIDER,
|
||||
isTabAlive,
|
||||
LUCKMAIL_PROVIDER,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
CLOUD_MAIL_PROVIDER = 'cloudmail',
|
||||
resolveVerificationStep,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
isRetryableContentScriptTransportError = () => false,
|
||||
shouldUseCustomRegistrationEmail,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
throwIfStopped,
|
||||
waitForTabStableComplete = null,
|
||||
phoneVerificationHelpers = null,
|
||||
resolveSignupMethod = () => 'email',
|
||||
} = deps;
|
||||
|
||||
function buildSignupProfileForVerificationStep() {
|
||||
const name = typeof generateRandomName === 'function' ? generateRandomName() : null;
|
||||
const birthday = typeof generateRandomBirthday === 'function' ? generateRandomBirthday() : null;
|
||||
if (!name?.firstName || !name?.lastName || !birthday) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
firstName: name.firstName,
|
||||
lastName: name.lastName,
|
||||
year: birthday.year,
|
||||
month: birthday.month,
|
||||
day: birthday.day,
|
||||
};
|
||||
}
|
||||
|
||||
function getExpectedMail2925MailboxEmail(state = {}) {
|
||||
if (Boolean(state?.mail2925UseAccountPool)) {
|
||||
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
|
||||
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
|
||||
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
|
||||
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
|
||||
if (accountEmail) {
|
||||
return accountEmail;
|
||||
}
|
||||
}
|
||||
|
||||
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isPhoneSignupState(state = {}) {
|
||||
return resolveSignupMethod(state) === 'phone'
|
||||
|| state?.accountIdentifierType === 'phone'
|
||||
|| Boolean(state?.signupPhoneActivation);
|
||||
}
|
||||
|
||||
async function executeSignupPhoneCodeStep(state, signupTabId) {
|
||||
if (typeof phoneVerificationHelpers?.completeSignupPhoneVerificationFlow !== 'function') {
|
||||
throw new Error('步骤 4:手机号注册验证码流程不可用,接码模块尚未初始化。');
|
||||
}
|
||||
|
||||
const signupProfile = buildSignupProfileForVerificationStep();
|
||||
const result = await phoneVerificationHelpers.completeSignupPhoneVerificationFlow(signupTabId, {
|
||||
state,
|
||||
signupProfile,
|
||||
});
|
||||
|
||||
if (result?.emailVerificationRequired || result?.emailVerificationPage) {
|
||||
return result || {};
|
||||
}
|
||||
|
||||
await completeNodeFromBackground('fetch-signup-code', {
|
||||
phoneVerification: true,
|
||||
code: result?.code || '',
|
||||
...(result?.skipProfileStep ? { skipProfileStep: true } : {}),
|
||||
...(result?.skipProfileStepReason ? { skipProfileStepReason: result.skipProfileStepReason } : {}),
|
||||
});
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey) {
|
||||
if (shouldUseCustomRegistrationEmail(state)) {
|
||||
await confirmCustomVerificationStepBypass(4);
|
||||
return;
|
||||
}
|
||||
|
||||
const mail = getMailConfig(state);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
|
||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: stepStartedAt;
|
||||
|
||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
||||
await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info');
|
||||
await ensureIcloudMailSession({
|
||||
state,
|
||||
step: 4,
|
||||
actionLabel: '步骤 4:确认 iCloud 邮箱登录态',
|
||||
});
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
if (
|
||||
mail.provider === HOTMAIL_PROVIDER
|
||||
|| mail.provider === LUCKMAIL_PROVIDER
|
||||
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|
||||
|| mail.provider === CLOUD_MAIL_PROVIDER
|
||||
) {
|
||||
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
|
||||
} else if (mail.provider === '2925') {
|
||||
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||
if (typeof ensureMail2925MailboxSession === 'function') {
|
||||
await ensureMail2925MailboxSession({
|
||||
accountId: state.currentMail2925AccountId || null,
|
||||
forceRelogin: false,
|
||||
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
|
||||
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
|
||||
actionLabel: '步骤 4:确认 2925 邮箱登录态',
|
||||
});
|
||||
} else {
|
||||
await focusOrOpenMailTab(mail);
|
||||
}
|
||||
await addLog(`步骤 4:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
|
||||
} else {
|
||||
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||
await focusOrOpenMailTab(mail);
|
||||
}
|
||||
|
||||
const shouldRequestFreshCodeFirst = ![
|
||||
HOTMAIL_PROVIDER,
|
||||
LUCKMAIL_PROVIDER,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
CLOUD_MAIL_PROVIDER,
|
||||
].includes(mail.provider);
|
||||
const signupProfile = buildSignupProfileForVerificationStep();
|
||||
|
||||
await resolveVerificationStep(4, state, mail, {
|
||||
filterAfterTimestamp: verificationFilterAfterTimestamp,
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
|
||||
signupProfile,
|
||||
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
|
||||
? 15000
|
||||
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
? 0
|
||||
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
|
||||
});
|
||||
}
|
||||
|
||||
async function focusOrOpenMailTab(mail) {
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
if (mail.navigateOnReuse) {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = await getTabId(mail.source);
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
return;
|
||||
}
|
||||
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeStep4(state) {
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationSessionKey = `4:${stepStartedAt}`;
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
|
||||
if (!signupTabId) {
|
||||
throw new Error('认证页面标签页已关闭,无法继续步骤 4。请先执行步骤 1 或步骤 2,重新打开认证页后再试。');
|
||||
}
|
||||
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
throwIfStopped();
|
||||
if (typeof waitForTabStableComplete === 'function') {
|
||||
await addLog('步骤 4:等待注册验证码页面完成加载后再继续...', 'info');
|
||||
await waitForTabStableComplete(signupTabId, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
stableMs: 800,
|
||||
initialDelayMs: 300,
|
||||
});
|
||||
}
|
||||
throwIfStopped();
|
||||
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
|
||||
|
||||
const prepareRequest = {
|
||||
type: 'PREPARE_SIGNUP_VERIFICATION',
|
||||
step: 4,
|
||||
source: 'background',
|
||||
payload: {
|
||||
password: state.password || state.customPassword || '',
|
||||
prepareSource: 'step4_execute',
|
||||
prepareLogLabel: '步骤 4 执行',
|
||||
},
|
||||
};
|
||||
const prepareTimeoutMs = 30000;
|
||||
const prepareResponseTimeoutMs = 30000;
|
||||
const prepareStartAt = Date.now();
|
||||
let prepareResult = null;
|
||||
|
||||
while (Date.now() - prepareStartAt < prepareTimeoutMs) {
|
||||
throwIfStopped();
|
||||
|
||||
try {
|
||||
prepareResult = typeof sendToContentScript === 'function'
|
||||
? await sendToContentScript('signup-page', prepareRequest, {
|
||||
responseTimeoutMs: prepareResponseTimeoutMs,
|
||||
})
|
||||
: await sendToContentScriptResilient('signup-page', prepareRequest, {
|
||||
timeoutMs: Math.max(1000, prepareTimeoutMs - (Date.now() - prepareStartAt)),
|
||||
responseTimeoutMs: prepareResponseTimeoutMs,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...',
|
||||
});
|
||||
break;
|
||||
} catch (error) {
|
||||
if (!isRetryableContentScriptTransportError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const remainingMs = Math.max(0, prepareTimeoutMs - (Date.now() - prepareStartAt));
|
||||
if (remainingMs <= 0) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const recoverResult = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'RECOVER_AUTH_RETRY_PAGE',
|
||||
step: 4,
|
||||
source: 'background',
|
||||
payload: {
|
||||
flow: 'signup',
|
||||
step: 4,
|
||||
timeoutMs: Math.min(12000, remainingMs),
|
||||
maxClickAttempts: 2,
|
||||
logLabel: '步骤 4:检测到注册认证重试页,正在点击“重试”恢复',
|
||||
},
|
||||
}, {
|
||||
timeoutMs: Math.min(12000, remainingMs),
|
||||
responseTimeoutMs: Math.min(12000, remainingMs),
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...',
|
||||
});
|
||||
|
||||
if (recoverResult?.error) {
|
||||
throw new Error(recoverResult.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prepareResult) {
|
||||
throw new Error('步骤 4:等待注册验证码页面就绪超时,请刷新认证页后重试。');
|
||||
}
|
||||
|
||||
if (prepareResult && prepareResult.error) {
|
||||
throw new Error(prepareResult.error);
|
||||
}
|
||||
if (prepareResult?.alreadyVerified) {
|
||||
await completeNodeFromBackground('fetch-signup-code', prepareResult?.skipProfileStep ? { skipProfileStep: true } : {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPhoneSignupState(state)) {
|
||||
const phoneResult = await executeSignupPhoneCodeStep(state, signupTabId);
|
||||
if (phoneResult?.emailVerificationRequired || phoneResult?.emailVerificationPage) {
|
||||
await addLog('步骤 4:手机验证码已通过,OpenAI 要求继续邮箱验证,切换到邮箱验证码轮询。', 'info');
|
||||
return executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey);
|
||||
}
|
||||
return phoneResult;
|
||||
}
|
||||
|
||||
return executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey);
|
||||
}
|
||||
|
||||
return { executeStep4 };
|
||||
}
|
||||
|
||||
return { createStep4Executor };
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
(function attachBackgroundStep3(root, factory) {
|
||||
root.MultiPageBackgroundStep3 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep3Module() {
|
||||
function createStep3Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
appendAccountRunRecord,
|
||||
chrome,
|
||||
ensureContentScriptReadyOnTab,
|
||||
generatePassword,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
resolveSignupMethod,
|
||||
sendToContentScript,
|
||||
setPasswordState,
|
||||
setState,
|
||||
SIGNUP_PAGE_INJECT_FILES,
|
||||
} = deps;
|
||||
|
||||
function normalizeSignupMethod(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'phone'
|
||||
? 'phone'
|
||||
: 'email';
|
||||
}
|
||||
|
||||
function getResolvedSignupMethodForStep3(state = {}) {
|
||||
if (typeof resolveSignupMethod === 'function') {
|
||||
return normalizeSignupMethod(resolveSignupMethod(state));
|
||||
}
|
||||
const frozenMethod = String(state?.resolvedSignupMethod || '').trim().toLowerCase();
|
||||
if (frozenMethod === 'phone' || frozenMethod === 'email') {
|
||||
return normalizeSignupMethod(frozenMethod);
|
||||
}
|
||||
return normalizeSignupMethod(state?.signupMethod);
|
||||
}
|
||||
|
||||
function resolveStep3AccountIdentity(state = {}) {
|
||||
const resolvedEmail = String(state?.email || '').trim();
|
||||
const rawAccountIdentifierType = String(state?.accountIdentifierType || '').trim().toLowerCase();
|
||||
const signupPhoneNumber = String(
|
||||
state?.signupPhoneNumber
|
||||
|| (rawAccountIdentifierType === 'phone' ? state?.accountIdentifier : '')
|
||||
|| ''
|
||||
).trim();
|
||||
const explicitEmailIdentity = rawAccountIdentifierType === 'email' && resolvedEmail;
|
||||
const shouldUsePhoneIdentity = !explicitEmailIdentity && (
|
||||
rawAccountIdentifierType === 'phone'
|
||||
|| Boolean(signupPhoneNumber)
|
||||
|| getResolvedSignupMethodForStep3(state) === 'phone'
|
||||
);
|
||||
const accountIdentifierType = shouldUsePhoneIdentity
|
||||
? 'phone'
|
||||
: (resolvedEmail ? 'email' : 'email');
|
||||
const accountIdentifier = accountIdentifierType === 'phone'
|
||||
? signupPhoneNumber
|
||||
: resolvedEmail;
|
||||
|
||||
return {
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
email: resolvedEmail,
|
||||
phoneNumber: signupPhoneNumber,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeStep3(state) {
|
||||
const identity = resolveStep3AccountIdentity(state);
|
||||
if (!identity.accountIdentifier) {
|
||||
if (identity.accountIdentifierType === 'phone') {
|
||||
throw new Error('缺少注册手机号,请先完成步骤 2 或在侧栏填写注册手机号后再执行步骤 3。');
|
||||
}
|
||||
throw new Error('缺少注册账号,请先完成步骤 2。');
|
||||
}
|
||||
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
if (!signupTabId || !(await isTabAlive('signup-page'))) {
|
||||
throw new Error('认证页面标签页已关闭,请先重新完成步骤 2。');
|
||||
}
|
||||
|
||||
const password = state.customPassword || state.password || generatePassword();
|
||||
await setPasswordState(password);
|
||||
|
||||
const accounts = Array.isArray(state.accounts) ? state.accounts.slice() : [];
|
||||
accounts.push({
|
||||
email: identity.email,
|
||||
phoneNumber: identity.phoneNumber,
|
||||
accountIdentifierType: identity.accountIdentifierType,
|
||||
accountIdentifier: identity.accountIdentifier,
|
||||
password,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
await setState({ accounts });
|
||||
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: '步骤 3:密码页内容脚本未就绪,正在等待页面恢复...',
|
||||
});
|
||||
|
||||
const identityLabel = identity.accountIdentifierType === 'phone'
|
||||
? `注册手机号为 ${identity.accountIdentifier}`
|
||||
: `邮箱为 ${identity.accountIdentifier}`;
|
||||
await addLog(
|
||||
`步骤 3:正在填写密码,${identityLabel},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
|
||||
);
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'fill-password',
|
||||
step: 3,
|
||||
source: 'background',
|
||||
payload: {
|
||||
email: identity.email,
|
||||
phoneNumber: identity.phoneNumber,
|
||||
accountIdentifierType: identity.accountIdentifierType,
|
||||
accountIdentifier: identity.accountIdentifier,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof appendAccountRunRecord === 'function') {
|
||||
try {
|
||||
await appendAccountRunRecord('running', {
|
||||
...state,
|
||||
...identity,
|
||||
password,
|
||||
currentNodeId: 'fill-password',
|
||||
});
|
||||
} catch (err) {
|
||||
await addLog(`步骤 3:密码已填写,但预保存账号记录失败:${err?.message || err}`, 'warn');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { executeStep3 };
|
||||
}
|
||||
|
||||
return { createStep3Executor };
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
(function attachBackgroundStep5(root, factory) {
|
||||
root.MultiPageBackgroundStep5 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep5Module() {
|
||||
function createStep5Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
generateRandomBirthday,
|
||||
generateRandomName,
|
||||
sendToContentScript,
|
||||
} = deps;
|
||||
|
||||
async function executeStep5() {
|
||||
const { firstName, lastName } = generateRandomName();
|
||||
const { year, month, day } = generateRandomBirthday();
|
||||
|
||||
await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
|
||||
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'fill-profile',
|
||||
step: 5,
|
||||
source: 'background',
|
||||
payload: {
|
||||
firstName,
|
||||
lastName,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { executeStep5 };
|
||||
}
|
||||
|
||||
return { createStep5Executor };
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
(function attachBackgroundGoPayManualConfirm(root, factory) {
|
||||
root.MultiPageBackgroundGoPayManualConfirm = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGoPayManualConfirmModule() {
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const DEFAULT_CONFIRM_TITLE = 'GoPay 订阅确认';
|
||||
const DEFAULT_CONFIRM_MESSAGE = 'GoPay 订阅页已打开。请先手动完成订阅,完成后确认继续 OAuth 登录。';
|
||||
|
||||
function createGoPayManualConfirmExecutor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
broadcastDataUpdate,
|
||||
chrome,
|
||||
createAutomationTab = null,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
registerTab,
|
||||
setState,
|
||||
} = deps;
|
||||
|
||||
function buildRequestId() {
|
||||
return `gopay-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
async function resolveCheckoutTabId(state = {}) {
|
||||
const registeredTabId = typeof getTabId === 'function'
|
||||
? await getTabId(PLUS_CHECKOUT_SOURCE)
|
||||
: null;
|
||||
if (registeredTabId && typeof isTabAlive === 'function' && await isTabAlive(PLUS_CHECKOUT_SOURCE)) {
|
||||
return Number(registeredTabId) || 0;
|
||||
}
|
||||
|
||||
const storedTabId = Number(state?.plusCheckoutTabId) || 0;
|
||||
if (storedTabId && chrome?.tabs?.get) {
|
||||
const tab = await chrome.tabs.get(storedTabId).catch(() => null);
|
||||
if (tab?.id) {
|
||||
if (typeof registerTab === 'function') {
|
||||
await registerTab(PLUS_CHECKOUT_SOURCE, tab.id);
|
||||
}
|
||||
return tab.id;
|
||||
}
|
||||
}
|
||||
|
||||
const checkoutUrl = String(state?.plusCheckoutUrl || '').trim();
|
||||
if (!checkoutUrl) {
|
||||
throw new Error('步骤 7:未检测到 GoPay 订阅页,请先执行步骤 6。');
|
||||
}
|
||||
|
||||
if (!chrome?.tabs?.create) {
|
||||
throw new Error('步骤 7:无法自动重新打开 GoPay 订阅页。');
|
||||
}
|
||||
|
||||
const tab = typeof createAutomationTab === 'function'
|
||||
? await createAutomationTab({ url: checkoutUrl, active: true })
|
||||
: await chrome.tabs.create({ url: checkoutUrl, active: true });
|
||||
const tabId = Number(tab?.id) || 0;
|
||||
if (!tabId) {
|
||||
throw new Error('步骤 7:重新打开 GoPay 订阅页失败。');
|
||||
}
|
||||
if (typeof registerTab === 'function') {
|
||||
await registerTab(PLUS_CHECKOUT_SOURCE, tabId);
|
||||
}
|
||||
return tabId;
|
||||
}
|
||||
|
||||
async function executeGoPayManualConfirm(state = {}) {
|
||||
const tabId = await resolveCheckoutTabId(state);
|
||||
if (chrome?.tabs?.update && tabId) {
|
||||
await chrome.tabs.update(tabId, { active: true }).catch(() => {});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
plusCheckoutTabId: tabId,
|
||||
plusManualConfirmationPending: true,
|
||||
plusManualConfirmationRequestId: buildRequestId(),
|
||||
plusManualConfirmationStep: 7,
|
||||
plusManualConfirmationMethod: 'gopay',
|
||||
plusManualConfirmationTitle: DEFAULT_CONFIRM_TITLE,
|
||||
plusManualConfirmationMessage: DEFAULT_CONFIRM_MESSAGE,
|
||||
};
|
||||
|
||||
await setState(payload);
|
||||
if (typeof broadcastDataUpdate === 'function') {
|
||||
broadcastDataUpdate(payload);
|
||||
}
|
||||
|
||||
await addLog('步骤 7:正在等待手动完成 GoPay 订阅,确认后继续 OAuth 登录。', 'info');
|
||||
}
|
||||
|
||||
return {
|
||||
executeGoPayManualConfirm,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createGoPayManualConfirmExecutor,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,438 @@
|
||||
(function attachBackgroundStep7(root, factory) {
|
||||
root.MultiPageBackgroundStep7 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep7Module() {
|
||||
function createStep7Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
completeNodeFromBackground,
|
||||
getErrorMessage,
|
||||
getLoginAuthStateLabel,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getState,
|
||||
isAddPhoneAuthFailure = (error) => {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||
if (/\u624b\u673a\u53f7\u8f93\u5165\u6a21\u5f0f|phone\s+entry/i.test(message)) {
|
||||
return false;
|
||||
}
|
||||
return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)|\badd-phone\b|\u6dfb\u52a0\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u8fdb\u5165\u624b\u673a\u53f7\u9875\u9762|\u624b\u673a\u53f7\u9875|\u624b\u673a\u53f7\u9875\u9762|phone\s+number|telephone/i.test(message);
|
||||
},
|
||||
isStep6RecoverableResult,
|
||||
isStep6SuccessResult,
|
||||
getTabId,
|
||||
refreshOAuthUrlBeforeStep6,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScriptResilient,
|
||||
startOAuthFlowTimeoutWindow,
|
||||
STEP6_MAX_ATTEMPTS,
|
||||
throwIfStopped,
|
||||
} = deps;
|
||||
|
||||
function isManagementSecretConfigError(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '').trim();
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mentionsSecret = /管理密钥|Admin Secret|X-Admin-Key|CPA Key/i.test(message);
|
||||
if (!mentionsSecret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message);
|
||||
}
|
||||
|
||||
function normalizeStep7IdentifierType(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'phone' || normalized === 'email' ? normalized : '';
|
||||
}
|
||||
|
||||
function normalizeStep7SignupMethod(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
|
||||
}
|
||||
|
||||
function shouldForceStep7EmailLogin(state = {}) {
|
||||
return normalizeStep7IdentifierType(state?.forceLoginIdentifierType) === 'email'
|
||||
|| Boolean(state?.forceEmailLogin);
|
||||
}
|
||||
|
||||
function canUseConfiguredPhoneSignup(state = {}) {
|
||||
return normalizeStep7SignupMethod(state?.signupMethod) === 'phone'
|
||||
&& Boolean(state?.phoneVerificationEnabled)
|
||||
&& !Boolean(state?.plusModeEnabled)
|
||||
&& !Boolean(state?.contributionMode);
|
||||
}
|
||||
|
||||
function hasStep7PhoneSignupIdentity(state = {}) {
|
||||
return Boolean(
|
||||
String(state?.signupPhoneNumber || '').trim()
|
||||
|| String(state?.signupPhoneCompletedActivation?.phoneNumber || '').trim()
|
||||
|| String(state?.signupPhoneActivation?.phoneNumber || '').trim()
|
||||
|| (
|
||||
normalizeStep7IdentifierType(state?.accountIdentifierType) === 'phone'
|
||||
&& String(state?.accountIdentifier || '').trim()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function shouldPreferStep7PhoneSignupIdentity(state = {}) {
|
||||
return canUseConfiguredPhoneSignup(state)
|
||||
&& hasStep7PhoneSignupIdentity(state);
|
||||
}
|
||||
|
||||
function resolveStep7LoginIdentifierType(state = {}, fallbackType = '') {
|
||||
if (shouldForceStep7EmailLogin(state)) {
|
||||
return 'email';
|
||||
}
|
||||
|
||||
if (shouldPreferStep7PhoneSignupIdentity(state)) {
|
||||
return 'phone';
|
||||
}
|
||||
|
||||
const explicitIdentifierType = normalizeStep7IdentifierType(state?.accountIdentifierType);
|
||||
if (explicitIdentifierType) {
|
||||
return explicitIdentifierType;
|
||||
}
|
||||
|
||||
const frozenSignupMethod = normalizeStep7IdentifierType(state?.resolvedSignupMethod);
|
||||
if (frozenSignupMethod) {
|
||||
return frozenSignupMethod;
|
||||
}
|
||||
|
||||
if (canUseConfiguredPhoneSignup(state)) {
|
||||
return 'phone';
|
||||
}
|
||||
|
||||
return normalizeStep7IdentifierType(fallbackType) || 'email';
|
||||
}
|
||||
|
||||
function resolveStep7FallbackEmail(state = {}) {
|
||||
const registrationEmail = String(state?.registrationEmailState?.current || '').trim();
|
||||
if (registrationEmail) {
|
||||
return registrationEmail;
|
||||
}
|
||||
|
||||
const currentHotmailAccountId = String(state?.currentHotmailAccountId || '').trim();
|
||||
if (currentHotmailAccountId && Array.isArray(state?.hotmailAccounts)) {
|
||||
const matchedHotmailAccount = state.hotmailAccounts.find((account) => String(account?.id || '').trim() === currentHotmailAccountId);
|
||||
const hotmailEmail = String(matchedHotmailAccount?.email || '').trim();
|
||||
if (hotmailEmail) {
|
||||
return hotmailEmail;
|
||||
}
|
||||
}
|
||||
|
||||
const currentMail2925AccountId = String(state?.currentMail2925AccountId || '').trim();
|
||||
if (currentMail2925AccountId && Array.isArray(state?.mail2925Accounts)) {
|
||||
const matchedMail2925Account = state.mail2925Accounts.find((account) => String(account?.id || '').trim() === currentMail2925AccountId);
|
||||
const mail2925Email = String(matchedMail2925Account?.email || '').trim();
|
||||
if (mail2925Email) {
|
||||
return mail2925Email;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractAddPhoneUrl(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||
const match = message.match(/https:\/\/auth\.openai\.com\/add-phone(?:[^\s]*)?/i);
|
||||
return match ? match[0] : 'https://auth.openai.com/add-phone';
|
||||
}
|
||||
|
||||
function getStep7ResultState(result = {}) {
|
||||
return String(result?.state || '').trim();
|
||||
}
|
||||
|
||||
function isStep7OauthConsentResult(result = {}) {
|
||||
return Boolean(result?.directOAuthConsentPage)
|
||||
|| getStep7ResultState(result) === 'oauth_consent_page';
|
||||
}
|
||||
|
||||
function isStep7AddEmailResult(result = {}) {
|
||||
return Boolean(result?.addEmailPage) || getStep7ResultState(result) === 'add_email_page';
|
||||
}
|
||||
|
||||
function isStep7AddPhoneResult(result = {}) {
|
||||
return Boolean(result?.addPhonePage) || getStep7ResultState(result) === 'add_phone_page';
|
||||
}
|
||||
|
||||
function isStep7PhoneVerificationResult(result = {}) {
|
||||
return Boolean(result?.phoneVerificationPage) || getStep7ResultState(result) === 'phone_verification_page';
|
||||
}
|
||||
|
||||
function isStep7PlainVerificationResult(result = {}) {
|
||||
return getStep7ResultState(result) === 'verification_page' && !isStep7PhoneVerificationResult(result);
|
||||
}
|
||||
|
||||
function buildStep7CompletionPayload(result = {}, currentState = {}, currentIdentifierType = '', currentPhoneNumber = '') {
|
||||
const phoneSignupMode = currentIdentifierType === 'phone';
|
||||
const payload = {
|
||||
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
|
||||
};
|
||||
|
||||
if (currentIdentifierType === 'phone') {
|
||||
payload.accountIdentifierType = 'phone';
|
||||
payload.accountIdentifier = currentPhoneNumber;
|
||||
payload.signupPhoneNumber = currentPhoneNumber;
|
||||
payload.signupPhoneCompletedActivation = currentState?.signupPhoneCompletedActivation || null;
|
||||
payload.signupPhoneActivation = currentState?.signupPhoneActivation || null;
|
||||
}
|
||||
|
||||
if (isStep7OauthConsentResult(result)) {
|
||||
payload.skipLoginVerificationStep = true;
|
||||
payload.directOAuthConsentPage = true;
|
||||
return payload;
|
||||
}
|
||||
|
||||
if (phoneSignupMode) {
|
||||
if (isStep7AddPhoneResult(result)) {
|
||||
throw new Error(`步骤 ${completionStepForState(currentState)}:手机号注册模式 OAuth 登录不应进入添加手机号页。URL: ${result?.url || ''}`.trim());
|
||||
}
|
||||
if (isStep7AddEmailResult(result)) {
|
||||
payload.skipLoginVerificationStep = true;
|
||||
payload.addEmailPage = true;
|
||||
return payload;
|
||||
}
|
||||
if (isStep7PhoneVerificationResult(result)) {
|
||||
return payload;
|
||||
}
|
||||
if (isStep7PlainVerificationResult(result)) {
|
||||
throw new Error(`步骤 ${completionStepForState(currentState)}:手机号注册模式 OAuth 登录进入了普通邮箱登录验证码页,当前流程不会回落到邮箱验证码。URL: ${result?.url || ''}`.trim());
|
||||
}
|
||||
throw new Error(`步骤 ${completionStepForState(currentState)}:手机号注册模式 OAuth 登录进入了不允许的页面:${getLoginAuthStateLabel(result.state)}。URL: ${result?.url || ''}`.trim());
|
||||
}
|
||||
|
||||
if (isStep7AddEmailResult(result)) {
|
||||
throw new Error(`步骤 ${completionStepForState(currentState)}:邮箱注册模式 OAuth 登录不应进入添加邮箱页。URL: ${result?.url || ''}`.trim());
|
||||
}
|
||||
if (isStep7AddPhoneResult(result) || isStep7PhoneVerificationResult(result)) {
|
||||
payload.skipLoginVerificationStep = true;
|
||||
payload.addPhonePage = isStep7AddPhoneResult(result);
|
||||
payload.phoneVerificationPage = isStep7PhoneVerificationResult(result);
|
||||
return payload;
|
||||
}
|
||||
if (isStep7PlainVerificationResult(result)) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
throw new Error(`步骤 ${completionStepForState(currentState)}:邮箱注册模式 OAuth 登录进入了不允许的页面:${getLoginAuthStateLabel(result.state)}。URL: ${result?.url || ''}`.trim());
|
||||
}
|
||||
|
||||
function completionStepForState(state = {}) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep > 0 ? visibleStep : 7;
|
||||
}
|
||||
|
||||
async function completeStep7PostLoginPhoneHandoff(state = {}, err, completionStep) {
|
||||
if (normalizeStep7SignupMethod(state?.resolvedSignupMethod || state?.signupMethod) === 'phone') {
|
||||
throw new Error(
|
||||
`步骤 ${completionStep}:手机号注册模式 OAuth 登录进入了添加手机号页,当前流程不允许在手机号注册模式补手机号。URL: ${extractAddPhoneUrl(err)}`
|
||||
);
|
||||
}
|
||||
await completeNodeFromBackground(state?.nodeId || 'oauth-login', {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
addPhonePage: true,
|
||||
directOAuthConsentPage: false,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeStep7(state) {
|
||||
const initialState = typeof getState === 'function'
|
||||
? {
|
||||
...(state || {}),
|
||||
...(await getState().catch(() => ({}))),
|
||||
}
|
||||
: (state || {});
|
||||
const visibleStep = Math.floor(Number(initialState?.visibleStep) || 0);
|
||||
const completionStep = visibleStep > 0 ? visibleStep : 7;
|
||||
const resolvedIdentifierType = resolveStep7LoginIdentifierType(initialState);
|
||||
const phoneNumber = resolvedIdentifierType === 'phone'
|
||||
? String(
|
||||
initialState?.signupPhoneNumber
|
||||
|| (normalizeStep7IdentifierType(initialState?.accountIdentifierType) === 'phone' ? initialState?.accountIdentifier : '')
|
||||
|| initialState?.signupPhoneCompletedActivation?.phoneNumber
|
||||
|| initialState?.signupPhoneActivation?.phoneNumber
|
||||
|| ''
|
||||
).trim()
|
||||
: '';
|
||||
const email = resolvedIdentifierType === 'email'
|
||||
? String(
|
||||
initialState?.email
|
||||
|| (normalizeStep7IdentifierType(initialState?.accountIdentifierType) === 'email' ? initialState?.accountIdentifier : '')
|
||||
|| resolveStep7FallbackEmail(initialState)
|
||||
|| ''
|
||||
).trim()
|
||||
: '';
|
||||
if (
|
||||
(resolvedIdentifierType === 'phone' && !phoneNumber)
|
||||
|| (resolvedIdentifierType !== 'phone' && !email)
|
||||
) {
|
||||
throw new Error('缺少登录账号:请先完成步骤 2,或在侧栏“注册邮箱/注册手机号”中手动填写账号后再执行当前步骤。');
|
||||
}
|
||||
|
||||
let attempt = 0;
|
||||
let lastError = null;
|
||||
|
||||
while (attempt < STEP6_MAX_ATTEMPTS) {
|
||||
throwIfStopped();
|
||||
attempt += 1;
|
||||
try {
|
||||
const rawCurrentState = attempt === 1 ? initialState : await getState();
|
||||
const currentState = shouldForceStep7EmailLogin(state)
|
||||
? {
|
||||
...rawCurrentState,
|
||||
forceLoginIdentifierType: 'email',
|
||||
forceEmailLogin: true,
|
||||
signupMethod: 'email',
|
||||
resolvedSignupMethod: 'email',
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: email,
|
||||
email,
|
||||
}
|
||||
: rawCurrentState;
|
||||
const password = currentState.password || currentState.customPassword || '';
|
||||
const currentIdentifierType = resolveStep7LoginIdentifierType(currentState, resolvedIdentifierType);
|
||||
const currentPhoneNumber = currentIdentifierType === 'phone'
|
||||
? String(
|
||||
currentState?.signupPhoneNumber
|
||||
|| (normalizeStep7IdentifierType(currentState?.accountIdentifierType) === 'phone' ? currentState?.accountIdentifier : '')
|
||||
|| currentState?.signupPhoneCompletedActivation?.phoneNumber
|
||||
|| currentState?.signupPhoneActivation?.phoneNumber
|
||||
|| phoneNumber
|
||||
).trim()
|
||||
: '';
|
||||
const currentEmail = currentIdentifierType === 'email'
|
||||
? String(
|
||||
currentState?.email
|
||||
|| (normalizeStep7IdentifierType(currentState?.accountIdentifierType) === 'email' ? currentState?.accountIdentifier : '')
|
||||
|| resolveStep7FallbackEmail(currentState)
|
||||
|| email
|
||||
).trim()
|
||||
: '';
|
||||
const accountIdentifier = currentIdentifierType === 'phone'
|
||||
? currentPhoneNumber
|
||||
: currentEmail;
|
||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
||||
if (typeof startOAuthFlowTimeoutWindow === 'function') {
|
||||
await startOAuthFlowTimeoutWindow({ step: completionStep, oauthUrl });
|
||||
}
|
||||
const loginTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(180000, {
|
||||
step: completionStep,
|
||||
actionLabel: 'OAuth 登录并进入验证码页',
|
||||
oauthUrl,
|
||||
})
|
||||
: 180000;
|
||||
|
||||
if (attempt === 1) {
|
||||
await addLog('正在打开最新 OAuth 链接并登录...', 'info', {
|
||||
step: completionStep,
|
||||
stepKey: 'oauth-login',
|
||||
});
|
||||
} else {
|
||||
await addLog(`上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn', {
|
||||
step: completionStep,
|
||||
stepKey: 'oauth-login',
|
||||
});
|
||||
}
|
||||
|
||||
await reuseOrCreateTab('signup-page', oauthUrl, { forceNew: true });
|
||||
|
||||
const result = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
{
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'oauth-login',
|
||||
step: 7,
|
||||
source: 'background',
|
||||
payload: {
|
||||
email: currentEmail,
|
||||
phoneNumber: currentPhoneNumber,
|
||||
countryId: currentState?.signupPhoneCompletedActivation?.countryId
|
||||
?? currentState?.signupPhoneActivation?.countryId
|
||||
?? null,
|
||||
countryLabel: String(
|
||||
currentState?.signupPhoneCompletedActivation?.countryLabel
|
||||
|| currentState?.signupPhoneActivation?.countryLabel
|
||||
|| ''
|
||||
).trim(),
|
||||
accountIdentifier,
|
||||
loginIdentifierType: currentIdentifierType,
|
||||
password,
|
||||
visibleStep: completionStep,
|
||||
},
|
||||
},
|
||||
{
|
||||
timeoutMs: loginTimeoutMs,
|
||||
responseTimeoutMs: loginTimeoutMs,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '认证页正在切换,等待页面重新就绪后继续登录...',
|
||||
logStep: completionStep,
|
||||
logStepKey: 'oauth-login',
|
||||
}
|
||||
);
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
if (isStep6SuccessResult(result)) {
|
||||
const completionPayload = buildStep7CompletionPayload(
|
||||
result,
|
||||
{ ...(currentState || {}), visibleStep: completionStep },
|
||||
currentIdentifierType,
|
||||
currentPhoneNumber
|
||||
);
|
||||
|
||||
await completeNodeFromBackground(state?.nodeId || 'oauth-login', completionPayload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isStep6RecoverableResult(result)) {
|
||||
const reasonMessage = result.message
|
||||
|| `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 ${completionStep}。`;
|
||||
throw new Error(reasonMessage);
|
||||
}
|
||||
|
||||
throw new Error(`步骤 ${completionStep}:认证页未返回可识别的登录结果。`);
|
||||
} catch (err) {
|
||||
throwIfStopped(err);
|
||||
if (isAddPhoneAuthFailure(err)) {
|
||||
const latestAddPhoneState = typeof getState === 'function'
|
||||
? await getState().catch(() => state)
|
||||
: state;
|
||||
await completeStep7PostLoginPhoneHandoff(
|
||||
{ ...(state || {}), ...(latestAddPhoneState || {}) },
|
||||
err,
|
||||
completionStep
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isManagementSecretConfigError(err)) {
|
||||
await addLog(
|
||||
`检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
|
||||
'error',
|
||||
{ step: completionStep, stepKey: 'oauth-login' }
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
lastError = err;
|
||||
if (attempt >= STEP6_MAX_ATTEMPTS) {
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn', {
|
||||
step: completionStep,
|
||||
stepKey: 'oauth-login',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`步骤 ${completionStep}:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
|
||||
}
|
||||
|
||||
return { executeStep7 };
|
||||
}
|
||||
|
||||
return { createStep7Executor };
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
(function attachBackgroundStep1(root, factory) {
|
||||
root.MultiPageBackgroundStep1 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep1Module() {
|
||||
const STEP1_COOKIE_CLEAR_DOMAINS = [
|
||||
'chatgpt.com',
|
||||
'chat.openai.com',
|
||||
'pay.openai.com',
|
||||
'openai.com',
|
||||
'auth.openai.com',
|
||||
'auth0.openai.com',
|
||||
'accounts.openai.com',
|
||||
'paypal.com',
|
||||
'stripe.com',
|
||||
'checkout.stripe.com',
|
||||
'meiguodizhi.com',
|
||||
'mail-api.yuecheng.shop',
|
||||
'yuecheng.shop',
|
||||
];
|
||||
const STEP1_COOKIE_CLEAR_ORIGINS = [
|
||||
'https://chatgpt.com',
|
||||
'https://chat.openai.com',
|
||||
'https://pay.openai.com',
|
||||
'https://auth.openai.com',
|
||||
'https://auth0.openai.com',
|
||||
'https://accounts.openai.com',
|
||||
'https://openai.com',
|
||||
'https://www.paypal.com',
|
||||
'https://paypal.com',
|
||||
'https://checkout.stripe.com',
|
||||
'https://www.meiguodizhi.com',
|
||||
'https://meiguodizhi.com',
|
||||
'https://mail-api.yuecheng.shop',
|
||||
];
|
||||
|
||||
function normalizeCookieDomainForStep1(domain) {
|
||||
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
|
||||
}
|
||||
|
||||
function shouldClearStep1Cookie(cookie) {
|
||||
const domain = normalizeCookieDomainForStep1(cookie?.domain);
|
||||
if (!domain) return false;
|
||||
return STEP1_COOKIE_CLEAR_DOMAINS.some((target) => (
|
||||
domain === target || domain.endsWith(`.${target}`)
|
||||
));
|
||||
}
|
||||
|
||||
function buildStep1CookieRemovalUrl(cookie) {
|
||||
const host = normalizeCookieDomainForStep1(cookie?.domain);
|
||||
const rawPath = String(cookie?.path || '/');
|
||||
const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
||||
return `https://${host}${path}`;
|
||||
}
|
||||
|
||||
function getStep1ErrorMessage(error) {
|
||||
return error?.message || String(error || '未知错误');
|
||||
}
|
||||
|
||||
async function collectStep1Cookies(chromeApi) {
|
||||
if (!chromeApi.cookies?.getAll) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stores = chromeApi.cookies.getAllCookieStores
|
||||
? await chromeApi.cookies.getAllCookieStores()
|
||||
: [{ id: undefined }];
|
||||
const cookies = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const store of stores) {
|
||||
const storeId = store?.id;
|
||||
const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {});
|
||||
for (const cookie of batch || []) {
|
||||
if (!shouldClearStep1Cookie(cookie)) continue;
|
||||
const key = [
|
||||
cookie.storeId || storeId || '',
|
||||
cookie.domain || '',
|
||||
cookie.path || '',
|
||||
cookie.name || '',
|
||||
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||||
].join('|');
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
cookies.push(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
async function removeStep1Cookie(chromeApi, cookie) {
|
||||
const details = {
|
||||
url: buildStep1CookieRemovalUrl(cookie),
|
||||
name: cookie.name,
|
||||
};
|
||||
if (cookie.storeId) {
|
||||
details.storeId = cookie.storeId;
|
||||
}
|
||||
if (cookie.partitionKey) {
|
||||
details.partitionKey = cookie.partitionKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await chromeApi.cookies.remove(details);
|
||||
return Boolean(result);
|
||||
} catch (error) {
|
||||
console.warn('[MultiPage:step1] remove cookie failed', {
|
||||
domain: cookie?.domain,
|
||||
name: cookie?.name,
|
||||
message: getStep1ErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createStep1Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome: chromeApi = globalThis.chrome,
|
||||
completeNodeFromBackground,
|
||||
openSignupEntryTab,
|
||||
} = deps;
|
||||
|
||||
async function clearOpenAiCookiesBeforeStep1() {
|
||||
if (!chromeApi?.cookies?.getAll || !chromeApi.cookies?.remove) {
|
||||
await addLog('步骤 1:当前浏览器不支持 cookies API,跳过打开官网前 cookie 清理。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
await addLog('步骤 1:打开 ChatGPT 官网前清理 ChatGPT / OpenAI cookies...', 'info');
|
||||
const cookies = await collectStep1Cookies(chromeApi);
|
||||
let removedCount = 0;
|
||||
for (const cookie of cookies) {
|
||||
if (await removeStep1Cookie(chromeApi, cookie)) {
|
||||
removedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (chromeApi.browsingData?.removeCookies) {
|
||||
try {
|
||||
await chromeApi.browsingData.removeCookies({
|
||||
since: 0,
|
||||
origins: STEP1_COOKIE_CLEAR_ORIGINS,
|
||||
});
|
||||
} catch (error) {
|
||||
await addLog(`步骤 1:browsingData 补扫 cookies 失败:${getStep1ErrorMessage(error)}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies。`, 'ok');
|
||||
}
|
||||
|
||||
async function executeStep1() {
|
||||
await clearOpenAiCookiesBeforeStep1();
|
||||
await addLog('步骤 1:正在打开 ChatGPT 官网...');
|
||||
await openSignupEntryTab(1);
|
||||
await completeNodeFromBackground('open-chatgpt', {});
|
||||
}
|
||||
|
||||
return { executeStep1 };
|
||||
}
|
||||
|
||||
return { createStep1Executor };
|
||||
});
|
||||
@@ -0,0 +1,310 @@
|
||||
(function attachBackgroundPayPalApprove(root, factory) {
|
||||
root.MultiPageBackgroundPayPalApprove = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPayPalApproveModule() {
|
||||
const PAYPAL_SOURCE = 'paypal-flow';
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const PAYPAL_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/paypal-flow.js'];
|
||||
const PAYPAL_LOGIN_TRANSITION_TIMEOUT_MS = 30000;
|
||||
const PAYPAL_LOGIN_TRANSITION_POLL_MS = 500;
|
||||
|
||||
function createPayPalApproveExecutor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
queryTabsInAutomationWindow = null,
|
||||
sendTabMessageUntilStopped,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForTabCompleteUntilStopped,
|
||||
waitForTabUrlMatchUntilStopped,
|
||||
} = deps;
|
||||
|
||||
async function resolvePayPalTabId(state = {}) {
|
||||
const paypalTabId = await getTabId(PAYPAL_SOURCE);
|
||||
if (paypalTabId && await isTabAlive(PAYPAL_SOURCE)) {
|
||||
return paypalTabId;
|
||||
}
|
||||
const discoveredPayPalTabId = await findOpenPayPalTabId();
|
||||
if (discoveredPayPalTabId) {
|
||||
await addLog('步骤 8:已从当前浏览器标签中发现 PayPal 页面,正在接管继续执行。', 'info');
|
||||
return discoveredPayPalTabId;
|
||||
}
|
||||
const checkoutTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
|
||||
if (checkoutTabId) {
|
||||
return checkoutTabId;
|
||||
}
|
||||
const storedTabId = Number(state.plusCheckoutTabId) || 0;
|
||||
if (storedTabId) {
|
||||
return storedTabId;
|
||||
}
|
||||
throw new Error('步骤 8:未找到 PayPal 标签页,请先完成步骤 7。');
|
||||
}
|
||||
|
||||
async function findOpenPayPalTabId() {
|
||||
if (!chrome?.tabs?.query) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const queryTabs = typeof queryTabsInAutomationWindow === 'function'
|
||||
? queryTabsInAutomationWindow
|
||||
: (queryInfo) => chrome.tabs.query(queryInfo);
|
||||
const tabs = await queryTabs({}).catch(() => []);
|
||||
const candidates = (Array.isArray(tabs) ? tabs : [])
|
||||
.filter((tab) => Number.isInteger(tab?.id) && isPayPalUrl(tab.url || ''));
|
||||
if (!candidates.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const match = candidates.find((tab) => tab.active && tab.currentWindow)
|
||||
|| candidates.find((tab) => tab.active)
|
||||
|| candidates[0];
|
||||
if (match?.id && chrome?.tabs?.update) {
|
||||
await chrome.tabs.update(match.id, { active: true }).catch(() => {});
|
||||
}
|
||||
return match?.id || 0;
|
||||
}
|
||||
|
||||
async function ensurePayPalReady(tabId, logMessage = '') {
|
||||
await waitForTabUrlMatchUntilStopped(tabId, (url) => /paypal\./i.test(url));
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
await sleepWithStop(1000);
|
||||
await ensureContentScriptReadyOnTabUntilStopped(PAYPAL_SOURCE, tabId, {
|
||||
inject: PAYPAL_INJECT_FILES,
|
||||
injectSource: PAYPAL_SOURCE,
|
||||
logMessage: logMessage || '步骤 8:PayPal 页面仍在加载,等待脚本就绪...',
|
||||
});
|
||||
}
|
||||
|
||||
async function getPayPalState(tabId) {
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_GET_STATE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function dismissPrompts(tabId) {
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_DISMISS_PROMPTS',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
function resolvePayPalCredentials(state = {}) {
|
||||
const currentId = String(state?.currentPayPalAccountId || '').trim();
|
||||
const accounts = Array.isArray(state?.paypalAccounts) ? state.paypalAccounts : [];
|
||||
const selectedAccount = currentId
|
||||
? accounts.find((account) => String(account?.id || '').trim() === currentId) || null
|
||||
: null;
|
||||
return {
|
||||
email: String(selectedAccount?.email || state?.paypalEmail || '').trim(),
|
||||
password: String(selectedAccount?.password || state?.paypalPassword || ''),
|
||||
};
|
||||
}
|
||||
|
||||
async function submitLogin(tabId, state = {}) {
|
||||
const credentials = resolvePayPalCredentials(state);
|
||||
if (!credentials.password) {
|
||||
throw new Error('步骤 8:未配置可用的 PayPal 账号,请先在侧边栏添加并选择账号。');
|
||||
}
|
||||
await addLog('步骤 8:正在填写 PayPal 登录信息并提交...', 'info');
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_SUBMIT_LOGIN',
|
||||
source: 'background',
|
||||
payload: {
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
},
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
function isPayPalUrl(url = '') {
|
||||
return /paypal\./i.test(String(url || ''));
|
||||
}
|
||||
|
||||
function isPayPalPasswordState(pageState = {}) {
|
||||
return Boolean(pageState.hasPasswordInput)
|
||||
|| pageState.loginPhase === 'password'
|
||||
|| pageState.loginPhase === 'login_combined';
|
||||
}
|
||||
|
||||
async function waitForPayPalPostLoginDecision(tabId, actionResult = {}) {
|
||||
const phase = String(actionResult?.phase || '').trim();
|
||||
const startedAt = Date.now();
|
||||
|
||||
while (Date.now() - startedAt < PAYPAL_LOGIN_TRANSITION_TIMEOUT_MS) {
|
||||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||
if (!tab) {
|
||||
throw new Error('步骤 8:PayPal 标签页已关闭,无法继续识别登录后的页面。');
|
||||
}
|
||||
|
||||
const currentUrl = tab.url || '';
|
||||
if (!currentUrl) {
|
||||
await sleepWithStop(PAYPAL_LOGIN_TRANSITION_POLL_MS);
|
||||
continue;
|
||||
}
|
||||
if (currentUrl && !isPayPalUrl(currentUrl)) {
|
||||
return {
|
||||
outcome: 'left_paypal',
|
||||
url: currentUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (tab.status !== 'complete') {
|
||||
await sleepWithStop(PAYPAL_LOGIN_TRANSITION_POLL_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
await ensurePayPalReady(
|
||||
tabId,
|
||||
phase === 'email_submitted'
|
||||
? '步骤 8:PayPal 账号已提交,正在识别下一页...'
|
||||
: '步骤 8:PayPal 密码已提交,正在识别跳转结果...'
|
||||
);
|
||||
const pageState = await getPayPalState(tabId);
|
||||
|
||||
if (pageState.hasPasskeyPrompt) {
|
||||
return {
|
||||
outcome: 'prompt',
|
||||
pageState,
|
||||
};
|
||||
}
|
||||
|
||||
if (pageState.approveReady) {
|
||||
return {
|
||||
outcome: 'approve_ready',
|
||||
pageState,
|
||||
};
|
||||
}
|
||||
|
||||
if (phase === 'email_submitted' && isPayPalPasswordState(pageState)) {
|
||||
return {
|
||||
outcome: 'password_ready',
|
||||
pageState,
|
||||
};
|
||||
}
|
||||
|
||||
if (phase === 'password_submitted' && !pageState.needsLogin) {
|
||||
return {
|
||||
outcome: 'post_login_state',
|
||||
pageState,
|
||||
};
|
||||
}
|
||||
|
||||
await sleepWithStop(PAYPAL_LOGIN_TRANSITION_POLL_MS);
|
||||
}
|
||||
|
||||
return {
|
||||
outcome: 'timeout',
|
||||
phase,
|
||||
};
|
||||
}
|
||||
|
||||
async function clickApprove(tabId) {
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_CLICK_APPROVE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return Boolean(result?.clicked);
|
||||
}
|
||||
|
||||
async function executePayPalApprove(state = {}) {
|
||||
const tabId = await resolvePayPalTabId(state);
|
||||
await ensurePayPalReady(tabId);
|
||||
await setState({ plusCheckoutTabId: tabId });
|
||||
|
||||
let loggedWaiting = false;
|
||||
while (true) {
|
||||
const currentUrl = (await chrome.tabs.get(tabId).catch(() => null))?.url || '';
|
||||
if (currentUrl && !isPayPalUrl(currentUrl)) {
|
||||
await addLog('步骤 8:PayPal 已跳转离开授权页,准备进入回跳确认。', 'ok');
|
||||
break;
|
||||
}
|
||||
|
||||
await ensurePayPalReady(tabId, '步骤 8:PayPal 页面正在切换,等待脚本重新就绪...');
|
||||
const pageState = await getPayPalState(tabId);
|
||||
|
||||
if (pageState.needsLogin) {
|
||||
const submitResult = await submitLogin(tabId, state);
|
||||
const decision = await waitForPayPalPostLoginDecision(tabId, submitResult);
|
||||
if (decision.outcome === 'left_paypal') {
|
||||
await addLog('步骤 8:PayPal 登录后已跳转离开登录/授权页,继续进入回跳确认。', 'ok');
|
||||
break;
|
||||
}
|
||||
if (decision.outcome === 'password_ready') {
|
||||
await addLog('步骤 8:PayPal 账号页提交后已识别到密码页,继续填写密码。', 'info');
|
||||
} else if (decision.outcome === 'approve_ready') {
|
||||
await addLog('步骤 8:PayPal 登录后已识别到授权确认页,继续点击授权。', 'info');
|
||||
} else if (decision.outcome === 'prompt') {
|
||||
await addLog('步骤 8:PayPal 登录后已识别到提示弹窗,继续处理弹窗。', 'info');
|
||||
} else if (decision.outcome === 'timeout') {
|
||||
await addLog('步骤 8:PayPal 登录动作后暂未识别到新页面,重新检查当前页面状态。', 'warn');
|
||||
}
|
||||
loggedWaiting = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageState.hasPasskeyPrompt) {
|
||||
await addLog('步骤 8:检测到 PayPal 通行密钥提示,正在关闭...', 'info');
|
||||
await dismissPrompts(tabId);
|
||||
await sleepWithStop(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
const dismissed = await dismissPrompts(tabId).catch(() => ({ clicked: 0 }));
|
||||
if (dismissed.clicked) {
|
||||
await sleepWithStop(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageState.approveReady) {
|
||||
await addLog('步骤 8:正在点击 PayPal“同意并继续”...', 'info');
|
||||
const clicked = await clickApprove(tabId);
|
||||
if (clicked) {
|
||||
await setState({ plusPaypalApprovedAt: Date.now() });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!loggedWaiting) {
|
||||
loggedWaiting = true;
|
||||
await addLog('步骤 8:等待 PayPal 授权按钮或下一步页面出现...', 'info');
|
||||
}
|
||||
await sleepWithStop(500);
|
||||
}
|
||||
|
||||
await completeNodeFromBackground('paypal-approve', {
|
||||
plusPaypalApprovedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
executePayPalApprove,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createPayPalApproveExecutor,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,526 @@
|
||||
(function attachBackgroundStep10(root, factory) {
|
||||
root.MultiPageBackgroundStep10 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep10Module() {
|
||||
function createStep10Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
buildLocalHelperEndpoint = null,
|
||||
chrome,
|
||||
closeConflictingTabsForSource,
|
||||
completeNodeFromBackground,
|
||||
createLocalCliProxyApi = null,
|
||||
ensureContentScriptReadyOnTab,
|
||||
getPanelMode,
|
||||
getTabId,
|
||||
isLocalhostOAuthCallbackUrl,
|
||||
isTabAlive,
|
||||
normalizeHotmailLocalBaseUrl = (value) => String(value || '').trim(),
|
||||
normalizeCodex2ApiUrl,
|
||||
normalizeSub2ApiUrl,
|
||||
rememberSourceLastUrl,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
shouldBypassStep9ForLocalCpa,
|
||||
DEFAULT_SUB2API_GROUP_NAME = 'codex',
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
} = deps;
|
||||
|
||||
let sub2ApiApi = null;
|
||||
let localCliProxyApi = null;
|
||||
|
||||
function getSub2ApiApi() {
|
||||
if (sub2ApiApi) {
|
||||
return sub2ApiApi;
|
||||
}
|
||||
const factory = deps.createSub2ApiApi
|
||||
|| self.MultiPageBackgroundSub2ApiApi?.createSub2ApiApi;
|
||||
if (typeof factory !== 'function') {
|
||||
throw new Error('SUB2API 直连接口模块未加载,无法提交回调。');
|
||||
}
|
||||
sub2ApiApi = factory({
|
||||
addLog,
|
||||
normalizeSub2ApiUrl,
|
||||
DEFAULT_SUB2API_GROUP_NAME,
|
||||
});
|
||||
return sub2ApiApi;
|
||||
}
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function getLocalCliProxyApi() {
|
||||
if (localCliProxyApi) {
|
||||
return localCliProxyApi;
|
||||
}
|
||||
const factory = createLocalCliProxyApi
|
||||
|| self.MultiPageBackgroundLocalCliProxyApi?.createLocalCliProxyApi;
|
||||
if (typeof factory !== 'function') {
|
||||
throw new Error('本地 CPA JSON 有RT 模块未加载,无法导出认证文件。');
|
||||
}
|
||||
localCliProxyApi = factory({
|
||||
crypto: globalThis.crypto,
|
||||
fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||
sessionToJsonConverter: self.MultiPageSessionToJsonConverter,
|
||||
});
|
||||
return localCliProxyApi;
|
||||
}
|
||||
|
||||
function resolvePlatformVerifyStep(state = {}) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep >= 10 ? visibleStep : 10;
|
||||
}
|
||||
|
||||
function resolveConfirmOauthStep(platformVerifyStep = 10) {
|
||||
return Number(platformVerifyStep) >= 13 ? 12 : 9;
|
||||
}
|
||||
|
||||
function resolveAuthLoginStep(platformVerifyStep = 10) {
|
||||
return Number(platformVerifyStep) >= 13 ? 10 : 7;
|
||||
}
|
||||
|
||||
function addStepLog(step, message, level = 'info') {
|
||||
return addLog(message, level, { step, stepKey: 'platform-verify' });
|
||||
}
|
||||
|
||||
function parseLocalhostCallback(rawUrl, platformVerifyStep = 10) {
|
||||
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const state = normalizeString(parsed.searchParams.get('state'));
|
||||
if (!code || !state) {
|
||||
throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
|
||||
return {
|
||||
url: parsed.toString(),
|
||||
code,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
function getCodex2ApiErrorMessage(payload, responseStatus = 500) {
|
||||
const details = [
|
||||
payload?.error,
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.reason,
|
||||
]
|
||||
.map((value) => normalizeString(value))
|
||||
.find(Boolean);
|
||||
return details || `Codex2API 请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
function deriveCpaManagementOrigin(vpsUrl) {
|
||||
const normalizedUrl = normalizeString(vpsUrl);
|
||||
if (!normalizedUrl) {
|
||||
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(normalizedUrl);
|
||||
} catch {
|
||||
throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
|
||||
}
|
||||
return parsed.origin;
|
||||
}
|
||||
|
||||
function getCpaApiErrorMessage(payload, responseStatus = 500) {
|
||||
const details = [
|
||||
payload?.error,
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.reason,
|
||||
]
|
||||
.map((value) => normalizeString(value))
|
||||
.find(Boolean);
|
||||
return details || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
async function fetchCpaManagementJson(origin, path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const managementKey = normalizeString(options.managementKey);
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (managementKey) {
|
||||
headers.Authorization = `Bearer ${managementKey}`;
|
||||
headers['X-Management-Key'] = managementKey;
|
||||
}
|
||||
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method: options.method || 'POST',
|
||||
headers,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getCpaApiErrorMessage(payload, response.status));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('CPA 管理接口请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function isSub2ApiTransientExchangeError(error) {
|
||||
const message = normalizeString(error?.message || error);
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
const tokenExchangeFailure = /auth\.openai\.com\/oauth\/token/i.test(message);
|
||||
const transientNetworkSignal = /unexpected\s+eof|eof|connection\s+refused|i\/o\s+timeout|context\s+deadline\s+exceeded|connection\s+reset|broken\s+pipe|failed\s+to\s+fetch|temporarily\s+unavailable|timeout/i.test(message);
|
||||
const transientExchangeUserSignal = /token_exchange_user_error|invalid\s+request\.\s+please\s+try\s+again\s+later/i.test(message);
|
||||
if (transientExchangeUserSignal) {
|
||||
return true;
|
||||
}
|
||||
return tokenExchangeFailure && transientNetworkSignal;
|
||||
}
|
||||
|
||||
async function sleep(ms = 0) {
|
||||
const timeout = Math.max(0, Number(ms) || 0);
|
||||
if (!timeout) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
}
|
||||
|
||||
async function fetchCodex2ApiJson(origin, path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method: options.method || 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Key': normalizeString(options.adminKey),
|
||||
},
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getCodex2ApiErrorMessage(payload, response.status));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('Codex2API 请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLocalCpaJsonArtifactViaHelper(helperBaseUrl, artifact) {
|
||||
const endpoint = typeof buildLocalHelperEndpoint === 'function'
|
||||
? buildLocalHelperEndpoint(helperBaseUrl, '/save-auth-json')
|
||||
: new URL('/save-auth-json', `${helperBaseUrl.replace(/\/+$/, '')}/`).toString();
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filePath: artifact.filePath,
|
||||
directoryPath: artifact.directoryPath,
|
||||
content: artifact.jsonText,
|
||||
}),
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok || payload?.ok === false) {
|
||||
throw new Error(normalizeString(payload?.error) || `本地 helper 写入失败(HTTP ${response.status})。`);
|
||||
}
|
||||
|
||||
return {
|
||||
...artifact,
|
||||
filePath: normalizeString(payload?.filePath) || artifact.filePath,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeStep10(state) {
|
||||
if (getPanelMode(state) === 'local-cpa-json') {
|
||||
return executeLocalCpaJsonStep10(state);
|
||||
}
|
||||
if (getPanelMode(state) === 'codex2api') {
|
||||
return executeCodex2ApiStep10(state);
|
||||
}
|
||||
if (getPanelMode(state) === 'sub2api') {
|
||||
return executeSub2ApiStep10(state);
|
||||
}
|
||||
return executeCpaStep10(state);
|
||||
}
|
||||
|
||||
async function executeCpaStep10(state) {
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep);
|
||||
const authLoginStep = resolveAuthLoginStep(platformVerifyStep);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.vpsUrl) {
|
||||
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
||||
}
|
||||
|
||||
if (shouldBypassStep9ForLocalCpa(state)) {
|
||||
await addStepLog(platformVerifyStep, '检测到本地 CPA,且当前策略为“跳过平台回调验证”,本轮不再重复提交回调地址。', 'info');
|
||||
await completeNodeFromBackground(state?.nodeId || 'platform-verify', {
|
||||
localhostUrl: state.localhostUrl,
|
||||
verifiedStatus: 'local-auto',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep);
|
||||
const expectedState = normalizeString(state.cpaOAuthState);
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error(`CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}。`);
|
||||
}
|
||||
const managementKey = normalizeString(state.vpsPassword);
|
||||
if (!managementKey) {
|
||||
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await addStepLog(platformVerifyStep, '正在通过 CPA 管理接口提交回调地址...');
|
||||
try {
|
||||
const origin = normalizeString(state.cpaManagementOrigin) || deriveCpaManagementOrigin(state.vpsUrl);
|
||||
const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', {
|
||||
method: 'POST',
|
||||
managementKey,
|
||||
body: {
|
||||
provider: 'codex',
|
||||
redirect_url: callback.url,
|
||||
},
|
||||
});
|
||||
|
||||
const verifiedStatus = normalizeString(result?.message)
|
||||
|| normalizeString(result?.status_message)
|
||||
|| 'CPA 已通过接口提交回调';
|
||||
await addStepLog(platformVerifyStep, verifiedStatus, 'ok');
|
||||
await completeNodeFromBackground(state?.nodeId || 'platform-verify', {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
});
|
||||
} catch (error) {
|
||||
const reason = normalizeString(error?.message) || 'unknown error';
|
||||
await addStepLog(platformVerifyStep, `CPA 接口提交失败:${reason}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeLocalCpaJsonStep10(state) {
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep);
|
||||
const authLoginStep = resolveAuthLoginStep(platformVerifyStep);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
|
||||
const helperBaseUrl = normalizeHotmailLocalBaseUrl(state.hotmailLocalBaseUrl);
|
||||
const pluginDir = normalizeString(state.localCpaJsonPluginDir);
|
||||
if (!helperBaseUrl) {
|
||||
throw new Error('尚未配置 Hotmail 本地助手地址,请先在侧边栏填写。');
|
||||
}
|
||||
if (!pluginDir) {
|
||||
throw new Error('尚未配置本地插件目录,请先在侧边栏填写。');
|
||||
}
|
||||
if (!state.localCpaJsonPkceCodes?.codeVerifier) {
|
||||
throw new Error(`缺少本地 CPA JSON 有RT PKCE 会话信息,请重新执行步骤 ${authLoginStep}。`);
|
||||
}
|
||||
|
||||
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep);
|
||||
const expectedState = normalizeString(state.localCpaJsonOAuthState);
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error(`本地 CPA JSON 有RT 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}。`);
|
||||
}
|
||||
|
||||
await addStepLog(platformVerifyStep, '正在交换 OAuth 授权码并导出本地 CPA JSON 有RT...');
|
||||
const api = getLocalCliProxyApi();
|
||||
const artifact = await api.exchangeCallbackToAuthArtifact({
|
||||
callbackUrl: callback.url,
|
||||
expectedState,
|
||||
pkceCodes: state.localCpaJsonPkceCodes,
|
||||
pluginDir,
|
||||
relativeAuthDir: state.localCpaJsonRelativeAuthDir,
|
||||
sourceName: 'CLIProxyAPI Local OAuth',
|
||||
});
|
||||
|
||||
for (const warning of Array.isArray(artifact.warnings) ? artifact.warnings : []) {
|
||||
await addStepLog(platformVerifyStep, warning, 'warn');
|
||||
}
|
||||
|
||||
const saved = await saveLocalCpaJsonArtifactViaHelper(helperBaseUrl, artifact);
|
||||
const verifiedStatus = `本地CPA JSON 有RT 已导出:${saved.filePath}`;
|
||||
await addStepLog(platformVerifyStep, verifiedStatus, 'ok');
|
||||
await completeNodeFromBackground(state?.nodeId || 'platform-verify', {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
localCpaJsonFilePath: saved.filePath,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeCodex2ApiStep10(state) {
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep);
|
||||
const authLoginStep = resolveAuthLoginStep(platformVerifyStep);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.codex2apiSessionId) {
|
||||
throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${authLoginStep}。`);
|
||||
}
|
||||
if (!normalizeString(state.codex2apiAdminKey)) {
|
||||
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep);
|
||||
const expectedState = normalizeString(state.codex2apiOAuthState);
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}。`);
|
||||
}
|
||||
|
||||
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
|
||||
const origin = new URL(codex2apiUrl).origin;
|
||||
|
||||
await addStepLog(platformVerifyStep, '正在向 Codex2API 提交回调并创建账号...');
|
||||
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', {
|
||||
adminKey: state.codex2apiAdminKey,
|
||||
method: 'POST',
|
||||
body: {
|
||||
session_id: state.codex2apiSessionId,
|
||||
code: callback.code,
|
||||
state: callback.state,
|
||||
},
|
||||
});
|
||||
|
||||
const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功';
|
||||
await addStepLog(platformVerifyStep, verifiedStatus, 'ok');
|
||||
await completeNodeFromBackground(state?.nodeId || 'platform-verify', {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeSub2ApiStep10(state) {
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
const visibleStep = platformVerifyStep;
|
||||
const confirmOauthStep = resolveConfirmOauthStep(visibleStep);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.sub2apiSessionId) {
|
||||
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
|
||||
}
|
||||
if (!state.sub2apiEmail) {
|
||||
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
||||
}
|
||||
if (!state.sub2apiPassword) {
|
||||
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
}
|
||||
const api = getSub2ApiApi();
|
||||
const maxExchangeAttempts = 3;
|
||||
let lastError = null;
|
||||
for (let attempt = 1; attempt <= maxExchangeAttempts; attempt += 1) {
|
||||
try {
|
||||
const result = await api.submitOpenAiCallback({
|
||||
...state,
|
||||
visibleStep,
|
||||
sub2apiUrl,
|
||||
}, {
|
||||
visibleStep,
|
||||
logLabel: `步骤 ${visibleStep}`,
|
||||
logOptions: { step: visibleStep, stepKey: 'platform-verify' },
|
||||
timeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
});
|
||||
await completeNodeFromBackground(state?.nodeId || 'platform-verify', result);
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (!isSub2ApiTransientExchangeError(error) || attempt >= maxExchangeAttempts) {
|
||||
throw error;
|
||||
}
|
||||
await addLog(
|
||||
`SUB2API 回调交换出现临时网络波动(${error.message}),正在重试 ${attempt + 1}/${maxExchangeAttempts}...`,
|
||||
'warn',
|
||||
{ step: visibleStep, stepKey: 'platform-verify' }
|
||||
);
|
||||
await sleep(1200 * attempt);
|
||||
}
|
||||
}
|
||||
if (lastError) {
|
||||
throw lastError;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
executeCpaStep10,
|
||||
executeCodex2ApiStep10,
|
||||
executeLocalCpaJsonStep10,
|
||||
executeStep10,
|
||||
executeSub2ApiStep10,
|
||||
};
|
||||
}
|
||||
|
||||
return { createStep10Executor };
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
(function attachBackgroundPlusReturnConfirm(root, factory) {
|
||||
root.MultiPageBackgroundPlusReturnConfirm = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusReturnConfirmModule() {
|
||||
const PAYPAL_SOURCE = 'paypal-flow';
|
||||
const GOPAY_SOURCE = 'gopay-flow';
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const PLUS_RETURN_SETTLE_WAIT_MS = 20000;
|
||||
|
||||
function createPlusReturnConfirmExecutor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
completeNodeFromBackground,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForTabUrlMatchUntilStopped,
|
||||
} = deps;
|
||||
|
||||
async function resolveReturnTabId(state = {}) {
|
||||
const paypalTabId = await getTabId(PAYPAL_SOURCE);
|
||||
if (paypalTabId && await isTabAlive(PAYPAL_SOURCE)) {
|
||||
return paypalTabId;
|
||||
}
|
||||
const gopayTabId = await getTabId(GOPAY_SOURCE);
|
||||
if (gopayTabId && await isTabAlive(GOPAY_SOURCE)) {
|
||||
return gopayTabId;
|
||||
}
|
||||
const checkoutTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
|
||||
if (checkoutTabId) {
|
||||
return checkoutTabId;
|
||||
}
|
||||
const storedTabId = Number(state.plusCheckoutTabId) || 0;
|
||||
if (storedTabId) {
|
||||
return storedTabId;
|
||||
}
|
||||
throw new Error('步骤 9:未找到 Plus / PayPal / GoPay 标签页,无法确认订阅回跳。');
|
||||
}
|
||||
|
||||
function isReturnUrl(url = '') {
|
||||
return /https:\/\/(?:chatgpt\.com|chat\.openai\.com|openai\.com)\//i.test(String(url || ''))
|
||||
&& !/paypal\.|gopay|gojek|midtrans|xendit|stripe/i.test(String(url || ''));
|
||||
}
|
||||
|
||||
async function executePlusReturnConfirm(state = {}) {
|
||||
const tabId = await resolveReturnTabId(state);
|
||||
await addLog('步骤 9:正在等待支付授权后回跳到 ChatGPT / OpenAI 页面...', 'info');
|
||||
const tab = await waitForTabUrlMatchUntilStopped(tabId, isReturnUrl);
|
||||
await addLog('步骤 9:已检测到订阅回跳页面,固定等待 20 秒让页面完成加载。', 'info');
|
||||
await sleepWithStop(PLUS_RETURN_SETTLE_WAIT_MS);
|
||||
|
||||
await setState({
|
||||
plusCheckoutTabId: tabId,
|
||||
plusReturnUrl: tab?.url || '',
|
||||
});
|
||||
await completeNodeFromBackground('plus-checkout-return', {
|
||||
plusReturnUrl: tab?.url || '',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
executePlusReturnConfirm,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createPlusReturnConfirmExecutor,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
(function attachBackgroundStepRegistry(root, factory) {
|
||||
root.MultiPageBackgroundStepRegistry = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStepRegistryModule() {
|
||||
function createNodeRegistry(definitions = []) {
|
||||
const ordered = (Array.isArray(definitions) ? definitions : [])
|
||||
.map((definition) => ({
|
||||
nodeId: String(definition?.nodeId || definition?.key || '').trim(),
|
||||
displayOrder: Number(definition?.displayOrder ?? definition?.order),
|
||||
executeKey: String(definition?.executeKey || definition?.key || definition?.nodeId || '').trim(),
|
||||
title: String(definition?.title || '').trim(),
|
||||
execute: definition?.execute,
|
||||
}))
|
||||
.filter((definition) => definition.nodeId && typeof definition.execute === 'function')
|
||||
.sort((left, right) => {
|
||||
const leftOrder = Number.isFinite(left.displayOrder) ? left.displayOrder : 0;
|
||||
const rightOrder = Number.isFinite(right.displayOrder) ? right.displayOrder : 0;
|
||||
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||
return left.nodeId.localeCompare(right.nodeId);
|
||||
});
|
||||
|
||||
const byId = new Map(ordered.map((definition) => [definition.nodeId, definition]));
|
||||
|
||||
function getNodeDefinition(nodeId) {
|
||||
return byId.get(String(nodeId || '').trim()) || null;
|
||||
}
|
||||
|
||||
function getOrderedNodes() {
|
||||
return ordered.slice();
|
||||
}
|
||||
|
||||
function executeNode(nodeId, state) {
|
||||
const definition = getNodeDefinition(nodeId);
|
||||
if (!definition) {
|
||||
throw new Error(`未知节点:${nodeId}`);
|
||||
}
|
||||
return definition.execute(state);
|
||||
}
|
||||
|
||||
return {
|
||||
executeNode,
|
||||
getNodeDefinition,
|
||||
getOrderedNodes,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createNodeRegistry,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,509 @@
|
||||
(function attachBackgroundStep2(root, factory) {
|
||||
root.MultiPageBackgroundStep2 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep2Module() {
|
||||
function createStep2Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTab,
|
||||
ensureSignupAuthEntryPageReady,
|
||||
ensureSignupEntryPageReady,
|
||||
ensureSignupPostEmailPageReadyInTab,
|
||||
ensureSignupPostIdentityPageReadyInTab = ensureSignupPostEmailPageReadyInTab,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
phoneVerificationHelpers = null,
|
||||
resolveSignupMethod = () => 'email',
|
||||
resolveSignupEmailForFlow,
|
||||
sendToContentScriptResilient,
|
||||
SIGNUP_PAGE_INJECT_FILES,
|
||||
waitForTabStableComplete = null,
|
||||
} = deps;
|
||||
|
||||
function getErrorMessage(error) {
|
||||
return String(typeof error === 'string' ? error : error?.message || '');
|
||||
}
|
||||
|
||||
function isSignupEntryUnavailableErrorMessage(errorLike) {
|
||||
const message = getErrorMessage(errorLike);
|
||||
return /未找到可用的邮箱输入入口|当前页面没有可用的注册入口,也不在邮箱\/密码页/i.test(message);
|
||||
}
|
||||
|
||||
function isSignupPhoneEntryUnavailableErrorMessage(errorLike) {
|
||||
const message = getErrorMessage(errorLike);
|
||||
return /未找到可用的手机号输入入口|当前页面没有可用的手机号注册入口,也不在密码页/i.test(message);
|
||||
}
|
||||
|
||||
function isRetryableStep2TransportErrorMessage(errorLike) {
|
||||
const message = getErrorMessage(errorLike);
|
||||
return /Content script on signup-page did not respond in \d+s|内容脚本\s+\d+(?:\.\d+)?\s*秒内未响应|Receiving end does not exist|message channel closed|A listener indicated an asynchronous response|port closed before a response was received|did not respond in \d+s/i.test(message);
|
||||
}
|
||||
|
||||
function isLikelyLoggedInChatgptHomeUrl(rawUrl) {
|
||||
const url = String(rawUrl || '').trim();
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const host = String(parsed.hostname || '').toLowerCase();
|
||||
if (!['chatgpt.com', 'www.chatgpt.com'].includes(host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = String(parsed.pathname || '');
|
||||
if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isReadySignupEntryState(state = '') {
|
||||
const normalized = String(state || '').trim().toLowerCase();
|
||||
return normalized === 'entry_home'
|
||||
|| normalized === 'email_entry'
|
||||
|| normalized === 'phone_entry'
|
||||
|| normalized === 'password_page';
|
||||
}
|
||||
|
||||
async function getSignupEntryReadyState(tabId) {
|
||||
if (!Number.isInteger(tabId) || typeof sendToContentScriptResilient !== 'function') {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'ENSURE_SIGNUP_ENTRY_READY',
|
||||
step: 2,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
}, {
|
||||
timeoutMs: 12000,
|
||||
retryDelayMs: 500,
|
||||
logMessage: '步骤 2:正在检查官网注册入口状态...',
|
||||
});
|
||||
if (result?.error) {
|
||||
return '';
|
||||
}
|
||||
return String(result?.state || '').trim().toLowerCase();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function isLikelyLoggedInChatgptHomeTab(tabId) {
|
||||
if (typeof chrome?.tabs?.get !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const readyState = await getSignupEntryReadyState(tabId);
|
||||
if (isReadySignupEntryState(readyState)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentUrl = await getTabUrl(tabId);
|
||||
return isLikelyLoggedInChatgptHomeUrl(currentUrl);
|
||||
}
|
||||
|
||||
async function shouldForceAuthEntryRetry(tabId) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
return false;
|
||||
}
|
||||
return isLikelyLoggedInChatgptHomeTab(tabId);
|
||||
}
|
||||
|
||||
async function getTabUrl(tabId) {
|
||||
if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
return String(tab?.url || '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function failStep2OnLoggedInSession(tabId, reasonMessage = '') {
|
||||
if (!(await isLikelyLoggedInChatgptHomeTab(tabId))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const reasonText = getErrorMessage(reasonMessage);
|
||||
const reasonSuffix = reasonText ? `(触发原因:${reasonText})` : '';
|
||||
const message = `步骤 2:检测到当前停留在已登录 ChatGPT 首页,已阻止自动跳过步骤 3/4/5。请先执行步骤 1 清理会话后重试。${reasonSuffix}`;
|
||||
await addLog(message, 'error');
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
async function sendSignupIdentity(payload = {}, options = {}) {
|
||||
const {
|
||||
timeoutMs = 35000,
|
||||
retryDelayMs = 700,
|
||||
logMessage = '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...',
|
||||
} = options;
|
||||
|
||||
try {
|
||||
return await sendToContentScriptResilient('signup-page', {
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'submit-signup-email',
|
||||
step: 2,
|
||||
source: 'background',
|
||||
payload,
|
||||
}, {
|
||||
timeoutMs,
|
||||
retryDelayMs,
|
||||
logMessage,
|
||||
});
|
||||
} catch (error) {
|
||||
return { error: getErrorMessage(error) };
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForStep2SignupTabToSettle(tabId, logMessage) {
|
||||
if (!Number.isInteger(tabId) || typeof waitForTabStableComplete !== 'function') {
|
||||
return null;
|
||||
}
|
||||
|
||||
await addLog(
|
||||
logMessage || '步骤 2:注册页标签已切换,正在等待页面加载完成并额外稳定 3 秒...',
|
||||
'info',
|
||||
{ step: 2, stepKey: 'signup-entry' }
|
||||
);
|
||||
|
||||
return waitForTabStableComplete(tabId, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
stableMs: 3000,
|
||||
initialDelayMs: 300,
|
||||
});
|
||||
}
|
||||
|
||||
async function keepSignupTabWindowInBackgroundForStep2(tabId) {
|
||||
// Intentionally no-op: the task tab is locked to the selected Chrome
|
||||
// window by the tab-runtime layer. Step 2 must not focus/raise that
|
||||
// window while the user is working in another app or browser window.
|
||||
void tabId;
|
||||
}
|
||||
|
||||
async function ensureSignupPhoneEntryReady(tabId) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error('步骤 2:未找到可用的注册页标签,无法切换到手机号注册入口。');
|
||||
}
|
||||
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'ENSURE_SIGNUP_PHONE_ENTRY_READY',
|
||||
step: 2,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:正在打开官网注册入口并切换到手机号注册...',
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function submitSignupEmail(resolvedEmail, options = {}) {
|
||||
return sendSignupIdentity({ email: resolvedEmail }, options);
|
||||
}
|
||||
|
||||
async function submitSignupPhone(phoneNumber, activation, options = {}) {
|
||||
return sendSignupIdentity({
|
||||
signupMethod: 'phone',
|
||||
phoneNumber,
|
||||
countryId: activation?.countryId ?? null,
|
||||
countryLabel: String(activation?.countryLabel || '').trim(),
|
||||
}, {
|
||||
logMessage: '步骤 2:官网注册入口正在切换,等待手机号注册入口恢复...',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureSignupTabForStep2() {
|
||||
let signupTabId = await getTabId('signup-page');
|
||||
if (!signupTabId || !(await isTabAlive('signup-page'))) {
|
||||
await addLog('步骤 2:未发现可用的注册页标签,正在重新打开 ChatGPT 官网...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
} else {
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await keepSignupTabWindowInBackgroundForStep2(signupTabId);
|
||||
await waitForStep2SignupTabToSettle(
|
||||
signupTabId,
|
||||
'步骤 2:已切换到注册页标签,正在等待页面加载完成并额外稳定 3 秒...'
|
||||
);
|
||||
await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...',
|
||||
});
|
||||
}
|
||||
return signupTabId;
|
||||
}
|
||||
|
||||
function normalizeSignupPhoneActivationForStep2(activation) {
|
||||
if (typeof phoneVerificationHelpers?.normalizeActivation === 'function') {
|
||||
return phoneVerificationHelpers.normalizeActivation(activation);
|
||||
}
|
||||
if (!activation || typeof activation !== 'object' || Array.isArray(activation)) {
|
||||
return null;
|
||||
}
|
||||
const activationId = String(activation.activationId ?? activation.id ?? activation.activation ?? '').trim();
|
||||
const phoneNumber = String(activation.phoneNumber ?? activation.number ?? activation.phone ?? '').trim();
|
||||
if (!activationId || !phoneNumber) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...activation,
|
||||
activationId,
|
||||
phoneNumber,
|
||||
};
|
||||
}
|
||||
|
||||
function getSignupPhoneNumberFromState(state = {}) {
|
||||
return String(
|
||||
state?.signupPhoneNumber
|
||||
|| (String(state?.accountIdentifierType || '').trim().toLowerCase() === 'phone' ? state?.accountIdentifier : '')
|
||||
|| ''
|
||||
).trim();
|
||||
}
|
||||
|
||||
async function resolveSignupPhoneForStep2(state = {}) {
|
||||
const existingActivation = normalizeSignupPhoneActivationForStep2(state?.signupPhoneActivation);
|
||||
if (existingActivation?.phoneNumber) {
|
||||
await addLog(`步骤 2:复用当前注册手机号 ${existingActivation.phoneNumber},不重新获取号码。`);
|
||||
return {
|
||||
phoneNumber: existingActivation.phoneNumber,
|
||||
activation: existingActivation,
|
||||
};
|
||||
}
|
||||
|
||||
const manualPhoneNumber = getSignupPhoneNumberFromState(state);
|
||||
if (manualPhoneNumber) {
|
||||
await addLog(`步骤 2:使用手动填写的注册手机号 ${manualPhoneNumber},本轮不会重新获取号码。`, 'warn');
|
||||
return {
|
||||
phoneNumber: manualPhoneNumber,
|
||||
activation: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof phoneVerificationHelpers?.prepareSignupPhoneActivation !== 'function') {
|
||||
throw new Error('手机号注册流程不可用:接码模块尚未初始化。');
|
||||
}
|
||||
const activation = await phoneVerificationHelpers.prepareSignupPhoneActivation(state);
|
||||
return {
|
||||
phoneNumber: activation.phoneNumber,
|
||||
activation,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeSignupPhoneEntry(state) {
|
||||
let signupTabId = await ensureSignupTabForStep2();
|
||||
if (await shouldForceAuthEntryRetry(signupTabId)) {
|
||||
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交手机号。', 'warn');
|
||||
try {
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
} catch (entryError) {
|
||||
const entryErrorMessage = getErrorMessage(entryError);
|
||||
if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交手机号...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureSignupPhoneEntryReady(signupTabId);
|
||||
} catch (entryError) {
|
||||
const entryErrorMessage = getErrorMessage(entryError);
|
||||
if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isSignupPhoneEntryUnavailableErrorMessage(entryErrorMessage)
|
||||
|| isSignupEntryUnavailableErrorMessage(entryErrorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(entryErrorMessage)
|
||||
) {
|
||||
await addLog('步骤 2:手机号注册入口尚未就绪,正在重新打开官网入口后重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
await ensureSignupPhoneEntryReady(signupTabId);
|
||||
} else {
|
||||
throw entryError;
|
||||
}
|
||||
}
|
||||
|
||||
const signupPhone = await resolveSignupPhoneForStep2(state);
|
||||
const { phoneNumber, activation } = signupPhone;
|
||||
let step2Result = await submitSignupPhone(phoneNumber, activation, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:官网注册入口正在切换,等待手机号注册入口恢复...',
|
||||
});
|
||||
|
||||
if (step2Result?.error) {
|
||||
const errorMessage = getErrorMessage(step2Result.error);
|
||||
if (
|
||||
isSignupPhoneEntryUnavailableErrorMessage(errorMessage)
|
||||
|| isSignupEntryUnavailableErrorMessage(errorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(errorMessage)
|
||||
) {
|
||||
await addLog('步骤 2:手机号注册入口不可用或通信超时,正在重新准备手机号注册入口后重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
await ensureSignupPhoneEntryReady(signupTabId);
|
||||
step2Result = await submitSignupPhone(phoneNumber, activation, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:手机号注册入口已就绪,正在重新提交手机号...',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (step2Result?.error) {
|
||||
const finalErrorMessage = getErrorMessage(step2Result.error);
|
||||
if (
|
||||
(isSignupEntryUnavailableErrorMessage(finalErrorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(finalErrorMessage))
|
||||
&& await failStep2OnLoggedInSession(signupTabId, finalErrorMessage)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (activation && typeof phoneVerificationHelpers?.cancelSignupPhoneActivation === 'function') {
|
||||
await phoneVerificationHelpers.cancelSignupPhoneActivation(state, activation).catch(() => {});
|
||||
}
|
||||
throw new Error(finalErrorMessage);
|
||||
}
|
||||
|
||||
await addLog(`步骤 2:手机号 ${phoneNumber} 已提交,正在等待页面加载并确认下一步入口...`);
|
||||
const landingResult = await ensureSignupPostIdentityPageReadyInTab(signupTabId, 2, {
|
||||
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
|
||||
});
|
||||
|
||||
await completeNodeFromBackground('submit-signup-email', {
|
||||
accountIdentifierType: 'phone',
|
||||
accountIdentifier: phoneNumber,
|
||||
signupPhoneNumber: phoneNumber,
|
||||
signupPhoneActivation: activation || null,
|
||||
nextSignupState: landingResult?.state || step2Result?.state || 'password_page',
|
||||
nextSignupUrl: landingResult?.url || step2Result?.url || '',
|
||||
skippedPasswordStep: landingResult?.state === 'phone_verification_page' || landingResult?.state === 'profile_page',
|
||||
});
|
||||
}
|
||||
|
||||
async function executeSignupEmailEntry(state) {
|
||||
const resolvedEmail = await resolveSignupEmailForFlow(state);
|
||||
|
||||
let signupTabId = await ensureSignupTabForStep2();
|
||||
|
||||
if (await shouldForceAuthEntryRetry(signupTabId)) {
|
||||
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交邮箱。', 'warn');
|
||||
try {
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
} catch (entryError) {
|
||||
const entryErrorMessage = getErrorMessage(entryError);
|
||||
if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交邮箱...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
}
|
||||
}
|
||||
|
||||
let step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 35000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...',
|
||||
});
|
||||
|
||||
if (step2Result?.error) {
|
||||
const errorMessage = getErrorMessage(step2Result.error);
|
||||
if (isSignupEntryUnavailableErrorMessage(errorMessage)) {
|
||||
await addLog('步骤 2:未找到邮箱输入入口,正在切换认证入口页后重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 35000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...',
|
||||
});
|
||||
|
||||
if (step2Result?.error) {
|
||||
const retryErrorMessage = getErrorMessage(step2Result.error);
|
||||
if (isSignupEntryUnavailableErrorMessage(retryErrorMessage)) {
|
||||
if (await failStep2OnLoggedInSession(signupTabId, retryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
await addLog('步骤 2:认证入口仍不可用,正在重新进入官网注册入口再重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 35000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:重试官网注册入口后正在重新提交邮箱...',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (isRetryableStep2TransportErrorMessage(errorMessage)) {
|
||||
await addLog('步骤 2:注册入口页通信超时,正在切换认证入口页并重试提交邮箱...', 'warn');
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (step2Result?.error) {
|
||||
const finalErrorMessage = getErrorMessage(step2Result.error);
|
||||
if (
|
||||
(isSignupEntryUnavailableErrorMessage(finalErrorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(finalErrorMessage))
|
||||
&& await failStep2OnLoggedInSession(signupTabId, finalErrorMessage)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
throw new Error(finalErrorMessage);
|
||||
}
|
||||
|
||||
if (!step2Result?.alreadyOnPasswordPage) {
|
||||
await addLog(`步骤 2:邮箱 ${resolvedEmail} 已提交,正在等待页面加载并确认下一步入口...`);
|
||||
}
|
||||
|
||||
const landingResult = await ensureSignupPostEmailPageReadyInTab(signupTabId, 2, {
|
||||
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
|
||||
});
|
||||
|
||||
await completeNodeFromBackground('submit-signup-email', {
|
||||
email: resolvedEmail,
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: resolvedEmail,
|
||||
nextSignupState: landingResult?.state || 'password_page',
|
||||
nextSignupUrl: landingResult?.url || step2Result?.url || '',
|
||||
skippedPasswordStep: landingResult?.state === 'verification_page',
|
||||
});
|
||||
}
|
||||
|
||||
async function executeStep2(state) {
|
||||
if (resolveSignupMethod(state) === 'phone') {
|
||||
return executeSignupPhoneEntry(state);
|
||||
}
|
||||
return executeSignupEmailEntry(state);
|
||||
}
|
||||
|
||||
return { executeStep2 };
|
||||
}
|
||||
|
||||
return { createStep2Executor };
|
||||
});
|
||||
@@ -0,0 +1,375 @@
|
||||
(function attachBackgroundStep6(root, factory) {
|
||||
root.MultiPageBackgroundStep6 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep6Module() {
|
||||
const DEFAULT_REGISTRATION_SUCCESS_WAIT_MS = 4000;
|
||||
const LOCAL_CPA_JSON_NO_RT_PANEL_MODE = 'local-cpa-json-no-rt';
|
||||
const LOCAL_CPA_JSON_EXPORT_NODE_ID = 'local-cpa-json-export';
|
||||
const CHATGPT_SESSION_EXPORT_URL = 'https://chatgpt.com/';
|
||||
const STEP6_COOKIE_CLEAR_DOMAINS = [
|
||||
'chatgpt.com',
|
||||
'chat.openai.com',
|
||||
'pay.openai.com',
|
||||
'openai.com',
|
||||
'auth.openai.com',
|
||||
'auth0.openai.com',
|
||||
'accounts.openai.com',
|
||||
'paypal.com',
|
||||
'stripe.com',
|
||||
'checkout.stripe.com',
|
||||
'meiguodizhi.com',
|
||||
'mail-api.yuecheng.shop',
|
||||
'yuecheng.shop',
|
||||
];
|
||||
const STEP6_COOKIE_CLEAR_ORIGINS = [
|
||||
'https://chatgpt.com',
|
||||
'https://chat.openai.com',
|
||||
'https://pay.openai.com',
|
||||
'https://auth.openai.com',
|
||||
'https://auth0.openai.com',
|
||||
'https://accounts.openai.com',
|
||||
'https://openai.com',
|
||||
'https://www.paypal.com',
|
||||
'https://paypal.com',
|
||||
'https://checkout.stripe.com',
|
||||
'https://www.meiguodizhi.com',
|
||||
'https://meiguodizhi.com',
|
||||
'https://mail-api.yuecheng.shop',
|
||||
];
|
||||
|
||||
function normalizeStep6CookieDomain(domain) {
|
||||
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
|
||||
}
|
||||
|
||||
function shouldClearStep6Cookie(cookie) {
|
||||
const domain = normalizeStep6CookieDomain(cookie?.domain);
|
||||
if (!domain) return false;
|
||||
return STEP6_COOKIE_CLEAR_DOMAINS.some((target) => (
|
||||
domain === target || domain.endsWith(`.${target}`)
|
||||
));
|
||||
}
|
||||
|
||||
function buildStep6CookieRemovalUrl(cookie) {
|
||||
const host = normalizeStep6CookieDomain(cookie?.domain);
|
||||
const rawPath = String(cookie?.path || '/');
|
||||
const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
||||
return `https://${host}${path}`;
|
||||
}
|
||||
|
||||
async function collectStep6Cookies(chromeApi) {
|
||||
if (!chromeApi.cookies?.getAll) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stores = chromeApi.cookies.getAllCookieStores
|
||||
? await chromeApi.cookies.getAllCookieStores()
|
||||
: [{ id: undefined }];
|
||||
const cookies = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const store of stores) {
|
||||
const storeId = store?.id;
|
||||
const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {});
|
||||
for (const cookie of batch || []) {
|
||||
if (!shouldClearStep6Cookie(cookie)) continue;
|
||||
const key = [
|
||||
cookie.storeId || storeId || '',
|
||||
cookie.domain || '',
|
||||
cookie.path || '',
|
||||
cookie.name || '',
|
||||
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||||
].join('|');
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
cookies.push(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
async function removeStep6Cookie(chromeApi, cookie, getErrorMessage) {
|
||||
const details = {
|
||||
url: buildStep6CookieRemovalUrl(cookie),
|
||||
name: cookie.name,
|
||||
};
|
||||
if (cookie.storeId) {
|
||||
details.storeId = cookie.storeId;
|
||||
}
|
||||
if (cookie.partitionKey) {
|
||||
details.partitionKey = cookie.partitionKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await chromeApi.cookies.remove(details);
|
||||
return Boolean(result);
|
||||
} catch (error) {
|
||||
console.warn('[MultiPage:step6] remove cookie failed', {
|
||||
domain: cookie?.domain,
|
||||
name: cookie?.name,
|
||||
message: getErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createStep6Executor(deps = {}) {
|
||||
const {
|
||||
addLog = async () => {},
|
||||
buildLocalHelperEndpoint = null,
|
||||
chrome: chromeApi = globalThis.chrome,
|
||||
completeNodeFromBackground,
|
||||
createLocalCliProxyApi = null,
|
||||
ensureContentScriptReadyOnTab = async () => {},
|
||||
getErrorMessage = (error) => error?.message || String(error || '未知错误'),
|
||||
getPanelMode = (state = {}) => String(state?.panelMode || '').trim() || 'cpa',
|
||||
getTabId = async () => null,
|
||||
normalizeHotmailLocalBaseUrl = (value) => String(value || '').trim(),
|
||||
registrationSuccessWaitMs = DEFAULT_REGISTRATION_SUCCESS_WAIT_MS,
|
||||
sessionExportInjectFiles = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'],
|
||||
sendToContentScriptResilient = null,
|
||||
sleepWithStop = async (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0))),
|
||||
} = deps;
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function isLocalCpaJsonNoRtMode(state = {}) {
|
||||
return normalizeString(getPanelMode(state)) === LOCAL_CPA_JSON_NO_RT_PANEL_MODE;
|
||||
}
|
||||
|
||||
function getLocalCliProxyApi() {
|
||||
const factory = createLocalCliProxyApi
|
||||
|| globalThis.MultiPageBackgroundLocalCliProxyApi?.createLocalCliProxyApi
|
||||
|| null;
|
||||
if (typeof factory !== 'function') {
|
||||
throw new Error('本地 CPA JSON 无RT 模块未加载,无法导出认证文件。');
|
||||
}
|
||||
return factory({
|
||||
crypto: globalThis.crypto,
|
||||
fetch: typeof globalThis.fetch === 'function' ? globalThis.fetch.bind(globalThis) : null,
|
||||
sessionToJsonConverter: globalThis.MultiPageSessionToJsonConverter,
|
||||
});
|
||||
}
|
||||
|
||||
async function saveLocalCpaJsonArtifactViaHelper(helperBaseUrl, artifact) {
|
||||
const endpoint = typeof buildLocalHelperEndpoint === 'function'
|
||||
? buildLocalHelperEndpoint(helperBaseUrl, '/save-auth-json')
|
||||
: new URL('/save-auth-json', `${helperBaseUrl.replace(/\/+$/, '')}/`).toString();
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filePath: artifact.filePath,
|
||||
directoryPath: artifact.directoryPath,
|
||||
content: artifact.jsonText,
|
||||
}),
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok || payload?.ok === false) {
|
||||
const helperError = normalizeString(payload?.error);
|
||||
if (/Missing email\/clientId\/refreshToken/i.test(helperError)) {
|
||||
throw new Error('本地 helper 未识别 /save-auth-json,当前运行的 hotmail_helper.py 版本过旧或不是当前项目目录。请停止旧 helper,并从当前 FlowPilot-FlowPilot1.0.2 目录重新启动本地助手。');
|
||||
}
|
||||
throw new Error(helperError || `本地 helper 写入失败(HTTP ${response.status})。`);
|
||||
}
|
||||
|
||||
return {
|
||||
...artifact,
|
||||
filePath: normalizeString(payload?.filePath) || artifact.filePath,
|
||||
};
|
||||
}
|
||||
|
||||
async function openChatGptSessionExportTab(state = {}) {
|
||||
if (chromeApi?.tabs?.create) {
|
||||
const tab = await chromeApi.tabs.create({
|
||||
url: CHATGPT_SESSION_EXPORT_URL,
|
||||
active: false,
|
||||
});
|
||||
const tabId = Number(tab?.id);
|
||||
if (Number.isInteger(tabId) && tabId > 0) {
|
||||
return {
|
||||
source: 'plus-checkout',
|
||||
tabId,
|
||||
temporary: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackTabId = Number(state?.plusCheckoutTabId || await getTabId('plus-checkout') || await getTabId('signup-page'));
|
||||
if (!Number.isInteger(fallbackTabId) || fallbackTabId <= 0) {
|
||||
throw new Error('未找到可读取 ChatGPT 会话的标签页,无法导出本地 CPA JSON 无RT。');
|
||||
}
|
||||
return {
|
||||
source: 'plus-checkout',
|
||||
tabId: fallbackTabId,
|
||||
temporary: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function closeTemporarySessionExportTab(tabInfo = {}) {
|
||||
if (!tabInfo?.temporary || !Number.isInteger(Number(tabInfo?.tabId)) || !chromeApi?.tabs?.remove) {
|
||||
return;
|
||||
}
|
||||
await chromeApi.tabs.remove(Number(tabInfo.tabId)).catch(() => {});
|
||||
}
|
||||
|
||||
async function readChatGptSessionForExport(state = {}, visibleStep = 7) {
|
||||
if (typeof sendToContentScriptResilient !== 'function') {
|
||||
throw new Error('当前环境缺少 ChatGPT 会话读取通道,无法导出本地 CPA JSON 无RT。');
|
||||
}
|
||||
|
||||
const tabInfo = await openChatGptSessionExportTab(state);
|
||||
try {
|
||||
await ensureContentScriptReadyOnTab(tabInfo.source, tabInfo.tabId, {
|
||||
inject: sessionExportInjectFiles,
|
||||
injectSource: tabInfo.source,
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 800,
|
||||
logMessage: `步骤 ${visibleStep}:正在连接 ChatGPT 页面,准备读取当前会话并导出 JSON...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: LOCAL_CPA_JSON_EXPORT_NODE_ID,
|
||||
});
|
||||
|
||||
const sessionResult = await sendToContentScriptResilient(tabInfo.source, {
|
||||
type: 'PLUS_CHECKOUT_GET_STATE',
|
||||
step: visibleStep,
|
||||
source: 'background',
|
||||
payload: {
|
||||
includeSession: true,
|
||||
includeAccessToken: true,
|
||||
},
|
||||
}, {
|
||||
timeoutMs: 15000,
|
||||
retryDelayMs: 500,
|
||||
logMessage: `步骤 ${visibleStep}:正在等待 ChatGPT 页面返回当前登录会话...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: LOCAL_CPA_JSON_EXPORT_NODE_ID,
|
||||
});
|
||||
|
||||
if (sessionResult?.error) {
|
||||
throw new Error(sessionResult.error);
|
||||
}
|
||||
return sessionResult;
|
||||
} finally {
|
||||
await closeTemporarySessionExportTab(tabInfo);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportLocalCpaJsonNoRt(state = {}, options = {}) {
|
||||
const visibleStep = Math.max(1, Math.floor(Number(options.visibleStep) || 7));
|
||||
const helperBaseUrl = normalizeHotmailLocalBaseUrl(state.hotmailLocalBaseUrl);
|
||||
const pluginDir = normalizeString(state.localCpaJsonPluginDir);
|
||||
if (!helperBaseUrl) {
|
||||
throw new Error('尚未配置 Hotmail 本地助手地址,请先在侧边栏填写。');
|
||||
}
|
||||
if (!pluginDir) {
|
||||
throw new Error('尚未配置本地插件目录,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const sessionResult = await readChatGptSessionForExport(state, visibleStep);
|
||||
const api = getLocalCliProxyApi();
|
||||
const artifact = await api.buildAuthJsonArtifact({
|
||||
pluginDir,
|
||||
relativeAuthDir: state.localCpaJsonRelativeAuthDir,
|
||||
session: sessionResult?.session,
|
||||
accessToken: sessionResult?.accessToken,
|
||||
sessionToken: sessionResult?.session?.sessionToken,
|
||||
email: sessionResult?.email || sessionResult?.session?.user?.email || state?.email,
|
||||
expiresAt: sessionResult?.expiresAt || sessionResult?.session?.expires,
|
||||
accountId: sessionResult?.session?.account?.id,
|
||||
userId: sessionResult?.session?.user?.id,
|
||||
planType: sessionResult?.session?.account?.planType,
|
||||
lastRefresh: '',
|
||||
sourceName: 'SessionToJson Local No RT',
|
||||
});
|
||||
|
||||
for (const warning of Array.isArray(artifact.warnings) ? artifact.warnings : []) {
|
||||
await addLog(`步骤 ${visibleStep}:${warning}`, 'warn');
|
||||
}
|
||||
|
||||
const saved = await saveLocalCpaJsonArtifactViaHelper(helperBaseUrl, artifact);
|
||||
const verifiedStatus = `本地CPA JSON 无RT 已导出:${saved.filePath}`;
|
||||
await addLog(`步骤 ${visibleStep}:${verifiedStatus}`, 'ok');
|
||||
return {
|
||||
verifiedStatus,
|
||||
localCpaJsonFilePath: saved.filePath,
|
||||
};
|
||||
}
|
||||
|
||||
async function clearCookiesIfEnabled(state = {}) {
|
||||
if (!state?.step6CookieCleanupEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!chromeApi?.cookies?.getAll || !chromeApi.cookies?.remove) {
|
||||
await addLog('步骤 6:当前浏览器不支持 cookies API,跳过第六步 Cookies 清理。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await addLog('步骤 6:已开启 Cookies 清理,正在清理 ChatGPT / OpenAI cookies...', 'info');
|
||||
const cookies = await collectStep6Cookies(chromeApi);
|
||||
let removedCount = 0;
|
||||
for (const cookie of cookies) {
|
||||
if (await removeStep6Cookie(chromeApi, cookie, getErrorMessage)) {
|
||||
removedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (chromeApi.browsingData?.removeCookies) {
|
||||
try {
|
||||
await chromeApi.browsingData.removeCookies({
|
||||
since: 0,
|
||||
origins: STEP6_COOKIE_CLEAR_ORIGINS,
|
||||
});
|
||||
} catch (error) {
|
||||
await addLog(`步骤 6:browsingData 补扫 cookies 失败:${getErrorMessage(error)}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
await addLog(`步骤 6:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies。`, 'ok');
|
||||
} catch (error) {
|
||||
await addLog(`步骤 6:Cookies 清理失败,已跳过并继续后续流程:${getErrorMessage(error)}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
async function executeStep6(state = {}) {
|
||||
const baseWaitMs = Math.max(0, Math.floor(Number(registrationSuccessWaitMs) || 0));
|
||||
const waitMs = baseWaitMs;
|
||||
if (waitMs > 0) {
|
||||
await addLog(`步骤 6:等待 ${Math.round(waitMs / 1000)} 秒,确认注册成功并让页面稳定...`, 'info');
|
||||
await sleepWithStop(waitMs);
|
||||
}
|
||||
await clearCookiesIfEnabled(state);
|
||||
await addLog('步骤 6:注册成功等待完成,注册阶段已结束。', 'ok');
|
||||
await completeNodeFromBackground('wait-registration-success', {});
|
||||
}
|
||||
|
||||
async function executeLocalCpaJsonNoRtExport(state = {}) {
|
||||
if (!isLocalCpaJsonNoRtMode(state)) {
|
||||
throw new Error('当前不是本地CPA JSON 无RT 模式,不能执行无RT导出节点。');
|
||||
}
|
||||
await addLog('步骤 7:Plus Checkout 已完成,等待 5 秒后导出本地 CPA JSON 无RT...', 'info');
|
||||
await sleepWithStop(5000);
|
||||
const completionPayload = await exportLocalCpaJsonNoRt(state, { visibleStep: 7 });
|
||||
await completeNodeFromBackground(LOCAL_CPA_JSON_EXPORT_NODE_ID, completionPayload);
|
||||
}
|
||||
|
||||
return {
|
||||
executeLocalCpaJsonNoRtExport,
|
||||
executeStep6,
|
||||
};
|
||||
}
|
||||
|
||||
return { createStep6Executor };
|
||||
});
|
||||
@@ -0,0 +1,606 @@
|
||||
(function attachBackgroundSub2ApiApi(root, factory) {
|
||||
root.MultiPageBackgroundSub2ApiApi = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundSub2ApiApiModule() {
|
||||
function createSub2ApiApi(deps = {}) {
|
||||
const {
|
||||
addLog = async () => {},
|
||||
normalizeSub2ApiUrl = (value) => value,
|
||||
DEFAULT_SUB2API_GROUP_NAME = 'codex',
|
||||
fetchImpl = (...args) => fetch(...args),
|
||||
} = deps;
|
||||
|
||||
const DEFAULT_REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
||||
const DEFAULT_PROXY_NAME = '';
|
||||
const DEFAULT_CONCURRENCY = 10;
|
||||
const DEFAULT_PRIORITY = 1;
|
||||
const DEFAULT_RATE_MULTIPLIER = 1;
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function extractStateFromAuthUrl(authUrl = '') {
|
||||
try {
|
||||
return new URL(authUrl).searchParams.get('state') || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRedirectUri(input = DEFAULT_REDIRECT_URI) {
|
||||
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
|
||||
const parsed = new URL(withProtocol);
|
||||
if (!parsed.pathname || parsed.pathname === '/') {
|
||||
parsed.pathname = '/auth/callback';
|
||||
}
|
||||
if (parsed.pathname !== '/auth/callback') {
|
||||
throw new Error('SUB2API 回调地址必须是 /auth/callback,例如 http://localhost:1455/auth/callback');
|
||||
}
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function getSub2ApiOrigin(rawUrl = '') {
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(rawUrl);
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
}
|
||||
try {
|
||||
return new URL(sub2apiUrl).origin;
|
||||
} catch {
|
||||
throw new Error('SUB2API URL 格式无效,请先在侧边栏检查。');
|
||||
}
|
||||
}
|
||||
|
||||
function getSub2ApiErrorMessage(payload, responseStatus = 500, path = '') {
|
||||
const candidates = [
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.error,
|
||||
payload?.reason,
|
||||
];
|
||||
const message = candidates.map(normalizeString).find(Boolean);
|
||||
return message || `SUB2API 请求失败(HTTP ${responseStatus}):${path}`;
|
||||
}
|
||||
|
||||
async function requestJson(origin, path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const token = normalizeString(options.token);
|
||||
const response = await fetchImpl(`${origin}${path}`, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
let payload = null;
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
payload = null;
|
||||
}
|
||||
|
||||
if (payload && typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'code')) {
|
||||
if (Number(payload.code) === 0) {
|
||||
return payload.data;
|
||||
}
|
||||
throw new Error(getSub2ApiErrorMessage(payload, response.status, path));
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getSub2ApiErrorMessage(payload, response.status, path));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(`SUB2API 请求超时:${path}`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function loginSub2Api(state = {}, options = {}) {
|
||||
const email = normalizeString(state.sub2apiEmail);
|
||||
const password = String(state.sub2apiPassword || '');
|
||||
const origin = getSub2ApiOrigin(state.sub2apiUrl);
|
||||
|
||||
if (!email) {
|
||||
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
||||
}
|
||||
if (!password) {
|
||||
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const loginData = await requestJson(origin, '/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
timeoutMs: options.timeoutMs,
|
||||
body: { email, password },
|
||||
});
|
||||
|
||||
const token = normalizeString(loginData?.access_token || loginData?.accessToken);
|
||||
if (!token) {
|
||||
throw new Error('SUB2API 登录返回缺少 access_token。');
|
||||
}
|
||||
|
||||
return {
|
||||
origin,
|
||||
token,
|
||||
user: loginData?.user || null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSub2ApiGroupNames(value) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '').split(/[\r\n,,;;]+/);
|
||||
const seen = new Set();
|
||||
const names = [];
|
||||
for (const item of source) {
|
||||
const name = normalizeString(item);
|
||||
const key = name.toLowerCase();
|
||||
if (!name || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
names.push(name);
|
||||
}
|
||||
return names.length ? names : [DEFAULT_SUB2API_GROUP_NAME];
|
||||
}
|
||||
|
||||
async function getGroupsByNames(origin, token, groupNames, options = {}) {
|
||||
const targetNames = normalizeSub2ApiGroupNames(groupNames);
|
||||
const groups = await requestJson(origin, '/api/v1/admin/groups/all', {
|
||||
method: 'GET',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
});
|
||||
const matched = [];
|
||||
const missing = [];
|
||||
|
||||
for (const targetName of targetNames) {
|
||||
const normalized = targetName.toLowerCase();
|
||||
const group = (Array.isArray(groups) ? groups : []).find((item) => {
|
||||
const itemName = normalizeString(item?.name).toLowerCase();
|
||||
if (!itemName || itemName !== normalized) return false;
|
||||
return !item.platform || item.platform === 'openai';
|
||||
});
|
||||
if (group) {
|
||||
matched.push(group);
|
||||
} else {
|
||||
missing.push(targetName);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
throw new Error(`SUB2API 中未找到以下 openai 分组:${missing.join('、')}。`);
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
function normalizeSub2ApiProxyPreference(value) {
|
||||
return normalizeString(value);
|
||||
}
|
||||
|
||||
function resolveSub2ApiProxyPreference(state = {}) {
|
||||
if (state.sub2apiDefaultProxyName !== undefined) {
|
||||
return normalizeSub2ApiProxyPreference(state.sub2apiDefaultProxyName);
|
||||
}
|
||||
return DEFAULT_PROXY_NAME;
|
||||
}
|
||||
|
||||
function resolveSub2ApiAccountPriority(state = {}) {
|
||||
const rawValue = normalizeString(state.sub2apiAccountPriority);
|
||||
if (!rawValue) {
|
||||
return DEFAULT_PRIORITY;
|
||||
}
|
||||
const numeric = Number(rawValue);
|
||||
if (!Number.isSafeInteger(numeric) || numeric < 1) {
|
||||
throw new Error('SUB2API 账号优先级必须是大于等于 1 的整数。');
|
||||
}
|
||||
return numeric;
|
||||
}
|
||||
|
||||
function normalizeProxyId(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return null;
|
||||
}
|
||||
const normalized = Number(value);
|
||||
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildProxyDisplayName(proxy = {}) {
|
||||
const id = normalizeProxyId(proxy.id);
|
||||
const name = normalizeString(proxy.name);
|
||||
const protocol = normalizeString(proxy.protocol);
|
||||
const host = normalizeString(proxy.host);
|
||||
const port = proxy.port === undefined || proxy.port === null ? '' : normalizeString(proxy.port);
|
||||
const address = protocol && host && port ? `${protocol}://${host}:${port}` : '';
|
||||
return [
|
||||
name || '(未命名代理)',
|
||||
id ? `#${id}` : '',
|
||||
address,
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function buildProxySearchText(proxy = {}) {
|
||||
return [
|
||||
proxy.id,
|
||||
proxy.name,
|
||||
proxy.protocol,
|
||||
proxy.host,
|
||||
proxy.port,
|
||||
buildProxyDisplayName(proxy),
|
||||
]
|
||||
.filter((value) => value !== undefined && value !== null && value !== '')
|
||||
.map((value) => normalizeString(value).toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function isActiveProxy(proxy = {}) {
|
||||
const status = normalizeString(proxy.status).toLowerCase();
|
||||
return !status || status === 'active';
|
||||
}
|
||||
|
||||
function findSub2ApiProxy(proxies = [], preference = '') {
|
||||
const activeProxies = (Array.isArray(proxies) ? proxies : [])
|
||||
.filter(isActiveProxy)
|
||||
.filter((proxy) => normalizeProxyId(proxy.id));
|
||||
const normalizedPreference = normalizeSub2ApiProxyPreference(preference).toLowerCase();
|
||||
const preferredId = normalizeProxyId(normalizedPreference);
|
||||
|
||||
if (preferredId) {
|
||||
const matchedById = activeProxies.find((proxy) => normalizeProxyId(proxy.id) === preferredId);
|
||||
return {
|
||||
proxy: matchedById || null,
|
||||
reason: matchedById ? 'id' : 'missing-id',
|
||||
candidates: activeProxies,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedPreference) {
|
||||
const exactMatches = activeProxies.filter((proxy) => normalizeString(proxy.name).toLowerCase() === normalizedPreference);
|
||||
if (exactMatches.length === 1) {
|
||||
return { proxy: exactMatches[0], reason: 'name', candidates: activeProxies };
|
||||
}
|
||||
if (exactMatches.length > 1) {
|
||||
return { proxy: null, reason: 'ambiguous-name', candidates: exactMatches };
|
||||
}
|
||||
|
||||
const fuzzyMatches = activeProxies.filter((proxy) => buildProxySearchText(proxy).includes(normalizedPreference));
|
||||
if (fuzzyMatches.length === 1) {
|
||||
return { proxy: fuzzyMatches[0], reason: 'fuzzy', candidates: activeProxies };
|
||||
}
|
||||
if (fuzzyMatches.length > 1) {
|
||||
return { proxy: null, reason: 'ambiguous-fuzzy', candidates: fuzzyMatches };
|
||||
}
|
||||
|
||||
return { proxy: null, reason: 'missing-name', candidates: activeProxies };
|
||||
}
|
||||
|
||||
if (activeProxies.length === 1) {
|
||||
return { proxy: activeProxies[0], reason: 'single-active', candidates: activeProxies };
|
||||
}
|
||||
return {
|
||||
proxy: null,
|
||||
reason: activeProxies.length ? 'no-preference' : 'none-active',
|
||||
candidates: activeProxies,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveSub2ApiProxy(origin, token, preference = '', options = {}) {
|
||||
const proxies = await requestJson(origin, '/api/v1/admin/proxies/all?with_count=true', {
|
||||
method: 'GET',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
});
|
||||
if (!Array.isArray(proxies)) {
|
||||
throw new Error('SUB2API 代理列表返回格式异常,无法自动选择代理。');
|
||||
}
|
||||
|
||||
const { proxy, reason, candidates } = findSub2ApiProxy(proxies, preference);
|
||||
if (proxy) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
const configured = normalizeSub2ApiProxyPreference(preference) || '(未配置)';
|
||||
const available = (candidates || [])
|
||||
.slice(0, 8)
|
||||
.map(buildProxyDisplayName)
|
||||
.join(';') || '无可用代理';
|
||||
if (reason === 'ambiguous-name' || reason === 'ambiguous-fuzzy') {
|
||||
throw new Error(`SUB2API 默认代理“${configured}”匹配到多个代理,请改填代理 ID。候选:${available}`);
|
||||
}
|
||||
if (reason === 'missing-id') {
|
||||
throw new Error(`SUB2API 默认代理 ID “${configured}”不存在或未启用。可用代理:${available}`);
|
||||
}
|
||||
if (reason === 'missing-name') {
|
||||
throw new Error(`SUB2API 默认代理“${configured}”不存在或未启用。可用代理:${available}`);
|
||||
}
|
||||
if (reason === 'no-preference') {
|
||||
throw new Error(`SUB2API 存在多个可用代理,请在侧边栏填写默认代理名称或 ID;留空则不使用代理。可用代理:${available}`);
|
||||
}
|
||||
throw new Error('SUB2API 没有可用代理;请检查默认代理配置,或将其留空以禁用代理。');
|
||||
}
|
||||
|
||||
function buildDraftAccountName(groupName) {
|
||||
const prefix = normalizeString(groupName || DEFAULT_SUB2API_GROUP_NAME)
|
||||
.replace(/[^\w\u4e00-\u9fa5-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '') || DEFAULT_SUB2API_GROUP_NAME;
|
||||
const stamp = new Date().toISOString().replace(/\D/g, '').slice(2, 14);
|
||||
const random = Math.floor(Math.random() * 9000 + 1000);
|
||||
return `${prefix}-${stamp}-${random}`;
|
||||
}
|
||||
|
||||
function parseLocalhostCallback(rawUrl, visibleStep = 10) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址格式无效。`);
|
||||
}
|
||||
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
throw new Error('回调 URL 协议不正确。');
|
||||
}
|
||||
if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) {
|
||||
throw new Error(`步骤 ${visibleStep} 只接受 localhost / 127.0.0.1 回调地址。`);
|
||||
}
|
||||
if (parsed.pathname !== '/auth/callback') {
|
||||
throw new Error('回调 URL 路径必须是 /auth/callback。');
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const state = normalizeString(parsed.searchParams.get('state'));
|
||||
if (!code || !state) {
|
||||
throw new Error('回调 URL 中缺少 code 或 state。');
|
||||
}
|
||||
|
||||
return {
|
||||
url: parsed.toString(),
|
||||
code,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
function buildOpenAiCredentials(exchangeData) {
|
||||
const credentials = {};
|
||||
const allowedKeys = [
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'id_token',
|
||||
'expires_at',
|
||||
'email',
|
||||
'chatgpt_account_id',
|
||||
'chatgpt_user_id',
|
||||
'organization_id',
|
||||
'plan_type',
|
||||
'client_id',
|
||||
];
|
||||
|
||||
for (const key of allowedKeys) {
|
||||
if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
|
||||
credentials[key] = exchangeData[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!credentials.access_token) {
|
||||
throw new Error('SUB2API 交换授权码后未返回 access_token。');
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
function buildOpenAiExtra(exchangeData) {
|
||||
const extra = {};
|
||||
const allowedKeys = ['email', 'name', 'privacy_mode'];
|
||||
|
||||
for (const key of allowedKeys) {
|
||||
if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
|
||||
extra[key] = exchangeData[key];
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(extra).length ? extra : undefined;
|
||||
}
|
||||
|
||||
async function logWithOptions(message, level = 'info', options = {}) {
|
||||
await addLog(message, level, options.logOptions || {});
|
||||
}
|
||||
|
||||
async function generateOpenAiAuthUrl(state = {}, options = {}) {
|
||||
const logLabel = normalizeString(options.logLabel) || 'OAuth 刷新';
|
||||
const redirectUri = normalizeRedirectUri(options.redirectUri || DEFAULT_REDIRECT_URI);
|
||||
const groupNames = normalizeSub2ApiGroupNames(state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME);
|
||||
const groupName = groupNames[0] || DEFAULT_SUB2API_GROUP_NAME;
|
||||
|
||||
await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口登录并生成 OpenAI Auth 链接...`, 'info', options);
|
||||
const { origin, token } = await loginSub2Api(state, options);
|
||||
const groups = await getGroupsByNames(origin, token, groupNames, options);
|
||||
const group = groups[0];
|
||||
const proxyPreference = resolveSub2ApiProxyPreference(state);
|
||||
const proxy = proxyPreference ? await resolveSub2ApiProxy(origin, token, proxyPreference, options) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const draftName = buildDraftAccountName(group.name || groupName);
|
||||
const groupLabel = groups.map((item) => `${item.name}(#${item.id})`).join('、');
|
||||
|
||||
await logWithOptions(`${logLabel}:已登录 SUB2API,使用分组 ${groupLabel}。`, 'info', options);
|
||||
if (proxy) {
|
||||
await logWithOptions(`${logLabel}:已选择 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`, 'info', options);
|
||||
} else {
|
||||
await logWithOptions(`${logLabel}:未配置 SUB2API 默认代理,本次将不使用代理。`, 'info', options);
|
||||
}
|
||||
|
||||
const authRequestBody = { redirect_uri: redirectUri };
|
||||
if (proxyId) {
|
||||
authRequestBody.proxy_id = proxyId;
|
||||
}
|
||||
|
||||
const authData = await requestJson(origin, '/api/v1/admin/openai/generate-auth-url', {
|
||||
method: 'POST',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
body: authRequestBody,
|
||||
});
|
||||
|
||||
const oauthUrl = normalizeString(authData?.auth_url || authData?.authUrl);
|
||||
const sessionId = normalizeString(authData?.session_id || authData?.sessionId);
|
||||
const oauthState = normalizeString(authData?.state || extractStateFromAuthUrl(oauthUrl));
|
||||
|
||||
if (!oauthUrl || !sessionId) {
|
||||
throw new Error('SUB2API 未返回完整的 auth_url / session_id。');
|
||||
}
|
||||
|
||||
await logWithOptions(`${logLabel}:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok', options);
|
||||
return {
|
||||
oauthUrl,
|
||||
sub2apiSessionId: sessionId,
|
||||
sub2apiOAuthState: oauthState,
|
||||
sub2apiGroupId: group.id,
|
||||
sub2apiGroupIds: groups.map((item) => item.id),
|
||||
sub2apiDraftName: draftName,
|
||||
sub2apiProxyId: proxyId,
|
||||
};
|
||||
}
|
||||
|
||||
async function submitOpenAiCallback(state = {}, options = {}) {
|
||||
const visibleStep = Number(options.visibleStep || state.visibleStep) || 10;
|
||||
const callback = parseLocalhostCallback(state.localhostUrl || '', visibleStep);
|
||||
const flowEmail = normalizeString(state.email);
|
||||
const sessionId = normalizeString(state.sub2apiSessionId);
|
||||
const expectedState = normalizeString(state.sub2apiOAuthState);
|
||||
const logLabel = normalizeString(options.logLabel) || `步骤 ${visibleStep}`;
|
||||
|
||||
if (!sessionId) {
|
||||
throw new Error('缺少 SUB2API session_id,请重新执行步骤 1。');
|
||||
}
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error('本次 localhost 回调中的 state 与步骤 1 生成的 state 不一致,请重新执行步骤 1。');
|
||||
}
|
||||
|
||||
const { origin, token } = await loginSub2Api(state, options);
|
||||
const proxyPreference = resolveSub2ApiProxyPreference(state);
|
||||
const preferredProxyId = normalizeProxyId(state.sub2apiProxyId);
|
||||
const proxySelector = preferredProxyId || proxyPreference;
|
||||
const proxy = proxySelector ? await resolveSub2ApiProxy(origin, token, proxySelector, options) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const accountPriority = resolveSub2ApiAccountPriority(state);
|
||||
const storedGroupIds = Array.isArray(state.sub2apiGroupIds) ? state.sub2apiGroupIds : [];
|
||||
const groupIdsFromState = storedGroupIds
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
const groups = groupIdsFromState.length
|
||||
? groupIdsFromState.map((id) => ({ id }))
|
||||
: (state.sub2apiGroupId
|
||||
? [{ id: state.sub2apiGroupId, name: state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME }]
|
||||
: await getGroupsByNames(origin, token, state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME, options));
|
||||
|
||||
await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口交换 OpenAI 授权码...`, 'info', options);
|
||||
if (proxy) {
|
||||
await logWithOptions(`${logLabel}:使用 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`, 'info', options);
|
||||
} else {
|
||||
await logWithOptions(`${logLabel}:未配置 SUB2API 默认代理,本次将不使用代理。`, 'info', options);
|
||||
}
|
||||
|
||||
const exchangeRequestBody = {
|
||||
session_id: sessionId,
|
||||
code: callback.code,
|
||||
state: callback.state,
|
||||
};
|
||||
if (proxyId) {
|
||||
exchangeRequestBody.proxy_id = proxyId;
|
||||
}
|
||||
|
||||
const exchangeData = await requestJson(origin, '/api/v1/admin/openai/exchange-code', {
|
||||
method: 'POST',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
body: exchangeRequestBody,
|
||||
});
|
||||
|
||||
const credentials = buildOpenAiCredentials(exchangeData);
|
||||
const extra = buildOpenAiExtra(exchangeData);
|
||||
const resolvedEmail = normalizeString(exchangeData?.email || credentials?.email);
|
||||
const groupIds = groups
|
||||
.map((group) => Number(group.id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
if (!groupIds.length) {
|
||||
throw new Error('SUB2API 返回的目标分组 ID 无效。');
|
||||
}
|
||||
|
||||
const accountName = resolvedEmail
|
||||
|| flowEmail
|
||||
|| normalizeString(state.sub2apiDraftName)
|
||||
|| buildDraftAccountName(state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME);
|
||||
const createPayload = {
|
||||
name: accountName,
|
||||
notes: '',
|
||||
platform: 'openai',
|
||||
type: 'oauth',
|
||||
credentials,
|
||||
concurrency: DEFAULT_CONCURRENCY,
|
||||
priority: accountPriority,
|
||||
rate_multiplier: DEFAULT_RATE_MULTIPLIER,
|
||||
group_ids: groupIds,
|
||||
auto_pause_on_expired: true,
|
||||
};
|
||||
if (proxyId) {
|
||||
createPayload.proxy_id = proxyId;
|
||||
}
|
||||
if (extra) {
|
||||
createPayload.extra = extra;
|
||||
}
|
||||
|
||||
await logWithOptions(`${logLabel}:授权码交换成功,正在创建 SUB2API 账号(名称:${accountName})...`, 'info', options);
|
||||
const createdAccount = await requestJson(origin, '/api/v1/admin/accounts', {
|
||||
method: 'POST',
|
||||
token,
|
||||
timeoutMs: options.createTimeoutMs,
|
||||
body: createPayload,
|
||||
});
|
||||
|
||||
const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
|
||||
await logWithOptions(verifiedStatus, 'ok', options);
|
||||
return {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
buildDraftAccountName,
|
||||
buildOpenAiCredentials,
|
||||
buildOpenAiExtra,
|
||||
buildProxyDisplayName,
|
||||
extractStateFromAuthUrl,
|
||||
generateOpenAiAuthUrl,
|
||||
getGroupsByNames,
|
||||
loginSub2Api,
|
||||
normalizeProxyId,
|
||||
normalizeRedirectUri,
|
||||
normalizeSub2ApiGroupNames,
|
||||
parseLocalhostCallback,
|
||||
requestJson,
|
||||
resolveSub2ApiAccountPriority,
|
||||
resolveSub2ApiProxy,
|
||||
submitOpenAiCallback,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createSub2ApiApi,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
||||
(function attachBackgroundWorkflowEngine(root, factory) {
|
||||
root.MultiPageBackgroundWorkflowEngine = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundWorkflowEngineModule() {
|
||||
function createWorkflowEngine(deps = {}) {
|
||||
const {
|
||||
defaultFlowId = 'openai',
|
||||
workflowDefinitions = null,
|
||||
} = deps;
|
||||
|
||||
function normalizeFlowId(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized || defaultFlowId;
|
||||
}
|
||||
|
||||
function normalizeNodeId(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function resolveStateFlowId(state = {}) {
|
||||
return normalizeFlowId(state?.flowId || state?.activeFlowId || defaultFlowId);
|
||||
}
|
||||
|
||||
function getWorkflow(options = {}) {
|
||||
const flowId = normalizeFlowId(options?.flowId || options?.activeFlowId || defaultFlowId);
|
||||
if (workflowDefinitions?.getWorkflow) {
|
||||
return workflowDefinitions.getWorkflow({
|
||||
...options,
|
||||
activeFlowId: flowId,
|
||||
flowId,
|
||||
});
|
||||
}
|
||||
return {
|
||||
flowId,
|
||||
workflowVersion: 1,
|
||||
nodes: [],
|
||||
nodeIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
function getWorkflowForState(state = {}) {
|
||||
return getWorkflow({
|
||||
...state,
|
||||
flowId: resolveStateFlowId(state),
|
||||
activeFlowId: resolveStateFlowId(state),
|
||||
});
|
||||
}
|
||||
|
||||
function getNodesForState(state = {}) {
|
||||
return Array.isArray(getWorkflowForState(state).nodes)
|
||||
? getWorkflowForState(state).nodes
|
||||
: [];
|
||||
}
|
||||
|
||||
function getNodeIdsForState(state = {}) {
|
||||
return getNodesForState(state).map((node) => node.nodeId).filter(Boolean);
|
||||
}
|
||||
|
||||
function getNodeById(nodeId, state = {}) {
|
||||
const normalizedNodeId = normalizeNodeId(nodeId);
|
||||
if (!normalizedNodeId) {
|
||||
return null;
|
||||
}
|
||||
return getNodesForState(state).find((node) => node.nodeId === normalizedNodeId) || null;
|
||||
}
|
||||
|
||||
function getDisplayOrderForNode(nodeId, state = {}) {
|
||||
const node = getNodeById(nodeId, state);
|
||||
return Number.isFinite(Number(node?.displayOrder)) ? Number(node.displayOrder) : null;
|
||||
}
|
||||
|
||||
function getNodeTitle(nodeId, state = {}) {
|
||||
return getNodeById(nodeId, state)?.title || nodeId || '';
|
||||
}
|
||||
|
||||
function normalizeNodeStatus(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized || 'pending';
|
||||
}
|
||||
|
||||
function buildDefaultNodeStatuses(state = {}) {
|
||||
return Object.fromEntries(getNodeIdsForState(state).map((nodeId) => [nodeId, 'pending']));
|
||||
}
|
||||
|
||||
function normalizeNodeStatuses(statuses = {}, state = {}) {
|
||||
const defaults = buildDefaultNodeStatuses(state);
|
||||
const next = { ...defaults };
|
||||
if (!statuses || typeof statuses !== 'object' || Array.isArray(statuses)) {
|
||||
return next;
|
||||
}
|
||||
for (const [rawNodeId, rawStatus] of Object.entries(statuses)) {
|
||||
const nodeId = normalizeNodeId(rawNodeId);
|
||||
if (!nodeId || !Object.prototype.hasOwnProperty.call(defaults, nodeId)) {
|
||||
continue;
|
||||
}
|
||||
next[nodeId] = normalizeNodeStatus(rawStatus);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function isNodeDoneStatus(status = '') {
|
||||
return ['completed', 'manual_completed', 'skipped'].includes(normalizeNodeStatus(status));
|
||||
}
|
||||
|
||||
function isNodeTerminalStatus(status = '') {
|
||||
return ['completed', 'manual_completed', 'skipped', 'failed', 'stopped'].includes(normalizeNodeStatus(status));
|
||||
}
|
||||
|
||||
function getRunningNodeIds(statuses = {}, state = {}) {
|
||||
const normalizedStatuses = normalizeNodeStatuses(statuses, state);
|
||||
return getNodeIdsForState(state).filter((nodeId) => normalizedStatuses[nodeId] === 'running');
|
||||
}
|
||||
|
||||
function getFirstUnfinishedNodeId(statuses = {}, state = {}) {
|
||||
const normalizedStatuses = normalizeNodeStatuses(statuses, state);
|
||||
return getNodeIdsForState(state).find((nodeId) => !isNodeDoneStatus(normalizedStatuses[nodeId])) || '';
|
||||
}
|
||||
|
||||
function hasSavedProgress(statuses = {}, state = {}) {
|
||||
const normalizedStatuses = normalizeNodeStatuses(statuses, state);
|
||||
return getNodeIdsForState(state).some((nodeId) => normalizeNodeStatus(normalizedStatuses[nodeId]) !== 'pending');
|
||||
}
|
||||
|
||||
function getNextNodeIds(nodeId, state = {}) {
|
||||
const node = getNodeById(nodeId, state);
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(node.next) ? node.next.map(normalizeNodeId).filter(Boolean) : [];
|
||||
}
|
||||
|
||||
return {
|
||||
buildDefaultNodeStatuses,
|
||||
getDisplayOrderForNode,
|
||||
getFirstUnfinishedNodeId,
|
||||
getNextNodeIds,
|
||||
getNodeById,
|
||||
getNodeIdsForState,
|
||||
getNodesForState,
|
||||
getNodeTitle,
|
||||
getRunningNodeIds,
|
||||
getWorkflow,
|
||||
getWorkflowForState,
|
||||
hasSavedProgress,
|
||||
isNodeDoneStatus,
|
||||
isNodeTerminalStatus,
|
||||
normalizeFlowId,
|
||||
normalizeNodeId,
|
||||
normalizeNodeStatuses,
|
||||
normalizeNodeStatus,
|
||||
resolveStateFlowId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createWorkflowEngine,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user