refactor flows into canonical runtime architecture
This commit is contained in:
@@ -106,8 +106,7 @@
|
||||
return {
|
||||
activeFlowId: state.activeFlowId,
|
||||
flowId: state.flowId || state.activeFlowId,
|
||||
panelMode: state.panelMode,
|
||||
kiroTargetId: state.kiroTargetId,
|
||||
targetId: state.targetId,
|
||||
vpsUrl: state.vpsUrl,
|
||||
vpsPassword: state.vpsPassword,
|
||||
customPassword: state.customPassword,
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
(function attachBackgroundKiroCredentialArtifact(root, factory) {
|
||||
root.MultiPageBackgroundKiroCredentialArtifact = factory(root);
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroCredentialArtifactModule(root) {
|
||||
const kiroStateApi = root?.MultiPageBackgroundKiroState || null;
|
||||
const FLOW_ID = 'kiro';
|
||||
const ADAPTER_ID = 'kiro-builder-id';
|
||||
const ARTIFACT_KIND = 'kiro-builder-id';
|
||||
const DEFAULT_REGION = kiroStateApi?.DEFAULT_REGION || 'us-east-1';
|
||||
const DEFAULT_TARGET_ID = kiroStateApi?.DEFAULT_TARGET_ID || 'kiro-rs';
|
||||
const BUILDER_ID_PROFILE_ARN = 'arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX';
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cleanString(value = '') {
|
||||
return String(value ?? '').trim();
|
||||
}
|
||||
|
||||
function readKiroRuntime(state = {}) {
|
||||
return kiroStateApi?.ensureRuntimeState
|
||||
? kiroStateApi.ensureRuntimeState(state)
|
||||
: (isPlainObject(state?.kiroRuntime) ? state.kiroRuntime : {});
|
||||
}
|
||||
|
||||
function resolveKiroTargetId(state = {}, runtimeState = readKiroRuntime(state)) {
|
||||
return cleanString(
|
||||
state?.settingsState?.flows?.kiro?.targetId
|
||||
|| state?.flows?.kiro?.targetId
|
||||
|| state?.kiroTargetId
|
||||
|| runtimeState?.upload?.targetId
|
||||
|| DEFAULT_TARGET_ID
|
||||
) || DEFAULT_TARGET_ID;
|
||||
}
|
||||
|
||||
function resolveEmail(state = {}, runtimeState = readKiroRuntime(state)) {
|
||||
return cleanString(
|
||||
runtimeState?.register?.email
|
||||
|| state?.email
|
||||
|| state?.registrationEmailState?.current
|
||||
|| state?.accountIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRegion(state = {}, runtimeState = readKiroRuntime(state), targetId = DEFAULT_TARGET_ID) {
|
||||
return cleanString(
|
||||
runtimeState?.desktopAuth?.region
|
||||
|| state?.settingsState?.flows?.kiro?.targets?.[targetId]?.region
|
||||
|| state?.flows?.kiro?.targets?.[targetId]?.region
|
||||
|| DEFAULT_REGION
|
||||
) || DEFAULT_REGION;
|
||||
}
|
||||
|
||||
function assertRequiredField(name, value, message) {
|
||||
if (!cleanString(value)) {
|
||||
const error = new Error(message);
|
||||
error.code = `missing_${name}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function redactSecret(value = '') {
|
||||
const normalized = cleanString(value);
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
if (normalized.length <= 10) {
|
||||
return `${normalized.slice(0, 2)}***`;
|
||||
}
|
||||
return `${normalized.slice(0, 6)}...${normalized.slice(-4)}`;
|
||||
}
|
||||
|
||||
function buildKiroBuilderIdArtifact(state = {}, options = {}) {
|
||||
const runtimeState = readKiroRuntime(state);
|
||||
const desktopAuth = runtimeState?.desktopAuth || {};
|
||||
const targetId = resolveKiroTargetId(state, runtimeState);
|
||||
const region = resolveRegion(state, runtimeState, targetId);
|
||||
const email = resolveEmail(state, runtimeState);
|
||||
const refreshToken = String(desktopAuth.refreshToken || '');
|
||||
const clientId = cleanString(desktopAuth.clientId);
|
||||
const clientSecret = String(desktopAuth.clientSecret || '');
|
||||
|
||||
assertRequiredField('refreshToken', refreshToken, '缺少桌面授权 refreshToken,无法提交 Kiro Builder ID 贡献。');
|
||||
assertRequiredField('clientId', clientId, '缺少桌面授权 clientId,无法提交 Kiro Builder ID 贡献。');
|
||||
assertRequiredField('clientSecret', clientSecret, '缺少桌面授权 clientSecret,无法提交 Kiro Builder ID 贡献。');
|
||||
assertRequiredField('email', email, '缺少注册邮箱,无法提交 Kiro Builder ID 贡献。');
|
||||
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
flow: FLOW_ID,
|
||||
adapter: ADAPTER_ID,
|
||||
artifact: ARTIFACT_KIND,
|
||||
account: {
|
||||
email,
|
||||
},
|
||||
credentials: {
|
||||
refreshToken,
|
||||
clientId,
|
||||
clientSecret,
|
||||
profileArn: cleanString(options.profileArn) || BUILDER_ID_PROFILE_ARN,
|
||||
authMethod: 'idc',
|
||||
region,
|
||||
authRegion: region,
|
||||
apiRegion: region,
|
||||
tokenSource: cleanString(desktopAuth.tokenSource) || 'desktop_authorization_code_pkce',
|
||||
},
|
||||
metadata: {
|
||||
targetId,
|
||||
authorizedAt: Math.max(0, Number(desktopAuth.authorizedAt) || 0),
|
||||
generatedAt: new Date().toISOString(),
|
||||
source: 'flowpilot-extension',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildSafeArtifactSummary(artifact = {}) {
|
||||
return {
|
||||
flow: cleanString(artifact.flow),
|
||||
adapter: cleanString(artifact.adapter),
|
||||
artifact: cleanString(artifact.artifact),
|
||||
email: cleanString(artifact.account?.email),
|
||||
clientId: cleanString(artifact.credentials?.clientId),
|
||||
refreshToken: redactSecret(artifact.credentials?.refreshToken),
|
||||
clientSecret: redactSecret(artifact.credentials?.clientSecret),
|
||||
region: cleanString(artifact.credentials?.region),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ADAPTER_ID,
|
||||
ARTIFACT_KIND,
|
||||
BUILDER_ID_PROFILE_ARN,
|
||||
FLOW_ID,
|
||||
buildKiroBuilderIdArtifact,
|
||||
buildSafeArtifactSummary,
|
||||
redactSecret,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,196 +0,0 @@
|
||||
(function attachBackgroundKiroDesktopClient(root, factory) {
|
||||
root.MultiPageBackgroundKiroDesktopClient = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroDesktopClientModule() {
|
||||
const DEFAULT_REGION = 'us-east-1';
|
||||
const DEFAULT_START_URL = 'https://view.awsapps.com/start';
|
||||
const DEFAULT_SCOPES = Object.freeze([
|
||||
'codewhisperer:completions',
|
||||
'codewhisperer:analysis',
|
||||
'codewhisperer:conversations',
|
||||
'codewhisperer:transformations',
|
||||
'codewhisperer:taskassist',
|
||||
]);
|
||||
|
||||
function cleanString(value = '') {
|
||||
return String(value ?? '').trim();
|
||||
}
|
||||
|
||||
function normalizeRegion(value = '', fallback = DEFAULT_REGION) {
|
||||
return cleanString(value) || fallback;
|
||||
}
|
||||
|
||||
function buildOidcBaseUrl(region = DEFAULT_REGION) {
|
||||
return `https://oidc.${normalizeRegion(region)}.amazonaws.com`;
|
||||
}
|
||||
|
||||
async function readResponse(response) {
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch (_error) {
|
||||
json = null;
|
||||
}
|
||||
return { text, json };
|
||||
}
|
||||
|
||||
async function sha256Bytes(input) {
|
||||
const encoder = new TextEncoder();
|
||||
return new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(String(input || ''))));
|
||||
}
|
||||
|
||||
function base64UrlEncode(bytes) {
|
||||
let binary = '';
|
||||
for (const byte of bytes) {
|
||||
binary += String.fromCharCode(byte);
|
||||
}
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
async function sha256Hex(input) {
|
||||
const bytes = await sha256Bytes(input);
|
||||
return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function randomUrlSafeString(length = 64) {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
||||
const size = Math.max(32, Math.floor(Number(length) || 64));
|
||||
const bytes = new Uint8Array(size);
|
||||
crypto.getRandomValues(bytes);
|
||||
let output = '';
|
||||
for (let index = 0; index < size; index += 1) {
|
||||
output += alphabet[bytes[index] % alphabet.length];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async function generatePkcePair() {
|
||||
const codeVerifier = randomUrlSafeString(64);
|
||||
const codeChallenge = base64UrlEncode(await sha256Bytes(codeVerifier));
|
||||
return {
|
||||
codeVerifier,
|
||||
codeChallenge,
|
||||
};
|
||||
}
|
||||
|
||||
function chooseRedirectPort() {
|
||||
return 49152 + Math.floor(Math.random() * 16384);
|
||||
}
|
||||
|
||||
function buildRedirectUri(port) {
|
||||
const normalizedPort = Math.max(1, Math.floor(Number(port) || 0));
|
||||
if (!normalizedPort) {
|
||||
throw new Error('缺少桌面授权回调端口。');
|
||||
}
|
||||
return `http://127.0.0.1:${normalizedPort}/oauth/callback`;
|
||||
}
|
||||
|
||||
async function registerDesktopClient(params = {}, fetchImpl) {
|
||||
if (typeof fetchImpl !== 'function') {
|
||||
throw new Error('registerDesktopClient requires fetch support.');
|
||||
}
|
||||
const region = normalizeRegion(params.region);
|
||||
const oidcBaseUrl = buildOidcBaseUrl(region);
|
||||
const response = await fetchImpl(`${oidcBaseUrl}/client/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientName: cleanString(params.clientName) || 'Kiro IDE',
|
||||
clientType: 'public',
|
||||
scopes: Array.isArray(params.scopes) && params.scopes.length ? params.scopes : DEFAULT_SCOPES,
|
||||
grantTypes: ['authorization_code', 'refresh_token'],
|
||||
redirectUris: ['http://127.0.0.1/oauth/callback'],
|
||||
issuerUrl: cleanString(params.issuerUrl) || DEFAULT_START_URL,
|
||||
}),
|
||||
});
|
||||
const body = await readResponse(response);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Kiro 桌面客户端注册失败:${cleanString(body.text || response.statusText) || response.status}`);
|
||||
}
|
||||
|
||||
const clientId = cleanString(body.json?.clientId);
|
||||
const clientSecret = String(body.json?.clientSecret || '');
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error('Kiro 桌面客户端注册响应缺少 clientId 或 clientSecret。');
|
||||
}
|
||||
|
||||
return {
|
||||
region,
|
||||
clientId,
|
||||
clientSecret,
|
||||
clientSecretExpiresAt: Number(body.json?.clientSecretExpiresAt || 0) || 0,
|
||||
clientIdHash: await sha256Hex(JSON.stringify({
|
||||
startUrl: cleanString(params.issuerUrl) || DEFAULT_START_URL,
|
||||
clientId,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function buildAuthorizeUrl(params = {}) {
|
||||
const region = normalizeRegion(params.region);
|
||||
const search = new URLSearchParams();
|
||||
search.set('response_type', 'code');
|
||||
search.set('client_id', cleanString(params.clientId));
|
||||
search.set('redirect_uri', cleanString(params.redirectUri));
|
||||
search.set('scopes', Array.isArray(params.scopes) && params.scopes.length
|
||||
? params.scopes.join(',')
|
||||
: DEFAULT_SCOPES.join(','));
|
||||
search.set('state', cleanString(params.state));
|
||||
search.set('code_challenge', cleanString(params.codeChallenge));
|
||||
search.set('code_challenge_method', 'S256');
|
||||
return `${buildOidcBaseUrl(region)}/authorize?${search.toString()}`;
|
||||
}
|
||||
|
||||
async function exchangeDesktopAuthorizationCode(params = {}, fetchImpl) {
|
||||
if (typeof fetchImpl !== 'function') {
|
||||
throw new Error('exchangeDesktopAuthorizationCode requires fetch support.');
|
||||
}
|
||||
const region = normalizeRegion(params.region);
|
||||
const response = await fetchImpl(`${buildOidcBaseUrl(region)}/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientId: cleanString(params.clientId),
|
||||
clientSecret: String(params.clientSecret || ''),
|
||||
grantType: 'authorization_code',
|
||||
code: cleanString(params.code),
|
||||
redirectUri: cleanString(params.redirectUri),
|
||||
codeVerifier: cleanString(params.codeVerifier),
|
||||
}),
|
||||
});
|
||||
const body = await readResponse(response);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Kiro 桌面授权换取 Token 失败:${cleanString(body.text || response.statusText) || response.status}`);
|
||||
}
|
||||
|
||||
const accessToken = String(body.json?.accessToken || '');
|
||||
const refreshToken = String(body.json?.refreshToken || '');
|
||||
if (!accessToken || !refreshToken) {
|
||||
throw new Error('Kiro 桌面授权换取 Token 响应缺少 accessToken 或 refreshToken。');
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn: Number(body.json?.expiresIn || 0) || 0,
|
||||
tokenType: cleanString(body.json?.tokenType),
|
||||
region,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_REGION,
|
||||
DEFAULT_SCOPES,
|
||||
DEFAULT_START_URL,
|
||||
buildAuthorizeUrl,
|
||||
buildRedirectUri,
|
||||
chooseRedirectPort,
|
||||
exchangeDesktopAuthorizationCode,
|
||||
generatePkcePair,
|
||||
registerDesktopClient,
|
||||
};
|
||||
});
|
||||
@@ -1,471 +0,0 @@
|
||||
(function attachBackgroundKiroPublisherKiroRs(root, factory) {
|
||||
root.MultiPageBackgroundKiroPublisherKiroRs = factory(root);
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroPublisherKiroRsModule(root) {
|
||||
const kiroStateApi = root?.MultiPageBackgroundKiroState || null;
|
||||
const DEFAULT_REGION = kiroStateApi?.DEFAULT_REGION || 'us-east-1';
|
||||
const DEFAULT_TARGET_ID = kiroStateApi?.DEFAULT_TARGET_ID || 'kiro-rs';
|
||||
const BUILDER_ID_PROFILE_ARN = 'arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX';
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => cloneValue(entry));
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function deepMerge(baseValue, patchValue) {
|
||||
if (Array.isArray(patchValue)) {
|
||||
return patchValue.map((entry) => cloneValue(entry));
|
||||
}
|
||||
if (!isPlainObject(patchValue)) {
|
||||
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
|
||||
}
|
||||
|
||||
const baseObject = isPlainObject(baseValue) ? baseValue : {};
|
||||
const next = {
|
||||
...cloneValue(baseObject),
|
||||
};
|
||||
Object.entries(patchValue).forEach(([key, value]) => {
|
||||
next[key] = deepMerge(baseObject[key], value);
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function cleanString(value = '') {
|
||||
return String(value ?? '').trim();
|
||||
}
|
||||
|
||||
function normalizeRegion(value = '', fallback = DEFAULT_REGION) {
|
||||
return cleanString(value) || fallback;
|
||||
}
|
||||
|
||||
function normalizeKiroRsApiKey(value = '') {
|
||||
return cleanString(value);
|
||||
}
|
||||
|
||||
function buildKiroRsAdminHeaders(apiKey = '', extraHeaders = {}) {
|
||||
const normalizedApiKey = normalizeKiroRsApiKey(apiKey);
|
||||
return {
|
||||
...extraHeaders,
|
||||
...(normalizedApiKey ? {
|
||||
'x-api-key': normalizedApiKey,
|
||||
Authorization: `Bearer ${normalizedApiKey}`,
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function readKiroRsResponseMessage(body = {}, fallback = '') {
|
||||
return cleanString(body?.json?.error?.message || body?.json?.message || body?.text || fallback);
|
||||
}
|
||||
|
||||
function normalizeKiroRsBaseUrl(value = '') {
|
||||
const normalized = cleanString(value).replace(/\/+$/, '');
|
||||
if (!normalized) {
|
||||
throw new Error('缺少 kiro.rs 管理后台地址。');
|
||||
}
|
||||
return normalized.endsWith('/admin')
|
||||
? normalized.slice(0, -'/admin'.length)
|
||||
: normalized;
|
||||
}
|
||||
|
||||
function normalizeKiroUploadMessage(value = '') {
|
||||
const rawValue = cleanString(value);
|
||||
if (!rawValue) {
|
||||
return '上传成功';
|
||||
}
|
||||
|
||||
const normalizedValue = rawValue.toLowerCase();
|
||||
if (normalizedValue === 'uploaded' || normalizedValue === 'credential uploaded.') {
|
||||
return '上传成功';
|
||||
}
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
function getErrorMessage(error) {
|
||||
return error instanceof Error ? error.message : String(error ?? '未知错误');
|
||||
}
|
||||
|
||||
async function readResponse(response) {
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch (_error) {
|
||||
json = null;
|
||||
}
|
||||
return { text, json };
|
||||
}
|
||||
|
||||
function readKiroRuntime(state = {}) {
|
||||
return kiroStateApi?.ensureRuntimeState
|
||||
? kiroStateApi.ensureRuntimeState(state)
|
||||
: (isPlainObject(state?.kiroRuntime) ? state.kiroRuntime : {});
|
||||
}
|
||||
|
||||
function mergeRuntimePatch(currentState = {}, patch = {}) {
|
||||
return {
|
||||
kiroRuntime: deepMerge(readKiroRuntime(currentState), patch),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveKiroTargetId(state = {}) {
|
||||
return cleanString(
|
||||
state?.settingsState?.flows?.kiro?.targetId
|
||||
|| state?.flows?.kiro?.targetId
|
||||
|| state?.kiroTargetId
|
||||
|| readKiroRuntime(state).upload?.targetId
|
||||
|| DEFAULT_TARGET_ID
|
||||
) || DEFAULT_TARGET_ID;
|
||||
}
|
||||
|
||||
function resolveKiroTargetConfig(state = {}, targetId = DEFAULT_TARGET_ID) {
|
||||
if (targetId !== DEFAULT_TARGET_ID) {
|
||||
throw new Error(`暂不支持 Kiro 发布目标:${targetId}`);
|
||||
}
|
||||
const nestedConfig = state?.settingsState?.flows?.kiro?.targets?.[targetId]
|
||||
|| state?.flows?.kiro?.targets?.[targetId]
|
||||
|| {};
|
||||
return {
|
||||
baseUrl: cleanString(nestedConfig.baseUrl || state?.kiroRsUrl),
|
||||
apiKey: normalizeKiroRsApiKey(nestedConfig.apiKey ?? state?.kiroRsKey ?? ''),
|
||||
};
|
||||
}
|
||||
|
||||
function buildProxyPayload(state = {}) {
|
||||
if (!state?.ipProxyEnabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const apiProxyUrl = cleanString(state?.ipProxyApiUrl);
|
||||
const host = cleanString(state?.ipProxyHost);
|
||||
const port = cleanString(state?.ipProxyPort);
|
||||
const protocol = cleanString(state?.ipProxyProtocol) || 'http';
|
||||
const proxyUrl = apiProxyUrl || (host && port ? `${protocol}://${host}:${port}` : '');
|
||||
const proxyUsername = cleanString(state?.ipProxyUsername);
|
||||
const proxyPassword = String(state?.ipProxyPassword || '');
|
||||
|
||||
return {
|
||||
...(proxyUrl ? { proxyUrl } : {}),
|
||||
...(proxyUsername ? { proxyUsername } : {}),
|
||||
...(proxyPassword ? { proxyPassword } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function sha256Hex(input = '') {
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(String(input ?? ''));
|
||||
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
|
||||
return Array.from(new Uint8Array(digest))
|
||||
.map((value) => value.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
async function buildMachineId(refreshToken = '') {
|
||||
const normalizedRefreshToken = cleanString(refreshToken);
|
||||
if (!normalizedRefreshToken) {
|
||||
throw new Error('缺少 refreshToken,无法生成 machineId。');
|
||||
}
|
||||
return sha256Hex(`KotlinNativeAPI/${normalizedRefreshToken}`);
|
||||
}
|
||||
|
||||
function buildUploadPayload(state = {}) {
|
||||
const runtimeState = readKiroRuntime(state);
|
||||
const targetId = resolveKiroTargetId(state);
|
||||
const desktopAuth = runtimeState.desktopAuth || {};
|
||||
const register = runtimeState.register || {};
|
||||
const refreshToken = String(desktopAuth.refreshToken || '');
|
||||
const clientId = cleanString(desktopAuth.clientId);
|
||||
const clientSecret = String(desktopAuth.clientSecret || '');
|
||||
const region = normalizeRegion(
|
||||
desktopAuth.region
|
||||
|| state?.settingsState?.flows?.kiro?.targets?.[targetId]?.region
|
||||
|| state?.flows?.kiro?.targets?.[targetId]?.region
|
||||
|| DEFAULT_REGION
|
||||
);
|
||||
const email = cleanString(register.email || state?.email);
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error('缺少桌面授权 refreshToken,请先完成步骤 8。');
|
||||
}
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error('缺少桌面授权 clientId 或 clientSecret,请先完成步骤 7-8。');
|
||||
}
|
||||
if (!email) {
|
||||
throw new Error('缺少注册邮箱,无法上传到 kiro.rs。');
|
||||
}
|
||||
|
||||
return {
|
||||
targetId,
|
||||
region,
|
||||
email,
|
||||
refreshToken,
|
||||
profileArn: BUILDER_ID_PROFILE_ARN,
|
||||
clientId,
|
||||
clientSecret,
|
||||
authMethod: 'idc',
|
||||
authRegion: region,
|
||||
apiRegion: region,
|
||||
...buildProxyPayload(state),
|
||||
};
|
||||
}
|
||||
|
||||
async function checkKiroRsConnection(baseUrl, apiKey, fetchImpl) {
|
||||
const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl);
|
||||
const normalizedApiKey = normalizeKiroRsApiKey(apiKey);
|
||||
const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, {
|
||||
method: 'GET',
|
||||
headers: buildKiroRsAdminHeaders(normalizedApiKey, {
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
});
|
||||
const body = await readResponse(response);
|
||||
const detail = readKiroRsResponseMessage(body, response.statusText);
|
||||
if (response.ok) {
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
message: `kiro.rs 连接正常(HTTP ${response.status})`,
|
||||
};
|
||||
}
|
||||
if (response.status === 405) {
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
message: 'kiro.rs 上传接口可访问。',
|
||||
};
|
||||
}
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
message: `kiro.rs API Key 被拒绝(HTTP ${response.status}${detail ? `:${detail}` : ''})`,
|
||||
};
|
||||
}
|
||||
if (response.status === 404) {
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
message: `未找到 kiro.rs 管理接口(HTTP 404${detail ? `:${detail}` : ''})`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
message: detail || `kiro.rs 连接失败(HTTP ${response.status})`,
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadBuilderIdCredential(baseUrl, apiKey, payload, fetchImpl) {
|
||||
const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl);
|
||||
const normalizedApiKey = normalizeKiroRsApiKey(apiKey);
|
||||
const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, {
|
||||
method: 'POST',
|
||||
headers: buildKiroRsAdminHeaders(normalizedApiKey, {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const body = await readResponse(response);
|
||||
if (!response.ok) {
|
||||
const message = readKiroRsResponseMessage(body, response.statusText) || `HTTP ${response.status}`;
|
||||
throw new Error(`kiro.rs 凭据上传失败:${message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
credentialId: Number(body.json?.credentialId || body.json?.credential_id || 0) || null,
|
||||
email: cleanString(body.json?.email),
|
||||
message: normalizeKiroUploadMessage(body.json?.message),
|
||||
raw: body.json,
|
||||
};
|
||||
}
|
||||
|
||||
function createKiroRsPublisher(deps = {}) {
|
||||
const {
|
||||
addLog = async () => {},
|
||||
completeNodeFromBackground,
|
||||
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||
getState = async () => ({}),
|
||||
maybeSubmitFlowContribution = async () => ({ ok: true, skipped: true, reason: 'not_configured' }),
|
||||
setState = async () => {},
|
||||
} = deps;
|
||||
|
||||
if (typeof completeNodeFromBackground !== 'function') {
|
||||
throw new Error('Kiro kiro.rs publisher requires completeNodeFromBackground.');
|
||||
}
|
||||
if (typeof fetchImpl !== 'function') {
|
||||
throw new Error('Kiro kiro.rs publisher requires fetch support.');
|
||||
}
|
||||
|
||||
async function log(message, level = 'info', nodeId = '') {
|
||||
await addLog(message, level, nodeId ? { nodeId } : {});
|
||||
}
|
||||
|
||||
async function applyRuntimeState(currentState = {}, patch = {}) {
|
||||
const nextPatch = mergeRuntimePatch(currentState, patch);
|
||||
await setState(nextPatch);
|
||||
return nextPatch;
|
||||
}
|
||||
|
||||
async function persistFailure(currentState = {}, message = '') {
|
||||
const nextPatch = mergeRuntimePatch(currentState, {
|
||||
session: {
|
||||
currentStage: 'upload',
|
||||
lastError: message,
|
||||
},
|
||||
upload: {
|
||||
status: 'error',
|
||||
error: message,
|
||||
},
|
||||
});
|
||||
await setState(nextPatch);
|
||||
}
|
||||
|
||||
function shouldUseContributionUpload(state = {}) {
|
||||
return Boolean(state?.accountContributionEnabled)
|
||||
&& cleanString(state?.activeFlowId || state?.flowId).toLowerCase() === 'kiro'
|
||||
&& cleanString(state?.contributionAdapterId).toLowerCase() === 'kiro-builder-id';
|
||||
}
|
||||
|
||||
async function executeKiroUploadCredential(state = {}) {
|
||||
const nodeId = String(state?.nodeId || 'kiro-upload-credential').trim();
|
||||
const currentState = await getState();
|
||||
try {
|
||||
if (shouldUseContributionUpload(currentState)) {
|
||||
await applyRuntimeState(currentState, {
|
||||
session: {
|
||||
currentStage: 'upload',
|
||||
lastError: '',
|
||||
lastWarning: '',
|
||||
},
|
||||
upload: {
|
||||
targetId: 'contribution',
|
||||
status: 'uploading',
|
||||
error: '',
|
||||
},
|
||||
});
|
||||
|
||||
await log('步骤 9:正在上传 Builder ID 到贡献池...', 'info', nodeId);
|
||||
const contributionResult = await maybeSubmitFlowContribution(currentState, {
|
||||
nodeId,
|
||||
trigger: 'kiro-step-9',
|
||||
});
|
||||
if (!contributionResult?.ok || contributionResult?.skipped) {
|
||||
throw new Error(contributionResult?.message || 'Kiro 贡献上传失败。');
|
||||
}
|
||||
|
||||
const uploadedAt = Date.now();
|
||||
const payload = await applyRuntimeState(currentState, {
|
||||
session: {
|
||||
currentStage: 'upload',
|
||||
lastError: '',
|
||||
},
|
||||
upload: {
|
||||
targetId: 'contribution',
|
||||
status: 'uploaded',
|
||||
error: '',
|
||||
credentialId: contributionResult.contributionId || '',
|
||||
lastMessage: contributionResult.message || '贡献上传成功',
|
||||
lastUploadedAt: uploadedAt,
|
||||
},
|
||||
});
|
||||
await log(`步骤 9:贡献上传完成,状态:${contributionResult.message || '贡献上传成功'}`, 'ok', nodeId);
|
||||
await completeNodeFromBackground(nodeId, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = resolveKiroTargetId(currentState);
|
||||
const targetConfig = resolveKiroTargetConfig(currentState, targetId);
|
||||
const baseUrl = normalizeKiroRsBaseUrl(targetConfig.baseUrl);
|
||||
const apiKey = String(targetConfig.apiKey || '');
|
||||
if (!apiKey) {
|
||||
throw new Error('缺少 kiro.rs API Key。');
|
||||
}
|
||||
|
||||
const uploadInput = buildUploadPayload(currentState);
|
||||
const machineId = await buildMachineId(uploadInput.refreshToken);
|
||||
|
||||
await applyRuntimeState(currentState, {
|
||||
session: {
|
||||
currentStage: 'upload',
|
||||
lastError: '',
|
||||
lastWarning: '',
|
||||
},
|
||||
upload: {
|
||||
targetId,
|
||||
status: 'uploading',
|
||||
error: '',
|
||||
},
|
||||
});
|
||||
|
||||
await log('步骤 9:正在上传 Builder ID 凭据到 kiro.rs...', 'info', nodeId);
|
||||
|
||||
const connection = await checkKiroRsConnection(baseUrl, apiKey, fetchImpl);
|
||||
if (!connection.ok) {
|
||||
throw new Error(connection.message);
|
||||
}
|
||||
|
||||
const uploadResult = await uploadBuilderIdCredential(baseUrl, apiKey, {
|
||||
refreshToken: uploadInput.refreshToken,
|
||||
profileArn: uploadInput.profileArn,
|
||||
authMethod: uploadInput.authMethod,
|
||||
clientId: uploadInput.clientId,
|
||||
clientSecret: uploadInput.clientSecret,
|
||||
region: uploadInput.region,
|
||||
authRegion: uploadInput.authRegion,
|
||||
apiRegion: uploadInput.apiRegion,
|
||||
machineId,
|
||||
email: uploadInput.email,
|
||||
...(uploadInput.proxyUrl ? { proxyUrl: uploadInput.proxyUrl } : {}),
|
||||
...(uploadInput.proxyUsername ? { proxyUsername: uploadInput.proxyUsername } : {}),
|
||||
...(uploadInput.proxyPassword ? { proxyPassword: uploadInput.proxyPassword } : {}),
|
||||
}, fetchImpl);
|
||||
|
||||
const uploadedAt = Date.now();
|
||||
const payload = await applyRuntimeState(currentState, {
|
||||
session: {
|
||||
currentStage: 'upload',
|
||||
lastError: '',
|
||||
},
|
||||
upload: {
|
||||
targetId,
|
||||
status: 'uploaded',
|
||||
error: '',
|
||||
credentialId: uploadResult.credentialId,
|
||||
lastMessage: uploadResult.message || '上传成功',
|
||||
lastUploadedAt: uploadedAt,
|
||||
},
|
||||
});
|
||||
await log(`步骤 9:kiro.rs 上传完成,状态:${uploadResult.message || '上传成功'}`, 'ok', nodeId);
|
||||
await completeNodeFromBackground(nodeId, payload);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
await persistFailure(currentState, message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
executeKiroUploadCredential,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
buildKiroRsPayload: buildUploadPayload,
|
||||
buildMachineId,
|
||||
checkKiroRsConnection,
|
||||
createKiroRsPublisher,
|
||||
normalizeKiroRsBaseUrl,
|
||||
normalizeKiroUploadMessage,
|
||||
uploadBuilderIdCredential,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,356 +0,0 @@
|
||||
(function attachBackgroundKiroState(root, factory) {
|
||||
root.MultiPageBackgroundKiroState = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroStateModule() {
|
||||
const DEFAULT_TARGET_ID = 'kiro-rs';
|
||||
const DEFAULT_REGION = 'us-east-1';
|
||||
const FLAT_FIELD_DEFINITIONS = Object.freeze([]);
|
||||
const FLAT_FIELD_KEYS = Object.freeze([]);
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => cloneValue(entry));
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function deepMerge(baseValue, patchValue) {
|
||||
if (Array.isArray(patchValue)) {
|
||||
return patchValue.map((entry) => cloneValue(entry));
|
||||
}
|
||||
if (!isPlainObject(patchValue)) {
|
||||
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
|
||||
}
|
||||
|
||||
const baseObject = isPlainObject(baseValue) ? baseValue : {};
|
||||
const next = {
|
||||
...cloneValue(baseObject),
|
||||
};
|
||||
Object.entries(patchValue).forEach(([key, value]) => {
|
||||
next[key] = deepMerge(baseObject[key], value);
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeString(value = '', fallback = '') {
|
||||
const normalized = String(value ?? '').trim();
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function normalizeInteger(value, fallback = 0) {
|
||||
const numeric = Math.floor(Number(value));
|
||||
return Number.isInteger(numeric) ? numeric : fallback;
|
||||
}
|
||||
|
||||
function normalizeNullableInteger(value, fallback = null) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return fallback;
|
||||
}
|
||||
const numeric = Math.floor(Number(value));
|
||||
return Number.isInteger(numeric) ? numeric : fallback;
|
||||
}
|
||||
|
||||
function normalizeBoolean(value, fallback = false) {
|
||||
if (value === true || value === false) {
|
||||
return value;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function buildDefaultRuntimeState() {
|
||||
return {
|
||||
session: {
|
||||
currentStage: '',
|
||||
registerTabId: null,
|
||||
desktopTabId: null,
|
||||
startedAt: 0,
|
||||
pageState: '',
|
||||
pageUrl: '',
|
||||
lastError: '',
|
||||
lastWarning: '',
|
||||
},
|
||||
register: {
|
||||
email: '',
|
||||
fullName: '',
|
||||
verificationRequestedAt: 0,
|
||||
loginUrl: '',
|
||||
status: '',
|
||||
completedAt: 0,
|
||||
},
|
||||
webAuth: {
|
||||
status: '',
|
||||
completedAt: 0,
|
||||
hasAccessToken: false,
|
||||
hasSessionToken: false,
|
||||
},
|
||||
desktopAuth: {
|
||||
region: DEFAULT_REGION,
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
clientIdHash: '',
|
||||
state: '',
|
||||
codeVerifier: '',
|
||||
codeChallenge: '',
|
||||
redirectUri: '',
|
||||
redirectPort: 0,
|
||||
authorizeUrl: '',
|
||||
authorizationCode: '',
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
status: '',
|
||||
authorizedAt: 0,
|
||||
otpRequestedAt: 0,
|
||||
tokenSource: 'desktop_authorization_code_pkce',
|
||||
},
|
||||
upload: {
|
||||
targetId: DEFAULT_TARGET_ID,
|
||||
status: '',
|
||||
error: '',
|
||||
credentialId: null,
|
||||
lastMessage: '',
|
||||
lastUploadedAt: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRuntimeState(runtimeState = {}) {
|
||||
const merged = deepMerge(buildDefaultRuntimeState(), runtimeState);
|
||||
return {
|
||||
session: {
|
||||
currentStage: normalizeString(merged.session?.currentStage),
|
||||
registerTabId: normalizeNullableInteger(merged.session?.registerTabId),
|
||||
desktopTabId: normalizeNullableInteger(merged.session?.desktopTabId),
|
||||
startedAt: Math.max(0, normalizeInteger(merged.session?.startedAt)),
|
||||
pageState: normalizeString(merged.session?.pageState),
|
||||
pageUrl: normalizeString(merged.session?.pageUrl),
|
||||
lastError: normalizeString(merged.session?.lastError),
|
||||
lastWarning: normalizeString(merged.session?.lastWarning),
|
||||
},
|
||||
register: {
|
||||
email: normalizeString(merged.register?.email),
|
||||
fullName: normalizeString(merged.register?.fullName),
|
||||
verificationRequestedAt: Math.max(0, normalizeInteger(merged.register?.verificationRequestedAt)),
|
||||
loginUrl: normalizeString(merged.register?.loginUrl),
|
||||
status: normalizeString(merged.register?.status),
|
||||
completedAt: Math.max(0, normalizeInteger(merged.register?.completedAt)),
|
||||
},
|
||||
webAuth: {
|
||||
status: normalizeString(merged.webAuth?.status),
|
||||
completedAt: Math.max(0, normalizeInteger(merged.webAuth?.completedAt)),
|
||||
hasAccessToken: normalizeBoolean(merged.webAuth?.hasAccessToken),
|
||||
hasSessionToken: normalizeBoolean(merged.webAuth?.hasSessionToken),
|
||||
},
|
||||
desktopAuth: {
|
||||
region: normalizeString(merged.desktopAuth?.region, DEFAULT_REGION),
|
||||
clientId: normalizeString(merged.desktopAuth?.clientId),
|
||||
clientSecret: normalizeString(merged.desktopAuth?.clientSecret),
|
||||
clientIdHash: normalizeString(merged.desktopAuth?.clientIdHash),
|
||||
state: normalizeString(merged.desktopAuth?.state),
|
||||
codeVerifier: normalizeString(merged.desktopAuth?.codeVerifier),
|
||||
codeChallenge: normalizeString(merged.desktopAuth?.codeChallenge),
|
||||
redirectUri: normalizeString(merged.desktopAuth?.redirectUri),
|
||||
redirectPort: Math.max(0, normalizeInteger(merged.desktopAuth?.redirectPort)),
|
||||
authorizeUrl: normalizeString(merged.desktopAuth?.authorizeUrl),
|
||||
authorizationCode: normalizeString(merged.desktopAuth?.authorizationCode),
|
||||
accessToken: normalizeString(merged.desktopAuth?.accessToken),
|
||||
refreshToken: normalizeString(merged.desktopAuth?.refreshToken),
|
||||
status: normalizeString(merged.desktopAuth?.status),
|
||||
authorizedAt: Math.max(0, normalizeInteger(merged.desktopAuth?.authorizedAt)),
|
||||
otpRequestedAt: Math.max(0, normalizeInteger(merged.desktopAuth?.otpRequestedAt)),
|
||||
tokenSource: normalizeString(
|
||||
merged.desktopAuth?.tokenSource,
|
||||
'desktop_authorization_code_pkce'
|
||||
),
|
||||
},
|
||||
upload: {
|
||||
targetId: normalizeString(merged.upload?.targetId, DEFAULT_TARGET_ID),
|
||||
status: normalizeString(merged.upload?.status),
|
||||
error: normalizeString(merged.upload?.error),
|
||||
credentialId: normalizeNullableInteger(merged.upload?.credentialId),
|
||||
lastMessage: normalizeString(merged.upload?.lastMessage),
|
||||
lastUploadedAt: Math.max(0, normalizeInteger(merged.upload?.lastUploadedAt)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ensureRuntimeState(state = {}) {
|
||||
return normalizeRuntimeState(isPlainObject(state?.kiroRuntime) ? state.kiroRuntime : {});
|
||||
}
|
||||
|
||||
function projectRuntimeFields() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function buildStateView(state = {}) {
|
||||
return {
|
||||
...state,
|
||||
kiroRuntime: ensureRuntimeState(state),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSessionStatePatch(currentState = {}, updates = {}) {
|
||||
if (!isPlainObject(updates?.kiroRuntime)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const nextRuntimeState = normalizeRuntimeState(
|
||||
deepMerge(ensureRuntimeState(currentState), updates.kiroRuntime)
|
||||
);
|
||||
return {
|
||||
kiroRuntime: nextRuntimeState,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRuntimeResetPatch(currentState = {}, patch = {}) {
|
||||
return {
|
||||
kiroRuntime: normalizeRuntimeState(
|
||||
deepMerge(ensureRuntimeState(currentState), patch)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function buildStartRegisterResetPatch(currentState = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
const nextRuntimeState = buildDefaultRuntimeState();
|
||||
nextRuntimeState.upload.targetId = currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID;
|
||||
return {
|
||||
kiroRuntime: nextRuntimeState,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRegisterOnlyResetPatch(currentState = {}, registerPatch = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
const nextRuntimeState = normalizeRuntimeState({
|
||||
...buildDefaultRuntimeState(),
|
||||
session: {
|
||||
...currentRuntimeState.session,
|
||||
currentStage: 'register',
|
||||
desktopTabId: null,
|
||||
pageState: '',
|
||||
pageUrl: '',
|
||||
lastError: '',
|
||||
lastWarning: '',
|
||||
},
|
||||
register: {
|
||||
...currentRuntimeState.register,
|
||||
completedAt: 0,
|
||||
status: '',
|
||||
...registerPatch,
|
||||
},
|
||||
upload: {
|
||||
...buildDefaultRuntimeState().upload,
|
||||
targetId: currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID,
|
||||
},
|
||||
});
|
||||
return {
|
||||
kiroRuntime: nextRuntimeState,
|
||||
};
|
||||
}
|
||||
|
||||
function buildDesktopResetPatch(currentState = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
return {
|
||||
kiroRuntime: normalizeRuntimeState({
|
||||
...currentRuntimeState,
|
||||
session: {
|
||||
...currentRuntimeState.session,
|
||||
currentStage: 'desktop-authorize',
|
||||
desktopTabId: null,
|
||||
pageState: '',
|
||||
pageUrl: '',
|
||||
lastError: '',
|
||||
lastWarning: '',
|
||||
},
|
||||
desktopAuth: buildDefaultRuntimeState().desktopAuth,
|
||||
upload: {
|
||||
...buildDefaultRuntimeState().upload,
|
||||
targetId: currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildUploadResetPatch(currentState = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
return {
|
||||
kiroRuntime: normalizeRuntimeState({
|
||||
...currentRuntimeState,
|
||||
upload: {
|
||||
...buildDefaultRuntimeState().upload,
|
||||
targetId: currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildDownstreamResetPatch(stepKey = '', currentState = {}) {
|
||||
switch (normalizeString(stepKey)) {
|
||||
case 'kiro-open-register-page':
|
||||
return buildStartRegisterResetPatch(currentState);
|
||||
case 'kiro-submit-email':
|
||||
return buildRegisterOnlyResetPatch(currentState, {
|
||||
email: '',
|
||||
fullName: '',
|
||||
verificationRequestedAt: 0,
|
||||
});
|
||||
case 'kiro-submit-name':
|
||||
return buildRegisterOnlyResetPatch(currentState, {
|
||||
fullName: '',
|
||||
verificationRequestedAt: 0,
|
||||
});
|
||||
case 'kiro-submit-verification-code':
|
||||
return buildRegisterOnlyResetPatch(currentState, {});
|
||||
case 'kiro-submit-password':
|
||||
return buildRegisterOnlyResetPatch(currentState, {});
|
||||
case 'kiro-complete-register-consent':
|
||||
return buildDesktopResetPatch(currentState);
|
||||
case 'kiro-start-desktop-authorize':
|
||||
return buildDesktopResetPatch(currentState);
|
||||
case 'kiro-complete-desktop-authorize':
|
||||
return buildUploadResetPatch(currentState);
|
||||
case 'kiro-upload-credential':
|
||||
return buildUploadResetPatch(currentState);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function applyNodeCompletionPayload(currentState = {}, payload = {}) {
|
||||
return buildSessionStatePatch(currentState, payload);
|
||||
}
|
||||
|
||||
function buildFreshKeepState(currentState = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
const nextRuntimeState = buildDefaultRuntimeState();
|
||||
nextRuntimeState.upload.targetId = currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID;
|
||||
return {
|
||||
kiroRuntime: nextRuntimeState,
|
||||
...(Object.prototype.hasOwnProperty.call(currentState, 'kiroTargetId')
|
||||
? { kiroTargetId: normalizeString(currentState.kiroTargetId, DEFAULT_TARGET_ID).toLowerCase() }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_REGION,
|
||||
DEFAULT_TARGET_ID,
|
||||
FLAT_FIELD_DEFINITIONS,
|
||||
FLAT_FIELD_KEYS,
|
||||
applyNodeCompletionPayload,
|
||||
buildDefaultRuntimeState,
|
||||
buildDownstreamResetPatch,
|
||||
buildFreshKeepState,
|
||||
buildSessionStatePatch,
|
||||
buildStateView,
|
||||
ensureRuntimeState,
|
||||
projectRuntimeFields,
|
||||
};
|
||||
});
|
||||
@@ -22,7 +22,6 @@
|
||||
'openai-auth': '认证页',
|
||||
'gmail-mail': 'Gmail 邮箱',
|
||||
'sidepanel': '侧边栏',
|
||||
'signup-page': '认证页',
|
||||
'vps-panel': 'CPA 面板',
|
||||
'sub2api-panel': 'SUB2API 后台',
|
||||
'codex2api-panel': 'Codex2API 后台',
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
}
|
||||
return capabilityRegistry.validateAutoRunStart({
|
||||
activeFlowId: options?.activeFlowId ?? validationState?.activeFlowId,
|
||||
panelMode: options?.panelMode ?? validationState?.panelMode,
|
||||
targetId: options?.targetId ?? validationState?.targetId,
|
||||
signupMethod: options?.signupMethod ?? validationState?.signupMethod,
|
||||
state: validationState,
|
||||
});
|
||||
@@ -113,7 +113,7 @@
|
||||
return capabilityRegistry.validateModeSwitch({
|
||||
activeFlowId: options?.activeFlowId ?? validationState?.activeFlowId,
|
||||
changedKeys: options?.changedKeys,
|
||||
panelMode: options?.panelMode ?? validationState?.panelMode,
|
||||
targetId: options?.targetId ?? validationState?.targetId,
|
||||
signupMethod: options?.signupMethod ?? validationState?.signupMethod,
|
||||
state: validationState,
|
||||
});
|
||||
@@ -217,10 +217,6 @@
|
||||
return String(targetId || fallbackSourceId).trim().toLowerCase() || fallbackSourceId;
|
||||
}
|
||||
|
||||
function mapAutoRunTargetIdToPanelMode(targetId = '', fallback = 'cpa') {
|
||||
return String(targetId || fallback || 'cpa').trim().toLowerCase() || 'cpa';
|
||||
}
|
||||
|
||||
function buildAutoRunFlowStateUpdates(payload = {}) {
|
||||
const hasActiveFlowId = Object.prototype.hasOwnProperty.call(payload, 'activeFlowId');
|
||||
const hasTargetId = Object.prototype.hasOwnProperty.call(payload, 'targetId');
|
||||
@@ -233,11 +229,11 @@
|
||||
flowId: activeFlowId,
|
||||
};
|
||||
if (hasTargetId) {
|
||||
if (activeFlowId === 'kiro') {
|
||||
updates.kiroTargetId = normalizeMessageTargetId('kiro', payload.targetId, 'kiro-rs');
|
||||
} else {
|
||||
updates.panelMode = mapAutoRunTargetIdToPanelMode(payload.targetId, 'cpa');
|
||||
}
|
||||
updates.targetId = normalizeMessageTargetId(
|
||||
activeFlowId,
|
||||
payload.targetId,
|
||||
activeFlowId === 'kiro' ? 'kiro-rs' : 'cpa'
|
||||
);
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
@@ -297,10 +293,10 @@
|
||||
}
|
||||
|
||||
const signupTabId = typeof getTabId === 'function'
|
||||
? await getTabId('signup-page')
|
||||
? await getTabId('openai-auth')
|
||||
: null;
|
||||
const signupTabAlive = signupTabId && typeof isTabAlive === 'function'
|
||||
? await isTabAlive('signup-page')
|
||||
? await isTabAlive('openai-auth')
|
||||
: Boolean(signupTabId);
|
||||
|
||||
if (!signupTabId || !signupTabAlive) {
|
||||
@@ -1309,7 +1305,7 @@
|
||||
const state = await getState();
|
||||
const autoRunStartValidation = validateAutoRunStart(state, {
|
||||
activeFlowId: autoRunFlowStateUpdates.activeFlowId ?? state?.activeFlowId,
|
||||
panelMode: autoRunFlowStateUpdates.panelMode ?? state?.panelMode,
|
||||
targetId: autoRunFlowStateUpdates.targetId ?? state?.targetId,
|
||||
state,
|
||||
});
|
||||
if (autoRunStartValidation?.ok === false) {
|
||||
@@ -1352,7 +1348,7 @@
|
||||
const state = await getState();
|
||||
const autoRunStartValidation = validateAutoRunStart(state, {
|
||||
activeFlowId: autoRunFlowStateUpdates.activeFlowId ?? state?.activeFlowId,
|
||||
panelMode: autoRunFlowStateUpdates.panelMode ?? state?.panelMode,
|
||||
targetId: autoRunFlowStateUpdates.targetId ?? state?.targetId,
|
||||
state,
|
||||
});
|
||||
if (autoRunStartValidation?.ok === false) {
|
||||
@@ -1451,7 +1447,7 @@
|
||||
Object.prototype.hasOwnProperty.call(updates, 'phoneVerificationEnabled')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'panelMode')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'targetId')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'activeFlowId')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'accountContributionEnabled')
|
||||
) {
|
||||
@@ -1600,10 +1596,8 @@
|
||||
);
|
||||
const selectedPlusAccountAccessStrategy = getPlusAccountAccessStrategyLabel(
|
||||
stateUpdates.plusAccountAccessStrategy ?? currentState?.plusAccountAccessStrategy ?? 'oauth',
|
||||
stateUpdates.panelMode
|
||||
?? currentState?.panelMode
|
||||
?? stateUpdates.openaiIntegrationTargetId
|
||||
?? currentState?.openaiIntegrationTargetId
|
||||
stateUpdates.targetId
|
||||
?? currentState?.targetId
|
||||
?? 'cpa'
|
||||
);
|
||||
await addLog(
|
||||
@@ -1620,10 +1614,8 @@
|
||||
} else if (plusAccountAccessStrategyChanged && nextPlusModeEnabled) {
|
||||
const selectedPlusAccountAccessStrategy = getPlusAccountAccessStrategyLabel(
|
||||
stateUpdates.plusAccountAccessStrategy ?? currentState?.plusAccountAccessStrategy ?? 'oauth',
|
||||
stateUpdates.panelMode
|
||||
?? currentState?.panelMode
|
||||
?? stateUpdates.openaiIntegrationTargetId
|
||||
?? currentState?.openaiIntegrationTargetId
|
||||
stateUpdates.targetId
|
||||
?? currentState?.targetId
|
||||
?? 'cpa'
|
||||
);
|
||||
await addLog(`Plus 账号接入策略已切换为 ${selectedPlusAccountAccessStrategy},已更新对应的 Plus 尾链。`, 'info');
|
||||
@@ -1661,7 +1653,7 @@
|
||||
);
|
||||
const targetId = normalizeMessageTargetId(
|
||||
activeFlowId,
|
||||
message.payload?.targetId || currentState?.kiroTargetId || 'kiro-rs',
|
||||
message.payload?.targetId || currentState?.targetId || 'kiro-rs',
|
||||
'kiro-rs'
|
||||
);
|
||||
const nestedTargetConfig = currentState?.settingsState?.flows?.kiro?.targets?.[targetId]
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
}
|
||||
|
||||
function getPanelMode(state = {}) {
|
||||
if (state.panelMode === 'sub2api') {
|
||||
if (state.targetId === 'sub2api') {
|
||||
return 'sub2api';
|
||||
}
|
||||
if (state.panelMode === 'codex2api') {
|
||||
if (state.targetId === 'codex2api') {
|
||||
return 'codex2api';
|
||||
}
|
||||
return 'cpa';
|
||||
@@ -128,7 +128,6 @@
|
||||
|
||||
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/');
|
||||
|
||||
@@ -4585,7 +4585,7 @@
|
||||
visibleStep,
|
||||
logStepKey: 'phone-verification',
|
||||
});
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'STEP8_GET_STATE',
|
||||
source: 'background',
|
||||
payload: { visibleStep },
|
||||
@@ -4676,7 +4676,7 @@
|
||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(30000, { step: visibleStep, actionLabel: '提交添加手机号' })
|
||||
: 30000;
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'SUBMIT_PHONE_NUMBER',
|
||||
source: 'background',
|
||||
payload: {
|
||||
@@ -4723,7 +4723,7 @@
|
||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(45000, { step: visibleStep, actionLabel: '提交手机验证码' })
|
||||
: 45000;
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
|
||||
source: 'background',
|
||||
payload: {
|
||||
@@ -4756,7 +4756,7 @@
|
||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: 'resend phone verification code' })
|
||||
: 65000;
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'RESEND_PHONE_VERIFICATION_CODE',
|
||||
source: 'background',
|
||||
payload: options || {},
|
||||
@@ -4780,7 +4780,7 @@
|
||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(45000, { step: visibleStep, actionLabel: '提交注册手机验证码' })
|
||||
: 45000;
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
|
||||
step: visibleStep,
|
||||
source: 'background',
|
||||
@@ -4809,7 +4809,7 @@
|
||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: '重新发送注册手机验证码' })
|
||||
: 65000;
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'RESEND_VERIFICATION_CODE',
|
||||
step: visibleStep,
|
||||
source: 'background',
|
||||
@@ -4834,7 +4834,7 @@
|
||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(30000, { step: visibleStep, actionLabel: 'return to add-phone page' })
|
||||
: 30000;
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'RETURN_TO_ADD_PHONE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
@@ -4863,7 +4863,7 @@
|
||||
}
|
||||
const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9;
|
||||
try {
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'CHECK_PHONE_RESEND_ERROR',
|
||||
source: 'background',
|
||||
payload: { visibleStep },
|
||||
@@ -6135,7 +6135,7 @@
|
||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(45000, { step: visibleStep, actionLabel: '提交登录手机验证码' })
|
||||
: 45000;
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
|
||||
step: visibleStep,
|
||||
source: 'background',
|
||||
@@ -6164,7 +6164,7 @@
|
||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: '重新发送登录手机验证码' })
|
||||
: 65000;
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'RESEND_VERIFICATION_CODE',
|
||||
step: visibleStep,
|
||||
source: 'background',
|
||||
|
||||
@@ -1,509 +0,0 @@
|
||||
(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',
|
||||
'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',
|
||||
'currentYydsMailInbox',
|
||||
]),
|
||||
identity: Object.freeze([
|
||||
'resolvedSignupMethod',
|
||||
'accountIdentifierType',
|
||||
'accountIdentifier',
|
||||
'registrationEmailState',
|
||||
'email',
|
||||
'password',
|
||||
'lastEmailTimestamp',
|
||||
'lastSignupCode',
|
||||
'lastLoginCode',
|
||||
'step8VerificationTargetEmail',
|
||||
]),
|
||||
});
|
||||
const FLOW_FIELD_GROUPS = Object.freeze({
|
||||
openai: OPENAI_FLOW_FIELD_GROUPS,
|
||||
});
|
||||
|
||||
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 buildScopedFlowState(baseFlowState = {}, state = {}, flowId = 'openai') {
|
||||
const fieldGroups = FLOW_FIELD_GROUPS[flowId] || {};
|
||||
const baseScopedState = cloneValue(normalizePlainObject(baseFlowState[flowId]));
|
||||
const scopedState = {
|
||||
...baseScopedState,
|
||||
};
|
||||
|
||||
for (const [groupKey, fields] of Object.entries(fieldGroups)) {
|
||||
scopedState[groupKey] = {
|
||||
...cloneValue(normalizePlainObject(baseScopedState[groupKey])),
|
||||
...pickDefinedFields(state, fields),
|
||||
};
|
||||
}
|
||||
|
||||
return scopedState;
|
||||
}
|
||||
|
||||
function buildFlowState(baseValue = {}, state = {}) {
|
||||
const baseFlowState = cloneValue(normalizePlainObject(baseValue));
|
||||
return {
|
||||
...baseFlowState,
|
||||
openai: buildScopedFlowState(baseFlowState, state, 'openai'),
|
||||
};
|
||||
}
|
||||
|
||||
function listFlowFieldNames() {
|
||||
const fields = [];
|
||||
for (const flowGroups of Object.values(FLOW_FIELD_GROUPS)) {
|
||||
for (const groupFields of Object.values(normalizePlainObject(flowGroups))) {
|
||||
for (const field of Array.isArray(groupFields) ? groupFields : []) {
|
||||
const normalizedField = String(field || '').trim();
|
||||
if (normalizedField) {
|
||||
fields.push(normalizedField);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(new Set(fields));
|
||||
}
|
||||
|
||||
const FLOW_RUNTIME_FIELDS = Object.freeze(listFlowFieldNames());
|
||||
const RUNTIME_TOP_LEVEL_FIELDS = Object.freeze([
|
||||
...RUNTIME_SHARED_FIELDS,
|
||||
...RUNTIME_PROXY_FIELDS,
|
||||
...FLOW_RUNTIME_FIELDS,
|
||||
]);
|
||||
const RUNTIME_TOP_LEVEL_FIELD_SET = new Set(RUNTIME_TOP_LEVEL_FIELDS);
|
||||
const RUNTIME_PATCH_IGNORED_KEYS = new Set([
|
||||
'runtimeState',
|
||||
'flowState',
|
||||
'sharedState',
|
||||
'serviceState',
|
||||
'flows',
|
||||
'shared',
|
||||
'services',
|
||||
'currentStep',
|
||||
'stepStatuses',
|
||||
'legacyStepCompat',
|
||||
'flowId',
|
||||
'runId',
|
||||
'activeFlowId',
|
||||
'activeRunId',
|
||||
'currentNodeId',
|
||||
'nodeStatuses',
|
||||
...RUNTIME_TOP_LEVEL_FIELDS,
|
||||
]);
|
||||
|
||||
function projectScopedFlowFields(flowState = {}) {
|
||||
const next = {};
|
||||
for (const [flowId, fieldGroups] of Object.entries(FLOW_FIELD_GROUPS)) {
|
||||
const scopedState = normalizePlainObject(normalizePlainObject(flowState)[flowId]);
|
||||
for (const [groupKey, fields] of Object.entries(fieldGroups)) {
|
||||
const group = normalizePlainObject(scopedState[groupKey]);
|
||||
Object.assign(next, pickDefinedFields(group, fields));
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function projectRuntimeViewFields(runtimeState = {}) {
|
||||
const normalizedRuntimeState = normalizePlainObject(runtimeState);
|
||||
return {
|
||||
...pickDefinedFields(
|
||||
normalizePlainObject(normalizedRuntimeState.sharedState),
|
||||
RUNTIME_SHARED_FIELDS
|
||||
),
|
||||
...pickDefinedFields(
|
||||
normalizePlainObject(normalizePlainObject(normalizedRuntimeState.serviceState).proxy),
|
||||
RUNTIME_PROXY_FIELDS
|
||||
),
|
||||
...projectScopedFlowFields(normalizedRuntimeState.flowState),
|
||||
};
|
||||
}
|
||||
|
||||
function buildRuntimeInputFromPatch(updates = {}) {
|
||||
const normalizedUpdates = normalizePlainObject(updates);
|
||||
const runtimeStatePatch = normalizePlainObject(normalizedUpdates.runtimeState);
|
||||
const next = {
|
||||
...pickDefinedFields(normalizedUpdates, [
|
||||
'flowId',
|
||||
'runId',
|
||||
'activeFlowId',
|
||||
'activeRunId',
|
||||
'currentNodeId',
|
||||
'nodeStatuses',
|
||||
]),
|
||||
...pickDefinedFields(runtimeStatePatch, [
|
||||
'flowId',
|
||||
'runId',
|
||||
'activeFlowId',
|
||||
'activeRunId',
|
||||
'currentNodeId',
|
||||
'nodeStatuses',
|
||||
]),
|
||||
...projectRuntimeViewFields(runtimeStatePatch),
|
||||
...pickDefinedFields(normalizedUpdates, RUNTIME_TOP_LEVEL_FIELDS),
|
||||
};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(normalizedUpdates, 'sharedState')) {
|
||||
Object.assign(
|
||||
next,
|
||||
pickDefinedFields(normalizePlainObject(normalizedUpdates.sharedState), RUNTIME_SHARED_FIELDS)
|
||||
);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(normalizedUpdates, 'serviceState')) {
|
||||
Object.assign(
|
||||
next,
|
||||
pickDefinedFields(
|
||||
normalizePlainObject(normalizePlainObject(normalizedUpdates.serviceState).proxy),
|
||||
RUNTIME_PROXY_FIELDS
|
||||
)
|
||||
);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(normalizedUpdates, 'flowState')) {
|
||||
Object.assign(next, projectScopedFlowFields(normalizedUpdates.flowState));
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(next, 'flowId') && Object.prototype.hasOwnProperty.call(next, 'activeFlowId')) {
|
||||
next.flowId = next.activeFlowId;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(next, 'runId') && Object.prototype.hasOwnProperty.call(next, 'activeRunId')) {
|
||||
next.runId = next.activeRunId;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
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, 'activeFlowId')
|
||||
? state.activeFlowId
|
||||
: Object.prototype.hasOwnProperty.call(state, 'flowId')
|
||||
? state.flowId
|
||||
: Object.prototype.hasOwnProperty.call(baseRuntimeState, 'activeFlowId')
|
||||
? baseRuntimeState.activeFlowId
|
||||
: baseRuntimeState.flowId
|
||||
);
|
||||
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: buildFlowState(baseRuntimeState.flowState, state),
|
||||
};
|
||||
}
|
||||
|
||||
function buildPersistentPatchPayload(updates = {}) {
|
||||
const next = {};
|
||||
for (const [key, value] of Object.entries(normalizePlainObject(updates))) {
|
||||
if (RUNTIME_PATCH_IGNORED_KEYS.has(key)) {
|
||||
continue;
|
||||
}
|
||||
next[key] = cloneValue(value);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildStateView(state = {}) {
|
||||
const runtimeState = ensureRuntimeState(state);
|
||||
return {
|
||||
...state,
|
||||
...projectRuntimeViewFields(runtimeState),
|
||||
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),
|
||||
flows: cloneValue(runtimeState.flowState),
|
||||
shared: cloneValue(runtimeState.sharedState),
|
||||
services: cloneValue(runtimeState.serviceState),
|
||||
runtimeState,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSessionStatePatch(currentState = {}, updates = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
const runtimeState = ensureRuntimeState({
|
||||
runtimeState: currentRuntimeState,
|
||||
...projectRuntimeViewFields(currentRuntimeState),
|
||||
...buildRuntimeInputFromPatch(updates),
|
||||
});
|
||||
|
||||
return {
|
||||
...buildPersistentPatchPayload(updates),
|
||||
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,
|
||||
FLOW_FIELD_GROUPS,
|
||||
OPENAI_FLOW_FIELD_GROUPS,
|
||||
RUNTIME_PROXY_FIELDS,
|
||||
RUNTIME_SHARED_FIELDS,
|
||||
buildDefaultRuntimeState: buildRuntimeStateDefault,
|
||||
buildSessionStatePatch,
|
||||
buildStateView,
|
||||
ensureRuntimeState,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createRuntimeStateHelpers,
|
||||
};
|
||||
});
|
||||
@@ -26,7 +26,7 @@
|
||||
setEmailState,
|
||||
setState,
|
||||
SIGNUP_ENTRY_URL,
|
||||
SIGNUP_PAGE_INJECT_FILES,
|
||||
OPENAI_AUTH_INJECT_FILES,
|
||||
waitForTabStableComplete = null,
|
||||
waitForTabUrlMatch,
|
||||
} = deps;
|
||||
@@ -57,16 +57,16 @@
|
||||
}
|
||||
|
||||
async function openSignupEntryTab(step = 1) {
|
||||
const tabId = await reuseOrCreateTab('signup-page', SIGNUP_ENTRY_URL, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
const tabId = await reuseOrCreateTab('openai-auth', SIGNUP_ENTRY_URL, {
|
||||
inject: OPENAI_AUTH_INJECT_FILES,
|
||||
injectSource: 'openai-auth',
|
||||
});
|
||||
|
||||
await waitForSignupEntryTabToSettle(tabId, step);
|
||||
|
||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
await ensureContentScriptReadyOnTab('openai-auth', tabId, {
|
||||
inject: OPENAI_AUTH_INJECT_FILES,
|
||||
injectSource: 'openai-auth',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `步骤 ${step}:ChatGPT 官网仍在加载,正在重试连接内容脚本...`,
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
async function ensureSignupEntryPageReady(step = 1) {
|
||||
const tabId = await openSignupEntryTab(step);
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'ENSURE_SIGNUP_ENTRY_READY',
|
||||
step,
|
||||
source: 'background',
|
||||
@@ -186,9 +186,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
await ensureContentScriptReadyOnTab('openai-auth', tabId, {
|
||||
inject: OPENAI_AUTH_INJECT_FILES,
|
||||
injectSource: 'openai-auth',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: landingState === 'password_page'
|
||||
@@ -204,7 +204,7 @@
|
||||
};
|
||||
}
|
||||
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
const result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'ENSURE_SIGNUP_PASSWORD_PAGE_READY',
|
||||
step,
|
||||
source: 'background',
|
||||
@@ -244,9 +244,9 @@
|
||||
throw new Error(`认证页面标签页已关闭,无法完成步骤 ${step} 的提交后确认。`);
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
await ensureContentScriptReadyOnTab('openai-auth', tabId, {
|
||||
inject: OPENAI_AUTH_INJECT_FILES,
|
||||
injectSource: 'openai-auth',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `步骤 ${step}:认证页仍在切换,正在等待页面恢复后继续确认提交流程...`,
|
||||
@@ -254,7 +254,7 @@
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await sendToContentScriptResilient('signup-page', {
|
||||
result = await sendToContentScriptResilient('openai-auth', {
|
||||
type: 'PREPARE_SIGNUP_VERIFICATION',
|
||||
step,
|
||||
source: 'background',
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
(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 };
|
||||
});
|
||||
@@ -1,276 +0,0 @@
|
||||
(function attachBackgroundCpaSessionImport(root, factory) {
|
||||
root.MultiPageBackgroundCpaSessionImport = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundCpaSessionImportModule() {
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'];
|
||||
|
||||
function createCpaSessionImportExecutor(deps = {}) {
|
||||
const {
|
||||
addLog: rawAddLog = async () => {},
|
||||
chrome,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
registerTab,
|
||||
sendTabMessageUntilStopped,
|
||||
sleepWithStop = async () => {},
|
||||
throwIfStopped = () => {},
|
||||
waitForTabCompleteUntilStopped = async () => {},
|
||||
} = deps;
|
||||
|
||||
let cpaApi = null;
|
||||
|
||||
function addStepLog(step, message, level = 'info') {
|
||||
return rawAddLog(message, level, {
|
||||
step,
|
||||
stepKey: 'cpa-session-import',
|
||||
});
|
||||
}
|
||||
|
||||
function getCpaApi() {
|
||||
if (cpaApi) {
|
||||
return cpaApi;
|
||||
}
|
||||
const factory = deps.createCpaApi
|
||||
|| self.MultiPageBackgroundCpaApi?.createCpaApi;
|
||||
if (typeof factory !== 'function') {
|
||||
throw new Error('CPA 接口模块未加载,无法导入当前 ChatGPT 会话。');
|
||||
}
|
||||
cpaApi = factory({
|
||||
addLog: rawAddLog,
|
||||
});
|
||||
return cpaApi;
|
||||
}
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function resolveVisibleStep(state = {}) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep > 0 ? visibleStep : 10;
|
||||
}
|
||||
|
||||
function isSupportedChatGptSessionUrl(url = '') {
|
||||
try {
|
||||
const parsed = new URL(String(url || ''));
|
||||
if (!/^https?:$/i.test(parsed.protocol)) {
|
||||
return false;
|
||||
}
|
||||
const hostname = String(parsed.hostname || '').trim().toLowerCase();
|
||||
return /(^|\.)chatgpt\.com$/.test(hostname)
|
||||
|| hostname === 'chat.openai.com'
|
||||
|| /(^|\.)openai\.com$/.test(hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionTabHostPriority(url = '') {
|
||||
try {
|
||||
const hostname = String(new URL(String(url || '')).hostname || '').trim().toLowerCase();
|
||||
if (/(^|\.)chatgpt\.com$/.test(hostname)) {
|
||||
return 0;
|
||||
}
|
||||
if (hostname === 'chat.openai.com') {
|
||||
return 1;
|
||||
}
|
||||
if (/(^|\.)openai\.com$/.test(hostname)) {
|
||||
return 2;
|
||||
}
|
||||
} catch {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
function getSessionTabActivityPriority(tab = {}) {
|
||||
if (tab?.active && tab?.currentWindow) {
|
||||
return 0;
|
||||
}
|
||||
if (tab?.active) {
|
||||
return 1;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
function pickPreferredSessionTab(tabs = []) {
|
||||
const candidates = (Array.isArray(tabs) ? tabs : [])
|
||||
.filter((tab) => Number.isInteger(tab?.id) && isSupportedChatGptSessionUrl(tab.url));
|
||||
if (!candidates.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidates.reduce((best, candidate) => {
|
||||
if (!best) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
const candidateHostPriority = getSessionTabHostPriority(candidate.url);
|
||||
const bestHostPriority = getSessionTabHostPriority(best.url);
|
||||
if (candidateHostPriority !== bestHostPriority) {
|
||||
return candidateHostPriority < bestHostPriority ? candidate : best;
|
||||
}
|
||||
|
||||
const candidateActivityPriority = getSessionTabActivityPriority(candidate);
|
||||
const bestActivityPriority = getSessionTabActivityPriority(best);
|
||||
if (candidateActivityPriority !== bestActivityPriority) {
|
||||
return candidateActivityPriority < bestActivityPriority ? candidate : best;
|
||||
}
|
||||
|
||||
const candidateLastAccessed = Number(candidate?.lastAccessed) || 0;
|
||||
const bestLastAccessed = Number(best?.lastAccessed) || 0;
|
||||
if (candidateLastAccessed !== bestLastAccessed) {
|
||||
return candidateLastAccessed > bestLastAccessed ? candidate : best;
|
||||
}
|
||||
|
||||
return Number(candidate.id) < Number(best.id) ? candidate : best;
|
||||
}, null);
|
||||
}
|
||||
|
||||
async function readSupportedSessionTab(tabId) {
|
||||
const numericTabId = Number(tabId) || 0;
|
||||
if (!numericTabId || !chrome?.tabs?.get) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tab = await chrome.tabs.get(numericTabId).catch(() => null);
|
||||
return tab?.id && isSupportedChatGptSessionUrl(tab.url)
|
||||
? tab
|
||||
: null;
|
||||
}
|
||||
|
||||
async function findFallbackSessionTab() {
|
||||
if (!chrome?.tabs?.query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true }).catch(() => []);
|
||||
const activeMatch = pickPreferredSessionTab(activeTabs);
|
||||
const allTabs = await chrome.tabs.query({}).catch(() => []);
|
||||
const globalMatch = pickPreferredSessionTab(allTabs);
|
||||
return pickPreferredSessionTab([activeMatch, globalMatch]);
|
||||
}
|
||||
|
||||
async function resolveSessionTabId(state = {}) {
|
||||
const registeredTabId = typeof getTabId === 'function'
|
||||
? await getTabId(PLUS_CHECKOUT_SOURCE)
|
||||
: null;
|
||||
if (registeredTabId && typeof isTabAlive === 'function' && await isTabAlive(PLUS_CHECKOUT_SOURCE)) {
|
||||
const registeredTab = await readSupportedSessionTab(registeredTabId);
|
||||
if (registeredTab?.id) {
|
||||
return registeredTab.id;
|
||||
}
|
||||
}
|
||||
|
||||
const storedTabId = Number(state?.plusCheckoutTabId) || 0;
|
||||
const storedTab = await readSupportedSessionTab(storedTabId);
|
||||
if (storedTab?.id) {
|
||||
if (typeof registerTab === 'function') {
|
||||
await registerTab(PLUS_CHECKOUT_SOURCE, storedTab.id);
|
||||
}
|
||||
return storedTab.id;
|
||||
}
|
||||
|
||||
const fallbackTab = await findFallbackSessionTab();
|
||||
if (fallbackTab?.id) {
|
||||
if (typeof registerTab === 'function') {
|
||||
await registerTab(PLUS_CHECKOUT_SOURCE, fallbackTab.id);
|
||||
}
|
||||
return fallbackTab.id;
|
||||
}
|
||||
|
||||
throw new Error('未找到可读取 ChatGPT 会话的标签页,请先打开一个已登录的 ChatGPT / OpenAI 页面,或完成当前 Plus 支付链路。');
|
||||
}
|
||||
|
||||
async function getResolvedSessionTab(tabId, visibleStep) {
|
||||
const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
|
||||
if (!tab?.id) {
|
||||
throw new Error(`步骤 ${visibleStep}:ChatGPT 会话标签页不存在或已关闭,无法继续导入 CPA。`);
|
||||
}
|
||||
if (!isSupportedChatGptSessionUrl(tab.url)) {
|
||||
throw new Error(`步骤 ${visibleStep}:当前标签页不在 ChatGPT / OpenAI 页面,无法读取当前登录会话。`);
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
|
||||
async function readCurrentChatGptSession(tabId, visibleStep) {
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
await sleepWithStop(1000);
|
||||
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
|
||||
inject: PLUS_CHECKOUT_INJECT_FILES,
|
||||
injectSource: PLUS_CHECKOUT_SOURCE,
|
||||
logMessage: `步骤 ${visibleStep}:正在等待 ChatGPT 会话页完成加载,再继续读取当前登录会话...`,
|
||||
});
|
||||
|
||||
const sessionResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||
type: 'PLUS_CHECKOUT_GET_STATE',
|
||||
source: 'background',
|
||||
payload: {
|
||||
includeSession: true,
|
||||
includeAccessToken: true,
|
||||
},
|
||||
});
|
||||
if (sessionResult?.error) {
|
||||
throw new Error(sessionResult.error);
|
||||
}
|
||||
|
||||
const session = sessionResult?.session && typeof sessionResult.session === 'object' && !Array.isArray(sessionResult.session)
|
||||
? sessionResult.session
|
||||
: null;
|
||||
const accessToken = normalizeString(
|
||||
sessionResult?.accessToken
|
||||
|| session?.accessToken
|
||||
);
|
||||
if (!session && !accessToken) {
|
||||
throw new Error(`步骤 ${visibleStep}:未读取到有效的 ChatGPT 会话或 accessToken,请确认当前标签页仍处于已登录状态。`);
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeCpaSessionImport(state = {}) {
|
||||
throwIfStopped();
|
||||
const visibleStep = resolveVisibleStep(state);
|
||||
const api = getCpaApi();
|
||||
|
||||
await addStepLog(visibleStep, '正在定位当前 ChatGPT 会话页并准备导入 CPA...', 'info');
|
||||
const tabId = await resolveSessionTabId(state);
|
||||
const tab = await getResolvedSessionTab(tabId, visibleStep);
|
||||
if (chrome?.tabs?.update) {
|
||||
await chrome.tabs.update(tab.id, { active: true }).catch(() => {});
|
||||
}
|
||||
|
||||
await addStepLog(visibleStep, '正在读取当前 ChatGPT 登录会话...', 'info');
|
||||
const sessionState = await readCurrentChatGptSession(tab.id, visibleStep);
|
||||
throwIfStopped();
|
||||
|
||||
const result = await api.importCurrentChatGptSession({
|
||||
...state,
|
||||
session: sessionState.session,
|
||||
accessToken: sessionState.accessToken,
|
||||
}, {
|
||||
visibleStep,
|
||||
logLabel: `步骤 ${visibleStep}`,
|
||||
logOptions: { step: visibleStep, stepKey: 'cpa-session-import' },
|
||||
timeoutMs: 120000,
|
||||
importTimeoutMs: 120000,
|
||||
});
|
||||
|
||||
await completeNodeFromBackground(state?.nodeId || 'cpa-session-import', result);
|
||||
}
|
||||
|
||||
return {
|
||||
executeCpaSessionImport,
|
||||
isSupportedChatGptSessionUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createCpaSessionImportExecutor,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,962 +0,0 @@
|
||||
(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,
|
||||
nodeId: state?.nodeId || activeFetchLoginCodeStepKey || 'fetch-login-code',
|
||||
},
|
||||
},
|
||||
{
|
||||
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 };
|
||||
});
|
||||
@@ -1,303 +0,0 @@
|
||||
(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 };
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
(function attachBackgroundStep3(root, factory) {
|
||||
root.MultiPageBackgroundStep3 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep3Module() {
|
||||
function createStep3Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
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 = state.accounts || [];
|
||||
accounts.push({
|
||||
email: identity.email,
|
||||
phoneNumber: identity.phoneNumber,
|
||||
accountIdentifierType: identity.accountIdentifierType,
|
||||
accountIdentifier: identity.accountIdentifier,
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { executeStep3 };
|
||||
}
|
||||
|
||||
return { createStep3Executor };
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,37 +0,0 @@
|
||||
(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
@@ -1,156 +0,0 @@
|
||||
(function attachBackgroundGoPayManualConfirm(root, factory) {
|
||||
root.MultiPageBackgroundGoPayManualConfirm = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGoPayManualConfirmModule() {
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const GOPAY_CONFIRM_NODE_ID = 'gopay-subscription-confirm';
|
||||
const SUB2API_SESSION_IMPORT_NODE_ID = 'sub2api-session-import';
|
||||
const CPA_SESSION_IMPORT_NODE_ID = 'cpa-session-import';
|
||||
const DEFAULT_CONFIRM_TITLE = 'GoPay 订阅确认';
|
||||
const OAUTH_CONTINUATION_LABEL = 'OAuth 登录';
|
||||
const SUB2API_SESSION_CONTINUATION_LABEL = '导入当前 ChatGPT 会话到 SUB2API';
|
||||
|
||||
const CPA_SESSION_CONTINUATION_LABEL = '导入当前 ChatGPT 会话到 CPA';
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function getContinuationActionLabelForNodeId(nodeId = '') {
|
||||
const normalizedNodeId = normalizeString(nodeId);
|
||||
if (normalizedNodeId === SUB2API_SESSION_IMPORT_NODE_ID) {
|
||||
return SUB2API_SESSION_CONTINUATION_LABEL;
|
||||
}
|
||||
if (normalizedNodeId === CPA_SESSION_IMPORT_NODE_ID) {
|
||||
return CPA_SESSION_CONTINUATION_LABEL;
|
||||
}
|
||||
return OAUTH_CONTINUATION_LABEL;
|
||||
}
|
||||
|
||||
function getContinuationActionLabel(state = {}, options = {}) {
|
||||
const { getNodeIdsForState = null } = options;
|
||||
if (typeof getNodeIdsForState === 'function') {
|
||||
const nodeIds = getNodeIdsForState(state)
|
||||
.map((nodeId) => normalizeString(nodeId))
|
||||
.filter(Boolean);
|
||||
const currentNodeId = normalizeString(state?.nodeId || GOPAY_CONFIRM_NODE_ID) || GOPAY_CONFIRM_NODE_ID;
|
||||
const currentNodeIndex = nodeIds.indexOf(currentNodeId);
|
||||
const nextNodeId = currentNodeIndex >= 0
|
||||
? normalizeString(nodeIds[currentNodeIndex + 1])
|
||||
: '';
|
||||
|
||||
if (nextNodeId) {
|
||||
return getContinuationActionLabelForNodeId(nextNodeId);
|
||||
}
|
||||
if (currentNodeIndex < 0) {
|
||||
if (nodeIds.includes(SUB2API_SESSION_IMPORT_NODE_ID)) {
|
||||
return SUB2API_SESSION_CONTINUATION_LABEL;
|
||||
}
|
||||
if (nodeIds.includes(CPA_SESSION_IMPORT_NODE_ID)) {
|
||||
return CPA_SESSION_CONTINUATION_LABEL;
|
||||
}
|
||||
}
|
||||
}
|
||||
return OAUTH_CONTINUATION_LABEL;
|
||||
}
|
||||
|
||||
function buildDefaultConfirmMessage(state = {}, options = {}) {
|
||||
const continuationActionLabel = getContinuationActionLabel(state, options);
|
||||
return `GoPay 订阅页已打开。请先手动完成订阅,完成后确认继续${continuationActionLabel}。`;
|
||||
}
|
||||
|
||||
function createGoPayManualConfirmExecutor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
broadcastDataUpdate,
|
||||
chrome,
|
||||
createAutomationTab = null,
|
||||
getTabId,
|
||||
getNodeIdsForState = null,
|
||||
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 = normalizeString(state?.plusCheckoutUrl);
|
||||
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 visibleStep = Number(state?.visibleStep) || 7;
|
||||
const tabId = await resolveCheckoutTabId(state);
|
||||
if (chrome?.tabs?.update && tabId) {
|
||||
await chrome.tabs.update(tabId, { active: true }).catch(() => {});
|
||||
}
|
||||
|
||||
const continuationActionLabel = getContinuationActionLabel(state, { getNodeIdsForState });
|
||||
const payload = {
|
||||
plusCheckoutTabId: tabId,
|
||||
plusManualConfirmationPending: true,
|
||||
plusManualConfirmationRequestId: buildRequestId(),
|
||||
plusManualConfirmationStep: visibleStep,
|
||||
plusManualConfirmationMethod: 'gopay',
|
||||
plusManualConfirmationTitle: DEFAULT_CONFIRM_TITLE,
|
||||
plusManualConfirmationMessage: buildDefaultConfirmMessage(state, { getNodeIdsForState }),
|
||||
};
|
||||
|
||||
await setState(payload);
|
||||
if (typeof broadcastDataUpdate === 'function') {
|
||||
broadcastDataUpdate(payload);
|
||||
}
|
||||
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:正在等待手动完成 GoPay 订阅,确认后继续${continuationActionLabel}。`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
executeGoPayManualConfirm,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createGoPayManualConfirmExecutor,
|
||||
};
|
||||
});
|
||||
@@ -1,403 +0,0 @@
|
||||
(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?.accountContributionEnabled);
|
||||
}
|
||||
|
||||
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 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 visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
const completionStep = visibleStep > 0 ? visibleStep : 7;
|
||||
const resolvedIdentifierType = resolveStep7LoginIdentifierType(state);
|
||||
const phoneNumber = resolvedIdentifierType === 'phone'
|
||||
? String(
|
||||
state?.signupPhoneNumber
|
||||
|| (normalizeStep7IdentifierType(state?.accountIdentifierType) === 'phone' ? state?.accountIdentifier : '')
|
||||
|| state?.signupPhoneCompletedActivation?.phoneNumber
|
||||
|| state?.signupPhoneActivation?.phoneNumber
|
||||
|| ''
|
||||
).trim()
|
||||
: '';
|
||||
const email = resolvedIdentifierType === 'email'
|
||||
? String(
|
||||
state?.email
|
||||
|| (normalizeStep7IdentifierType(state?.accountIdentifierType) === 'email' ? state?.accountIdentifier : '')
|
||||
|| ''
|
||||
).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 ? state : 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 : '')
|
||||
|| 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: state?.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 };
|
||||
});
|
||||
@@ -1,159 +0,0 @@
|
||||
(function attachBackgroundStep1(root, factory) {
|
||||
root.MultiPageBackgroundStep1 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep1Module() {
|
||||
const STEP1_COOKIE_CLEAR_DOMAINS = [
|
||||
'chatgpt.com',
|
||||
'chat.openai.com',
|
||||
'openai.com',
|
||||
'auth.openai.com',
|
||||
'auth0.openai.com',
|
||||
'accounts.openai.com',
|
||||
];
|
||||
|
||||
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 buildStep1CookieKey(cookie, fallbackStoreId = '') {
|
||||
return [
|
||||
cookie?.storeId || fallbackStoreId || '',
|
||||
cookie?.domain || '',
|
||||
cookie?.path || '',
|
||||
cookie?.name || '',
|
||||
cookie?.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||||
].join('|');
|
||||
}
|
||||
|
||||
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();
|
||||
const queryDomains = Array.from(
|
||||
new Set(
|
||||
STEP1_COOKIE_CLEAR_DOMAINS
|
||||
.map((domain) => normalizeCookieDomainForStep1(domain))
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
|
||||
for (const store of stores) {
|
||||
const storeId = store?.id;
|
||||
for (const domain of queryDomains) {
|
||||
let batch = [];
|
||||
try {
|
||||
batch = await chromeApi.cookies.getAll(
|
||||
storeId
|
||||
? { storeId, domain }
|
||||
: { domain }
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[MultiPage:step1] query cookies failed', {
|
||||
storeId: storeId || '',
|
||||
domain,
|
||||
message: getStep1ErrorMessage(error),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const cookie of batch || []) {
|
||||
if (!shouldClearStep1Cookie(cookie)) continue;
|
||||
const key = buildStep1CookieKey(cookie, storeId);
|
||||
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;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies(耗时 ${elapsedMs}ms)。`, 'ok');
|
||||
}
|
||||
|
||||
async function executeStep1() {
|
||||
await clearOpenAiCookiesBeforeStep1();
|
||||
await addLog('步骤 1:正在打开 ChatGPT 官网...');
|
||||
await openSignupEntryTab(1);
|
||||
await completeNodeFromBackground('open-chatgpt', {});
|
||||
}
|
||||
|
||||
return { executeStep1 };
|
||||
}
|
||||
|
||||
return { createStep1Executor };
|
||||
});
|
||||
@@ -1,310 +0,0 @@
|
||||
(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,
|
||||
};
|
||||
});
|
||||
@@ -1,434 +0,0 @@
|
||||
(function attachBackgroundStep10(root, factory) {
|
||||
root.MultiPageBackgroundStep10 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep10Module() {
|
||||
function createStep10Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
closeConflictingTabsForSource,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTab,
|
||||
getPanelMode,
|
||||
getTabId,
|
||||
getStepIdByKeyForState,
|
||||
isLocalhostOAuthCallbackUrl,
|
||||
isTabAlive,
|
||||
normalizeCodex2ApiUrl,
|
||||
normalizeSub2ApiUrl,
|
||||
rememberSourceLastUrl,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
shouldBypassStep9ForLocalCpa,
|
||||
DEFAULT_SUB2API_GROUP_NAME = 'codex',
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
} = deps;
|
||||
|
||||
let sub2ApiApi = 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 resolvePlatformVerifyStep(state = {}) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep >= 10 ? visibleStep : 10;
|
||||
}
|
||||
|
||||
function resolveStepIdByKey(state = {}, stepKey = '') {
|
||||
if (typeof getStepIdByKeyForState !== 'function') {
|
||||
return null;
|
||||
}
|
||||
const step = Number(getStepIdByKeyForState(stepKey, state));
|
||||
return Number.isInteger(step) && step > 0 ? step : null;
|
||||
}
|
||||
|
||||
function resolveConfirmOauthStep(platformVerifyStep = 10, state = {}) {
|
||||
const dynamicStep = resolveStepIdByKey(state, 'confirm-oauth');
|
||||
if (dynamicStep && dynamicStep < Number(platformVerifyStep)) {
|
||||
return dynamicStep;
|
||||
}
|
||||
return Math.max(1, Number(platformVerifyStep) - 1);
|
||||
}
|
||||
|
||||
function resolveAuthLoginStep(platformVerifyStep = 10, state = {}) {
|
||||
const reloginStep = resolveStepIdByKey(state, 'relogin-bound-email');
|
||||
if (reloginStep && reloginStep < Number(platformVerifyStep)) {
|
||||
return reloginStep;
|
||||
}
|
||||
const oauthLoginStep = resolveStepIdByKey(state, 'oauth-login');
|
||||
if (oauthLoginStep && oauthLoginStep < Number(platformVerifyStep)) {
|
||||
return oauthLoginStep;
|
||||
}
|
||||
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, state = {}) {
|
||||
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep, state);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const oauthState = normalizeString(parsed.searchParams.get('state'));
|
||||
if (!code || !oauthState) {
|
||||
throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
|
||||
return {
|
||||
url: parsed.toString(),
|
||||
code,
|
||||
state: oauthState,
|
||||
};
|
||||
}
|
||||
|
||||
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 executeStep10(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, state);
|
||||
const authLoginStep = resolveAuthLoginStep(platformVerifyStep, state);
|
||||
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, state);
|
||||
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 executeCodex2ApiStep10(state) {
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep, state);
|
||||
const authLoginStep = resolveAuthLoginStep(platformVerifyStep, state);
|
||||
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, state);
|
||||
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, state);
|
||||
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,
|
||||
executeStep10,
|
||||
executeSub2ApiStep10,
|
||||
};
|
||||
}
|
||||
|
||||
return { createStep10Executor };
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
(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,
|
||||
};
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
(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) => ({
|
||||
flowId: String(definition?.flowId || '').trim(),
|
||||
nodeId: String(definition?.nodeId || definition?.key || '').trim(),
|
||||
displayOrder: Number(definition?.displayOrder ?? definition?.order ?? definition?.id),
|
||||
executeKey: String(definition?.executeKey || definition?.key || definition?.nodeId || '').trim(),
|
||||
title: String(definition?.title || '').trim(),
|
||||
execute: definition?.execute,
|
||||
}))
|
||||
.filter((definition) => definition.nodeId)
|
||||
.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(`Unknown node: ${nodeId}`);
|
||||
}
|
||||
if (typeof definition.execute !== 'function') {
|
||||
throw new Error(`Missing node executor: ${definition.executeKey || definition.nodeId}`);
|
||||
}
|
||||
return definition.execute(state);
|
||||
}
|
||||
|
||||
return {
|
||||
executeNode,
|
||||
getNodeDefinition,
|
||||
getOrderedNodes,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createNodeRegistry,
|
||||
};
|
||||
});
|
||||
@@ -1,280 +0,0 @@
|
||||
(function attachBackgroundSub2ApiSessionImport(root, factory) {
|
||||
root.MultiPageBackgroundSub2ApiSessionImport = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundSub2ApiSessionImportModule() {
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'];
|
||||
|
||||
function createSub2ApiSessionImportExecutor(deps = {}) {
|
||||
const {
|
||||
addLog: rawAddLog = async () => {},
|
||||
chrome,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
normalizeSub2ApiUrl = (value) => value,
|
||||
registerTab,
|
||||
sendTabMessageUntilStopped,
|
||||
sleepWithStop = async () => {},
|
||||
throwIfStopped = () => {},
|
||||
waitForTabCompleteUntilStopped = async () => {},
|
||||
DEFAULT_SUB2API_GROUP_NAME = 'codex',
|
||||
} = deps;
|
||||
|
||||
let sub2ApiApi = null;
|
||||
|
||||
function addStepLog(step, message, level = 'info') {
|
||||
return rawAddLog(message, level, {
|
||||
step,
|
||||
stepKey: 'sub2api-session-import',
|
||||
});
|
||||
}
|
||||
|
||||
function getSub2ApiApi() {
|
||||
if (sub2ApiApi) {
|
||||
return sub2ApiApi;
|
||||
}
|
||||
const factory = deps.createSub2ApiApi
|
||||
|| self.MultiPageBackgroundSub2ApiApi?.createSub2ApiApi;
|
||||
if (typeof factory !== 'function') {
|
||||
throw new Error('SUB2API 接口模块未加载,无法导入当前 ChatGPT 会话。');
|
||||
}
|
||||
sub2ApiApi = factory({
|
||||
addLog: rawAddLog,
|
||||
normalizeSub2ApiUrl,
|
||||
DEFAULT_SUB2API_GROUP_NAME,
|
||||
});
|
||||
return sub2ApiApi;
|
||||
}
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function resolveVisibleStep(state = {}) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep > 0 ? visibleStep : 10;
|
||||
}
|
||||
|
||||
function isSupportedChatGptSessionUrl(url = '') {
|
||||
try {
|
||||
const parsed = new URL(String(url || ''));
|
||||
if (!/^https?:$/i.test(parsed.protocol)) {
|
||||
return false;
|
||||
}
|
||||
const hostname = String(parsed.hostname || '').trim().toLowerCase();
|
||||
return /(^|\.)chatgpt\.com$/.test(hostname)
|
||||
|| hostname === 'chat.openai.com'
|
||||
|| /(^|\.)openai\.com$/.test(hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionTabHostPriority(url = '') {
|
||||
try {
|
||||
const hostname = String(new URL(String(url || '')).hostname || '').trim().toLowerCase();
|
||||
if (/(^|\.)chatgpt\.com$/.test(hostname)) {
|
||||
return 0;
|
||||
}
|
||||
if (hostname === 'chat.openai.com') {
|
||||
return 1;
|
||||
}
|
||||
if (/(^|\.)openai\.com$/.test(hostname)) {
|
||||
return 2;
|
||||
}
|
||||
} catch {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
function getSessionTabActivityPriority(tab = {}) {
|
||||
if (tab?.active && tab?.currentWindow) {
|
||||
return 0;
|
||||
}
|
||||
if (tab?.active) {
|
||||
return 1;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
function pickPreferredSessionTab(tabs = []) {
|
||||
const candidates = (Array.isArray(tabs) ? tabs : [])
|
||||
.filter((tab) => Number.isInteger(tab?.id) && isSupportedChatGptSessionUrl(tab.url));
|
||||
if (!candidates.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidates.reduce((best, candidate) => {
|
||||
if (!best) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
const candidateHostPriority = getSessionTabHostPriority(candidate.url);
|
||||
const bestHostPriority = getSessionTabHostPriority(best.url);
|
||||
if (candidateHostPriority !== bestHostPriority) {
|
||||
return candidateHostPriority < bestHostPriority ? candidate : best;
|
||||
}
|
||||
|
||||
const candidateActivityPriority = getSessionTabActivityPriority(candidate);
|
||||
const bestActivityPriority = getSessionTabActivityPriority(best);
|
||||
if (candidateActivityPriority !== bestActivityPriority) {
|
||||
return candidateActivityPriority < bestActivityPriority ? candidate : best;
|
||||
}
|
||||
|
||||
const candidateLastAccessed = Number(candidate?.lastAccessed) || 0;
|
||||
const bestLastAccessed = Number(best?.lastAccessed) || 0;
|
||||
if (candidateLastAccessed !== bestLastAccessed) {
|
||||
return candidateLastAccessed > bestLastAccessed ? candidate : best;
|
||||
}
|
||||
|
||||
return Number(candidate.id) < Number(best.id) ? candidate : best;
|
||||
}, null);
|
||||
}
|
||||
|
||||
async function readSupportedSessionTab(tabId) {
|
||||
const numericTabId = Number(tabId) || 0;
|
||||
if (!numericTabId || !chrome?.tabs?.get) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tab = await chrome.tabs.get(numericTabId).catch(() => null);
|
||||
return tab?.id && isSupportedChatGptSessionUrl(tab.url)
|
||||
? tab
|
||||
: null;
|
||||
}
|
||||
|
||||
async function findFallbackSessionTab() {
|
||||
if (!chrome?.tabs?.query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true }).catch(() => []);
|
||||
const activeMatch = pickPreferredSessionTab(activeTabs);
|
||||
const allTabs = await chrome.tabs.query({}).catch(() => []);
|
||||
const globalMatch = pickPreferredSessionTab(allTabs);
|
||||
return pickPreferredSessionTab([activeMatch, globalMatch]);
|
||||
}
|
||||
|
||||
async function resolveSessionTabId(state = {}) {
|
||||
const registeredTabId = typeof getTabId === 'function'
|
||||
? await getTabId(PLUS_CHECKOUT_SOURCE)
|
||||
: null;
|
||||
if (registeredTabId && typeof isTabAlive === 'function' && await isTabAlive(PLUS_CHECKOUT_SOURCE)) {
|
||||
const registeredTab = await readSupportedSessionTab(registeredTabId);
|
||||
if (registeredTab?.id) {
|
||||
return registeredTab.id;
|
||||
}
|
||||
}
|
||||
|
||||
const storedTabId = Number(state?.plusCheckoutTabId) || 0;
|
||||
const storedTab = await readSupportedSessionTab(storedTabId);
|
||||
if (storedTab?.id) {
|
||||
if (typeof registerTab === 'function') {
|
||||
await registerTab(PLUS_CHECKOUT_SOURCE, storedTab.id);
|
||||
}
|
||||
return storedTab.id;
|
||||
}
|
||||
|
||||
const fallbackTab = await findFallbackSessionTab();
|
||||
if (fallbackTab?.id) {
|
||||
if (typeof registerTab === 'function') {
|
||||
await registerTab(PLUS_CHECKOUT_SOURCE, fallbackTab.id);
|
||||
}
|
||||
return fallbackTab.id;
|
||||
}
|
||||
|
||||
throw new Error('未找到可读取 ChatGPT 会话的标签页,请先打开一个已登录的 ChatGPT / OpenAI 页面,或完成当前 Plus 支付链路。');
|
||||
}
|
||||
|
||||
async function getResolvedSessionTab(tabId, visibleStep) {
|
||||
const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
|
||||
if (!tab?.id) {
|
||||
throw new Error(`步骤 ${visibleStep}:ChatGPT 会话标签页不存在或已关闭,无法继续导入 SUB2API。`);
|
||||
}
|
||||
if (!isSupportedChatGptSessionUrl(tab.url)) {
|
||||
throw new Error(`步骤 ${visibleStep}:当前标签页不在 ChatGPT / OpenAI 页面,无法读取当前登录会话。`);
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
|
||||
async function readCurrentChatGptSession(tabId, visibleStep) {
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
await sleepWithStop(1000);
|
||||
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
|
||||
inject: PLUS_CHECKOUT_INJECT_FILES,
|
||||
injectSource: PLUS_CHECKOUT_SOURCE,
|
||||
logMessage: `步骤 ${visibleStep}:正在等待 ChatGPT 会话页完成加载,再继续读取当前登录会话...`,
|
||||
});
|
||||
|
||||
const sessionResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||
type: 'PLUS_CHECKOUT_GET_STATE',
|
||||
source: 'background',
|
||||
payload: {
|
||||
includeSession: true,
|
||||
includeAccessToken: true,
|
||||
},
|
||||
});
|
||||
if (sessionResult?.error) {
|
||||
throw new Error(sessionResult.error);
|
||||
}
|
||||
|
||||
const session = sessionResult?.session && typeof sessionResult.session === 'object' && !Array.isArray(sessionResult.session)
|
||||
? sessionResult.session
|
||||
: null;
|
||||
const accessToken = normalizeString(
|
||||
sessionResult?.accessToken
|
||||
|| session?.accessToken
|
||||
);
|
||||
if (!session && !accessToken) {
|
||||
throw new Error(`步骤 ${visibleStep}:未读取到有效的 ChatGPT 会话或 accessToken,请确认当前标签页仍处于已登录状态。`);
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeSub2ApiSessionImport(state = {}) {
|
||||
throwIfStopped();
|
||||
const visibleStep = resolveVisibleStep(state);
|
||||
const api = getSub2ApiApi();
|
||||
|
||||
await addStepLog(visibleStep, '正在定位当前 ChatGPT 会话页并准备导入 SUB2API...', 'info');
|
||||
const tabId = await resolveSessionTabId(state);
|
||||
const tab = await getResolvedSessionTab(tabId, visibleStep);
|
||||
if (chrome?.tabs?.update) {
|
||||
await chrome.tabs.update(tab.id, { active: true }).catch(() => {});
|
||||
}
|
||||
|
||||
await addStepLog(visibleStep, '正在读取当前 ChatGPT 登录会话...', 'info');
|
||||
const sessionState = await readCurrentChatGptSession(tab.id, visibleStep);
|
||||
throwIfStopped();
|
||||
|
||||
const result = await api.importCurrentChatGptSession({
|
||||
...state,
|
||||
session: sessionState.session,
|
||||
accessToken: sessionState.accessToken,
|
||||
}, {
|
||||
visibleStep,
|
||||
logLabel: `步骤 ${visibleStep}`,
|
||||
logOptions: { step: visibleStep, stepKey: 'sub2api-session-import' },
|
||||
timeoutMs: 120000,
|
||||
importTimeoutMs: 120000,
|
||||
});
|
||||
|
||||
await completeNodeFromBackground(state?.nodeId || 'sub2api-session-import', result);
|
||||
}
|
||||
|
||||
return {
|
||||
executeSub2ApiSessionImport,
|
||||
isSupportedChatGptSessionUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createSub2ApiSessionImportExecutor,
|
||||
};
|
||||
});
|
||||
@@ -1,509 +0,0 @@
|
||||
(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 };
|
||||
});
|
||||
@@ -1,159 +0,0 @@
|
||||
(function attachBackgroundStep6(root, factory) {
|
||||
root.MultiPageBackgroundStep6 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep6Module() {
|
||||
const DEFAULT_REGISTRATION_SUCCESS_WAIT_MS = 20000;
|
||||
const STEP6_COOKIE_CLEAR_DOMAINS = [
|
||||
'chatgpt.com',
|
||||
'chat.openai.com',
|
||||
'openai.com',
|
||||
'auth.openai.com',
|
||||
'auth0.openai.com',
|
||||
'accounts.openai.com',
|
||||
];
|
||||
const STEP6_COOKIE_CLEAR_ORIGINS = [
|
||||
'https://chatgpt.com',
|
||||
'https://chat.openai.com',
|
||||
'https://auth.openai.com',
|
||||
'https://auth0.openai.com',
|
||||
'https://accounts.openai.com',
|
||||
'https://openai.com',
|
||||
];
|
||||
|
||||
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 () => {},
|
||||
chrome: chromeApi = globalThis.chrome,
|
||||
completeNodeFromBackground,
|
||||
getErrorMessage = (error) => error?.message || String(error || '未知错误'),
|
||||
registrationSuccessWaitMs = DEFAULT_REGISTRATION_SUCCESS_WAIT_MS,
|
||||
sleepWithStop = async (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0))),
|
||||
} = deps;
|
||||
|
||||
async function clearCookiesIfEnabled(state = {}) {
|
||||
if (!state?.step6CookieCleanupEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!chromeApi?.cookies?.getAll || !chromeApi.cookies?.remove) {
|
||||
await addLog('步骤 6:当前浏览器不支持 cookies API,跳过第六步 Cookies 清理。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await addLog('步骤 6:已开启 Cookies 清理,正在清理 ChatGPT / OpenAI cookies...', 'info');
|
||||
const cookies = await collectStep6Cookies(chromeApi);
|
||||
let removedCount = 0;
|
||||
for (const cookie of cookies) {
|
||||
if (await removeStep6Cookie(chromeApi, cookie, getErrorMessage)) {
|
||||
removedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (chromeApi.browsingData?.removeCookies) {
|
||||
try {
|
||||
await chromeApi.browsingData.removeCookies({
|
||||
since: 0,
|
||||
origins: STEP6_COOKIE_CLEAR_ORIGINS,
|
||||
});
|
||||
} catch (error) {
|
||||
await addLog(`步骤 6:browsingData 补扫 cookies 失败:${getErrorMessage(error)}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
await addLog(`步骤 6:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies。`, 'ok');
|
||||
} catch (error) {
|
||||
await addLog(`步骤 6:Cookies 清理失败,已跳过并继续后续流程:${getErrorMessage(error)}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
async function executeStep6(state = {}) {
|
||||
const waitMs = Math.max(0, Math.floor(Number(registrationSuccessWaitMs) || 0));
|
||||
if (waitMs > 0) {
|
||||
await addLog(`步骤 6:等待 ${Math.round(waitMs / 1000)} 秒,确认注册成功并让页面稳定...`, 'info');
|
||||
await sleepWithStop(waitMs);
|
||||
}
|
||||
await clearCookiesIfEnabled(state);
|
||||
await addLog('步骤 6:注册成功等待完成,准备继续获取 OAuth 链接并登录。', 'ok');
|
||||
await completeNodeFromBackground('wait-registration-success');
|
||||
}
|
||||
|
||||
return { executeStep6 };
|
||||
}
|
||||
|
||||
return { createStep6Executor };
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -242,7 +242,7 @@
|
||||
const requestTimeoutMs = Math.max(1200, Math.min(5000, timeoutMs));
|
||||
const result = typeof sendToContentScriptResilient === 'function'
|
||||
? await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
'openai-auth',
|
||||
request,
|
||||
{
|
||||
timeoutMs: requestTimeoutMs,
|
||||
@@ -251,7 +251,7 @@
|
||||
logMessage: `步骤 ${step}:验证码提交后页面正在切换,等待页面恢复并确认授权状态...`,
|
||||
}
|
||||
)
|
||||
: await sendToContentScript('signup-page', request, {
|
||||
: await sendToContentScript('openai-auth', request, {
|
||||
responseTimeoutMs: requestTimeoutMs,
|
||||
});
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
|
||||
async function requestVerificationCodeResend(step, options = {}) {
|
||||
throwIfStopped();
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
const signupTabId = await getTabId('openai-auth');
|
||||
if (!signupTabId) {
|
||||
throw new Error('认证页面标签页已关闭,无法重新请求验证码。');
|
||||
}
|
||||
@@ -525,7 +525,7 @@
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
throwIfStopped();
|
||||
|
||||
const result = await sendToContentScript('signup-page', {
|
||||
const result = await sendToContentScript('openai-auth', {
|
||||
type: 'RESEND_VERIFICATION_CODE',
|
||||
step,
|
||||
source: 'background',
|
||||
@@ -1111,7 +1111,7 @@
|
||||
async function submitVerificationCode(step, code, options = {}) {
|
||||
const completionStep = getCompletionStep(step, options);
|
||||
const authLoginStep = completionStep >= 11 ? 10 : 7;
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
const signupTabId = await getTabId('openai-auth');
|
||||
if (!signupTabId) {
|
||||
throw new Error('认证页面标签页已关闭,无法填写验证码。');
|
||||
}
|
||||
@@ -1143,7 +1143,7 @@
|
||||
const shouldAvoidReplaySubmit = step === 8;
|
||||
if (typeof sendToContentScriptResilient === 'function' && !shouldAvoidReplaySubmit) {
|
||||
try {
|
||||
result = await sendToContentScriptResilient('signup-page', message, {
|
||||
result = await sendToContentScriptResilient('openai-auth', message, {
|
||||
timeoutMs: Math.max(baseResponseTimeoutMs + 15000, 30000),
|
||||
retryDelayMs: 700,
|
||||
responseTimeoutMs: baseResponseTimeoutMs,
|
||||
@@ -1206,7 +1206,7 @@
|
||||
}
|
||||
} else if (shouldAvoidReplaySubmit) {
|
||||
try {
|
||||
result = await sendToContentScript('signup-page', message, {
|
||||
result = await sendToContentScript('openai-auth', message, {
|
||||
responseTimeoutMs: baseResponseTimeoutMs,
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -1255,7 +1255,7 @@
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
result = await sendToContentScript('signup-page', message, {
|
||||
result = await sendToContentScript('openai-auth', message, {
|
||||
responseTimeoutMs: baseResponseTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
(function attachBackgroundWorkflowEngine(root, factory) {
|
||||
root.MultiPageBackgroundWorkflowEngine = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundWorkflowEngineModule() {
|
||||
function createWorkflowEngine(deps = {}) {
|
||||
const {
|
||||
defaultFlowId = 'openai',
|
||||
workflowDefinitions = null,
|
||||
} = deps;
|
||||
|
||||
function normalizeFlowId(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized || defaultFlowId;
|
||||
}
|
||||
|
||||
function normalizeNodeId(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function resolveStateFlowId(state = {}) {
|
||||
return normalizeFlowId(state?.flowId || state?.activeFlowId || defaultFlowId);
|
||||
}
|
||||
|
||||
function getWorkflow(options = {}) {
|
||||
const flowId = normalizeFlowId(options?.flowId || options?.activeFlowId || defaultFlowId);
|
||||
if (workflowDefinitions?.getWorkflow) {
|
||||
return workflowDefinitions.getWorkflow({
|
||||
...options,
|
||||
activeFlowId: flowId,
|
||||
flowId,
|
||||
});
|
||||
}
|
||||
return {
|
||||
flowId,
|
||||
workflowVersion: 1,
|
||||
nodes: [],
|
||||
nodeIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
function getWorkflowForState(state = {}) {
|
||||
return getWorkflow({
|
||||
...state,
|
||||
flowId: resolveStateFlowId(state),
|
||||
activeFlowId: resolveStateFlowId(state),
|
||||
});
|
||||
}
|
||||
|
||||
function getNodesForState(state = {}) {
|
||||
return Array.isArray(getWorkflowForState(state).nodes)
|
||||
? getWorkflowForState(state).nodes
|
||||
: [];
|
||||
}
|
||||
|
||||
function getNodeIdsForState(state = {}) {
|
||||
return getNodesForState(state).map((node) => node.nodeId).filter(Boolean);
|
||||
}
|
||||
|
||||
function getNodeById(nodeId, state = {}) {
|
||||
const normalizedNodeId = normalizeNodeId(nodeId);
|
||||
if (!normalizedNodeId) {
|
||||
return null;
|
||||
}
|
||||
return getNodesForState(state).find((node) => node.nodeId === normalizedNodeId) || null;
|
||||
}
|
||||
|
||||
function getDisplayOrderForNode(nodeId, state = {}) {
|
||||
const node = getNodeById(nodeId, state);
|
||||
return Number.isFinite(Number(node?.displayOrder)) ? Number(node.displayOrder) : null;
|
||||
}
|
||||
|
||||
function getNodeTitle(nodeId, state = {}) {
|
||||
return getNodeById(nodeId, state)?.title || nodeId || '';
|
||||
}
|
||||
|
||||
function normalizeNodeStatus(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized || 'pending';
|
||||
}
|
||||
|
||||
function buildDefaultNodeStatuses(state = {}) {
|
||||
return Object.fromEntries(getNodeIdsForState(state).map((nodeId) => [nodeId, 'pending']));
|
||||
}
|
||||
|
||||
function normalizeNodeStatuses(statuses = {}, state = {}) {
|
||||
const defaults = buildDefaultNodeStatuses(state);
|
||||
const next = { ...defaults };
|
||||
if (!statuses || typeof statuses !== 'object' || Array.isArray(statuses)) {
|
||||
return next;
|
||||
}
|
||||
for (const [rawNodeId, rawStatus] of Object.entries(statuses)) {
|
||||
const nodeId = normalizeNodeId(rawNodeId);
|
||||
if (!nodeId || !Object.prototype.hasOwnProperty.call(defaults, nodeId)) {
|
||||
continue;
|
||||
}
|
||||
next[nodeId] = normalizeNodeStatus(rawStatus);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function isNodeDoneStatus(status = '') {
|
||||
return ['completed', 'manual_completed', 'skipped'].includes(normalizeNodeStatus(status));
|
||||
}
|
||||
|
||||
function isNodeTerminalStatus(status = '') {
|
||||
return ['completed', 'manual_completed', 'skipped', 'failed', 'stopped'].includes(normalizeNodeStatus(status));
|
||||
}
|
||||
|
||||
function getRunningNodeIds(statuses = {}, state = {}) {
|
||||
const normalizedStatuses = normalizeNodeStatuses(statuses, state);
|
||||
return getNodeIdsForState(state).filter((nodeId) => normalizedStatuses[nodeId] === 'running');
|
||||
}
|
||||
|
||||
function getFirstUnfinishedNodeId(statuses = {}, state = {}) {
|
||||
const normalizedStatuses = normalizeNodeStatuses(statuses, state);
|
||||
return getNodeIdsForState(state).find((nodeId) => !isNodeDoneStatus(normalizedStatuses[nodeId])) || '';
|
||||
}
|
||||
|
||||
function hasSavedProgress(statuses = {}, state = {}) {
|
||||
const normalizedStatuses = normalizeNodeStatuses(statuses, state);
|
||||
return getNodeIdsForState(state).some((nodeId) => normalizeNodeStatus(normalizedStatuses[nodeId]) !== 'pending');
|
||||
}
|
||||
|
||||
function getNextNodeIds(nodeId, state = {}) {
|
||||
const node = getNodeById(nodeId, state);
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(node.next) ? node.next.map(normalizeNodeId).filter(Boolean) : [];
|
||||
}
|
||||
|
||||
return {
|
||||
buildDefaultNodeStatuses,
|
||||
getDisplayOrderForNode,
|
||||
getFirstUnfinishedNodeId,
|
||||
getNextNodeIds,
|
||||
getNodeById,
|
||||
getNodeIdsForState,
|
||||
getNodesForState,
|
||||
getNodeTitle,
|
||||
getRunningNodeIds,
|
||||
getWorkflow,
|
||||
getWorkflowForState,
|
||||
hasSavedProgress,
|
||||
isNodeDoneStatus,
|
||||
isNodeTerminalStatus,
|
||||
normalizeFlowId,
|
||||
normalizeNodeId,
|
||||
normalizeNodeStatuses,
|
||||
normalizeNodeStatus,
|
||||
resolveStateFlowId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createWorkflowEngine,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user