import GuJumpgate snapshot from GitHub

This commit is contained in:
2026-05-20 20:33:51 +08:00
commit f8903dcd14
335 changed files with 179790 additions and 0 deletions
+777
View File
@@ -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
+336
View File
@@ -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,
};
});
+762
View File
@@ -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,
};
});
+376
View File
@@ -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
+110
View File
@@ -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);
+463
View File
@@ -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,
};
});
+252
View File
@@ -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': 'HotmailAPI对接/本地助手)',
'luckmail-api': 'LuckMailAPI 购邮)',
'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,
};
});
+771
View File
@@ -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,
};
});
+80
View File
@@ -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
+228
View File
@@ -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,
};
});
+344
View File
@@ -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,
};
});
+110
View File
@@ -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
+146
View File
@@ -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,
};
});
+128
View File
@@ -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,
};
});
+443
View File
@@ -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,
};
});
+401
View File
@@ -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,
};
});
+352
View File
@@ -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
+959
View File
@@ -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 };
});
+303
View File
@@ -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 };
});
+141
View File
@@ -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
+37
View File
@@ -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
+97
View File
@@ -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,
};
});
+438
View File
@@ -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 };
});
+163
View File
@@ -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(`步骤 1browsingData 补扫 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 };
});
+310
View File
@@ -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,
};
});
+526
View File
@@ -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 };
});
+69
View File
@@ -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,
};
});
+49
View File
@@ -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,
};
});
+509
View File
@@ -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(`步骤 6browsingData 补扫 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('步骤 7Plus 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 };
});
+606
View File
@@ -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
+157
View File
@@ -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,
};
});