refactor flows into canonical runtime architecture
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
(function attachMultiPageFlowsIndex(root, factory) {
|
||||
root.MultiPageFlowsIndex = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createFlowsIndexModule() {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
|
||||
const FLOW_ENTRY_DEFINITIONS = Object.freeze({
|
||||
openai: {
|
||||
id: 'openai',
|
||||
path: 'flows/openai/',
|
||||
},
|
||||
kiro: {
|
||||
id: 'kiro',
|
||||
path: 'flows/kiro/',
|
||||
},
|
||||
});
|
||||
|
||||
function normalizeFlowId(value = '') {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getRegisteredFlowIds() {
|
||||
return Object.keys(FLOW_ENTRY_DEFINITIONS);
|
||||
}
|
||||
|
||||
function getFlowEntry(flowId) {
|
||||
const normalized = normalizeFlowId(flowId);
|
||||
const baseEntry = FLOW_ENTRY_DEFINITIONS[normalized];
|
||||
if (!baseEntry) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...baseEntry,
|
||||
definition: normalized === 'openai'
|
||||
? (rootScope.MultiPageOpenAiFlowDefinition || null)
|
||||
: (rootScope.MultiPageKiroFlowDefinition || null),
|
||||
workflow: normalized === 'openai'
|
||||
? (rootScope.MultiPageOpenAiWorkflow || null)
|
||||
: (rootScope.MultiPageKiroWorkflow || null),
|
||||
};
|
||||
}
|
||||
|
||||
function getFlowDefinition(flowId) {
|
||||
return getFlowEntry(flowId)?.definition || null;
|
||||
}
|
||||
|
||||
function getFlowDefinitions() {
|
||||
const next = {};
|
||||
getRegisteredFlowIds().forEach((flowId) => {
|
||||
const definition = getFlowDefinition(flowId);
|
||||
if (definition) {
|
||||
next[flowId] = definition;
|
||||
}
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function getFlowWorkflow(flowId) {
|
||||
return getFlowEntry(flowId)?.workflow || null;
|
||||
}
|
||||
|
||||
return {
|
||||
getFlowEntry,
|
||||
getFlowDefinition,
|
||||
getFlowDefinitions,
|
||||
getFlowWorkflow,
|
||||
getRegisteredFlowIds,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
(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?.runtimeState?.flowState?.kiro)
|
||||
? state.runtimeState.flowState.kiro
|
||||
: (isPlainObject(state?.flowState?.kiro) ? state.flowState.kiro : {}));
|
||||
}
|
||||
|
||||
function resolveKiroTargetId(state = {}, runtimeState = readKiroRuntime(state)) {
|
||||
return cleanString(
|
||||
state?.settingsState?.flows?.kiro?.selectedTargetId
|
||||
|| state?.targetId
|
||||
|| 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
|
||||
|| 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
@@ -0,0 +1,196 @@
|
||||
(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,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,491 @@
|
||||
(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?.runtimeState?.flowState?.kiro)
|
||||
? state.runtimeState.flowState.kiro
|
||||
: (isPlainObject(state?.flowState?.kiro) ? state.flowState.kiro : {}));
|
||||
}
|
||||
|
||||
function buildCanonicalRuntimePatch(currentState = {}, nextRuntimeState = {}) {
|
||||
if (typeof kiroStateApi?.buildRuntimeStatePatch === 'function') {
|
||||
return kiroStateApi.buildRuntimeStatePatch(currentState, nextRuntimeState);
|
||||
}
|
||||
const baseRuntimeState = isPlainObject(currentState?.runtimeState)
|
||||
? cloneValue(currentState.runtimeState)
|
||||
: {};
|
||||
const baseFlowState = isPlainObject(baseRuntimeState.flowState)
|
||||
? cloneValue(baseRuntimeState.flowState)
|
||||
: {};
|
||||
return {
|
||||
runtimeState: {
|
||||
...baseRuntimeState,
|
||||
flowState: {
|
||||
...baseFlowState,
|
||||
kiro: deepMerge(readKiroRuntime(currentState), nextRuntimeState),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mergeRuntimePatch(currentState = {}, patch = {}) {
|
||||
return buildCanonicalRuntimePatch(
|
||||
currentState,
|
||||
deepMerge(readKiroRuntime(currentState), patch)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveKiroTargetId(state = {}) {
|
||||
return cleanString(
|
||||
state?.settingsState?.flows?.kiro?.selectedTargetId
|
||||
|| state?.targetId
|
||||
|| 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] || {};
|
||||
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
|
||||
|| 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
@@ -0,0 +1,424 @@
|
||||
(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 normalizeNullableIdentifier(value, fallback = null) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return fallback;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
const numeric = Math.floor(value);
|
||||
return Number.isInteger(numeric) ? numeric : fallback;
|
||||
}
|
||||
const normalized = String(value ?? '').trim();
|
||||
if (!normalized) {
|
||||
return fallback;
|
||||
}
|
||||
const numeric = Math.floor(Number(normalized));
|
||||
return Number.isInteger(numeric) && String(numeric) === normalized
|
||||
? numeric
|
||||
: normalized;
|
||||
}
|
||||
|
||||
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: normalizeNullableIdentifier(merged.upload?.credentialId),
|
||||
lastMessage: normalizeString(merged.upload?.lastMessage),
|
||||
lastUploadedAt: Math.max(0, normalizeInteger(merged.upload?.lastUploadedAt)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildCanonicalRuntimeStatePatch(state = {}, runtimeState = {}) {
|
||||
const normalizedRuntimeState = normalizeRuntimeState(runtimeState);
|
||||
const baseRuntimeState = isPlainObject(state?.runtimeState)
|
||||
? cloneValue(state.runtimeState)
|
||||
: {};
|
||||
const baseFlowState = isPlainObject(baseRuntimeState.flowState)
|
||||
? cloneValue(baseRuntimeState.flowState)
|
||||
: {};
|
||||
return {
|
||||
...baseRuntimeState,
|
||||
flowState: {
|
||||
...baseFlowState,
|
||||
kiro: normalizedRuntimeState,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildRuntimeStateView(runtimeState = {}) {
|
||||
const normalizedFlowState = isPlainObject(runtimeState?.flowState)
|
||||
? runtimeState.flowState
|
||||
: {};
|
||||
return {
|
||||
flowState: cloneValue(normalizedFlowState),
|
||||
flows: cloneValue(normalizedFlowState),
|
||||
};
|
||||
}
|
||||
|
||||
function buildRuntimeStatePatch(currentState = {}, patch = {}) {
|
||||
if (!isPlainObject(patch)) {
|
||||
return {};
|
||||
}
|
||||
const nextRuntimeState = normalizeRuntimeState(
|
||||
deepMerge(ensureRuntimeState(currentState), patch)
|
||||
);
|
||||
return {
|
||||
runtimeState: buildCanonicalRuntimeStatePatch(currentState, nextRuntimeState),
|
||||
};
|
||||
}
|
||||
|
||||
function ensureRuntimeState(state = {}) {
|
||||
const runtimeFlowState = isPlainObject(state?.runtimeState?.flowState)
|
||||
? state.runtimeState.flowState
|
||||
: {};
|
||||
if (isPlainObject(runtimeFlowState.kiro)) {
|
||||
return normalizeRuntimeState(runtimeFlowState.kiro);
|
||||
}
|
||||
if (isPlainObject(state?.flowState?.kiro)) {
|
||||
return normalizeRuntimeState(state.flowState.kiro);
|
||||
}
|
||||
return buildDefaultRuntimeState();
|
||||
}
|
||||
|
||||
function projectRuntimeFields() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function buildStateView(state = {}) {
|
||||
const nextRuntimeState = ensureRuntimeState(state);
|
||||
const runtimeState = buildCanonicalRuntimeStatePatch(state, nextRuntimeState);
|
||||
return {
|
||||
...state,
|
||||
runtimeState,
|
||||
...buildRuntimeStateView(runtimeState),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSessionStatePatch(currentState = {}, updates = {}) {
|
||||
const runtimePatch = isPlainObject(updates?.runtimeState?.flowState?.kiro)
|
||||
? updates.runtimeState.flowState.kiro
|
||||
: (isPlainObject(updates?.flowState?.kiro)
|
||||
? updates.flowState.kiro
|
||||
: null);
|
||||
if (!runtimePatch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return buildRuntimeStatePatch(currentState, runtimePatch);
|
||||
}
|
||||
|
||||
function buildRuntimeResetPatch(currentState = {}, patch = {}) {
|
||||
return buildRuntimeStatePatch(currentState, patch);
|
||||
}
|
||||
|
||||
function buildStartRegisterResetPatch(currentState = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
const nextRuntimeState = buildDefaultRuntimeState();
|
||||
nextRuntimeState.upload.targetId = currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID;
|
||||
return {
|
||||
runtimeState: buildCanonicalRuntimeStatePatch(currentState, 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 {
|
||||
runtimeState: buildCanonicalRuntimeStatePatch(currentState, nextRuntimeState),
|
||||
};
|
||||
}
|
||||
|
||||
function buildDesktopResetPatch(currentState = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
const nextRuntimeState = 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,
|
||||
},
|
||||
});
|
||||
return {
|
||||
runtimeState: buildCanonicalRuntimeStatePatch(currentState, nextRuntimeState),
|
||||
};
|
||||
}
|
||||
|
||||
function buildUploadResetPatch(currentState = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
const nextRuntimeState = normalizeRuntimeState({
|
||||
...currentRuntimeState,
|
||||
upload: {
|
||||
...buildDefaultRuntimeState().upload,
|
||||
targetId: currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID,
|
||||
},
|
||||
});
|
||||
return {
|
||||
runtimeState: buildCanonicalRuntimeStatePatch(currentState, nextRuntimeState),
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
runtimeState: buildCanonicalRuntimeStatePatch(currentState, nextRuntimeState),
|
||||
...(Object.prototype.hasOwnProperty.call(currentState, 'targetId')
|
||||
? { targetId: normalizeString(currentState.targetId, DEFAULT_TARGET_ID).toLowerCase() }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_REGION,
|
||||
DEFAULT_TARGET_ID,
|
||||
FLAT_FIELD_DEFINITIONS,
|
||||
FLAT_FIELD_KEYS,
|
||||
applyNodeCompletionPayload,
|
||||
buildDefaultRuntimeState,
|
||||
buildDownstreamResetPatch,
|
||||
buildFreshKeepState,
|
||||
buildRuntimeStatePatch,
|
||||
buildSessionStatePatch,
|
||||
buildStateView,
|
||||
ensureRuntimeState,
|
||||
projectRuntimeFields,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,358 @@
|
||||
console.log('[MultiPage:kiro-desktop-authorize-page] Content script loaded on', location.href);
|
||||
|
||||
const KIRO_DESKTOP_AUTHORIZE_LISTENER_SENTINEL = 'data-multipage-kiro-desktop-authorize-page-listener';
|
||||
const KIRO_DESKTOP_ALLOW_TEXT_PATTERN = /allow access|authorize|continue|allow|允许访问|授权|继续/i;
|
||||
const KIRO_DESKTOP_LOADING_TEXT_PATTERN = /loading|redirecting|please wait|加载中|跳转中|请稍候/i;
|
||||
const KIRO_DESKTOP_CLOUDFRONT_403_TEXT_PATTERN = /403 error|the request could not be satisfied|generated by cloudfront/i;
|
||||
const KIRO_DESKTOP_AWS_REQUEST_ERROR_TEXT_PATTERN = /sorry,\s*there was an error processing your request\.?\s*please try again\.?|抱歉,处理您的请求时出错。请重试。?/i;
|
||||
|
||||
const KIRO_DESKTOP_EMAIL_INPUT_SELECTOR = [
|
||||
'input[placeholder="username@example.com"]',
|
||||
'input[type="email"]',
|
||||
'input[name="email"]',
|
||||
'input[autocomplete="username"]',
|
||||
].join(', ');
|
||||
|
||||
const KIRO_DESKTOP_PASSWORD_INPUT_SELECTOR = [
|
||||
'input[type="password"]',
|
||||
'input[name="password"]',
|
||||
'input[id*="password" i]',
|
||||
'input[autocomplete="current-password"]',
|
||||
].join(', ');
|
||||
|
||||
const KIRO_DESKTOP_OTP_INPUT_SELECTOR = [
|
||||
'input[autocomplete="one-time-code"]',
|
||||
'input[inputmode="numeric"]',
|
||||
'input[placeholder*="6-digit" i]',
|
||||
'input[placeholder*="6 位" i]',
|
||||
'input[name*="otp" i]',
|
||||
'input[name*="code" i]',
|
||||
].join(', ');
|
||||
|
||||
function isVisibleDesktopElement(element) {
|
||||
if (!element) return false;
|
||||
const style = window.getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
return style.display !== 'none'
|
||||
&& style.visibility !== 'hidden'
|
||||
&& rect.width > 0
|
||||
&& rect.height > 0;
|
||||
}
|
||||
|
||||
function collectVisibleDesktopElements(selector) {
|
||||
return Array.from(document.querySelectorAll(selector))
|
||||
.filter((element) => isVisibleDesktopElement(element));
|
||||
}
|
||||
|
||||
function findFirstVisibleDesktopElement(selector) {
|
||||
return collectVisibleDesktopElements(selector)[0] || null;
|
||||
}
|
||||
|
||||
function getDesktopPageText() {
|
||||
return String(document.body?.textContent || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getDesktopActionText(element) {
|
||||
return [
|
||||
element?.textContent,
|
||||
element?.value,
|
||||
element?.getAttribute?.('aria-label'),
|
||||
element?.getAttribute?.('title'),
|
||||
element?.getAttribute?.('data-testid'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function findDesktopActionButton(options = {}) {
|
||||
const {
|
||||
preferredSelectors = [],
|
||||
textPattern = null,
|
||||
formOwner = null,
|
||||
} = options;
|
||||
|
||||
for (const selector of preferredSelectors) {
|
||||
const preferred = findFirstVisibleDesktopElement(selector);
|
||||
if (preferred && !preferred.disabled && preferred.getAttribute('aria-disabled') !== 'true') {
|
||||
return preferred;
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = collectVisibleDesktopElements('button, [role="button"], input[type="submit"], input[type="button"]')
|
||||
.filter((element) => !element.disabled && element.getAttribute('aria-disabled') !== 'true');
|
||||
|
||||
const prioritized = formOwner
|
||||
? candidates.filter((element) => (element.form || element.closest?.('form') || null) === formOwner)
|
||||
: [];
|
||||
const pool = prioritized.length ? prioritized : candidates;
|
||||
if (textPattern instanceof RegExp) {
|
||||
return pool.find((element) => textPattern.test(getDesktopActionText(element))) || null;
|
||||
}
|
||||
return pool[0] || null;
|
||||
}
|
||||
|
||||
function isKiroDesktopAuthHost(hostname = '') {
|
||||
const normalized = String(hostname || '').trim().toLowerCase();
|
||||
return normalized === 'view.awsapps.com'
|
||||
|| normalized === 'login.awsapps.com'
|
||||
|| normalized === 'profile.aws.amazon.com'
|
||||
|| normalized === 'profile.aws'
|
||||
|| normalized.endsWith('.profile.aws.amazon.com')
|
||||
|| normalized.endsWith('.profile.aws')
|
||||
|| normalized === 'signin.aws.amazon.com'
|
||||
|| normalized === 'signin.aws'
|
||||
|| normalized.endsWith('.signin.aws.amazon.com')
|
||||
|| normalized.endsWith('.signin.aws');
|
||||
}
|
||||
|
||||
function detectDesktopFatalState(pageText = '', currentUrl = '', pageTitle = '') {
|
||||
const combinedText = `${pageTitle} ${pageText}`.replace(/\s+/g, ' ').trim();
|
||||
const isKiroAuthHost = isKiroDesktopAuthHost(location.hostname || '');
|
||||
if (isKiroAuthHost && KIRO_DESKTOP_AWS_REQUEST_ERROR_TEXT_PATTERN.test(combinedText)) {
|
||||
return {
|
||||
state: 'proxy_error_page',
|
||||
url: currentUrl,
|
||||
fatalMessage: 'Kiro 桌面授权页出现 AWS 请求异常,通常是当前代理 IP 或出口区域异常,请先切换代理后再重试。',
|
||||
};
|
||||
}
|
||||
if (KIRO_DESKTOP_CLOUDFRONT_403_TEXT_PATTERN.test(combinedText)) {
|
||||
return {
|
||||
state: 'cloudfront_403_page',
|
||||
url: currentUrl,
|
||||
fatalMessage: 'Kiro 桌面授权页返回 403(CloudFront 拒绝请求),通常是当前代理 IP 或区域触发了 AWS 风控,请更换代理后重试。',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectKiroDesktopAuthorizeState() {
|
||||
const currentUrl = location.href;
|
||||
const pageText = getDesktopPageText();
|
||||
const fatalState = detectDesktopFatalState(pageText, currentUrl, document.title || '');
|
||||
if (fatalState) {
|
||||
return fatalState;
|
||||
}
|
||||
|
||||
if (/^https?:\/\/(127\.0\.0\.1|localhost):/i.test(currentUrl)) {
|
||||
const parsed = new URL(currentUrl);
|
||||
if (parsed.searchParams.get('error')) {
|
||||
return {
|
||||
state: 'callback_error',
|
||||
url: currentUrl,
|
||||
error: parsed.searchParams.get('error_description') || parsed.searchParams.get('error') || 'unknown_error',
|
||||
};
|
||||
}
|
||||
return {
|
||||
state: 'callback_page',
|
||||
url: currentUrl,
|
||||
code: parsed.searchParams.get('code') || '',
|
||||
stateValue: parsed.searchParams.get('state') || '',
|
||||
};
|
||||
}
|
||||
|
||||
const otpInput = findFirstVisibleDesktopElement(KIRO_DESKTOP_OTP_INPUT_SELECTOR);
|
||||
if (otpInput) {
|
||||
return {
|
||||
state: 'otp_page',
|
||||
url: currentUrl,
|
||||
otpInput,
|
||||
continueButton: findDesktopActionButton({
|
||||
preferredSelectors: ['button[data-testid="email-verification-verify-button"]'],
|
||||
textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN,
|
||||
formOwner: otpInput.form || otpInput.closest?.('form') || null,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const emailInput = findFirstVisibleDesktopElement(KIRO_DESKTOP_EMAIL_INPUT_SELECTOR);
|
||||
if (emailInput) {
|
||||
return {
|
||||
state: 'relogin_email',
|
||||
url: currentUrl,
|
||||
emailInput,
|
||||
continueButton: findDesktopActionButton({
|
||||
preferredSelectors: ['button[data-testid="test-primary-button"]'],
|
||||
textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN,
|
||||
formOwner: emailInput.form || emailInput.closest?.('form') || null,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const passwordInput = findFirstVisibleDesktopElement(KIRO_DESKTOP_PASSWORD_INPUT_SELECTOR);
|
||||
if (passwordInput) {
|
||||
return {
|
||||
state: 'relogin_password',
|
||||
url: currentUrl,
|
||||
passwordInput,
|
||||
continueButton: findDesktopActionButton({
|
||||
preferredSelectors: ['button[data-testid="test-primary-button"]'],
|
||||
textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN,
|
||||
formOwner: passwordInput.form || passwordInput.closest?.('form') || null,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const consentButton = findDesktopActionButton({
|
||||
preferredSelectors: [
|
||||
'button[data-testid="confirm-button"]',
|
||||
'button[data-testid="allow-access-button"]',
|
||||
],
|
||||
textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN,
|
||||
});
|
||||
if (consentButton) {
|
||||
return {
|
||||
state: 'consent_page',
|
||||
url: currentUrl,
|
||||
actionButton: consentButton,
|
||||
actionText: getDesktopActionText(consentButton),
|
||||
};
|
||||
}
|
||||
|
||||
if (KIRO_DESKTOP_LOADING_TEXT_PATTERN.test(pageText)) {
|
||||
return {
|
||||
state: 'redirecting',
|
||||
url: currentUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state: 'loading',
|
||||
url: currentUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async function getCurrentDesktopAuthorizeState() {
|
||||
const detected = detectKiroDesktopAuthorizeState();
|
||||
if (detected.state === 'cloudfront_403_page' || detected.state === 'proxy_error_page') {
|
||||
throw new Error(detected.fatalMessage || 'Kiro 桌面授权页出现代理异常。');
|
||||
}
|
||||
if (detected.state === 'callback_error') {
|
||||
throw new Error(`Kiro 桌面授权回调失败:${detected.error || 'unknown_error'}`);
|
||||
}
|
||||
return detected;
|
||||
}
|
||||
|
||||
async function submitDesktopEmail(payload = {}) {
|
||||
const email = String(payload?.email || '').trim();
|
||||
if (!email) {
|
||||
throw new Error('缺少桌面授权邮箱,无法继续。');
|
||||
}
|
||||
const currentState = await getCurrentDesktopAuthorizeState();
|
||||
if (currentState.state !== 'relogin_email' || !currentState.emailInput || !currentState.continueButton) {
|
||||
throw new Error(`当前桌面授权页不是邮箱重登状态:${currentState.state}`);
|
||||
}
|
||||
fillInput(currentState.emailInput, email);
|
||||
await sleep(150);
|
||||
simulateClick(currentState.continueButton);
|
||||
return {
|
||||
submitted: true,
|
||||
state: 'email_submitted',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function submitDesktopPassword(payload = {}) {
|
||||
const password = String(payload?.password || '');
|
||||
if (!password) {
|
||||
throw new Error('缺少桌面授权密码,无法继续。');
|
||||
}
|
||||
const currentState = await getCurrentDesktopAuthorizeState();
|
||||
if (currentState.state !== 'relogin_password' || !currentState.passwordInput || !currentState.continueButton) {
|
||||
throw new Error(`当前桌面授权页不是密码重登状态:${currentState.state}`);
|
||||
}
|
||||
fillInput(currentState.passwordInput, password);
|
||||
await sleep(150);
|
||||
simulateClick(currentState.continueButton);
|
||||
return {
|
||||
submitted: true,
|
||||
state: 'password_submitted',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function submitDesktopOtp(payload = {}) {
|
||||
const code = String(payload?.code || '').trim();
|
||||
if (!code) {
|
||||
throw new Error('缺少桌面授权验证码,无法继续。');
|
||||
}
|
||||
const currentState = await getCurrentDesktopAuthorizeState();
|
||||
if (currentState.state !== 'otp_page' || !currentState.otpInput || !currentState.continueButton) {
|
||||
throw new Error(`当前桌面授权页不是验证码状态:${currentState.state}`);
|
||||
}
|
||||
fillInput(currentState.otpInput, code);
|
||||
await sleep(150);
|
||||
simulateClick(currentState.continueButton);
|
||||
return {
|
||||
submitted: true,
|
||||
state: 'otp_submitted',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function confirmDesktopConsent() {
|
||||
const currentState = await getCurrentDesktopAuthorizeState();
|
||||
if (currentState.state !== 'consent_page' || !currentState.actionButton) {
|
||||
throw new Error(`当前桌面授权页不是授权确认状态:${currentState.state}`);
|
||||
}
|
||||
simulateClick(currentState.actionButton);
|
||||
return {
|
||||
submitted: true,
|
||||
state: 'consent_submitted',
|
||||
url: location.href,
|
||||
actionText: currentState.actionText || '',
|
||||
};
|
||||
}
|
||||
|
||||
async function handleKiroDesktopAuthorizeCommand(message) {
|
||||
switch (message.type) {
|
||||
case 'GET_KIRO_DESKTOP_AUTHORIZE_STATE':
|
||||
return getCurrentDesktopAuthorizeState();
|
||||
case 'EXECUTE_KIRO_DESKTOP_AUTHORIZE_ACTION': {
|
||||
const action = String(message.payload?.action || '').trim();
|
||||
if (action === 'submit-email') {
|
||||
return submitDesktopEmail(message.payload || {});
|
||||
}
|
||||
if (action === 'submit-password') {
|
||||
return submitDesktopPassword(message.payload || {});
|
||||
}
|
||||
if (action === 'submit-otp') {
|
||||
return submitDesktopOtp(message.payload || {});
|
||||
}
|
||||
if (action === 'confirm-consent') {
|
||||
return confirmDesktopConsent();
|
||||
}
|
||||
throw new Error(`desktop-authorize-page.js 不处理动作:${action}`);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (document.documentElement.getAttribute(KIRO_DESKTOP_AUTHORIZE_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(KIRO_DESKTOP_AUTHORIZE_LISTENER_SENTINEL, '1');
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (
|
||||
message.type === 'GET_KIRO_DESKTOP_AUTHORIZE_STATE'
|
||||
|| message.type === 'EXECUTE_KIRO_DESKTOP_AUTHORIZE_ACTION'
|
||||
) {
|
||||
resetStopState();
|
||||
handleKiroDesktopAuthorizeCommand(message)
|
||||
.then((result) => {
|
||||
sendResponse({ ok: true, ...(result || {}) });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (isStopError(error)) {
|
||||
sendResponse({ stopped: true, error: error.message });
|
||||
return;
|
||||
}
|
||||
sendResponse({ error: error?.message || String(error || '未知错误') });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,226 @@
|
||||
(function attachMultiPageKiroFlowDefinition(root, factory) {
|
||||
root.MultiPageKiroFlowDefinition = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createMultiPageKiroFlowDefinition() {
|
||||
function freezeDeep(entry) {
|
||||
if (!entry || typeof entry !== 'object' || Object.isFrozen(entry)) {
|
||||
return entry;
|
||||
}
|
||||
Object.getOwnPropertyNames(entry).forEach((key) => {
|
||||
freezeDeep(entry[key]);
|
||||
});
|
||||
return Object.freeze(entry);
|
||||
}
|
||||
|
||||
const VALUE = freezeDeep({
|
||||
"id": "kiro",
|
||||
"label": "Kiro",
|
||||
"services": [
|
||||
"account",
|
||||
"email",
|
||||
"proxy"
|
||||
],
|
||||
"capabilities": {
|
||||
"supportsEmailSignup": true,
|
||||
"supportsPhoneSignup": false,
|
||||
"supportsPhoneVerificationSettings": false,
|
||||
"supportsPlusMode": false,
|
||||
"supportsContributionMode": false,
|
||||
"supportsAccountContribution": true,
|
||||
"supportsOpenAiOAuthContribution": false,
|
||||
"contributionAdapterIds": [
|
||||
"kiro-builder-id"
|
||||
],
|
||||
"supportedTargetIds": [
|
||||
"kiro-rs"
|
||||
],
|
||||
"supportsLuckmail": false,
|
||||
"supportsOauthTimeoutBudget": false,
|
||||
"canSwitchFlow": true,
|
||||
"stepDefinitionMode": "kiro",
|
||||
"targetSelectorLabel": "来源"
|
||||
},
|
||||
"baseGroups": [
|
||||
"kiro-runtime-status"
|
||||
],
|
||||
"targets": {
|
||||
"kiro-rs": {
|
||||
"id": "kiro-rs",
|
||||
"label": "kiro.rs",
|
||||
"groups": [
|
||||
"kiro-target-kiro-rs"
|
||||
]
|
||||
}
|
||||
},
|
||||
"publicationTargets": {
|
||||
"kiro-rs": {
|
||||
"id": "kiro-rs",
|
||||
"label": "kiro.rs"
|
||||
}
|
||||
},
|
||||
"runtimeSources": {
|
||||
"kiro-register-page": {
|
||||
"flowId": "kiro",
|
||||
"kind": "flow-page",
|
||||
"label": "Kiro 注册页",
|
||||
"readyPolicy": "top-frame-only",
|
||||
"family": "kiro-register-page-family",
|
||||
"driverId": "flows/kiro/content/register-page",
|
||||
"cleanupScopes": [],
|
||||
"detectionMatchers": [
|
||||
{
|
||||
"hostnames": [
|
||||
"app.kiro.dev",
|
||||
"kiro.dev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hostnames": [
|
||||
"view.awsapps.com",
|
||||
"login.awsapps.com",
|
||||
"amazonaws.com"
|
||||
],
|
||||
"hostnameFamilies": [
|
||||
"signin.aws",
|
||||
"profile.aws"
|
||||
],
|
||||
"hostnameEndsWith": [
|
||||
".amazonaws.com"
|
||||
],
|
||||
"matchMode": "any"
|
||||
}
|
||||
],
|
||||
"familyMatchers": [
|
||||
{
|
||||
"hostnames": [
|
||||
"app.kiro.dev",
|
||||
"kiro.dev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hostnames": [
|
||||
"view.awsapps.com",
|
||||
"login.awsapps.com",
|
||||
"amazonaws.com"
|
||||
],
|
||||
"hostnameFamilies": [
|
||||
"signin.aws",
|
||||
"profile.aws"
|
||||
],
|
||||
"hostnameEndsWith": [
|
||||
".amazonaws.com"
|
||||
],
|
||||
"matchMode": "any"
|
||||
}
|
||||
]
|
||||
},
|
||||
"kiro-desktop-authorize": {
|
||||
"flowId": "kiro",
|
||||
"kind": "flow-page",
|
||||
"label": "Kiro 桌面授权页",
|
||||
"readyPolicy": "top-frame-only",
|
||||
"family": "kiro-desktop-authorize-family",
|
||||
"driverId": "flows/kiro/content/desktop-authorize-page",
|
||||
"cleanupScopes": [],
|
||||
"familyMatchers": [
|
||||
{
|
||||
"hostnames": [
|
||||
"view.awsapps.com",
|
||||
"login.awsapps.com",
|
||||
"amazonaws.com"
|
||||
],
|
||||
"hostnameFamilies": [
|
||||
"signin.aws",
|
||||
"profile.aws"
|
||||
],
|
||||
"hostnameEndsWith": [
|
||||
".amazonaws.com"
|
||||
],
|
||||
"matchMode": "any"
|
||||
}
|
||||
]
|
||||
},
|
||||
"kiro-rs-admin": {
|
||||
"flowId": "kiro",
|
||||
"kind": "virtual-page",
|
||||
"label": "kiro.rs Admin",
|
||||
"readyPolicy": "disabled",
|
||||
"family": "kiro-rs-admin-family",
|
||||
"driverId": null,
|
||||
"cleanupScopes": [],
|
||||
"familyMatchers": []
|
||||
}
|
||||
},
|
||||
"driverDefinitions": {
|
||||
"flows/kiro/content/register-page": {
|
||||
"sourceId": "kiro-register-page",
|
||||
"commands": [
|
||||
"kiro-open-register-page",
|
||||
"kiro-submit-email",
|
||||
"kiro-submit-name",
|
||||
"kiro-submit-verification-code",
|
||||
"kiro-submit-password",
|
||||
"kiro-complete-register-consent"
|
||||
]
|
||||
},
|
||||
"flows/kiro/content/desktop-authorize-page": {
|
||||
"sourceId": "kiro-desktop-authorize",
|
||||
"commands": [
|
||||
"kiro-complete-desktop-authorize"
|
||||
]
|
||||
},
|
||||
"flows/kiro/background/register-runner": {
|
||||
"sourceId": "kiro-register-page",
|
||||
"commands": [
|
||||
"kiro-open-register-page",
|
||||
"kiro-submit-email",
|
||||
"kiro-submit-name",
|
||||
"kiro-submit-verification-code",
|
||||
"kiro-submit-password",
|
||||
"kiro-complete-register-consent"
|
||||
]
|
||||
},
|
||||
"flows/kiro/background/desktop-authorize-runner": {
|
||||
"sourceId": "kiro-desktop-authorize",
|
||||
"commands": [
|
||||
"kiro-start-desktop-authorize",
|
||||
"kiro-complete-desktop-authorize"
|
||||
]
|
||||
},
|
||||
"flows/kiro/background/publisher-kiro-rs": {
|
||||
"sourceId": "kiro-rs-admin",
|
||||
"commands": [
|
||||
"kiro-upload-credential"
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultTargetId": "kiro-rs",
|
||||
"defaultPublicationTargetId": "kiro-rs",
|
||||
"defaultTargetState": {
|
||||
"baseUrl": "",
|
||||
"apiKey": ""
|
||||
},
|
||||
"settingsGroups": {
|
||||
"kiro-target-kiro-rs": {
|
||||
"id": "kiro-target-kiro-rs",
|
||||
"label": "kiro.rs 配置",
|
||||
"rowIds": [
|
||||
"row-kiro-rs-url",
|
||||
"row-kiro-rs-key",
|
||||
"row-kiro-rs-test-status"
|
||||
]
|
||||
},
|
||||
"kiro-runtime-status": {
|
||||
"id": "kiro-runtime-status",
|
||||
"label": "Kiro 运行态",
|
||||
"rowIds": [
|
||||
"row-kiro-web-status",
|
||||
"row-kiro-login-url",
|
||||
"row-kiro-upload-status"
|
||||
]
|
||||
}
|
||||
},
|
||||
"sourceAliases": {}
|
||||
});
|
||||
|
||||
return VALUE;
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
(function attachMultiPageKiroWorkflow(root, factory) {
|
||||
root.MultiPageKiroWorkflow = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createMultiPageKiroWorkflow() {
|
||||
const KIRO_CONTRIBUTION_STEP_TITLE = '\u8d21\u732e\u4e0a\u4f20';
|
||||
|
||||
function freezeDeep(entry) {
|
||||
if (!entry || typeof entry !== 'object' || Object.isFrozen(entry)) {
|
||||
return entry;
|
||||
}
|
||||
Object.getOwnPropertyNames(entry).forEach((key) => {
|
||||
freezeDeep(entry[key]);
|
||||
});
|
||||
return Object.freeze(entry);
|
||||
}
|
||||
|
||||
const STEP_VARIANTS = freezeDeep({
|
||||
"default": [
|
||||
{
|
||||
"id": 1,
|
||||
"order": 10,
|
||||
"key": "kiro-open-register-page",
|
||||
"title": "打开注册页",
|
||||
"sourceId": "kiro-register-page",
|
||||
"driverId": "flows/kiro/background/register-runner",
|
||||
"command": "kiro-open-register-page",
|
||||
"flowId": "kiro"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"order": 20,
|
||||
"key": "kiro-submit-email",
|
||||
"title": "获取邮箱并继续",
|
||||
"sourceId": "kiro-register-page",
|
||||
"driverId": "flows/kiro/background/register-runner",
|
||||
"command": "kiro-submit-email",
|
||||
"flowId": "kiro"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"order": 30,
|
||||
"key": "kiro-submit-name",
|
||||
"title": "填写姓名并继续",
|
||||
"sourceId": "kiro-register-page",
|
||||
"driverId": "flows/kiro/background/register-runner",
|
||||
"command": "kiro-submit-name",
|
||||
"flowId": "kiro"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"order": 40,
|
||||
"key": "kiro-submit-verification-code",
|
||||
"title": "获取验证码并继续",
|
||||
"sourceId": "kiro-register-page",
|
||||
"driverId": "flows/kiro/background/register-runner",
|
||||
"command": "kiro-submit-verification-code",
|
||||
"flowId": "kiro"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"order": 50,
|
||||
"key": "kiro-submit-password",
|
||||
"title": "设置密码并继续",
|
||||
"sourceId": "kiro-register-page",
|
||||
"driverId": "flows/kiro/background/register-runner",
|
||||
"command": "kiro-submit-password",
|
||||
"flowId": "kiro"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"order": 60,
|
||||
"key": "kiro-complete-register-consent",
|
||||
"title": "完成注册授权",
|
||||
"sourceId": "kiro-register-page",
|
||||
"driverId": "flows/kiro/background/register-runner",
|
||||
"command": "kiro-complete-register-consent",
|
||||
"flowId": "kiro"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"order": 70,
|
||||
"key": "kiro-start-desktop-authorize",
|
||||
"title": "启动桌面授权",
|
||||
"sourceId": "kiro-desktop-authorize",
|
||||
"driverId": "flows/kiro/background/desktop-authorize-runner",
|
||||
"command": "kiro-start-desktop-authorize",
|
||||
"flowId": "kiro"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"order": 80,
|
||||
"key": "kiro-complete-desktop-authorize",
|
||||
"title": "完成桌面授权",
|
||||
"sourceId": "kiro-desktop-authorize",
|
||||
"driverId": "flows/kiro/background/desktop-authorize-runner",
|
||||
"command": "kiro-complete-desktop-authorize",
|
||||
"flowId": "kiro"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"order": 90,
|
||||
"key": "kiro-upload-credential",
|
||||
"title": "上传凭据到 kiro.rs",
|
||||
"sourceId": "kiro-rs-admin",
|
||||
"driverId": "flows/kiro/background/publisher-kiro-rs",
|
||||
"command": "kiro-upload-credential",
|
||||
"flowId": "kiro"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
function getVariantStepDefinitions(variantKey = 'default') {
|
||||
return Array.isArray(STEP_VARIANTS[variantKey]) ? STEP_VARIANTS[variantKey] : STEP_VARIANTS.default;
|
||||
}
|
||||
|
||||
function getModeStepDefinitions() {
|
||||
return getVariantStepDefinitions('default');
|
||||
}
|
||||
|
||||
function getAllSteps() {
|
||||
return getVariantStepDefinitions('default');
|
||||
}
|
||||
|
||||
function getPlusPaymentStepTitle() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function resolveStepTitle(step = {}, options = {}) {
|
||||
if (step?.key === 'kiro-upload-credential' && Boolean(options?.accountContributionEnabled || options?.state?.accountContributionEnabled)) {
|
||||
return KIRO_CONTRIBUTION_STEP_TITLE;
|
||||
}
|
||||
return step?.title || '';
|
||||
}
|
||||
|
||||
return {
|
||||
flowId: 'kiro',
|
||||
getAllSteps,
|
||||
getModeStepDefinitions,
|
||||
getPlusPaymentStepTitle,
|
||||
getVariantStepDefinitions,
|
||||
resolveStepTitle,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,352 @@
|
||||
(function attachBackgroundStep9(root, factory) {
|
||||
root.MultiPageBackgroundStep9 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep9Module() {
|
||||
function createStep9Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
cleanupStep8NavigationListeners,
|
||||
clickWithDebugger,
|
||||
completeNodeFromBackground,
|
||||
ensureStep8SignupPageReady,
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getStep8CallbackUrlFromNavigation,
|
||||
getStep8CallbackUrlFromTabUpdate,
|
||||
getStep8EffectLabel,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
prepareStep8DebuggerClick,
|
||||
recoverOAuthLocalhostTimeout,
|
||||
reloadStep8ConsentPage,
|
||||
reuseOrCreateTab,
|
||||
sleepWithStop,
|
||||
STEP8_CLICK_RETRY_DELAY_MS,
|
||||
STEP8_MAX_ROUNDS,
|
||||
STEP8_READY_WAIT_TIMEOUT_MS,
|
||||
STEP8_STRATEGIES,
|
||||
throwIfStep8SettledOrStopped,
|
||||
triggerStep8ContentStrategy,
|
||||
waitForStep8ClickEffect,
|
||||
waitForStep8Ready,
|
||||
setWebNavListener,
|
||||
setWebNavCommittedListener,
|
||||
setStep8PendingReject,
|
||||
setStep8TabUpdatedListener,
|
||||
shouldDeferStep9CallbackTimeout,
|
||||
} = deps;
|
||||
|
||||
const LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS = 240000;
|
||||
const CALLBACK_TIMEOUT_CHECK_INTERVAL_MS = 1000;
|
||||
|
||||
function getVisibleStep(state, fallback = 9) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep > 0 ? visibleStep : fallback;
|
||||
}
|
||||
|
||||
function getAuthLoginStepForVisibleStep(visibleStep) {
|
||||
return visibleStep >= 12 ? 10 : 7;
|
||||
}
|
||||
|
||||
function addStepLog(step, message, level = 'info') {
|
||||
return addLog(message, level, { step, stepKey: 'confirm-oauth' });
|
||||
}
|
||||
|
||||
async function executeStep9(state) {
|
||||
const visibleStep = getVisibleStep(state, 9);
|
||||
let activeState = state;
|
||||
|
||||
if (!activeState.oauthUrl) {
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`);
|
||||
}
|
||||
|
||||
await addStepLog(visibleStep, '正在监听 localhost 回调地址...');
|
||||
|
||||
let callbackTimeoutMs = LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS;
|
||||
let timeoutRecoveryAttempted = false;
|
||||
while (true) {
|
||||
try {
|
||||
callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS, {
|
||||
step: visibleStep,
|
||||
actionLabel: 'OAuth localhost 回调',
|
||||
oauthUrl: activeState?.oauthUrl || '',
|
||||
})
|
||||
: LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS;
|
||||
break;
|
||||
} catch (error) {
|
||||
if (timeoutRecoveryAttempted || typeof recoverOAuthLocalhostTimeout !== 'function') {
|
||||
throw error;
|
||||
}
|
||||
const recoveredState = await recoverOAuthLocalhostTimeout({
|
||||
error,
|
||||
state: activeState,
|
||||
visibleStep,
|
||||
});
|
||||
if (!recoveredState) {
|
||||
throw error;
|
||||
}
|
||||
activeState = recoveredState;
|
||||
timeoutRecoveryAttempted = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolved = false;
|
||||
let signupTabId = null;
|
||||
const callbackWaitStartedAt = Date.now();
|
||||
let timeoutCheckTimer = null;
|
||||
let timeoutDeferredLogged = false;
|
||||
|
||||
const cleanupListener = () => {
|
||||
if (timeoutCheckTimer) {
|
||||
clearTimeout(timeoutCheckTimer);
|
||||
timeoutCheckTimer = null;
|
||||
}
|
||||
cleanupStep8NavigationListeners();
|
||||
setStep8PendingReject(null);
|
||||
};
|
||||
|
||||
const rejectStep9 = (error) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
cleanupListener();
|
||||
reject(error);
|
||||
};
|
||||
|
||||
const finalizeStep9Callback = (callbackUrl) => {
|
||||
if (resolved || !callbackUrl) return;
|
||||
|
||||
resolved = true;
|
||||
cleanupListener();
|
||||
|
||||
addStepLog(visibleStep, `已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
|
||||
return completeNodeFromBackground(state?.nodeId || 'confirm-oauth', { localhostUrl: callbackUrl });
|
||||
}).then(() => {
|
||||
resolve();
|
||||
}).catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
const isCallbackTimeoutDeferred = async (elapsedMs) => {
|
||||
if (typeof shouldDeferStep9CallbackTimeout !== 'function') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const deferred = await shouldDeferStep9CallbackTimeout({
|
||||
tabId: signupTabId,
|
||||
visibleStep,
|
||||
elapsedMs,
|
||||
oauthUrl: activeState?.oauthUrl || '',
|
||||
});
|
||||
if (deferred && !timeoutDeferredLogged) {
|
||||
timeoutDeferredLogged = true;
|
||||
await addStepLog(
|
||||
visibleStep,
|
||||
'检测到认证页仍在安全验证/授权跳转中,暂停本地回调超时判定,继续等待 localhost 回调...',
|
||||
'info'
|
||||
);
|
||||
}
|
||||
return Boolean(deferred);
|
||||
} catch (error) {
|
||||
await addStepLog(
|
||||
visibleStep,
|
||||
`复核认证页跳转状态失败(${error?.message || error}),继续按原超时规则等待回调。`,
|
||||
'warn'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkCallbackTimeout = async () => {
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
const elapsedMs = Date.now() - callbackWaitStartedAt;
|
||||
if (await isCallbackTimeoutDeferred(elapsedMs)) {
|
||||
timeoutCheckTimer = setTimeout(checkCallbackTimeout, CALLBACK_TIMEOUT_CHECK_INTERVAL_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (elapsedMs >= LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS) {
|
||||
rejectStep9(new Error(`${Math.round(LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof getOAuthFlowRemainingMs === 'function') {
|
||||
try {
|
||||
await getOAuthFlowRemainingMs({
|
||||
step: visibleStep,
|
||||
actionLabel: 'OAuth localhost 回调',
|
||||
oauthUrl: activeState?.oauthUrl || '',
|
||||
});
|
||||
} catch (error) {
|
||||
rejectStep9(error);
|
||||
return;
|
||||
}
|
||||
} else if (elapsedMs >= callbackTimeoutMs) {
|
||||
rejectStep9(new Error(`${Math.round(callbackTimeoutMs / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutCheckTimer = setTimeout(checkCallbackTimeout, CALLBACK_TIMEOUT_CHECK_INTERVAL_MS);
|
||||
};
|
||||
|
||||
timeoutCheckTimer = setTimeout(
|
||||
checkCallbackTimeout,
|
||||
Math.min(CALLBACK_TIMEOUT_CHECK_INTERVAL_MS, Math.max(1, callbackTimeoutMs))
|
||||
);
|
||||
|
||||
setStep8PendingReject((error) => {
|
||||
rejectStep9(error);
|
||||
});
|
||||
|
||||
setWebNavListener((details) => {
|
||||
const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId);
|
||||
finalizeStep9Callback(callbackUrl);
|
||||
});
|
||||
|
||||
setWebNavCommittedListener((details) => {
|
||||
const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId);
|
||||
finalizeStep9Callback(callbackUrl);
|
||||
});
|
||||
|
||||
setStep8TabUpdatedListener((tabId, changeInfo, tab) => {
|
||||
const callbackUrl = getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId);
|
||||
finalizeStep9Callback(callbackUrl);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
signupTabId = await getTabId('openai-auth');
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
|
||||
if (signupTabId && await isTabAlive('openai-auth')) {
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await addStepLog(visibleStep, '已切回认证页,正在准备调试器点击...');
|
||||
} else {
|
||||
signupTabId = await reuseOrCreateTab('openai-auth', 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 };
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
(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', 'flows/openai/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
@@ -0,0 +1,962 @@
|
||||
(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(
|
||||
'openai-auth',
|
||||
{
|
||||
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(
|
||||
'openai-auth',
|
||||
{
|
||||
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('openai-auth');
|
||||
if (authTabId) {
|
||||
await chrome.tabs.update(authTabId, { active: true });
|
||||
return authTabId;
|
||||
}
|
||||
if (!state?.oauthUrl) {
|
||||
throw new Error(`步骤 ${visibleStep}:缺少登录用 OAuth 链接,请先完成刷新 OAuth 并登录。`);
|
||||
}
|
||||
return reuseOrCreateTab('openai-auth', 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('openai-auth');
|
||||
|
||||
if (authTabId) {
|
||||
await chrome.tabs.update(authTabId, { active: true });
|
||||
} else {
|
||||
if (!preparedState.oauthUrl) {
|
||||
throw new Error(`步骤 ${visibleStep}:缺少登录用 OAuth 链接,请先完成绑定邮箱后刷新 OAuth 并登录。`);
|
||||
}
|
||||
await reuseOrCreateTab('openai-auth', 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('openai-auth');
|
||||
|
||||
if (authTabId) {
|
||||
await chrome.tabs.update(authTabId, { active: true });
|
||||
} else {
|
||||
if (!state.oauthUrl) {
|
||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`);
|
||||
}
|
||||
await reuseOrCreateTab('openai-auth', state.oauthUrl);
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
let pageState = await ensureStep8VerificationPageReady({
|
||||
visibleStep,
|
||||
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
|
||||
allowPhoneVerificationPage: true,
|
||||
allowAddEmailPage: true,
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
|
||||
});
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { nodeId: state?.nodeId });
|
||||
return;
|
||||
}
|
||||
const phoneLoginCodeMode = isPhoneLoginCodeMode(state);
|
||||
if (phoneLoginCodeMode) {
|
||||
if (pageState?.state === 'phone_verification_page') {
|
||||
return executeLoginPhoneCodeStep(state, authTabId, visibleStep);
|
||||
}
|
||||
if (pageState?.state === 'add_email_page') {
|
||||
await completeStep8WhenDeferredToBindEmail(visibleStep, { nodeId: state?.nodeId });
|
||||
return;
|
||||
}
|
||||
if (pageState?.state === 'verification_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:手机号注册模式只允许处理手机登录验证码,当前进入了普通邮箱登录验证码页,不会回落到邮箱 provider。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
if (pageState?.state === 'add_phone_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:手机号注册模式不应进入添加手机号页。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
throw new Error(`步骤 ${visibleStep}:手机号注册模式登录验证码步骤进入了不允许的页面:${pageState?.state || 'unknown'}。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
|
||||
if (pageState?.state === 'add_phone_page' || pageState?.state === 'phone_verification_page') {
|
||||
await completeStep8WhenDeferredToPostLoginPhone(visibleStep, pageState, { nodeId: state?.nodeId });
|
||||
return;
|
||||
}
|
||||
if (pageState?.state === 'add_email_page') {
|
||||
throw new Error(`步骤 ${visibleStep}:邮箱注册模式不应进入添加邮箱页。URL: ${pageState?.url || ''}`.trim());
|
||||
}
|
||||
|
||||
return pollEmailVerificationCode(state, pageState, visibleStep, runtime);
|
||||
}
|
||||
|
||||
function isStep8RestartStep7Error(error) {
|
||||
const message = String(error?.message || error || '');
|
||||
return /STEP8_RESTART_STEP7::/i.test(message);
|
||||
}
|
||||
|
||||
async function executeStep8(state) {
|
||||
let currentState = state;
|
||||
let mailPollingAttempt = 1;
|
||||
let lastMailPollingError = null;
|
||||
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||
let retryWithoutStep7Streak = 0;
|
||||
const maxRetryWithoutStep7Streak = 3;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const result = await runStep8Attempt(currentState, {
|
||||
stickyLastResendAt,
|
||||
onResendRequestedAt: async (requestedAt) => {
|
||||
const numericRequestedAt = Number(requestedAt) || 0;
|
||||
if (numericRequestedAt > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, numericRequestedAt);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (Number(result?.lastResendAt) > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, Number(result.lastResendAt) || 0);
|
||||
}
|
||||
retryWithoutStep7Streak = 0;
|
||||
return;
|
||||
} catch (err) {
|
||||
const visibleStep = getVisibleStep(currentState, 8);
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
let currentError = err;
|
||||
let retryWithoutStep7 = false;
|
||||
|
||||
const isMailPollingError = isVerificationMailPollingError(err);
|
||||
if (isMailPollingError && !isStep8RestartStep7Error(err)) {
|
||||
const recovery = await recoverStep8PollingFailure(currentState, visibleStep);
|
||||
if (recovery?.outcome === 'completed') {
|
||||
return;
|
||||
}
|
||||
if (recovery?.outcome === 'retry_without_step7') {
|
||||
retryWithoutStep7 = true;
|
||||
}
|
||||
if (recovery?.error) {
|
||||
currentError = recovery.error;
|
||||
}
|
||||
}
|
||||
if (!isVerificationMailPollingError(currentError) && !isStep8RestartStep7Error(currentError)) {
|
||||
throw currentError;
|
||||
}
|
||||
|
||||
lastMailPollingError = currentError;
|
||||
if (mailPollingAttempt >= STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS) {
|
||||
break;
|
||||
}
|
||||
|
||||
mailPollingAttempt += 1;
|
||||
if (retryWithoutStep7) {
|
||||
retryWithoutStep7Streak += 1;
|
||||
if (retryWithoutStep7Streak > maxRetryWithoutStep7Streak) {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:邮箱通信异常在当前链路已连续重试 ${retryWithoutStep7Streak} 次,改为回到步骤 ${authLoginStep} 重新发起授权链路,避免空轮询循环。`,
|
||||
'warn'
|
||||
);
|
||||
await rerunStep7ForStep8Recovery({
|
||||
logMessage: `邮箱通信异常持续未恢复,正在回到步骤 ${authLoginStep} 重新发起登录流程...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: 'fetch-login-code',
|
||||
});
|
||||
currentState = await getState();
|
||||
retryWithoutStep7Streak = 0;
|
||||
continue;
|
||||
}
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}(连续同链路重试 ${retryWithoutStep7Streak}/${maxRetryWithoutStep7Streak})。`,
|
||||
'warn'
|
||||
);
|
||||
const latestState = await getState();
|
||||
const latestStateResendAt = Number(latestState?.loginVerificationRequestedAt) || 0;
|
||||
if (latestStateResendAt > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, latestStateResendAt);
|
||||
}
|
||||
currentState = latestState;
|
||||
if (stickyLastResendAt > 0 && (!latestStateResendAt || latestStateResendAt < stickyLastResendAt)) {
|
||||
currentState = {
|
||||
...latestState,
|
||||
loginVerificationRequestedAt: stickyLastResendAt,
|
||||
};
|
||||
}
|
||||
const resendIntervalMs = getStep8ResendIntervalMs(currentState);
|
||||
const remainingBeforeRetryMs = stickyLastResendAt > 0 && resendIntervalMs > 0
|
||||
? Math.max(0, resendIntervalMs - (Date.now() - stickyLastResendAt))
|
||||
: 0;
|
||||
if (remainingBeforeRetryMs > 0 && typeof sleepWithStop === 'function') {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:上轮已触发重发验证码,为避免重复重发,先等待 ${Math.ceil(remainingBeforeRetryMs / 1000)} 秒后继续当前链路重试。`,
|
||||
'info'
|
||||
);
|
||||
await sleepWithStop(Math.min(remainingBeforeRetryMs, 3000));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
retryWithoutStep7Streak = 0;
|
||||
await addLog(
|
||||
isStep8RestartStep7Error(currentError)
|
||||
? `步骤 ${visibleStep}:检测到认证页进入重试/超时报错状态,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`
|
||||
: `步骤 ${visibleStep}:检测到邮箱轮询类失败,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`,
|
||||
'warn'
|
||||
);
|
||||
await rerunStep7ForStep8Recovery({
|
||||
logMessage: isStep8RestartStep7Error(currentError)
|
||||
? `认证页进入重试/超时报错状态,正在回到步骤 ${authLoginStep} 重新发起登录流程...`
|
||||
: `正在回到步骤 ${authLoginStep},重新发起登录验证码流程...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: 'fetch-login-code',
|
||||
});
|
||||
currentState = await getState();
|
||||
}
|
||||
}
|
||||
|
||||
const visibleStep = getVisibleStep(currentState, 8);
|
||||
if (lastMailPollingError) {
|
||||
throw new Error(
|
||||
`步骤 ${visibleStep}:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`步骤 ${visibleStep}:登录验证码流程未成功完成。`);
|
||||
}
|
||||
|
||||
return {
|
||||
executeStep8,
|
||||
executePostLoginPhoneVerification,
|
||||
executeBindEmail,
|
||||
executeFetchBindEmailCode,
|
||||
executeBoundEmailLoginCode,
|
||||
executeBoundEmailPostLoginPhoneVerification,
|
||||
};
|
||||
}
|
||||
|
||||
return { createStep8Executor };
|
||||
});
|
||||
@@ -0,0 +1,303 @@
|
||||
(function attachBackgroundStep4(root, factory) {
|
||||
root.MultiPageBackgroundStep4 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep4Module() {
|
||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||||
|
||||
function createStep4Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeNodeFromBackground,
|
||||
confirmCustomVerificationStepBypass,
|
||||
generateRandomBirthday,
|
||||
generateRandomName,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession,
|
||||
getMailConfig,
|
||||
getTabId,
|
||||
HOTMAIL_PROVIDER,
|
||||
isTabAlive,
|
||||
LUCKMAIL_PROVIDER,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
CLOUD_MAIL_PROVIDER = 'cloudmail',
|
||||
resolveVerificationStep,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
isRetryableContentScriptTransportError = () => false,
|
||||
shouldUseCustomRegistrationEmail,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
throwIfStopped,
|
||||
waitForTabStableComplete = null,
|
||||
phoneVerificationHelpers = null,
|
||||
resolveSignupMethod = () => 'email',
|
||||
} = deps;
|
||||
|
||||
function buildSignupProfileForVerificationStep() {
|
||||
const name = typeof generateRandomName === 'function' ? generateRandomName() : null;
|
||||
const birthday = typeof generateRandomBirthday === 'function' ? generateRandomBirthday() : null;
|
||||
if (!name?.firstName || !name?.lastName || !birthday) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
firstName: name.firstName,
|
||||
lastName: name.lastName,
|
||||
year: birthday.year,
|
||||
month: birthday.month,
|
||||
day: birthday.day,
|
||||
};
|
||||
}
|
||||
|
||||
function getExpectedMail2925MailboxEmail(state = {}) {
|
||||
if (Boolean(state?.mail2925UseAccountPool)) {
|
||||
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
|
||||
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
|
||||
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
|
||||
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
|
||||
if (accountEmail) {
|
||||
return accountEmail;
|
||||
}
|
||||
}
|
||||
|
||||
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isPhoneSignupState(state = {}) {
|
||||
return resolveSignupMethod(state) === 'phone'
|
||||
|| state?.accountIdentifierType === 'phone'
|
||||
|| Boolean(state?.signupPhoneActivation);
|
||||
}
|
||||
|
||||
async function executeSignupPhoneCodeStep(state, signupTabId) {
|
||||
if (typeof phoneVerificationHelpers?.completeSignupPhoneVerificationFlow !== 'function') {
|
||||
throw new Error('步骤 4:手机号注册验证码流程不可用,接码模块尚未初始化。');
|
||||
}
|
||||
|
||||
const signupProfile = buildSignupProfileForVerificationStep();
|
||||
const result = await phoneVerificationHelpers.completeSignupPhoneVerificationFlow(signupTabId, {
|
||||
state,
|
||||
signupProfile,
|
||||
});
|
||||
|
||||
if (result?.emailVerificationRequired || result?.emailVerificationPage) {
|
||||
return result || {};
|
||||
}
|
||||
|
||||
await completeNodeFromBackground('fetch-signup-code', {
|
||||
phoneVerification: true,
|
||||
code: result?.code || '',
|
||||
...(result?.skipProfileStep ? { skipProfileStep: true } : {}),
|
||||
...(result?.skipProfileStepReason ? { skipProfileStepReason: result.skipProfileStepReason } : {}),
|
||||
});
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey) {
|
||||
if (shouldUseCustomRegistrationEmail(state)) {
|
||||
await confirmCustomVerificationStepBypass(4);
|
||||
return;
|
||||
}
|
||||
|
||||
const mail = getMailConfig(state);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
|
||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: stepStartedAt;
|
||||
|
||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
||||
await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info');
|
||||
await ensureIcloudMailSession({
|
||||
state,
|
||||
step: 4,
|
||||
actionLabel: '步骤 4:确认 iCloud 邮箱登录态',
|
||||
});
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
if (
|
||||
mail.provider === HOTMAIL_PROVIDER
|
||||
|| mail.provider === LUCKMAIL_PROVIDER
|
||||
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|
||||
|| mail.provider === CLOUD_MAIL_PROVIDER
|
||||
) {
|
||||
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
|
||||
} else if (mail.provider === '2925') {
|
||||
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||
if (typeof ensureMail2925MailboxSession === 'function') {
|
||||
await ensureMail2925MailboxSession({
|
||||
accountId: state.currentMail2925AccountId || null,
|
||||
forceRelogin: false,
|
||||
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
|
||||
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
|
||||
actionLabel: '步骤 4:确认 2925 邮箱登录态',
|
||||
});
|
||||
} else {
|
||||
await focusOrOpenMailTab(mail);
|
||||
}
|
||||
await addLog(`步骤 4:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
|
||||
} else {
|
||||
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||
await focusOrOpenMailTab(mail);
|
||||
}
|
||||
|
||||
const shouldRequestFreshCodeFirst = ![
|
||||
HOTMAIL_PROVIDER,
|
||||
LUCKMAIL_PROVIDER,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
CLOUD_MAIL_PROVIDER,
|
||||
].includes(mail.provider);
|
||||
const signupProfile = buildSignupProfileForVerificationStep();
|
||||
|
||||
await resolveVerificationStep(4, state, mail, {
|
||||
filterAfterTimestamp: verificationFilterAfterTimestamp,
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
|
||||
signupProfile,
|
||||
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
|
||||
? 15000
|
||||
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
? 0
|
||||
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
|
||||
});
|
||||
}
|
||||
|
||||
async function focusOrOpenMailTab(mail) {
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
if (mail.navigateOnReuse) {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = await getTabId(mail.source);
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
return;
|
||||
}
|
||||
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeStep4(state) {
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationSessionKey = `4:${stepStartedAt}`;
|
||||
const signupTabId = await getTabId('openai-auth');
|
||||
|
||||
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('openai-auth', prepareRequest, {
|
||||
responseTimeoutMs: prepareResponseTimeoutMs,
|
||||
})
|
||||
: await sendToContentScriptResilient('openai-auth', 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('openai-auth', {
|
||||
type: 'RECOVER_AUTH_RETRY_PAGE',
|
||||
step: 4,
|
||||
source: 'background',
|
||||
payload: {
|
||||
flow: 'signup',
|
||||
step: 4,
|
||||
timeoutMs: Math.min(12000, remainingMs),
|
||||
maxClickAttempts: 2,
|
||||
logLabel: '步骤 4:检测到注册认证重试页,正在点击“重试”恢复',
|
||||
},
|
||||
}, {
|
||||
timeoutMs: Math.min(12000, remainingMs),
|
||||
responseTimeoutMs: Math.min(12000, remainingMs),
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...',
|
||||
});
|
||||
|
||||
if (recoverResult?.error) {
|
||||
throw new Error(recoverResult.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prepareResult) {
|
||||
throw new Error('步骤 4:等待注册验证码页面就绪超时,请刷新认证页后重试。');
|
||||
}
|
||||
|
||||
if (prepareResult && prepareResult.error) {
|
||||
throw new Error(prepareResult.error);
|
||||
}
|
||||
if (prepareResult?.alreadyVerified) {
|
||||
await completeNodeFromBackground('fetch-signup-code', prepareResult?.skipProfileStep ? { skipProfileStep: true } : {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPhoneSignupState(state)) {
|
||||
const phoneResult = await executeSignupPhoneCodeStep(state, signupTabId);
|
||||
if (phoneResult?.emailVerificationRequired || phoneResult?.emailVerificationPage) {
|
||||
await addLog('步骤 4:手机验证码已通过,OpenAI 要求继续邮箱验证,切换到邮箱验证码轮询。', 'info');
|
||||
return executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey);
|
||||
}
|
||||
return phoneResult;
|
||||
}
|
||||
|
||||
return executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey);
|
||||
}
|
||||
|
||||
return { executeStep4 };
|
||||
}
|
||||
|
||||
return { createStep4Executor };
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
(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,
|
||||
OPENAI_AUTH_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('openai-auth');
|
||||
if (!signupTabId || !(await isTabAlive('openai-auth'))) {
|
||||
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('openai-auth', signupTabId, {
|
||||
inject: OPENAI_AUTH_INJECT_FILES,
|
||||
injectSource: 'openai-auth',
|
||||
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('openai-auth', {
|
||||
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
@@ -0,0 +1,37 @@
|
||||
(function attachBackgroundStep5(root, factory) {
|
||||
root.MultiPageBackgroundStep5 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep5Module() {
|
||||
function createStep5Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
generateRandomBirthday,
|
||||
generateRandomName,
|
||||
sendToContentScript,
|
||||
} = deps;
|
||||
|
||||
async function executeStep5() {
|
||||
const { firstName, lastName } = generateRandomName();
|
||||
const { year, month, day } = generateRandomBirthday();
|
||||
|
||||
await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
|
||||
|
||||
await sendToContentScript('openai-auth', {
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'fill-profile',
|
||||
step: 5,
|
||||
source: 'background',
|
||||
payload: {
|
||||
firstName,
|
||||
lastName,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { executeStep5 };
|
||||
}
|
||||
|
||||
return { createStep5Executor };
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
||||
(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,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,403 @@
|
||||
(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('openai-auth', oauthUrl, { forceNew: true });
|
||||
|
||||
const result = await sendToContentScriptResilient(
|
||||
'openai-auth',
|
||||
{
|
||||
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 };
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
(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 };
|
||||
});
|
||||
@@ -0,0 +1,310 @@
|
||||
(function attachBackgroundPayPalApprove(root, factory) {
|
||||
root.MultiPageBackgroundPayPalApprove = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPayPalApproveModule() {
|
||||
const PAYPAL_SOURCE = 'paypal-flow';
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const PAYPAL_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'flows/openai/content/paypal-flow.js'];
|
||||
const PAYPAL_LOGIN_TRANSITION_TIMEOUT_MS = 30000;
|
||||
const PAYPAL_LOGIN_TRANSITION_POLL_MS = 500;
|
||||
|
||||
function createPayPalApproveExecutor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
queryTabsInAutomationWindow = null,
|
||||
sendTabMessageUntilStopped,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForTabCompleteUntilStopped,
|
||||
waitForTabUrlMatchUntilStopped,
|
||||
} = deps;
|
||||
|
||||
async function resolvePayPalTabId(state = {}) {
|
||||
const paypalTabId = await getTabId(PAYPAL_SOURCE);
|
||||
if (paypalTabId && await isTabAlive(PAYPAL_SOURCE)) {
|
||||
return paypalTabId;
|
||||
}
|
||||
const discoveredPayPalTabId = await findOpenPayPalTabId();
|
||||
if (discoveredPayPalTabId) {
|
||||
await addLog('步骤 8:已从当前浏览器标签中发现 PayPal 页面,正在接管继续执行。', 'info');
|
||||
return discoveredPayPalTabId;
|
||||
}
|
||||
const checkoutTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
|
||||
if (checkoutTabId) {
|
||||
return checkoutTabId;
|
||||
}
|
||||
const storedTabId = Number(state.plusCheckoutTabId) || 0;
|
||||
if (storedTabId) {
|
||||
return storedTabId;
|
||||
}
|
||||
throw new Error('步骤 8:未找到 PayPal 标签页,请先完成步骤 7。');
|
||||
}
|
||||
|
||||
async function findOpenPayPalTabId() {
|
||||
if (!chrome?.tabs?.query) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const queryTabs = typeof queryTabsInAutomationWindow === 'function'
|
||||
? queryTabsInAutomationWindow
|
||||
: (queryInfo) => chrome.tabs.query(queryInfo);
|
||||
const tabs = await queryTabs({}).catch(() => []);
|
||||
const candidates = (Array.isArray(tabs) ? tabs : [])
|
||||
.filter((tab) => Number.isInteger(tab?.id) && isPayPalUrl(tab.url || ''));
|
||||
if (!candidates.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const match = candidates.find((tab) => tab.active && tab.currentWindow)
|
||||
|| candidates.find((tab) => tab.active)
|
||||
|| candidates[0];
|
||||
if (match?.id && chrome?.tabs?.update) {
|
||||
await chrome.tabs.update(match.id, { active: true }).catch(() => {});
|
||||
}
|
||||
return match?.id || 0;
|
||||
}
|
||||
|
||||
async function ensurePayPalReady(tabId, logMessage = '') {
|
||||
await waitForTabUrlMatchUntilStopped(tabId, (url) => /paypal\./i.test(url));
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
await sleepWithStop(1000);
|
||||
await ensureContentScriptReadyOnTabUntilStopped(PAYPAL_SOURCE, tabId, {
|
||||
inject: PAYPAL_INJECT_FILES,
|
||||
injectSource: PAYPAL_SOURCE,
|
||||
logMessage: logMessage || '步骤 8:PayPal 页面仍在加载,等待脚本就绪...',
|
||||
});
|
||||
}
|
||||
|
||||
async function getPayPalState(tabId) {
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_GET_STATE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function dismissPrompts(tabId) {
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_DISMISS_PROMPTS',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
function resolvePayPalCredentials(state = {}) {
|
||||
const currentId = String(state?.currentPayPalAccountId || '').trim();
|
||||
const accounts = Array.isArray(state?.paypalAccounts) ? state.paypalAccounts : [];
|
||||
const selectedAccount = currentId
|
||||
? accounts.find((account) => String(account?.id || '').trim() === currentId) || null
|
||||
: null;
|
||||
return {
|
||||
email: String(selectedAccount?.email || state?.paypalEmail || '').trim(),
|
||||
password: String(selectedAccount?.password || state?.paypalPassword || ''),
|
||||
};
|
||||
}
|
||||
|
||||
async function submitLogin(tabId, state = {}) {
|
||||
const credentials = resolvePayPalCredentials(state);
|
||||
if (!credentials.password) {
|
||||
throw new Error('步骤 8:未配置可用的 PayPal 账号,请先在侧边栏添加并选择账号。');
|
||||
}
|
||||
await addLog('步骤 8:正在填写 PayPal 登录信息并提交...', 'info');
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_SUBMIT_LOGIN',
|
||||
source: 'background',
|
||||
payload: {
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
},
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
function isPayPalUrl(url = '') {
|
||||
return /paypal\./i.test(String(url || ''));
|
||||
}
|
||||
|
||||
function isPayPalPasswordState(pageState = {}) {
|
||||
return Boolean(pageState.hasPasswordInput)
|
||||
|| pageState.loginPhase === 'password'
|
||||
|| pageState.loginPhase === 'login_combined';
|
||||
}
|
||||
|
||||
async function waitForPayPalPostLoginDecision(tabId, actionResult = {}) {
|
||||
const phase = String(actionResult?.phase || '').trim();
|
||||
const startedAt = Date.now();
|
||||
|
||||
while (Date.now() - startedAt < PAYPAL_LOGIN_TRANSITION_TIMEOUT_MS) {
|
||||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||
if (!tab) {
|
||||
throw new Error('步骤 8:PayPal 标签页已关闭,无法继续识别登录后的页面。');
|
||||
}
|
||||
|
||||
const currentUrl = tab.url || '';
|
||||
if (!currentUrl) {
|
||||
await sleepWithStop(PAYPAL_LOGIN_TRANSITION_POLL_MS);
|
||||
continue;
|
||||
}
|
||||
if (currentUrl && !isPayPalUrl(currentUrl)) {
|
||||
return {
|
||||
outcome: 'left_paypal',
|
||||
url: currentUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (tab.status !== 'complete') {
|
||||
await sleepWithStop(PAYPAL_LOGIN_TRANSITION_POLL_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
await ensurePayPalReady(
|
||||
tabId,
|
||||
phase === 'email_submitted'
|
||||
? '步骤 8:PayPal 账号已提交,正在识别下一页...'
|
||||
: '步骤 8:PayPal 密码已提交,正在识别跳转结果...'
|
||||
);
|
||||
const pageState = await getPayPalState(tabId);
|
||||
|
||||
if (pageState.hasPasskeyPrompt) {
|
||||
return {
|
||||
outcome: 'prompt',
|
||||
pageState,
|
||||
};
|
||||
}
|
||||
|
||||
if (pageState.approveReady) {
|
||||
return {
|
||||
outcome: 'approve_ready',
|
||||
pageState,
|
||||
};
|
||||
}
|
||||
|
||||
if (phase === 'email_submitted' && isPayPalPasswordState(pageState)) {
|
||||
return {
|
||||
outcome: 'password_ready',
|
||||
pageState,
|
||||
};
|
||||
}
|
||||
|
||||
if (phase === 'password_submitted' && !pageState.needsLogin) {
|
||||
return {
|
||||
outcome: 'post_login_state',
|
||||
pageState,
|
||||
};
|
||||
}
|
||||
|
||||
await sleepWithStop(PAYPAL_LOGIN_TRANSITION_POLL_MS);
|
||||
}
|
||||
|
||||
return {
|
||||
outcome: 'timeout',
|
||||
phase,
|
||||
};
|
||||
}
|
||||
|
||||
async function clickApprove(tabId) {
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_CLICK_APPROVE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return Boolean(result?.clicked);
|
||||
}
|
||||
|
||||
async function executePayPalApprove(state = {}) {
|
||||
const tabId = await resolvePayPalTabId(state);
|
||||
await ensurePayPalReady(tabId);
|
||||
await setState({ plusCheckoutTabId: tabId });
|
||||
|
||||
let loggedWaiting = false;
|
||||
while (true) {
|
||||
const currentUrl = (await chrome.tabs.get(tabId).catch(() => null))?.url || '';
|
||||
if (currentUrl && !isPayPalUrl(currentUrl)) {
|
||||
await addLog('步骤 8:PayPal 已跳转离开授权页,准备进入回跳确认。', 'ok');
|
||||
break;
|
||||
}
|
||||
|
||||
await ensurePayPalReady(tabId, '步骤 8:PayPal 页面正在切换,等待脚本重新就绪...');
|
||||
const pageState = await getPayPalState(tabId);
|
||||
|
||||
if (pageState.needsLogin) {
|
||||
const submitResult = await submitLogin(tabId, state);
|
||||
const decision = await waitForPayPalPostLoginDecision(tabId, submitResult);
|
||||
if (decision.outcome === 'left_paypal') {
|
||||
await addLog('步骤 8:PayPal 登录后已跳转离开登录/授权页,继续进入回跳确认。', 'ok');
|
||||
break;
|
||||
}
|
||||
if (decision.outcome === 'password_ready') {
|
||||
await addLog('步骤 8:PayPal 账号页提交后已识别到密码页,继续填写密码。', 'info');
|
||||
} else if (decision.outcome === 'approve_ready') {
|
||||
await addLog('步骤 8:PayPal 登录后已识别到授权确认页,继续点击授权。', 'info');
|
||||
} else if (decision.outcome === 'prompt') {
|
||||
await addLog('步骤 8:PayPal 登录后已识别到提示弹窗,继续处理弹窗。', 'info');
|
||||
} else if (decision.outcome === 'timeout') {
|
||||
await addLog('步骤 8:PayPal 登录动作后暂未识别到新页面,重新检查当前页面状态。', 'warn');
|
||||
}
|
||||
loggedWaiting = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageState.hasPasskeyPrompt) {
|
||||
await addLog('步骤 8:检测到 PayPal 通行密钥提示,正在关闭...', 'info');
|
||||
await dismissPrompts(tabId);
|
||||
await sleepWithStop(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
const dismissed = await dismissPrompts(tabId).catch(() => ({ clicked: 0 }));
|
||||
if (dismissed.clicked) {
|
||||
await sleepWithStop(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageState.approveReady) {
|
||||
await addLog('步骤 8:正在点击 PayPal“同意并继续”...', 'info');
|
||||
const clicked = await clickApprove(tabId);
|
||||
if (clicked) {
|
||||
await setState({ plusPaypalApprovedAt: Date.now() });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!loggedWaiting) {
|
||||
loggedWaiting = true;
|
||||
await addLog('步骤 8:等待 PayPal 授权按钮或下一步页面出现...', 'info');
|
||||
}
|
||||
await sleepWithStop(500);
|
||||
}
|
||||
|
||||
await completeNodeFromBackground('paypal-approve', {
|
||||
plusPaypalApprovedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
executePayPalApprove,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createPayPalApproveExecutor,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,434 @@
|
||||
(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 };
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
(function attachBackgroundPlusReturnConfirm(root, factory) {
|
||||
root.MultiPageBackgroundPlusReturnConfirm = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusReturnConfirmModule() {
|
||||
const PAYPAL_SOURCE = 'paypal-flow';
|
||||
const GOPAY_SOURCE = 'gopay-flow';
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const PLUS_RETURN_SETTLE_WAIT_MS = 20000;
|
||||
|
||||
function createPlusReturnConfirmExecutor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
completeNodeFromBackground,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForTabUrlMatchUntilStopped,
|
||||
} = deps;
|
||||
|
||||
async function resolveReturnTabId(state = {}) {
|
||||
const paypalTabId = await getTabId(PAYPAL_SOURCE);
|
||||
if (paypalTabId && await isTabAlive(PAYPAL_SOURCE)) {
|
||||
return paypalTabId;
|
||||
}
|
||||
const gopayTabId = await getTabId(GOPAY_SOURCE);
|
||||
if (gopayTabId && await isTabAlive(GOPAY_SOURCE)) {
|
||||
return gopayTabId;
|
||||
}
|
||||
const checkoutTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
|
||||
if (checkoutTabId) {
|
||||
return checkoutTabId;
|
||||
}
|
||||
const storedTabId = Number(state.plusCheckoutTabId) || 0;
|
||||
if (storedTabId) {
|
||||
return storedTabId;
|
||||
}
|
||||
throw new Error('步骤 9:未找到 Plus / PayPal / GoPay 标签页,无法确认订阅回跳。');
|
||||
}
|
||||
|
||||
function isReturnUrl(url = '') {
|
||||
return /https:\/\/(?:chatgpt\.com|chat\.openai\.com|openai\.com)\//i.test(String(url || ''))
|
||||
&& !/paypal\.|gopay|gojek|midtrans|xendit|stripe/i.test(String(url || ''));
|
||||
}
|
||||
|
||||
async function executePlusReturnConfirm(state = {}) {
|
||||
const tabId = await resolveReturnTabId(state);
|
||||
await addLog('步骤 9:正在等待支付授权后回跳到 ChatGPT / OpenAI 页面...', 'info');
|
||||
const tab = await waitForTabUrlMatchUntilStopped(tabId, isReturnUrl);
|
||||
await addLog('步骤 9:已检测到订阅回跳页面,固定等待 20 秒让页面完成加载。', 'info');
|
||||
await sleepWithStop(PLUS_RETURN_SETTLE_WAIT_MS);
|
||||
|
||||
await setState({
|
||||
plusCheckoutTabId: tabId,
|
||||
plusReturnUrl: tab?.url || '',
|
||||
});
|
||||
await completeNodeFromBackground('plus-checkout-return', {
|
||||
plusReturnUrl: tab?.url || '',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
executePlusReturnConfirm,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createPlusReturnConfirmExecutor,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
(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', 'flows/openai/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,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,509 @@
|
||||
(function attachBackgroundStep2(root, factory) {
|
||||
root.MultiPageBackgroundStep2 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep2Module() {
|
||||
function createStep2Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTab,
|
||||
ensureSignupAuthEntryPageReady,
|
||||
ensureSignupEntryPageReady,
|
||||
ensureSignupPostEmailPageReadyInTab,
|
||||
ensureSignupPostIdentityPageReadyInTab = ensureSignupPostEmailPageReadyInTab,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
phoneVerificationHelpers = null,
|
||||
resolveSignupMethod = () => 'email',
|
||||
resolveSignupEmailForFlow,
|
||||
sendToContentScriptResilient,
|
||||
OPENAI_AUTH_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 [\w-]+ 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('openai-auth', {
|
||||
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('openai-auth', {
|
||||
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('openai-auth', {
|
||||
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('openai-auth');
|
||||
if (!signupTabId || !(await isTabAlive('openai-auth'))) {
|
||||
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('openai-auth', signupTabId, {
|
||||
inject: OPENAI_AUTH_INJECT_FILES,
|
||||
injectSource: 'openai-auth',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...',
|
||||
});
|
||||
}
|
||||
return signupTabId;
|
||||
}
|
||||
|
||||
function normalizeSignupPhoneActivationForStep2(activation) {
|
||||
if (typeof phoneVerificationHelpers?.normalizeActivation === 'function') {
|
||||
return phoneVerificationHelpers.normalizeActivation(activation);
|
||||
}
|
||||
if (!activation || typeof activation !== 'object' || Array.isArray(activation)) {
|
||||
return null;
|
||||
}
|
||||
const activationId = String(activation.activationId ?? activation.id ?? activation.activation ?? '').trim();
|
||||
const phoneNumber = String(activation.phoneNumber ?? activation.number ?? activation.phone ?? '').trim();
|
||||
if (!activationId || !phoneNumber) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...activation,
|
||||
activationId,
|
||||
phoneNumber,
|
||||
};
|
||||
}
|
||||
|
||||
function getSignupPhoneNumberFromState(state = {}) {
|
||||
return String(
|
||||
state?.signupPhoneNumber
|
||||
|| (String(state?.accountIdentifierType || '').trim().toLowerCase() === 'phone' ? state?.accountIdentifier : '')
|
||||
|| ''
|
||||
).trim();
|
||||
}
|
||||
|
||||
async function resolveSignupPhoneForStep2(state = {}) {
|
||||
const existingActivation = normalizeSignupPhoneActivationForStep2(state?.signupPhoneActivation);
|
||||
if (existingActivation?.phoneNumber) {
|
||||
await addLog(`步骤 2:复用当前注册手机号 ${existingActivation.phoneNumber},不重新获取号码。`);
|
||||
return {
|
||||
phoneNumber: existingActivation.phoneNumber,
|
||||
activation: existingActivation,
|
||||
};
|
||||
}
|
||||
|
||||
const manualPhoneNumber = getSignupPhoneNumberFromState(state);
|
||||
if (manualPhoneNumber) {
|
||||
await addLog(`步骤 2:使用手动填写的注册手机号 ${manualPhoneNumber},本轮不会重新获取号码。`, 'warn');
|
||||
return {
|
||||
phoneNumber: manualPhoneNumber,
|
||||
activation: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof phoneVerificationHelpers?.prepareSignupPhoneActivation !== 'function') {
|
||||
throw new Error('手机号注册流程不可用:接码模块尚未初始化。');
|
||||
}
|
||||
const activation = await phoneVerificationHelpers.prepareSignupPhoneActivation(state);
|
||||
return {
|
||||
phoneNumber: activation.phoneNumber,
|
||||
activation,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeSignupPhoneEntry(state) {
|
||||
let signupTabId = await ensureSignupTabForStep2();
|
||||
if (await shouldForceAuthEntryRetry(signupTabId)) {
|
||||
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交手机号。', 'warn');
|
||||
try {
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
} catch (entryError) {
|
||||
const entryErrorMessage = getErrorMessage(entryError);
|
||||
if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交手机号...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureSignupPhoneEntryReady(signupTabId);
|
||||
} catch (entryError) {
|
||||
const entryErrorMessage = getErrorMessage(entryError);
|
||||
if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isSignupPhoneEntryUnavailableErrorMessage(entryErrorMessage)
|
||||
|| isSignupEntryUnavailableErrorMessage(entryErrorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(entryErrorMessage)
|
||||
) {
|
||||
await addLog('步骤 2:手机号注册入口尚未就绪,正在重新打开官网入口后重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
await ensureSignupPhoneEntryReady(signupTabId);
|
||||
} else {
|
||||
throw entryError;
|
||||
}
|
||||
}
|
||||
|
||||
const signupPhone = await resolveSignupPhoneForStep2(state);
|
||||
const { phoneNumber, activation } = signupPhone;
|
||||
let step2Result = await submitSignupPhone(phoneNumber, activation, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:官网注册入口正在切换,等待手机号注册入口恢复...',
|
||||
});
|
||||
|
||||
if (step2Result?.error) {
|
||||
const errorMessage = getErrorMessage(step2Result.error);
|
||||
if (
|
||||
isSignupPhoneEntryUnavailableErrorMessage(errorMessage)
|
||||
|| isSignupEntryUnavailableErrorMessage(errorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(errorMessage)
|
||||
) {
|
||||
await addLog('步骤 2:手机号注册入口不可用或通信超时,正在重新准备手机号注册入口后重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
await ensureSignupPhoneEntryReady(signupTabId);
|
||||
step2Result = await submitSignupPhone(phoneNumber, activation, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:手机号注册入口已就绪,正在重新提交手机号...',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (step2Result?.error) {
|
||||
const finalErrorMessage = getErrorMessage(step2Result.error);
|
||||
if (
|
||||
(isSignupEntryUnavailableErrorMessage(finalErrorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(finalErrorMessage))
|
||||
&& await failStep2OnLoggedInSession(signupTabId, finalErrorMessage)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (activation && typeof phoneVerificationHelpers?.cancelSignupPhoneActivation === 'function') {
|
||||
await phoneVerificationHelpers.cancelSignupPhoneActivation(state, activation).catch(() => {});
|
||||
}
|
||||
throw new Error(finalErrorMessage);
|
||||
}
|
||||
|
||||
await addLog(`步骤 2:手机号 ${phoneNumber} 已提交,正在等待页面加载并确认下一步入口...`);
|
||||
const landingResult = await ensureSignupPostIdentityPageReadyInTab(signupTabId, 2, {
|
||||
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
|
||||
});
|
||||
|
||||
await completeNodeFromBackground('submit-signup-email', {
|
||||
accountIdentifierType: 'phone',
|
||||
accountIdentifier: phoneNumber,
|
||||
signupPhoneNumber: phoneNumber,
|
||||
signupPhoneActivation: activation || null,
|
||||
nextSignupState: landingResult?.state || step2Result?.state || 'password_page',
|
||||
nextSignupUrl: landingResult?.url || step2Result?.url || '',
|
||||
skippedPasswordStep: landingResult?.state === 'phone_verification_page' || landingResult?.state === 'profile_page',
|
||||
});
|
||||
}
|
||||
|
||||
async function executeSignupEmailEntry(state) {
|
||||
const resolvedEmail = await resolveSignupEmailForFlow(state);
|
||||
|
||||
let signupTabId = await ensureSignupTabForStep2();
|
||||
|
||||
if (await shouldForceAuthEntryRetry(signupTabId)) {
|
||||
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交邮箱。', 'warn');
|
||||
try {
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
} catch (entryError) {
|
||||
const entryErrorMessage = getErrorMessage(entryError);
|
||||
if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交邮箱...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
}
|
||||
}
|
||||
|
||||
let step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 35000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...',
|
||||
});
|
||||
|
||||
if (step2Result?.error) {
|
||||
const errorMessage = getErrorMessage(step2Result.error);
|
||||
if (isSignupEntryUnavailableErrorMessage(errorMessage)) {
|
||||
await addLog('步骤 2:未找到邮箱输入入口,正在切换认证入口页后重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 35000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...',
|
||||
});
|
||||
|
||||
if (step2Result?.error) {
|
||||
const retryErrorMessage = getErrorMessage(step2Result.error);
|
||||
if (isSignupEntryUnavailableErrorMessage(retryErrorMessage)) {
|
||||
if (await failStep2OnLoggedInSession(signupTabId, retryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
await addLog('步骤 2:认证入口仍不可用,正在重新进入官网注册入口再重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 35000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:重试官网注册入口后正在重新提交邮箱...',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (isRetryableStep2TransportErrorMessage(errorMessage)) {
|
||||
await addLog('步骤 2:注册入口页通信超时,正在切换认证入口页并重试提交邮箱...', 'warn');
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (step2Result?.error) {
|
||||
const finalErrorMessage = getErrorMessage(step2Result.error);
|
||||
if (
|
||||
(isSignupEntryUnavailableErrorMessage(finalErrorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(finalErrorMessage))
|
||||
&& await failStep2OnLoggedInSession(signupTabId, finalErrorMessage)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
throw new Error(finalErrorMessage);
|
||||
}
|
||||
|
||||
if (!step2Result?.alreadyOnPasswordPage) {
|
||||
await addLog(`步骤 2:邮箱 ${resolvedEmail} 已提交,正在等待页面加载并确认下一步入口...`);
|
||||
}
|
||||
|
||||
const landingResult = await ensureSignupPostEmailPageReadyInTab(signupTabId, 2, {
|
||||
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
|
||||
});
|
||||
|
||||
await completeNodeFromBackground('submit-signup-email', {
|
||||
email: resolvedEmail,
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: resolvedEmail,
|
||||
nextSignupState: landingResult?.state || 'password_page',
|
||||
nextSignupUrl: landingResult?.url || step2Result?.url || '',
|
||||
skippedPasswordStep: landingResult?.state === 'verification_page',
|
||||
});
|
||||
}
|
||||
|
||||
async function executeStep2(state) {
|
||||
if (resolveSignupMethod(state) === 'phone') {
|
||||
return executeSignupPhoneEntry(state);
|
||||
}
|
||||
return executeSignupEmailEntry(state);
|
||||
}
|
||||
|
||||
return { executeStep2 };
|
||||
}
|
||||
|
||||
return { createStep2Executor };
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
(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 };
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
(function authPageRecoveryModule(root, factory) {
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = factory();
|
||||
return;
|
||||
}
|
||||
|
||||
root.MultiPageAuthPageRecovery = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createAuthPageRecoveryModule() {
|
||||
function createAuthPageRecovery(deps = {}) {
|
||||
const {
|
||||
detailPattern = null,
|
||||
getActionText,
|
||||
getPageTextSnapshot,
|
||||
humanPause,
|
||||
isActionEnabled,
|
||||
isVisibleElement,
|
||||
log,
|
||||
performOperationWithDelay: injectedPerformOperationWithDelay,
|
||||
routeErrorPattern = null,
|
||||
simulateClick,
|
||||
sleep,
|
||||
throwIfStopped,
|
||||
titlePattern = null,
|
||||
} = deps;
|
||||
|
||||
function matchesPathPatterns(pathname, pathPatterns = []) {
|
||||
if (!Array.isArray(pathPatterns) || !pathPatterns.length) {
|
||||
return true;
|
||||
}
|
||||
return pathPatterns.some((pattern) => pattern instanceof RegExp && pattern.test(pathname));
|
||||
}
|
||||
|
||||
function getAuthRetryButton(options = {}) {
|
||||
const { allowDisabled = false } = options;
|
||||
const direct = document.querySelector('button[data-dd-action-name="Try again"]');
|
||||
if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const candidates = document.querySelectorAll('button, [role="button"]');
|
||||
return Array.from(candidates).find((element) => {
|
||||
if (!isVisibleElement(element) || (!allowDisabled && !isActionEnabled(element))) {
|
||||
return false;
|
||||
}
|
||||
const text = typeof getActionText === 'function' ? getActionText(element) : '';
|
||||
return /重试|try\s+again/i.test(text);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getAuthTimeoutErrorPageState(options = {}) {
|
||||
const { pathPatterns = [] } = options;
|
||||
const pathname = location.pathname || '';
|
||||
if (!matchesPathPatterns(pathname, pathPatterns)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const retryButton = getAuthRetryButton({ allowDisabled: true });
|
||||
if (!retryButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = typeof getPageTextSnapshot === 'function' ? getPageTextSnapshot() : '';
|
||||
const title = typeof document !== 'undefined' ? String(document.title || '') : '';
|
||||
const titleMatched = titlePattern instanceof RegExp
|
||||
? titlePattern.test(text) || titlePattern.test(title)
|
||||
: false;
|
||||
const detailMatched = detailPattern instanceof RegExp
|
||||
? detailPattern.test(text)
|
||||
: false;
|
||||
const routeErrorMatched = routeErrorPattern instanceof RegExp
|
||||
? routeErrorPattern.test(text)
|
||||
: false;
|
||||
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
|
||||
const userAlreadyExistsBlocked = /user_already_exists/i.test(text);
|
||||
const fetchFailedMatched = /failed\s+to\s+fetch|network\s+error|fetch\s+failed/i.test(text);
|
||||
|
||||
if (!titleMatched && !detailMatched && !routeErrorMatched && !fetchFailedMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
path: pathname,
|
||||
url: location.href,
|
||||
retryButton,
|
||||
retryEnabled: isActionEnabled(retryButton),
|
||||
titleMatched,
|
||||
detailMatched,
|
||||
routeErrorMatched,
|
||||
fetchFailedMatched,
|
||||
maxCheckAttemptsBlocked,
|
||||
userAlreadyExistsBlocked,
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForRetryPageRecoveryAfterClick(options = {}) {
|
||||
const {
|
||||
pathPatterns = [],
|
||||
pollIntervalMs = 250,
|
||||
settleAfterClickMs = 3000,
|
||||
} = options;
|
||||
const startedAt = Date.now();
|
||||
|
||||
while (Date.now() - startedAt < settleAfterClickMs) {
|
||||
if (typeof throwIfStopped === 'function') {
|
||||
throwIfStopped();
|
||||
}
|
||||
|
||||
const retryState = getAuthTimeoutErrorPageState({ pathPatterns });
|
||||
if (!retryState) {
|
||||
return {
|
||||
recovered: true,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
};
|
||||
}
|
||||
|
||||
await sleep(pollIntervalMs);
|
||||
}
|
||||
|
||||
return {
|
||||
recovered: false,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
};
|
||||
}
|
||||
|
||||
async function recoverAuthRetryPage(options = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
const performOperationWithDelay = injectedPerformOperationWithDelay
|
||||
|| rootScope.CodexOperationDelay?.performOperationWithDelay
|
||||
|| (async (_metadata, operation) => operation());
|
||||
const {
|
||||
logLabel = '',
|
||||
maxClickAttempts = 5,
|
||||
pathPatterns = [],
|
||||
pollIntervalMs = 250,
|
||||
step = null,
|
||||
timeoutMs = 12000,
|
||||
waitAfterClickMs = 3000,
|
||||
} = options;
|
||||
const maxIdlePolls = timeoutMs > 0
|
||||
? Math.max(1, Math.ceil(timeoutMs / Math.max(1, pollIntervalMs)))
|
||||
: Number.POSITIVE_INFINITY;
|
||||
let clickCount = 0;
|
||||
let idlePollCount = 0;
|
||||
|
||||
while (clickCount < maxClickAttempts) {
|
||||
if (typeof throwIfStopped === 'function') {
|
||||
throwIfStopped();
|
||||
}
|
||||
|
||||
const retryState = getAuthTimeoutErrorPageState({ pathPatterns });
|
||||
if (!retryState) {
|
||||
return {
|
||||
recovered: clickCount > 0,
|
||||
clickCount,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
if (retryState.maxCheckAttemptsBlocked) {
|
||||
throw new Error(
|
||||
'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'
|
||||
);
|
||||
}
|
||||
|
||||
if (retryState.userAlreadyExistsBlocked) {
|
||||
throw new Error(
|
||||
'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。'
|
||||
);
|
||||
}
|
||||
|
||||
if (retryState.retryButton && retryState.retryEnabled) {
|
||||
idlePollCount = 0;
|
||||
clickCount += 1;
|
||||
if (typeof log === 'function') {
|
||||
const prefix = logLabel || `步骤 ${step || '?'}:检测到重试页,正在点击“重试”恢复`;
|
||||
log(`${prefix}(第 ${clickCount} 次)...`, 'warn');
|
||||
}
|
||||
if (typeof humanPause === 'function') {
|
||||
await humanPause(300, 800);
|
||||
}
|
||||
await performOperationWithDelay({ stepKey: options.stepKey || '', kind: 'click', label: 'auth-retry-click' }, async () => {
|
||||
simulateClick(retryState.retryButton);
|
||||
});
|
||||
const recoveryResult = await waitForRetryPageRecoveryAfterClick({
|
||||
pathPatterns,
|
||||
pollIntervalMs,
|
||||
settleAfterClickMs: waitAfterClickMs,
|
||||
});
|
||||
if (recoveryResult.recovered) {
|
||||
return {
|
||||
recovered: true,
|
||||
clickCount,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
idlePollCount += 1;
|
||||
if (idlePollCount >= maxIdlePolls) {
|
||||
throw new Error(
|
||||
`${logLabel || `步骤 ${step || '?'}:重试页恢复`}超时:重试按钮长时间不可点击。URL: ${location.href}`
|
||||
);
|
||||
}
|
||||
|
||||
await sleep(pollIntervalMs);
|
||||
}
|
||||
|
||||
const finalRetryState = getAuthTimeoutErrorPageState({ pathPatterns });
|
||||
if (!finalRetryState) {
|
||||
return {
|
||||
recovered: clickCount > 0,
|
||||
clickCount,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
if (finalRetryState.maxCheckAttemptsBlocked) {
|
||||
throw new Error(
|
||||
'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'
|
||||
);
|
||||
}
|
||||
|
||||
if (finalRetryState.userAlreadyExistsBlocked) {
|
||||
throw new Error(
|
||||
'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。'
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
getAuthRetryButton,
|
||||
getAuthTimeoutErrorPageState,
|
||||
recoverAuthRetryPage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createAuthPageRecovery,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,828 @@
|
||||
// flows/openai/content/gopay-flow.js — GoPay authorization helper.
|
||||
|
||||
console.log('[MultiPage:gopay-flow] Content script loaded on', location.href);
|
||||
|
||||
const GOPAY_FLOW_LISTENER_SENTINEL = 'data-multipage-gopay-flow-listener';
|
||||
|
||||
if (document.documentElement.getAttribute(GOPAY_FLOW_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(GOPAY_FLOW_LISTENER_SENTINEL, '1');
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (
|
||||
message.type === 'GOPAY_GET_STATE'
|
||||
|| message.type === 'GOPAY_SUBMIT_PHONE'
|
||||
|| message.type === 'GOPAY_SUBMIT_OTP'
|
||||
|| message.type === 'GOPAY_SUBMIT_PIN'
|
||||
|| message.type === 'GOPAY_CLICK_CONTINUE'
|
||||
|| message.type === 'GOPAY_GET_CONTINUE_TARGET'
|
||||
|| message.type === 'GOPAY_CLICK_PAY_NOW'
|
||||
|| message.type === 'GOPAY_GET_PAY_NOW_TARGET'
|
||||
) {
|
||||
resetStopState();
|
||||
handleGoPayCommand(message).then((result) => {
|
||||
sendResponse({ ok: true, ...(result || {}) });
|
||||
}).catch((err) => {
|
||||
if (isStopError(err)) {
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('[MultiPage:gopay-flow] 消息监听已存在,跳过重复注册');
|
||||
}
|
||||
|
||||
async function performGoPayOperationWithDelay(metadata, operation) {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
}
|
||||
|
||||
async function handleGoPayCommand(message) {
|
||||
switch (message.type) {
|
||||
case 'GOPAY_GET_STATE':
|
||||
return inspectGoPayState();
|
||||
case 'GOPAY_SUBMIT_PHONE':
|
||||
return submitGoPayPhone(message.payload || {});
|
||||
case 'GOPAY_SUBMIT_OTP':
|
||||
return submitGoPayOtp(message.payload || {});
|
||||
case 'GOPAY_SUBMIT_PIN':
|
||||
return submitGoPayPin(message.payload || {});
|
||||
case 'GOPAY_CLICK_CONTINUE':
|
||||
return clickGoPayContinue();
|
||||
case 'GOPAY_GET_CONTINUE_TARGET':
|
||||
return getGoPayContinueTarget();
|
||||
case 'GOPAY_CLICK_PAY_NOW':
|
||||
return clickGoPayPayNow();
|
||||
case 'GOPAY_GET_PAY_NOW_TARGET':
|
||||
return getGoPayPayNowTarget();
|
||||
default:
|
||||
throw new Error(`gopay-flow.js 不处理消息:${message.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntil(predicate, options = {}) {
|
||||
const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 250));
|
||||
const timeoutMs = Math.max(0, Math.floor(Number(options.timeoutMs) || 0));
|
||||
const startedAt = Date.now();
|
||||
while (true) {
|
||||
throwIfStopped();
|
||||
const value = await predicate();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) {
|
||||
throw new Error(options.timeoutMessage || `${options.label || 'GoPay 页面状态'}等待超时`);
|
||||
}
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForDocumentComplete(options = {}) {
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 8000));
|
||||
const settleMs = Math.max(0, Math.floor(Number(options.settleMs) || 500));
|
||||
try {
|
||||
await waitUntil(() => {
|
||||
const readyState = String(document.readyState || '').toLowerCase();
|
||||
return readyState === 'complete'
|
||||
|| readyState === 'interactive'
|
||||
|| Boolean(document.querySelector?.('button, input, textarea, a, [role="button"]'))
|
||||
|| Boolean(normalizeText(document.body?.innerText || document.body?.textContent || ''));
|
||||
}, {
|
||||
intervalMs: 200,
|
||||
timeoutMs,
|
||||
label: 'GoPay DOM',
|
||||
});
|
||||
} catch (_) {
|
||||
// GoPay linking 页面有时长时间保持 loading;后续定位控件本身还有等待/重试。
|
||||
}
|
||||
await sleep(settleMs);
|
||||
}
|
||||
|
||||
function isVisibleElement(el) {
|
||||
if (!el) return false;
|
||||
let node = el;
|
||||
while (node && node.nodeType === 1) {
|
||||
if (node.hidden || node.getAttribute?.('aria-hidden') === 'true' || node.getAttribute?.('inert') !== null) {
|
||||
return false;
|
||||
}
|
||||
const nodeStyle = window.getComputedStyle(node);
|
||||
if (
|
||||
nodeStyle.display === 'none'
|
||||
|| nodeStyle.visibility === 'hidden'
|
||||
|| nodeStyle.visibility === 'collapse'
|
||||
|| Number(nodeStyle.opacity) === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.display !== 'none'
|
||||
&& style.visibility !== 'hidden'
|
||||
&& Number(rect.width) > 0
|
||||
&& Number(rect.height) > 0;
|
||||
}
|
||||
|
||||
function normalizeText(text = '') {
|
||||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getActionText(el) {
|
||||
return normalizeText([
|
||||
el?.textContent,
|
||||
el?.value,
|
||||
el?.getAttribute?.('data-testid'),
|
||||
el?.getAttribute?.('aria-label'),
|
||||
el?.getAttribute?.('title'),
|
||||
el?.getAttribute?.('placeholder'),
|
||||
el?.getAttribute?.('name'),
|
||||
el?.id,
|
||||
].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function getVisibleControls(selector) {
|
||||
return Array.from(document.querySelectorAll(selector)).filter(isVisibleElement);
|
||||
}
|
||||
|
||||
function isEnabledControl(el) {
|
||||
return Boolean(el)
|
||||
&& !el.disabled
|
||||
&& el.getAttribute?.('aria-disabled') !== 'true';
|
||||
}
|
||||
|
||||
function getVisibleTextInputs() {
|
||||
return getVisibleControls('input, textarea')
|
||||
.filter((input) => {
|
||||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||||
return isEnabledControl(input) && !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
|
||||
});
|
||||
}
|
||||
|
||||
function findInputByPatterns(patterns) {
|
||||
return getVisibleTextInputs().find((input) => {
|
||||
const text = getActionText(input);
|
||||
return patterns.some((pattern) => pattern.test(text));
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findPhoneInput() {
|
||||
const input = findInputByPatterns([
|
||||
/gopay|go\s*pay|phone|mobile|whatsapp|wa|nomor|ponsel|telepon|hp/i,
|
||||
/手机|手机号|电话号码|电话/i,
|
||||
]);
|
||||
if (input && !isCountrySearchInput(input)) {
|
||||
return input;
|
||||
}
|
||||
return getVisibleControls('input[type="tel"]').find((candidate) => isEnabledControl(candidate) && !isCountrySearchInput(candidate)) || null;
|
||||
}
|
||||
|
||||
function isCountrySearchInput(input) {
|
||||
const text = getActionText(input);
|
||||
return /country|country\s*name|country\s*code|国家|地区|区号/i.test(text);
|
||||
}
|
||||
|
||||
function getPageBodyText() {
|
||||
return normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||||
}
|
||||
|
||||
function isGoPayOtpPageText() {
|
||||
if (isGoPayPinPageText()) {
|
||||
return false;
|
||||
}
|
||||
if (getVisibleTextInputs().some((input) => isGoPayPinInput(input))) {
|
||||
return false;
|
||||
}
|
||||
const text = getPageBodyText();
|
||||
return /otp|one[-\s]*time|kode|verification|whatsapp|验证码|短信/i.test(text);
|
||||
}
|
||||
|
||||
function isGoPayPinPageText() {
|
||||
const text = getPageBodyText();
|
||||
return /pin|password|passcode|security|sandi|6\s*digit|masukkin\s+pin|masukkan\s+pin|ketik\s+6\s+digit|enter\s+pin|支付密码/i.test(text)
|
||||
|| /pin-web-client\.gopayapi\.com|\/auth\/pin|\/payment\/validate-pin|linking-validate-pin/i.test(location.href || '');
|
||||
}
|
||||
|
||||
function isGoPayPinInput(input) {
|
||||
if (!input || isCountrySearchInput(input)) {
|
||||
return false;
|
||||
}
|
||||
const text = getCombinedElementText(input);
|
||||
const testId = String(input.getAttribute?.('data-testid') || '').trim();
|
||||
const type = String(input.getAttribute?.('type') || input.type || '').trim().toLowerCase();
|
||||
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||
const pinPage = isGoPayPinPageText();
|
||||
const strongPinHint = /password|passcode|security|sandi|支付密码|密码/i.test(text);
|
||||
const ambiguousPinWidget = /^pin-input/i.test(testId)
|
||||
|| /pin-input(?:-field)?|(?:^|[\s_-])pin(?:$|[\s_-])/i.test(text);
|
||||
return strongPinHint
|
||||
|| (pinPage && (ambiguousPinWidget || type === 'password' || maxLength === 1));
|
||||
}
|
||||
|
||||
function detectGoPayTerminalError(text = getPageBodyText()) {
|
||||
const normalizedText = normalizeText(text);
|
||||
if (!normalizedText) return null;
|
||||
|
||||
if (/waktunya\s+habis|ulang(?:i)?\s+prosesnya\s+dari\s+awal|time(?:'s|\s+is)?\s+(?:out|expired)|session\s+expired|expired|kedaluwarsa/i.test(normalizedText)) {
|
||||
return {
|
||||
code: 'expired',
|
||||
message: 'GoPay 支付会话已超时,需要重新创建 Plus Checkout。',
|
||||
rawText: normalizedText.slice(0, 240),
|
||||
};
|
||||
}
|
||||
|
||||
if (/technical\s+error|don[’']t\s+worry|try\s+again|terjadi\s+kesalahan|error\s+teknis/i.test(normalizedText)) {
|
||||
return {
|
||||
code: 'technical-error',
|
||||
message: 'GoPay 页面显示技术错误,需要重新发起支付授权。',
|
||||
rawText: normalizedText.slice(0, 240),
|
||||
};
|
||||
}
|
||||
|
||||
if (/payment\s+failed|pembayaran\s+gagal|transaksi\s+gagal|ditolak|declined|failed/i.test(normalizedText)) {
|
||||
return {
|
||||
code: 'failed',
|
||||
message: 'GoPay 页面显示支付失败,需要重新发起支付授权。',
|
||||
rawText: normalizedText.slice(0, 240),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findOtpInput() {
|
||||
if (isGoPayPinPageText()) {
|
||||
return null;
|
||||
}
|
||||
const input = findInputByPatterns([
|
||||
/otp|one[-\s]*time|verification|verify|code|kode|whatsapp|wa/i,
|
||||
/验证码|短信|代码/i,
|
||||
]);
|
||||
if (input && !isCountrySearchInput(input) && !isGoPayPinInput(input)) {
|
||||
return input;
|
||||
}
|
||||
if (isGoPayOtpPageText()) {
|
||||
return getVisibleTextInputs().find((candidate) => !isCountrySearchInput(candidate) && !isGoPayPinInput(candidate)) || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getGoPayPinInputs() {
|
||||
return getVisibleTextInputs().filter((candidate) => {
|
||||
return isGoPayPinInput(candidate);
|
||||
});
|
||||
}
|
||||
|
||||
function findPinInput() {
|
||||
const pinInputs = getGoPayPinInputs();
|
||||
if (pinInputs[0]) {
|
||||
return pinInputs[0];
|
||||
}
|
||||
if (isGoPayOtpPageText()) {
|
||||
return null;
|
||||
}
|
||||
const input = findInputByPatterns([
|
||||
/pin|password|passcode|security|sandi|pin-input/i,
|
||||
/密码|支付密码/i,
|
||||
]);
|
||||
if (input && !isCountrySearchInput(input)) {
|
||||
return input;
|
||||
}
|
||||
return getVisibleControls('input[type="password"]').find((candidate) => isEnabledControl(candidate) && !isCountrySearchInput(candidate))
|
||||
|| null;
|
||||
}
|
||||
|
||||
function findClickableByText(patterns) {
|
||||
const normalizedPatterns = (Array.isArray(patterns) ? patterns : [patterns]).filter(Boolean);
|
||||
const candidates = getVisibleControls('button, a, [role="button"], input[type="button"], input[type="submit"]');
|
||||
return candidates.find((el) => {
|
||||
if (!isEnabledControl(el)) return false;
|
||||
const text = getActionText(el);
|
||||
return normalizedPatterns.some((pattern) => pattern.test(text));
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findPayNowButton() {
|
||||
return findClickableByText([
|
||||
/^\s*pay\s+now\s*$/i,
|
||||
/^\s*bayar(?:\s+sekarang)?(?:\s*rp[\s\S]*)?\s*$/i,
|
||||
/^\s*支付\s*$/i,
|
||||
/^\s*立即支付\s*$/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function findContinueButton() {
|
||||
return findClickableByText([
|
||||
/continue|next|submit|verify|confirm|pay|authorize|allow|lanjut|lanjutkan|berikut|kirim|bayar|konfirmasi|hubungkan|sambungkan|tautkan|setuju|izinkan|link/i,
|
||||
/继续|下一步|提交|验证|确认|支付|授权|绑定|关联/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function describeElement(el) {
|
||||
if (!el) return '';
|
||||
const rect = el.getBoundingClientRect?.();
|
||||
const parts = [String(el.tagName || '').toUpperCase()];
|
||||
if (el.id) parts.push(`#${el.id}`);
|
||||
const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class');
|
||||
if (className) parts.push(`.${String(className).trim().replace(/\s+/g, '.')}`);
|
||||
const text = getActionText(el) || normalizeText(el.innerText || el.textContent || '');
|
||||
if (text) parts.push(`"${text.slice(0, 60)}"`);
|
||||
if (rect) parts.push(`@${Math.round(rect.left)},${Math.round(rect.top)} ${Math.round(rect.width)}x${Math.round(rect.height)}`);
|
||||
return parts.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function dispatchPointerMouseSequence(target) {
|
||||
const rect = target.getBoundingClientRect?.();
|
||||
const clientX = rect ? Math.round(rect.left + rect.width / 2) : 0;
|
||||
const clientY = rect ? Math.round(rect.top + rect.height / 2) : 0;
|
||||
const eventInit = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
view: window,
|
||||
detail: 1,
|
||||
button: 0,
|
||||
buttons: 1,
|
||||
clientX,
|
||||
clientY,
|
||||
screenX: window.screenX + clientX,
|
||||
screenY: window.screenY + clientY,
|
||||
};
|
||||
if (typeof PointerEvent === 'function') {
|
||||
target.dispatchEvent(new PointerEvent('pointerover', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
||||
target.dispatchEvent(new PointerEvent('pointerenter', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true, bubbles: false }));
|
||||
target.dispatchEvent(new PointerEvent('pointermove', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
||||
target.dispatchEvent(new PointerEvent('pointerdown', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
||||
}
|
||||
target.dispatchEvent(new MouseEvent('mouseover', eventInit));
|
||||
target.dispatchEvent(new MouseEvent('mouseenter', { ...eventInit, bubbles: false }));
|
||||
target.dispatchEvent(new MouseEvent('mousemove', eventInit));
|
||||
target.dispatchEvent(new MouseEvent('mousedown', eventInit));
|
||||
if (typeof PointerEvent === 'function') {
|
||||
target.dispatchEvent(new PointerEvent('pointerup', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 0 }));
|
||||
}
|
||||
target.dispatchEvent(new MouseEvent('mouseup', { ...eventInit, buttons: 0 }));
|
||||
target.dispatchEvent(new MouseEvent('click', { ...eventInit, buttons: 0 }));
|
||||
}
|
||||
|
||||
async function humanClickElement(el, options = {}) {
|
||||
if (!el) {
|
||||
throw new Error('GoPay 页面未找到可点击元素。');
|
||||
}
|
||||
el.scrollIntoView?.({ block: 'center', inline: 'center' });
|
||||
await sleep(Math.max(0, Number(options.beforeMs) || 120));
|
||||
try {
|
||||
el.focus?.({ preventScroll: true });
|
||||
} catch (_) {
|
||||
try { el.focus?.(); } catch (__) {}
|
||||
}
|
||||
dispatchPointerMouseSequence(el);
|
||||
await sleep(Math.max(0, Number(options.afterDispatchMs) || 120));
|
||||
if (typeof el.click === 'function') {
|
||||
el.click();
|
||||
}
|
||||
await sleep(Math.max(0, Number(options.afterMs) || 1000));
|
||||
}
|
||||
|
||||
async function clickContinueIfPresent(options = {}) {
|
||||
const button = findContinueButton();
|
||||
if (!button) {
|
||||
return { clicked: false, target: '' };
|
||||
}
|
||||
await humanClickElement(button, options);
|
||||
return { clicked: true, target: describeElement(button) };
|
||||
}
|
||||
|
||||
function normalizePhoneNumber(value = '') {
|
||||
return String(value || '').trim().replace(/[^\d+]/g, '');
|
||||
}
|
||||
|
||||
function normalizeGoPayCountryCode(value = '') {
|
||||
const normalized = String(value || '').trim().replace(/[^\d+]/g, '');
|
||||
const digits = normalized.replace(/\D/g, '');
|
||||
return digits ? `+${digits}` : '+86';
|
||||
}
|
||||
|
||||
function getCountryCodeDigits(value = '') {
|
||||
return normalizeGoPayCountryCode(value).replace(/\D/g, '');
|
||||
}
|
||||
|
||||
function normalizeGoPayNationalPhone(value = '', countryCode = '+86') {
|
||||
const countryDigits = getCountryCodeDigits(countryCode);
|
||||
let digits = normalizePhoneNumber(value).replace(/\D/g, '');
|
||||
if (countryDigits && digits.startsWith(countryDigits)) {
|
||||
digits = digits.slice(countryDigits.length);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
function getCombinedElementText(el) {
|
||||
return normalizeText([
|
||||
getActionText(el),
|
||||
el?.innerText,
|
||||
el?.textContent,
|
||||
typeof el?.className === 'string' ? el.className : el?.getAttribute?.('class'),
|
||||
].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function robustClick(el) {
|
||||
if (!el) return;
|
||||
el.scrollIntoView?.({ block: 'center', inline: 'center' });
|
||||
try {
|
||||
el.focus?.();
|
||||
} catch (_) {}
|
||||
const eventInit = { bubbles: true, cancelable: true, view: window };
|
||||
if (typeof PointerEvent === 'function') {
|
||||
el.dispatchEvent(new PointerEvent('pointerdown', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
||||
}
|
||||
el.dispatchEvent(new MouseEvent('mousedown', eventInit));
|
||||
if (typeof PointerEvent === 'function') {
|
||||
el.dispatchEvent(new PointerEvent('pointerup', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
||||
}
|
||||
el.dispatchEvent(new MouseEvent('mouseup', eventInit));
|
||||
el.dispatchEvent(new MouseEvent('click', eventInit));
|
||||
if (typeof el.click === 'function') {
|
||||
el.click();
|
||||
}
|
||||
}
|
||||
|
||||
function readSelectedCountryCodeText() {
|
||||
const candidates = getVisibleControls('.phone-code, .phone-code-wrapper, [class*="phone-code"], button, [role="button"], [tabindex]');
|
||||
for (const candidate of candidates) {
|
||||
const match = getCombinedElementText(candidate).match(/\+\d{1,4}/);
|
||||
if (match) return normalizeGoPayCountryCode(match[0]);
|
||||
}
|
||||
const bodyMatch = normalizeText(document.body?.innerText || document.body?.textContent || '').match(/Phone number:\s*(\+\d{1,4})/i);
|
||||
return bodyMatch ? normalizeGoPayCountryCode(bodyMatch[1]) : '';
|
||||
}
|
||||
|
||||
function findCountryCodeToggle() {
|
||||
const preferred = getVisibleControls('.phone-code-wrapper, [class*="phone-code-wrapper"], .phone-code, [class*="phone-code"]')
|
||||
.find((el) => /\+\d{1,4}/.test(getCombinedElementText(el)));
|
||||
if (preferred) return preferred;
|
||||
return getVisibleControls('button, [role="button"], [tabindex], div, span')
|
||||
.find((el) => /\+\d{1,4}/.test(getCombinedElementText(el)) && /phone|code|country|\+\d{1,4}/i.test(getCombinedElementText(el))) || null;
|
||||
}
|
||||
|
||||
function findCountryCodeOption(countryCode = '+86') {
|
||||
const normalized = normalizeGoPayCountryCode(countryCode);
|
||||
const digits = getCountryCodeDigits(normalized);
|
||||
const countryAliases = {
|
||||
'1': [/United States|USA|Canada|美国|加拿大/i],
|
||||
'60': [/Malaysia|马来西亚/i],
|
||||
'62': [/Indonesia|印尼|印度尼西亚/i],
|
||||
'63': [/Philippines|菲律宾/i],
|
||||
'65': [/Singapore|新加坡/i],
|
||||
'66': [/Thailand|泰国/i],
|
||||
'84': [/Vietnam|越南/i],
|
||||
'86': [/China|中国|Mainland/i],
|
||||
'91': [/India|印度/i],
|
||||
'852': [/Hong Kong|香港/i],
|
||||
'853': [/Macau|Macao|澳门/i],
|
||||
'886': [/Taiwan|台湾/i],
|
||||
}[digits] || [];
|
||||
const controls = getVisibleControls('li.country-item, .country-item, [class*="country-item"], [role="option"], li, button, [role="button"], a, [tabindex], div, span')
|
||||
.map((el) => el.closest?.('.country-item') || el)
|
||||
.filter((el, index, list) => el && list.indexOf(el) === index)
|
||||
.filter((el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const text = getCombinedElementText(el);
|
||||
const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '';
|
||||
return text.includes(normalized)
|
||||
&& rect.width > 20
|
||||
&& rect.height > 8
|
||||
&& rect.width < Math.max(480, window.innerWidth * 0.8)
|
||||
&& rect.height < Math.max(120, window.innerHeight * 0.35)
|
||||
&& !/phone-number-input-wrapper|gopay-tokenization-content|asphalt-theme|search-country|country-list/i.test(className);
|
||||
});
|
||||
const matchesAlias = (el) => {
|
||||
const text = getCombinedElementText(el);
|
||||
return countryAliases.some((pattern) => pattern.test(text));
|
||||
};
|
||||
return controls.find((el) => /country-item/i.test(typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '') && matchesAlias(el))
|
||||
|| controls.find((el) => String(el.tagName || '').toUpperCase() === 'LI' && matchesAlias(el))
|
||||
|| controls.find((el) => el.getAttribute?.('role') === 'option' && matchesAlias(el))
|
||||
|| controls.find(matchesAlias)
|
||||
|| null;
|
||||
}
|
||||
|
||||
async function ensureGoPayCountryCode(countryCode = '+86') {
|
||||
const normalized = normalizeGoPayCountryCode(countryCode);
|
||||
const selected = readSelectedCountryCodeText();
|
||||
if (selected === normalized) {
|
||||
return { changed: false, countryCode: normalized, selected };
|
||||
}
|
||||
|
||||
const toggle = findCountryCodeToggle();
|
||||
if (!toggle) {
|
||||
throw new Error(`GoPay 页面未找到国家区号切换控件,当前识别区号:${selected || '未知'},目标区号:${normalized}`);
|
||||
}
|
||||
robustClick(toggle);
|
||||
await sleep(500);
|
||||
|
||||
const countryDropdown = document.querySelector('.search-country');
|
||||
if (countryDropdown && window.getComputedStyle(countryDropdown).display === 'none') {
|
||||
countryDropdown.style.display = 'block';
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
const countrySearchInput = getVisibleTextInputs().find(isCountrySearchInput);
|
||||
if (countrySearchInput) {
|
||||
fillInput(countrySearchInput, normalized);
|
||||
await sleep(300);
|
||||
}
|
||||
|
||||
const option = await waitUntil(() => findCountryCodeOption(normalized), {
|
||||
label: `GoPay 国家区号 ${normalized}`,
|
||||
intervalMs: 250,
|
||||
timeoutMs: 8000,
|
||||
});
|
||||
robustClick(option);
|
||||
await sleep(500);
|
||||
|
||||
const nextSelected = readSelectedCountryCodeText();
|
||||
if (nextSelected === normalized && countryDropdown) {
|
||||
countryDropdown.style.display = 'none';
|
||||
}
|
||||
if (nextSelected !== normalized) {
|
||||
throw new Error(`GoPay 国家区号切换失败:目标 ${normalized},当前 ${nextSelected || '未知'}`);
|
||||
}
|
||||
return {
|
||||
changed: true,
|
||||
countryCode: normalized,
|
||||
selected: nextSelected,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOtp(value = '') {
|
||||
return String(value || '').trim().replace(/[^\d]/g, '');
|
||||
}
|
||||
|
||||
|
||||
function fillDigitInputs(inputs = [], code = '') {
|
||||
const normalizedCode = normalizeOtp(code);
|
||||
if (!normalizedCode || !inputs.length) return false;
|
||||
normalizedCode.split('').forEach((digit, index) => {
|
||||
const input = inputs[index];
|
||||
if (!input) return;
|
||||
try {
|
||||
input.focus?.();
|
||||
} catch (_) {}
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { key: digit, code: `Digit${digit}`, bubbles: true, cancelable: true }));
|
||||
fillInput(input, digit);
|
||||
input.dispatchEvent(new KeyboardEvent('keyup', { key: digit, code: `Digit${digit}`, bubbles: true, cancelable: true }));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function fillVisiblePinInputs(pin = '') {
|
||||
const normalizedPin = normalizeOtp(pin);
|
||||
if (!normalizedPin) return false;
|
||||
const pinInputs = getGoPayPinInputs();
|
||||
const digitInputs = pinInputs.filter((input) => {
|
||||
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||
return maxLength > 0 && maxLength <= 1;
|
||||
});
|
||||
if (digitInputs.length >= Math.min(4, normalizedPin.length)) {
|
||||
return fillDigitInputs(digitInputs, normalizedPin);
|
||||
}
|
||||
const input = findPinInput() || pinInputs[0];
|
||||
if (!input) return false;
|
||||
fillInput(input, normalizedPin);
|
||||
return true;
|
||||
}
|
||||
|
||||
function fillVisibleOtpInputs(code = '') {
|
||||
const normalizedCode = normalizeOtp(code);
|
||||
if (!normalizedCode) return false;
|
||||
if (isGoPayPinPageText() || getVisibleTextInputs().some((input) => isGoPayPinInput(input))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const otpInputs = getVisibleTextInputs()
|
||||
.filter((input) => {
|
||||
if (isGoPayPinInput(input)) return false;
|
||||
const text = getActionText(input);
|
||||
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||
return /otp|code|kode|verification|验证码|短信/i.test(text)
|
||||
|| (maxLength > 0 && maxLength <= 1)
|
||||
|| (maxLength > 1 && maxLength <= 8);
|
||||
});
|
||||
|
||||
const digitInputs = otpInputs.filter((input) => {
|
||||
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||
return maxLength > 0 && maxLength <= 1;
|
||||
});
|
||||
if (digitInputs.length >= Math.min(4, normalizedCode.length)) {
|
||||
return fillDigitInputs(digitInputs, normalizedCode);
|
||||
}
|
||||
|
||||
const input = findOtpInput() || otpInputs[0];
|
||||
if (!input) return false;
|
||||
fillInput(input, normalizedCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function submitGoPayPhone(payload = {}) {
|
||||
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
|
||||
? performGoPayOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const countryCode = normalizeGoPayCountryCode(payload.countryCode || payload.gopayCountryCode || '+86');
|
||||
const phone = normalizeGoPayNationalPhone(payload.phone || payload.gopayPhone || '', countryCode);
|
||||
if (!phone) {
|
||||
throw new Error('GoPay 手机号为空,请先在侧边栏配置。');
|
||||
}
|
||||
const input = await waitUntil(() => findPhoneInput(), {
|
||||
label: 'GoPay 手机号输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
const { countryResult, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-phone' }, async () => {
|
||||
const nextCountryResult = await ensureGoPayCountryCode(countryCode);
|
||||
fillInput(input, phone);
|
||||
const nextClickResult = await clickContinueIfPresent();
|
||||
return {
|
||||
countryResult: nextCountryResult,
|
||||
clickResult: nextClickResult,
|
||||
};
|
||||
});
|
||||
return {
|
||||
phoneSubmitted: true,
|
||||
countryCode,
|
||||
countryChanged: Boolean(countryResult.changed),
|
||||
clicked: Boolean(clickResult.clicked),
|
||||
clickTarget: clickResult.target || '',
|
||||
phase: 'phone_submitted',
|
||||
};
|
||||
}
|
||||
|
||||
async function submitGoPayOtp(payload = {}) {
|
||||
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
|
||||
? performGoPayOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const code = normalizeOtp(payload.code || payload.otp || '');
|
||||
if (!code) {
|
||||
throw new Error('GoPay WhatsApp 验证码为空。');
|
||||
}
|
||||
const { filled, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-otp' }, async () => {
|
||||
const filledOtp = await waitUntil(() => fillVisibleOtpInputs(code), {
|
||||
label: 'GoPay 验证码输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
const continueResult = await clickContinueIfPresent();
|
||||
return { filled: filledOtp, clickResult: continueResult };
|
||||
});
|
||||
return {
|
||||
otpSubmitted: Boolean(filled),
|
||||
clicked: Boolean(clickResult.clicked),
|
||||
clickTarget: clickResult.target || '',
|
||||
phase: 'otp_submitted',
|
||||
};
|
||||
}
|
||||
|
||||
async function submitGoPayPin(payload = {}) {
|
||||
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
|
||||
? performGoPayOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const pin = normalizeOtp(payload.pin || payload.gopayPin || '');
|
||||
if (!pin) {
|
||||
throw new Error('GoPay PIN 为空,请先在侧边栏配置。');
|
||||
}
|
||||
const { filled, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-pin' }, async () => {
|
||||
const filledPin = await waitUntil(() => fillVisiblePinInputs(pin), {
|
||||
label: 'GoPay PIN 输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
const continueResult = await clickContinueIfPresent();
|
||||
return { filled: filledPin, clickResult: continueResult };
|
||||
});
|
||||
return {
|
||||
pinSubmitted: Boolean(filled),
|
||||
clicked: Boolean(clickResult.clicked),
|
||||
clickTarget: clickResult.target || '',
|
||||
phase: 'pin_submitted',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function getElementClickRect(el) {
|
||||
if (!el) return null;
|
||||
const rect = el.getBoundingClientRect?.();
|
||||
if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
centerX: rect.left + rect.width / 2,
|
||||
centerY: rect.top + rect.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
function getGoPayContinueTarget() {
|
||||
const button = findContinueButton();
|
||||
return {
|
||||
found: Boolean(button),
|
||||
target: describeElement(button),
|
||||
rect: getElementClickRect(button),
|
||||
};
|
||||
}
|
||||
|
||||
function getGoPayPayNowTarget() {
|
||||
const button = findPayNowButton();
|
||||
return {
|
||||
found: Boolean(button),
|
||||
target: describeElement(button),
|
||||
rect: getElementClickRect(button),
|
||||
};
|
||||
}
|
||||
|
||||
async function clickGoPayContinue() {
|
||||
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
|
||||
? performGoPayOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const button = findContinueButton();
|
||||
if (!button) {
|
||||
return { clicked: false, clickTarget: '' };
|
||||
}
|
||||
const clickResult = await delayOperation({ stepKey: 'gopay-approve', kind: 'click', label: 'click-continue' }, async () => {
|
||||
await humanClickElement(button, { afterMs: 1200 });
|
||||
return { clicked: true, target: describeElement(button) };
|
||||
});
|
||||
return { clicked: Boolean(clickResult.clicked), clickTarget: clickResult.target || '' };
|
||||
}
|
||||
|
||||
async function clickGoPayPayNow() {
|
||||
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
|
||||
? performGoPayOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const button = findPayNowButton();
|
||||
if (!button) {
|
||||
return { clicked: false, clickTarget: '' };
|
||||
}
|
||||
await delayOperation({ stepKey: 'gopay-approve', kind: 'click', label: 'click-pay-now' }, async () => {
|
||||
await humanClickElement(button, { afterMs: 1500 });
|
||||
});
|
||||
return { clicked: true, clickTarget: describeElement(button) };
|
||||
}
|
||||
|
||||
function inspectGoPayState() {
|
||||
const bodyText = normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||||
const phoneInput = findPhoneInput();
|
||||
const otpInput = findOtpInput();
|
||||
const pinInput = findPinInput();
|
||||
const payNowButton = findPayNowButton();
|
||||
const continueButton = findContinueButton();
|
||||
const terminalError = detectGoPayTerminalError(bodyText);
|
||||
const successTextMatched = /success|successful|completed|selesai|berhasil|approved|authorized|支付成功|绑定成功|已授权/i.test(bodyText);
|
||||
const completed = !phoneInput && !otpInput && !pinInput && successTextMatched;
|
||||
const selectedCountryCode = readSelectedCountryCodeText();
|
||||
return {
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
selectedCountryCode,
|
||||
hasPhoneInput: Boolean(phoneInput),
|
||||
hasOtpInput: Boolean(otpInput),
|
||||
hasPinInput: Boolean(pinInput),
|
||||
hasPayNowButton: Boolean(payNowButton),
|
||||
hasContinueButton: Boolean(continueButton),
|
||||
hasTerminalError: Boolean(terminalError),
|
||||
terminalError,
|
||||
completed,
|
||||
textPreview: bodyText.slice(0, 500),
|
||||
inputHints: getVisibleTextInputs().map((input) => getActionText(input).slice(0, 120)).filter(Boolean).slice(0, 12),
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,886 @@
|
||||
// flows/openai/content/paypal-flow.js — PayPal login and approval helper.
|
||||
|
||||
console.log('[MultiPage:paypal-flow] Content script loaded on', location.href);
|
||||
|
||||
const PAYPAL_FLOW_LISTENER_SENTINEL = 'data-multipage-paypal-flow-listener';
|
||||
const PAYPAL_HOSTED_DEFAULT_PHONE = '1234567890';
|
||||
const PAYPAL_HOSTED_STAGE_OUTSIDE = 'outside_paypal';
|
||||
const PAYPAL_HOSTED_STAGE_LOGIN = 'pay_login';
|
||||
const PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT = 'guest_checkout';
|
||||
const PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT = 'create_account';
|
||||
const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent';
|
||||
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
|
||||
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
|
||||
const PAYPAL_HOSTED_STEP_KEYS = {
|
||||
[PAYPAL_HOSTED_STAGE_LOGIN]: 'paypal-hosted-email',
|
||||
[PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT]: 'paypal-hosted-card',
|
||||
[PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT]: 'paypal-hosted-create-account',
|
||||
[PAYPAL_HOSTED_STAGE_REVIEW]: 'paypal-hosted-review',
|
||||
};
|
||||
|
||||
if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(PAYPAL_FLOW_LISTENER_SENTINEL, '1');
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (
|
||||
message.type === 'PAYPAL_GET_STATE'
|
||||
|| message.type === 'PAYPAL_SUBMIT_LOGIN'
|
||||
|| message.type === 'PAYPAL_DISMISS_PROMPTS'
|
||||
|| message.type === 'PAYPAL_CLICK_APPROVE'
|
||||
|| message.type === 'PAYPAL_HOSTED_GET_STATE'
|
||||
|| message.type === 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP'
|
||||
) {
|
||||
resetStopState();
|
||||
handlePayPalCommand(message).then((result) => {
|
||||
sendResponse({ ok: true, ...(result || {}) });
|
||||
}).catch((err) => {
|
||||
if (isStopError(err)) {
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('[MultiPage:paypal-flow] 消息监听已存在,跳过重复注册');
|
||||
}
|
||||
|
||||
async function performPayPalOperationWithDelay(metadata, operation) {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
}
|
||||
|
||||
async function handlePayPalCommand(message) {
|
||||
switch (message.type) {
|
||||
case 'PAYPAL_GET_STATE':
|
||||
return inspectPayPalState();
|
||||
case 'PAYPAL_SUBMIT_LOGIN':
|
||||
return submitPayPalLogin(message.payload || {});
|
||||
case 'PAYPAL_DISMISS_PROMPTS':
|
||||
return dismissPayPalPrompts();
|
||||
case 'PAYPAL_CLICK_APPROVE':
|
||||
return clickPayPalApprove();
|
||||
case 'PAYPAL_HOSTED_GET_STATE':
|
||||
return inspectPayPalHostedState();
|
||||
case 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP':
|
||||
return runPayPalHostedCheckoutStep(message.payload || {});
|
||||
default:
|
||||
throw new Error(`paypal-flow.js 不处理消息:${message.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntil(predicate, options = {}) {
|
||||
const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 250));
|
||||
const timeoutMs = Math.max(0, Math.floor(Number(options.timeoutMs) || 0));
|
||||
const startedAt = Date.now();
|
||||
while (true) {
|
||||
throwIfStopped();
|
||||
const value = await predicate();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) {
|
||||
throw new Error(options.timeoutMessage || 'PayPal page timed out waiting for target state.');
|
||||
}
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForDocumentComplete() {
|
||||
await waitUntil(() => document.readyState === 'complete', { intervalMs: 200 });
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
function isVisibleElement(el) {
|
||||
if (!el) return false;
|
||||
let node = el;
|
||||
while (node && node.nodeType === 1) {
|
||||
if (node.hidden || node.getAttribute?.('aria-hidden') === 'true' || node.getAttribute?.('inert') !== null) {
|
||||
return false;
|
||||
}
|
||||
const nodeStyle = window.getComputedStyle(node);
|
||||
if (
|
||||
nodeStyle.display === 'none'
|
||||
|| nodeStyle.visibility === 'hidden'
|
||||
|| nodeStyle.visibility === 'collapse'
|
||||
|| Number(nodeStyle.opacity) === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.display !== 'none'
|
||||
&& style.visibility !== 'hidden'
|
||||
&& Number(rect.width) > 0
|
||||
&& Number(rect.height) > 0;
|
||||
}
|
||||
|
||||
function normalizeText(text = '') {
|
||||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getActionText(el) {
|
||||
return normalizeText([
|
||||
el?.textContent,
|
||||
el?.value,
|
||||
el?.getAttribute?.('aria-label'),
|
||||
el?.getAttribute?.('title'),
|
||||
el?.getAttribute?.('placeholder'),
|
||||
el?.getAttribute?.('name'),
|
||||
el?.id,
|
||||
].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function getVisibleControls(selector) {
|
||||
return Array.from(document.querySelectorAll(selector)).filter(isVisibleElement);
|
||||
}
|
||||
|
||||
function isEnabledControl(el) {
|
||||
return Boolean(el)
|
||||
&& !el.disabled
|
||||
&& el.getAttribute?.('aria-disabled') !== 'true';
|
||||
}
|
||||
|
||||
function findClickableByText(patterns) {
|
||||
const normalizedPatterns = (Array.isArray(patterns) ? patterns : [patterns]).filter(Boolean);
|
||||
const candidates = getVisibleControls('button, a, [role="button"], input[type="button"], input[type="submit"]');
|
||||
return candidates.find((el) => {
|
||||
const text = getActionText(el);
|
||||
return normalizedPatterns.some((pattern) => pattern.test(text));
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findInputByPatterns(patterns) {
|
||||
const inputs = getVisibleControls('input')
|
||||
.filter((input) => {
|
||||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||||
return isEnabledControl(input) && !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
|
||||
});
|
||||
return inputs.find((input) => {
|
||||
const text = getActionText(input);
|
||||
return patterns.some((pattern) => pattern.test(text));
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findEmailInput() {
|
||||
const isPasswordCandidate = (input) => {
|
||||
const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
|
||||
const metadataText = normalizeText([
|
||||
input?.textContent,
|
||||
input?.getAttribute?.('aria-label'),
|
||||
input?.getAttribute?.('title'),
|
||||
input?.getAttribute?.('placeholder'),
|
||||
input?.getAttribute?.('name'),
|
||||
input?.id,
|
||||
].filter(Boolean).join(' '));
|
||||
return type === 'password' || /password|pass|密码/i.test(metadataText);
|
||||
};
|
||||
const inputs = getVisibleControls('input')
|
||||
.filter((input) => {
|
||||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||||
return isEnabledControl(input)
|
||||
&& !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type)
|
||||
&& !isPasswordCandidate(input);
|
||||
});
|
||||
return inputs.find((input) => [
|
||||
/email|login|user|账号|邮箱/i,
|
||||
].some((pattern) => pattern.test(getActionText(input))))
|
||||
|| getVisibleControls('input[type="email"]').find((input) => isVisibleElement(input) && !isPasswordCandidate(input))
|
||||
|| null;
|
||||
}
|
||||
|
||||
function findPasswordInput() {
|
||||
const inputs = getVisibleControls('input')
|
||||
.filter((input) => {
|
||||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||||
return isEnabledControl(input) && !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
|
||||
});
|
||||
return inputs.find((input) => {
|
||||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||||
const metadataText = normalizeText([
|
||||
input?.textContent,
|
||||
input?.getAttribute?.('aria-label'),
|
||||
input?.getAttribute?.('title'),
|
||||
input?.getAttribute?.('placeholder'),
|
||||
input?.getAttribute?.('name'),
|
||||
input?.id,
|
||||
].filter(Boolean).join(' '));
|
||||
return type === 'password' || /password|pass|密码/i.test(metadataText);
|
||||
}) || getVisibleControls('input[type="password"]').find(isVisibleElement) || null;
|
||||
}
|
||||
|
||||
function findLoginNextButton() {
|
||||
return findClickableByText([
|
||||
/next|continue|login|log\s*in|sign\s*in/i,
|
||||
/下一步|继续|登录|登入/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function findEmailNextButton() {
|
||||
return findClickableByText([
|
||||
/next|btn\s*next|btnnext/i,
|
||||
/下一页|下一步/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function findPasswordLoginButton() {
|
||||
const button = findClickableByText([
|
||||
/login|log\s*in|sign\s*in/i,
|
||||
/登录|登入/i,
|
||||
]);
|
||||
return button && button !== findEmailNextButton() ? button : null;
|
||||
}
|
||||
|
||||
function findApproveButton() {
|
||||
return findClickableByText([
|
||||
/同意并继续|同意|继续|授权|确认并继续/i,
|
||||
/agree\s*(?:and)?\s*continue|continue|accept|authorize|agree|pay\s*now/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function getPayPalPathname() {
|
||||
return String(location?.pathname || '').trim();
|
||||
}
|
||||
|
||||
function isHostedLoginPage() {
|
||||
return getPayPalPathname() === '/pay' || Boolean(document.getElementById('email'));
|
||||
}
|
||||
|
||||
function isHostedGuestCheckoutPage() {
|
||||
if (document.getElementById('cardNumber') || document.getElementById('billingLine1')) {
|
||||
return true;
|
||||
}
|
||||
const pageText = normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||||
if (/create\s*(?:paypal\s*)?account|agree\s*(?:&|and)?\s*create|创建.*(?:账户|账号)/i.test(pageText)
|
||||
&& findHostedCreateAccountButton()) {
|
||||
return false;
|
||||
}
|
||||
return /\/checkoutweb\//i.test(getPayPalPathname())
|
||||
&& Boolean(document.getElementById('phone') || document.getElementById('email'));
|
||||
}
|
||||
|
||||
function isHostedReviewPage() {
|
||||
return /\/webapps\/hermes/i.test(getPayPalPathname());
|
||||
}
|
||||
|
||||
function findHostedCreateAccountButton() {
|
||||
return document.getElementById('createAccount')
|
||||
|| document.getElementById('createAccountButton')
|
||||
|| document.querySelector('button[data-testid="createAccountButton"]')
|
||||
|| document.querySelector('button[data-testid="create-account-button"]')
|
||||
|| findClickableByText([
|
||||
/agree\s*(?:&|and)?\s*create\s*(?:paypal\s*)?account/i,
|
||||
/create\s*(?:paypal\s*)?account/i,
|
||||
/同意.*创建|创建.*账户|创建.*账号/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function isHostedCreateAccountPage() {
|
||||
if (isHostedLoginPage()) {
|
||||
return false;
|
||||
}
|
||||
if (document.getElementById('cardNumber') || document.getElementById('billingLine1')) {
|
||||
return false;
|
||||
}
|
||||
const button = findHostedCreateAccountButton();
|
||||
if (!button || !isVisibleElement(button) || !isEnabledControl(button)) {
|
||||
return false;
|
||||
}
|
||||
const pageText = normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||||
return /create\s*(?:paypal\s*)?account|agree\s*(?:&|and)?\s*create|创建.*(?:账户|账号)/i.test(pageText)
|
||||
|| /create/i.test(getActionText(button));
|
||||
}
|
||||
|
||||
function findHostedReviewConsentButton() {
|
||||
const direct = document.getElementById('consentButton')
|
||||
|| document.querySelector('button[data-testid="consentButton"]');
|
||||
if (direct && isVisibleElement(direct) && isEnabledControl(direct)) {
|
||||
return direct;
|
||||
}
|
||||
return findClickableByText([
|
||||
/agree\s*(?:and)?\s*continue|accept|continue/i,
|
||||
/同意并继续|同意|继续/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function detectPayPalHostedStage() {
|
||||
if (!/paypal\./i.test(String(location?.host || ''))) {
|
||||
return PAYPAL_HOSTED_STAGE_OUTSIDE;
|
||||
}
|
||||
if (isHostedGuestCheckoutPage()) {
|
||||
return PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT;
|
||||
}
|
||||
if (isHostedReviewPage() && findHostedReviewConsentButton()) {
|
||||
return PAYPAL_HOSTED_STAGE_REVIEW;
|
||||
}
|
||||
if (isHostedCreateAccountPage()) {
|
||||
return PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT;
|
||||
}
|
||||
if (isHostedLoginPage()) {
|
||||
return PAYPAL_HOSTED_STAGE_LOGIN;
|
||||
}
|
||||
return findApproveButton() ? PAYPAL_HOSTED_STAGE_APPROVAL : PAYPAL_HOSTED_STAGE_UNKNOWN;
|
||||
}
|
||||
|
||||
function fillHostedInputById(id, value) {
|
||||
const input = document.getElementById(String(id || '').trim());
|
||||
if (!input || !isVisibleElement(input) || !isEnabledControl(input)) {
|
||||
return false;
|
||||
}
|
||||
fillInput(input, String(value || ''));
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectHostedOptionByIdText(id, value) {
|
||||
const select = document.getElementById(String(id || '').trim());
|
||||
const expected = normalizeText(value).toLowerCase();
|
||||
if (!select || !expected) {
|
||||
return false;
|
||||
}
|
||||
const option = Array.from(select.options || []).find((item) => {
|
||||
const optionText = normalizeText(item.textContent || item.label || '').toLowerCase();
|
||||
const optionValue = normalizeText(item.value || '').toLowerCase();
|
||||
return optionText.includes(expected) || optionValue.includes(expected);
|
||||
});
|
||||
if (!option) {
|
||||
return false;
|
||||
}
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function findHostedSubmitButton() {
|
||||
return document.querySelector('button[data-testid="submit-button"]')
|
||||
|| document.querySelector('button[data-testid="hosted-payment-submit-button"]')
|
||||
|| document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
|
||||
|| document.querySelector('button.SubmitButton--complete')
|
||||
|| findEmailNextButton()
|
||||
|| findLoginNextButton()
|
||||
|| findClickableByText([
|
||||
/pay|continue|next|agree|subscribe/i,
|
||||
/支付|继续|下一步|同意|订阅/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function getHostedStepKey(stage = '', fallback = 'plus-checkout-create') {
|
||||
return PAYPAL_HOSTED_STEP_KEYS[stage] || fallback;
|
||||
}
|
||||
|
||||
async function clickHostedSubmitButton(options = {}) {
|
||||
const stepKey = String(options.stepKey || getHostedStepKey(options.stage)).trim();
|
||||
const label = String(options.label || 'hosted-paypal-submit').trim();
|
||||
const maxAttempts = Math.max(1, Math.floor(Number(options.maxAttempts) || 3));
|
||||
let lastButtonText = '';
|
||||
let lastDisabled = false;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
const button = await waitUntil(() => {
|
||||
const candidate = findHostedSubmitButton();
|
||||
return candidate && isVisibleElement(candidate) ? candidate : null;
|
||||
}, {
|
||||
intervalMs: 500,
|
||||
timeoutMs: 15000,
|
||||
timeoutMessage: 'PayPal hosted checkout 未找到可点击的继续/提交按钮。',
|
||||
});
|
||||
lastButtonText = getActionText(button);
|
||||
lastDisabled = !isEnabledControl(button);
|
||||
if (lastDisabled) {
|
||||
if (attempt >= maxAttempts) {
|
||||
throw new Error('PayPal hosted checkout 继续/提交按钮长时间不可用。');
|
||||
}
|
||||
await sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
await performPayPalOperationWithDelay({ stepKey, kind: 'click', label }, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
await sleep(1000);
|
||||
return {
|
||||
clicked: true,
|
||||
buttonText: lastButtonText,
|
||||
attempt,
|
||||
};
|
||||
}
|
||||
return {
|
||||
clicked: false,
|
||||
buttonText: lastButtonText,
|
||||
disabled: lastDisabled,
|
||||
};
|
||||
}
|
||||
|
||||
async function clickHostedEmailNextButton() {
|
||||
const button = await waitUntil(() => {
|
||||
const candidate = findEmailNextButton();
|
||||
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||||
}, {
|
||||
intervalMs: 500,
|
||||
timeoutMs: 15000,
|
||||
timeoutMessage: 'PayPal hosted checkout 未找到邮箱页“下一页”按钮。',
|
||||
});
|
||||
const buttonText = getActionText(button);
|
||||
await performPayPalOperationWithDelay({
|
||||
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_LOGIN),
|
||||
kind: 'click',
|
||||
label: 'hosted-paypal-email-next',
|
||||
}, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
return {
|
||||
clicked: true,
|
||||
buttonText,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHostedPhoneDigits(value = '') {
|
||||
return String(value || '').replace(/\D/g, '');
|
||||
}
|
||||
|
||||
function verifyHostedPhoneBeforeSubmit(expectedPhone = '') {
|
||||
const phoneInput = document.getElementById('phone');
|
||||
if (!phoneInput || !isVisibleElement(phoneInput)) {
|
||||
throw new Error('PayPal hosted checkout 未找到电话输入框。');
|
||||
}
|
||||
const expectedDigits = normalizeHostedPhoneDigits(expectedPhone || PAYPAL_HOSTED_DEFAULT_PHONE);
|
||||
const renderedDigits = normalizeHostedPhoneDigits(phoneInput.value || '');
|
||||
if (!expectedDigits) {
|
||||
throw new Error('PayPal hosted checkout 电话配置为空。');
|
||||
}
|
||||
const comparableRenderedDigits = renderedDigits.length > expectedDigits.length
|
||||
? renderedDigits.slice(-expectedDigits.length)
|
||||
: renderedDigits;
|
||||
if (comparableRenderedDigits !== expectedDigits) {
|
||||
throw new Error(`PayPal hosted checkout 电话不一致:配置 ${expectedDigits},页面 ${renderedDigits || '(空)'}。`);
|
||||
}
|
||||
return {
|
||||
payloadPhoneDigits: expectedDigits,
|
||||
renderedPhoneDigits: renderedDigits,
|
||||
phoneMatched: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function clickHostedCreateAccount(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const button = await waitUntil(() => {
|
||||
const candidate = findHostedCreateAccountButton();
|
||||
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||||
}, {
|
||||
intervalMs: 500,
|
||||
timeoutMs: 30000,
|
||||
timeoutMessage: 'PayPal hosted checkout 未找到创建账号确认按钮。',
|
||||
});
|
||||
await performPayPalOperationWithDelay({
|
||||
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT),
|
||||
kind: 'click',
|
||||
label: 'hosted-paypal-create-account',
|
||||
}, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT,
|
||||
clicked: true,
|
||||
submitted: true,
|
||||
buttonText: getActionText(button),
|
||||
};
|
||||
}
|
||||
|
||||
function buildHostedRandomEmail() {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let value = '';
|
||||
for (let index = 0; index < 16; index += 1) {
|
||||
value += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return `${value}@gmail.com`;
|
||||
}
|
||||
|
||||
function buildHostedRandomPassword() {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^';
|
||||
let value = 'Aa1!';
|
||||
while (value.length < 14) {
|
||||
value += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function buildHostedVisaCard() {
|
||||
const digits = [4, 1, 4, 7];
|
||||
while (digits.length < 15) {
|
||||
digits.push(Math.floor(Math.random() * 10));
|
||||
}
|
||||
const reversed = digits.slice().reverse();
|
||||
let sum = 0;
|
||||
for (let index = 0; index < reversed.length; index += 1) {
|
||||
let digit = reversed[index];
|
||||
if (index % 2 === 0) {
|
||||
digit *= 2;
|
||||
if (digit > 9) digit -= 9;
|
||||
}
|
||||
sum += digit;
|
||||
}
|
||||
digits.push((10 - (sum % 10)) % 10);
|
||||
const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
|
||||
const year = (new Date().getFullYear() % 100) + 3;
|
||||
return {
|
||||
number: digits.join(''),
|
||||
expiry: `${month} / ${year}`,
|
||||
cvv: String(Math.floor(100 + Math.random() * 900)),
|
||||
};
|
||||
}
|
||||
|
||||
async function submitHostedLogin(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const email = normalizeText(payload.email || buildHostedRandomEmail());
|
||||
const emailInput = document.getElementById('email') || findEmailInput();
|
||||
if (!emailInput) {
|
||||
throw new Error('PayPal hosted checkout 未找到邮箱输入框。');
|
||||
}
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
const clickResult = await clickHostedEmailNextButton();
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_LOGIN,
|
||||
submitted: true,
|
||||
generatedEmail: email,
|
||||
clicked: Boolean(clickResult.clicked),
|
||||
};
|
||||
}
|
||||
|
||||
async function fillHostedGuestCheckout(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const countrySelect = document.getElementById('country');
|
||||
if (countrySelect && String(countrySelect.value || '').trim().toUpperCase() !== 'US') {
|
||||
countrySelect.value = 'US';
|
||||
countrySelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
await sleep(1000);
|
||||
}
|
||||
const generatedCard = buildHostedVisaCard();
|
||||
const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
|
||||
const values = {
|
||||
email: normalizeText(payload.email || buildHostedRandomEmail()),
|
||||
phone: normalizeText(payload.phone || PAYPAL_HOSTED_DEFAULT_PHONE),
|
||||
cardNumber: String(payload.cardNumber || generatedCard.number).replace(/\s+/g, ''),
|
||||
cardExpiry: normalizeText(payload.cardExpiry || generatedCard.expiry),
|
||||
cardCvv: normalizeText(payload.cardCvv || generatedCard.cvv),
|
||||
password: String(payload.password || buildHostedRandomPassword()),
|
||||
firstName: normalizeText(payload.firstName || 'James'),
|
||||
lastName: normalizeText(payload.lastName || 'Smith'),
|
||||
};
|
||||
fillHostedInputById('email', values.email);
|
||||
fillHostedInputById('phone', values.phone);
|
||||
fillHostedInputById('cardNumber', values.cardNumber);
|
||||
fillHostedInputById('cardExpiry', values.cardExpiry);
|
||||
fillHostedInputById('cardCvv', values.cardCvv);
|
||||
fillHostedInputById('password', values.password);
|
||||
fillHostedInputById('firstName', values.firstName);
|
||||
fillHostedInputById('lastName', values.lastName);
|
||||
fillHostedInputById('billingLine1', address.street || address.address1 || '');
|
||||
fillHostedInputById('billingCity', address.city || '');
|
||||
fillHostedInputById('billingPostalCode', address.zip || address.postalCode || '');
|
||||
selectHostedOptionByIdText('billingState', address.state || address.region || '');
|
||||
const phoneCheck = verifyHostedPhoneBeforeSubmit(values.phone);
|
||||
const clickResult = await clickHostedSubmitButton({
|
||||
stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
|
||||
label: 'hosted-paypal-card-submit',
|
||||
maxAttempts: 4,
|
||||
});
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
|
||||
submitted: true,
|
||||
payloadPhone: values.phone,
|
||||
...phoneCheck,
|
||||
};
|
||||
}
|
||||
|
||||
async function clickHostedReviewConsent() {
|
||||
await waitForDocumentComplete();
|
||||
const button = await waitUntil(() => {
|
||||
const candidate = findHostedReviewConsentButton();
|
||||
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||||
}, {
|
||||
intervalMs: 500,
|
||||
timeoutMs: 30000,
|
||||
timeoutMessage: 'PayPal hosted checkout 未找到账单确认按钮。',
|
||||
});
|
||||
await performPayPalOperationWithDelay({
|
||||
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_REVIEW),
|
||||
kind: 'click',
|
||||
label: 'hosted-paypal-review-consent',
|
||||
}, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_REVIEW,
|
||||
submitted: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function runPayPalHostedCheckoutStep(payload = {}) {
|
||||
const stage = detectPayPalHostedStage();
|
||||
const expectedStage = String(payload.expectedStage || '').trim();
|
||||
if (expectedStage && stage !== expectedStage) {
|
||||
return {
|
||||
stage,
|
||||
expectedStage,
|
||||
submitted: false,
|
||||
skipped: true,
|
||||
approveReady: Boolean(findApproveButton()),
|
||||
};
|
||||
}
|
||||
if (stage === PAYPAL_HOSTED_STAGE_LOGIN) {
|
||||
return submitHostedLogin(payload);
|
||||
}
|
||||
if (stage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) {
|
||||
return fillHostedGuestCheckout(payload);
|
||||
}
|
||||
if (stage === PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) {
|
||||
return clickHostedCreateAccount(payload);
|
||||
}
|
||||
if (stage === PAYPAL_HOSTED_STAGE_REVIEW) {
|
||||
return clickHostedReviewConsent();
|
||||
}
|
||||
return {
|
||||
stage,
|
||||
submitted: false,
|
||||
approveReady: Boolean(findApproveButton()),
|
||||
};
|
||||
}
|
||||
|
||||
function inspectPayPalHostedState() {
|
||||
const stage = detectPayPalHostedStage();
|
||||
const createAccountButton = findHostedCreateAccountButton();
|
||||
return {
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
hostedStage: stage,
|
||||
hasGuestCardFields: Boolean(document.getElementById('cardNumber')),
|
||||
hasHostedEmailInput: Boolean(document.getElementById('email') || findEmailInput()),
|
||||
createAccountReady: Boolean(createAccountButton && isVisibleElement(createAccountButton) && isEnabledControl(createAccountButton)),
|
||||
reviewConsentReady: Boolean(findHostedReviewConsentButton()),
|
||||
approveReady: Boolean(findApproveButton()),
|
||||
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
|
||||
};
|
||||
}
|
||||
|
||||
function findPasskeyPromptButtons() {
|
||||
const promptPatterns = [
|
||||
/passkey|通行密钥|安全密钥|下次登录|faster|save/i,
|
||||
];
|
||||
const bodyText = normalizeText(document.body?.innerText || '');
|
||||
const likelyPrompt = promptPatterns.some((pattern) => pattern.test(bodyText));
|
||||
if (!likelyPrompt) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cancelOrClose = getVisibleControls('button, a, [role="button"]')
|
||||
.filter((el) => {
|
||||
const text = getActionText(el);
|
||||
return /取消|稍后|不保存|不用|关闭|cancel|not now|maybe later|skip|close|x/i.test(text)
|
||||
|| el.getAttribute?.('aria-label')?.match(/close|关闭/i);
|
||||
});
|
||||
|
||||
const iconCloseButtons = getVisibleControls('button, [role="button"]')
|
||||
.filter((el) => {
|
||||
const text = getActionText(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return (/^×$|^x$/i.test(text) || /close|关闭/i.test(text))
|
||||
&& rect.width <= 64
|
||||
&& rect.height <= 64;
|
||||
});
|
||||
|
||||
return [...cancelOrClose, ...iconCloseButtons];
|
||||
}
|
||||
|
||||
function hasPasskeyPrompt() {
|
||||
return findPasskeyPromptButtons().length > 0;
|
||||
}
|
||||
|
||||
function getPayPalLoginPhase(emailInput, passwordInput) {
|
||||
const emailNextButton = findEmailNextButton();
|
||||
const passwordLoginButton = findPasswordLoginButton();
|
||||
if (emailInput && emailNextButton && isEnabledControl(emailNextButton) && (!passwordInput || !passwordLoginButton)) {
|
||||
return 'email';
|
||||
}
|
||||
if (emailInput && passwordInput) return 'login_combined';
|
||||
if (passwordInput) return 'password';
|
||||
if (emailInput) return 'email';
|
||||
return '';
|
||||
}
|
||||
|
||||
function refillPayPalEmailInput(emailInput, email) {
|
||||
if (!emailInput) return;
|
||||
if (typeof emailInput.focus === 'function') {
|
||||
emailInput.focus();
|
||||
}
|
||||
fillInput(emailInput, '');
|
||||
fillInput(emailInput, email);
|
||||
if (typeof emailInput.blur === 'function') {
|
||||
emailInput.blur();
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPayPalLogin(payload = {}) {
|
||||
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
|
||||
? performPayPalOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
|
||||
const email = normalizeText(payload.email || '');
|
||||
const password = String(payload.password || '');
|
||||
if (!password) {
|
||||
throw new Error('PayPal 密码为空,请先在侧边栏配置。');
|
||||
}
|
||||
|
||||
let passwordInput = findPasswordInput();
|
||||
const emailInput = findEmailInput();
|
||||
const emailNextButton = findEmailNextButton();
|
||||
|
||||
if (emailInput && emailNextButton && isEnabledControl(emailNextButton) && (!passwordInput || !findPasswordLoginButton())) {
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-email' }, async () => {
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
simulateClick(emailNextButton);
|
||||
});
|
||||
return {
|
||||
submitted: false,
|
||||
phase: 'email_submitted',
|
||||
awaiting: 'password_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (!passwordInput && emailInput && email) {
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-email' }, async () => {
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
const nextButton = await waitUntil(() => {
|
||||
const button = findEmailNextButton() || findLoginNextButton();
|
||||
return button && isEnabledControl(button) ? button : null;
|
||||
}, {
|
||||
intervalMs: 250,
|
||||
timeoutMs: 8000,
|
||||
timeoutMessage: 'PayPal email page did not expose a clickable next/continue button.',
|
||||
});
|
||||
simulateClick(nextButton);
|
||||
});
|
||||
return {
|
||||
submitted: false,
|
||||
phase: 'email_submitted',
|
||||
awaiting: 'password_page',
|
||||
};
|
||||
} else if (!passwordInput && emailInput && !email) {
|
||||
throw new Error('PayPal 账号为空,请先在侧边栏配置。');
|
||||
} else if (emailInput && email) {
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'fill', label: 'paypal-email' }, async () => {
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
});
|
||||
}
|
||||
|
||||
passwordInput = passwordInput || await waitUntil(() => findPasswordInput(), {
|
||||
intervalMs: 250,
|
||||
timeoutMs: 8000,
|
||||
timeoutMessage: 'PayPal password page did not expose a password input.',
|
||||
});
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-password' }, async () => {
|
||||
fillInput(passwordInput, password);
|
||||
await sleep(1000);
|
||||
|
||||
const loginButton = await waitUntil(() => {
|
||||
const button = findClickableByText([
|
||||
/login|log\s*in|sign\s*in|continue/i,
|
||||
/登录|登入|继续/i,
|
||||
]);
|
||||
return button && isEnabledControl(button) ? button : null;
|
||||
}, {
|
||||
intervalMs: 250,
|
||||
timeoutMs: 8000,
|
||||
timeoutMessage: 'PayPal password page did not expose a clickable login/continue button.',
|
||||
});
|
||||
|
||||
simulateClick(loginButton);
|
||||
});
|
||||
return {
|
||||
submitted: true,
|
||||
phase: 'password_submitted',
|
||||
awaiting: 'redirect_or_approval',
|
||||
};
|
||||
}
|
||||
|
||||
async function dismissPayPalPrompts() {
|
||||
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
|
||||
? performPayPalOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const buttons = findPasskeyPromptButtons();
|
||||
let clicked = 0;
|
||||
for (const button of buttons) {
|
||||
if (!isVisibleElement(button) || !isEnabledControl(button)) {
|
||||
continue;
|
||||
}
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'click', label: 'paypal-dismiss-prompt' }, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
clicked += 1;
|
||||
await sleep(500);
|
||||
}
|
||||
return {
|
||||
clicked,
|
||||
hasPromptAfterClick: hasPasskeyPrompt(),
|
||||
};
|
||||
}
|
||||
|
||||
async function clickPayPalApprove() {
|
||||
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
|
||||
? performPayPalOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
await dismissPayPalPrompts().catch(() => ({ clicked: 0 }));
|
||||
|
||||
const button = findApproveButton();
|
||||
if (!button || !isEnabledControl(button)) {
|
||||
return {
|
||||
clicked: false,
|
||||
state: inspectPayPalState(),
|
||||
};
|
||||
}
|
||||
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'click', label: 'paypal-approve' }, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
return {
|
||||
clicked: true,
|
||||
buttonText: getActionText(button),
|
||||
};
|
||||
}
|
||||
|
||||
function inspectPayPalState() {
|
||||
const emailInput = findEmailInput();
|
||||
const passwordInput = findPasswordInput();
|
||||
const approveButton = findApproveButton();
|
||||
const loginPhase = getPayPalLoginPhase(emailInput, passwordInput);
|
||||
return {
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
needsLogin: Boolean(loginPhase),
|
||||
loginPhase,
|
||||
hasEmailInput: Boolean(emailInput),
|
||||
hasPasswordInput: Boolean(passwordInput),
|
||||
approveReady: Boolean(approveButton && isEnabledControl(approveButton)),
|
||||
approveButtonText: approveButton ? getActionText(approveButton) : '',
|
||||
hasPasskeyPrompt: hasPasskeyPrompt(),
|
||||
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,237 @@
|
||||
(function attachPhoneCountryUtils(root, factory) {
|
||||
root.MultiPagePhoneCountryUtils = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createPhoneCountryUtils() {
|
||||
const KNOWN_DIAL_CODES = Object.freeze([
|
||||
'1246', '1264', '1268', '1284', '1340', '1345', '1441', '1473', '1649', '1664', '1670', '1671', '1684',
|
||||
'1721', '1758', '1767', '1784', '1809', '1829', '1849', '1868', '1869', '1876',
|
||||
'971', '962', '886', '880', '856', '855', '852', '853', '673', '672', '670', '599', '598', '597', '596',
|
||||
'595', '594', '593', '592', '591', '590', '509', '508', '507', '506', '505', '504', '503', '502', '501',
|
||||
'423', '421', '420', '389', '387', '386', '385', '383', '382', '381', '380', '379', '378', '377', '376',
|
||||
'375', '374', '373', '372', '371', '370', '359', '358', '357', '356', '355', '354', '353', '352', '351',
|
||||
'350', '299', '298', '297', '291', '290', '269', '268', '267', '266', '265', '264', '263', '262', '261',
|
||||
'260', '258', '257', '256', '255', '254', '253', '252', '251', '250', '249', '248', '247', '246', '245',
|
||||
'244', '243', '242', '241', '240', '239', '238', '237', '236', '235', '234', '233', '232', '231', '230',
|
||||
'229', '228', '227', '226', '225', '224', '223', '222', '221', '220', '218', '216', '213', '212', '211',
|
||||
'98', '95', '94', '93', '92', '91', '90', '89', '88', '86', '84', '82', '81', '66', '65', '64', '63',
|
||||
'62', '61', '60', '58', '57', '56', '55', '54', '53', '52', '51', '49', '48', '47', '46', '45', '44',
|
||||
'43', '41', '40', '39', '36', '34', '33', '32', '31', '30', '27', '20', '7', '1',
|
||||
]);
|
||||
|
||||
function normalizePhoneDigits(value) {
|
||||
let digits = String(value || '').replace(/\D+/g, '');
|
||||
if (digits.startsWith('00')) {
|
||||
digits = digits.slice(2);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
function extractDialCodeFromText(value) {
|
||||
const match = String(value || '').match(/\(\+\s*(\d{1,4})\s*\)|\+\s*\(\s*(\d{1,4})\s*\)|\+\s*(\d{1,4})\b/);
|
||||
return String(match?.[1] || match?.[2] || match?.[3] || '').trim();
|
||||
}
|
||||
|
||||
function normalizeCountryLabel(value) {
|
||||
return String(value || '')
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/&/g, ' and ')
|
||||
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeCountryOptionValue(value) {
|
||||
return String(value || '').trim().toUpperCase();
|
||||
}
|
||||
|
||||
function getCountryLabelAliases(value) {
|
||||
const aliases = new Set();
|
||||
const addAlias = (alias) => {
|
||||
const normalized = normalizeCountryLabel(alias);
|
||||
if (normalized) {
|
||||
aliases.add(normalized);
|
||||
}
|
||||
};
|
||||
|
||||
const raw = String(value || '').trim();
|
||||
addAlias(raw);
|
||||
|
||||
const normalized = normalizeCountryLabel(raw);
|
||||
const compact = normalized.replace(/\s+/g, '');
|
||||
if (
|
||||
/(?:^|\s)(?:gb|uk)(?:\s|$)/i.test(raw)
|
||||
|| /england|united\s*kingdom|great\s*britain|\bbritain\b/i.test(raw)
|
||||
|| /英国|英格兰|大不列颠/.test(raw)
|
||||
|| ['gb', 'uk', 'england', 'unitedkingdom', 'greatbritain', 'britain'].includes(compact)
|
||||
) {
|
||||
[
|
||||
'GB',
|
||||
'UK',
|
||||
'United Kingdom',
|
||||
'Great Britain',
|
||||
'Britain',
|
||||
'England',
|
||||
'英国',
|
||||
'英格兰',
|
||||
'大不列颠',
|
||||
].forEach(addAlias);
|
||||
}
|
||||
|
||||
return Array.from(aliases);
|
||||
}
|
||||
|
||||
function getRegionDisplayName(regionCode, locale) {
|
||||
const normalizedRegionCode = normalizeCountryOptionValue(regionCode);
|
||||
const normalizedLocale = String(locale || '').trim();
|
||||
if (!/^[A-Z]{2}$/.test(normalizedRegionCode) || !normalizedLocale || typeof Intl?.DisplayNames !== 'function') {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return String(
|
||||
new Intl.DisplayNames([normalizedLocale], { type: 'region' }).of(normalizedRegionCode) || ''
|
||||
).trim();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getOptionLabel(option) {
|
||||
return String(option?.textContent || option?.label || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getOptionMatchLabels(option, options = {}) {
|
||||
const labels = new Set();
|
||||
const pushLabel = (value) => {
|
||||
const label = String(value || '').replace(/\s+/g, ' ').trim();
|
||||
if (label) {
|
||||
labels.add(label);
|
||||
}
|
||||
};
|
||||
|
||||
const getLabel = typeof options.getOptionLabel === 'function'
|
||||
? options.getOptionLabel
|
||||
: getOptionLabel;
|
||||
pushLabel(getLabel(option));
|
||||
|
||||
const regionCode = normalizeCountryOptionValue(option?.value);
|
||||
if (/^[A-Z]{2}$/.test(regionCode)) {
|
||||
pushLabel(regionCode);
|
||||
pushLabel(getRegionDisplayName(regionCode, 'en'));
|
||||
|
||||
const pageLocale = String(
|
||||
options.pageLocale
|
||||
|| options.document?.documentElement?.lang
|
||||
|| options.document?.documentElement?.getAttribute?.('lang')
|
||||
|| options.navigator?.language
|
||||
|| ''
|
||||
).trim();
|
||||
if (pageLocale && !/^en(?:[-_]|$)/i.test(pageLocale)) {
|
||||
pushLabel(getRegionDisplayName(regionCode, pageLocale));
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(labels);
|
||||
}
|
||||
|
||||
function resolveDialCodeFromPhoneNumber(phoneNumber = '', texts = []) {
|
||||
const digits = normalizePhoneDigits(phoneNumber);
|
||||
if (!digits) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const textDialCodes = texts
|
||||
.map((text) => normalizePhoneDigits(extractDialCodeFromText(text)))
|
||||
.filter((dialCode) => dialCode && digits.startsWith(dialCode) && digits.length > dialCode.length)
|
||||
.sort((left, right) => right.length - left.length);
|
||||
if (textDialCodes[0]) {
|
||||
return textDialCodes[0];
|
||||
}
|
||||
|
||||
return KNOWN_DIAL_CODES.find((code) => digits.startsWith(code) && digits.length > code.length) || '';
|
||||
}
|
||||
|
||||
function findOptionByCountryLabel(options, countryLabel, config = {}) {
|
||||
const source = Array.from(options || []);
|
||||
const normalizedTargets = getCountryLabelAliases(countryLabel);
|
||||
if (source.length === 0 || normalizedTargets.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.find((option) => (
|
||||
getOptionMatchLabels(option, config).some((label) => normalizedTargets.includes(normalizeCountryLabel(label)))
|
||||
))
|
||||
|| source.find((option) => {
|
||||
const normalizedLabels = getOptionMatchLabels(option, config)
|
||||
.map((label) => normalizeCountryLabel(label))
|
||||
.filter(Boolean);
|
||||
return normalizedLabels.some((optionLabel) => normalizedTargets.some((normalizedTarget) => (
|
||||
optionLabel.length > 2
|
||||
&& normalizedTarget.length > 2
|
||||
&& (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel))
|
||||
)));
|
||||
})
|
||||
|| null;
|
||||
}
|
||||
|
||||
function findOptionByPhoneNumber(options, phoneNumber, config = {}) {
|
||||
const source = Array.from(options || []);
|
||||
const digits = normalizePhoneDigits(phoneNumber);
|
||||
if (source.length === 0 || !digits) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getLabel = typeof config.getOptionLabel === 'function'
|
||||
? config.getOptionLabel
|
||||
: getOptionLabel;
|
||||
let bestMatch = null;
|
||||
let bestDialCodeLength = 0;
|
||||
for (const option of source) {
|
||||
const dialCode = normalizePhoneDigits(extractDialCodeFromText(getLabel(option)));
|
||||
if (!dialCode || !digits.startsWith(dialCode) || dialCode.length <= bestDialCodeLength) {
|
||||
continue;
|
||||
}
|
||||
bestMatch = option;
|
||||
bestDialCodeLength = dialCode.length;
|
||||
}
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
function findElementByDialCode(elements, phoneNumber, config = {}) {
|
||||
const source = Array.from(elements || []);
|
||||
const digits = normalizePhoneDigits(phoneNumber);
|
||||
if (source.length === 0 || !digits) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getText = typeof config.getText === 'function' ? config.getText : getOptionLabel;
|
||||
let bestMatch = null;
|
||||
let bestDialCodeLength = 0;
|
||||
for (const element of source) {
|
||||
const dialCode = normalizePhoneDigits(extractDialCodeFromText(getText(element)));
|
||||
if (!dialCode || !digits.startsWith(dialCode) || dialCode.length <= bestDialCodeLength) {
|
||||
continue;
|
||||
}
|
||||
bestMatch = element;
|
||||
bestDialCodeLength = dialCode.length;
|
||||
}
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
return {
|
||||
extractDialCodeFromText,
|
||||
findElementByDialCode,
|
||||
findOptionByCountryLabel,
|
||||
findOptionByPhoneNumber,
|
||||
getCountryLabelAliases,
|
||||
getOptionLabel,
|
||||
getOptionMatchLabels,
|
||||
getRegionDisplayName,
|
||||
normalizeCountryLabel,
|
||||
normalizeCountryOptionValue,
|
||||
normalizePhoneDigits,
|
||||
resolveDialCodeFromPhoneNumber,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,686 @@
|
||||
// flows/openai/content/sub2api-panel.js — 页内脚本:SUB2API 后台(OAuth 生成与回调提交)
|
||||
|
||||
console.log('[MultiPage:sub2api-panel] Content script loaded on', location.href);
|
||||
|
||||
const SUB2API_PANEL_LISTENER_SENTINEL = 'data-multipage-sub2api-panel-listener';
|
||||
const SUB2API_DEFAULT_GROUP_NAME = 'codex';
|
||||
const SUB2API_DEFAULT_PROXY_NAME = '';
|
||||
const SUB2API_DEFAULT_REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
||||
const SUB2API_DEFAULT_CONCURRENCY = 10;
|
||||
const SUB2API_DEFAULT_PRIORITY = 1;
|
||||
const SUB2API_DEFAULT_RATE_MULTIPLIER = 1;
|
||||
|
||||
if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(SUB2API_PANEL_LISTENER_SENTINEL, '1');
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'EXECUTE_NODE' || message.type === 'REQUEST_OAUTH_URL') {
|
||||
resetStopState();
|
||||
const handler = message.type === 'REQUEST_OAUTH_URL'
|
||||
? requestOAuthUrl(message.payload)
|
||||
: handleNode(message.nodeId || message.payload?.nodeId, message.payload);
|
||||
handler.then((result) => {
|
||||
sendResponse({ ok: true, ...(result || {}) });
|
||||
}).catch((err) => {
|
||||
if (isStopError(err)) {
|
||||
if (message.payload?.visibleStep || message.step) {
|
||||
log('已被用户停止。', 'warn', { step: message.payload?.visibleStep || message.step });
|
||||
}
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
if (message.nodeId || message.payload?.nodeId) {
|
||||
reportError(message.nodeId || message.payload?.nodeId, err.message);
|
||||
}
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('[MultiPage:sub2api-panel] 消息监听已存在,跳过重复注册');
|
||||
}
|
||||
|
||||
function getSub2ApiOrigin(payload = {}) {
|
||||
const rawUrl = payload.sub2apiUrl || location.href;
|
||||
try {
|
||||
return new URL(rawUrl).origin;
|
||||
} catch {
|
||||
return location.origin;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRedirectUri() {
|
||||
const input = SUB2API_DEFAULT_REDIRECT_URI;
|
||||
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
|
||||
const parsed = new URL(withProtocol);
|
||||
if (!parsed.pathname || parsed.pathname === '/') {
|
||||
parsed.pathname = '/auth/callback';
|
||||
}
|
||||
if (parsed.pathname !== '/auth/callback') {
|
||||
throw new Error('SUB2API 回调地址必须是 /auth/callback,例如 http://localhost:1455/auth/callback');
|
||||
}
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
async function handleStep(step, payload = {}) {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return step1_generateOpenAiAuthUrl(payload);
|
||||
case 10:
|
||||
case 12:
|
||||
case 13:
|
||||
return step9_submitOpenAiCallback({ ...(payload || {}), visibleStep: step });
|
||||
default:
|
||||
throw new Error(`sub2api-panel.js 不处理步骤 ${step}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNode(nodeId, payload = {}) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
switch (normalizedNodeId) {
|
||||
case 'platform-verify':
|
||||
return step9_submitOpenAiCallback(payload);
|
||||
default:
|
||||
throw new Error(`sub2api-panel.js 不处理节点 ${normalizedNodeId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestOAuthUrl(payload = {}) {
|
||||
return step1_generateOpenAiAuthUrl(payload, { report: false });
|
||||
}
|
||||
|
||||
async function requestJson(origin, path, options = {}) {
|
||||
throwIfStopped();
|
||||
const {
|
||||
method = 'GET',
|
||||
token = '',
|
||||
body = undefined,
|
||||
} = options;
|
||||
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method,
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
|
||||
if (json && typeof json === 'object' && 'code' in json) {
|
||||
if (json.code === 0) {
|
||||
return json.data;
|
||||
}
|
||||
throw new Error(json.message || json.detail || `请求失败(${path})`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error((json && (json.message || json.detail)) || `请求失败(HTTP ${response.status}):${path}`);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
function storeAuthSession(loginData) {
|
||||
if (!loginData?.access_token) {
|
||||
throw new Error('SUB2API 登录返回缺少 access_token。');
|
||||
}
|
||||
|
||||
localStorage.setItem('auth_token', loginData.access_token);
|
||||
if (loginData.refresh_token) {
|
||||
localStorage.setItem('refresh_token', loginData.refresh_token);
|
||||
} else {
|
||||
localStorage.removeItem('refresh_token');
|
||||
}
|
||||
if (loginData.expires_in) {
|
||||
localStorage.setItem('token_expires_at', String(Date.now() + Number(loginData.expires_in) * 1000));
|
||||
}
|
||||
if (loginData.user) {
|
||||
localStorage.setItem('auth_user', JSON.stringify(loginData.user));
|
||||
}
|
||||
sessionStorage.removeItem('auth_expired');
|
||||
}
|
||||
|
||||
async function loginSub2Api(payload = {}) {
|
||||
const email = (payload.sub2apiEmail || '').trim();
|
||||
const password = payload.sub2apiPassword || '';
|
||||
const origin = getSub2ApiOrigin(payload);
|
||||
|
||||
if (!email) {
|
||||
throw new Error('缺少 SUB2API 登录邮箱,请先在侧边栏填写。');
|
||||
}
|
||||
if (!password) {
|
||||
throw new Error('缺少 SUB2API 登录密码,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
log('步骤:正在登录 SUB2API 后台...');
|
||||
const loginData = await requestJson(origin, '/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
});
|
||||
storeAuthSession(loginData);
|
||||
|
||||
return {
|
||||
origin,
|
||||
token: loginData.access_token,
|
||||
user: loginData.user || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function getGroupByName(origin, token, groupName) {
|
||||
const targetName = (groupName || SUB2API_DEFAULT_GROUP_NAME).trim() || SUB2API_DEFAULT_GROUP_NAME;
|
||||
const groups = await requestJson(origin, '/api/v1/admin/groups/all', {
|
||||
method: 'GET',
|
||||
token,
|
||||
});
|
||||
|
||||
const normalized = targetName.toLowerCase();
|
||||
const group = (groups || []).find((item) => {
|
||||
const itemName = String(item?.name || '').trim().toLowerCase();
|
||||
if (!itemName) return false;
|
||||
if (itemName !== normalized) return false;
|
||||
return !item.platform || item.platform === 'openai';
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new Error(`SUB2API 中未找到名为“${targetName}”的 openai 分组。`);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
function normalizeSub2ApiGroupNames(value) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '').split(/[\r\n,,;;]+/);
|
||||
const seen = new Set();
|
||||
const names = [];
|
||||
for (const item of source) {
|
||||
const name = String(item || '').trim();
|
||||
const key = name.toLowerCase();
|
||||
if (!name || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
names.push(name);
|
||||
}
|
||||
return names.length ? names : [SUB2API_DEFAULT_GROUP_NAME];
|
||||
}
|
||||
|
||||
async function getGroupsByNames(origin, token, groupNames) {
|
||||
const targetNames = normalizeSub2ApiGroupNames(groupNames);
|
||||
const groups = await requestJson(origin, '/api/v1/admin/groups/all', {
|
||||
method: 'GET',
|
||||
token,
|
||||
});
|
||||
const matched = [];
|
||||
const missing = [];
|
||||
|
||||
for (const targetName of targetNames) {
|
||||
const normalized = targetName.toLowerCase();
|
||||
const group = (groups || []).find((item) => {
|
||||
const itemName = String(item?.name || '').trim().toLowerCase();
|
||||
if (!itemName) return false;
|
||||
if (itemName !== normalized) return false;
|
||||
return !item.platform || item.platform === 'openai';
|
||||
});
|
||||
if (group) {
|
||||
matched.push(group);
|
||||
} else {
|
||||
missing.push(targetName);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
throw new Error(`SUB2API 中未找到以下 openai 分组:${missing.join('、')}。`);
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
function normalizeSub2ApiProxyPreference(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function resolveSub2ApiProxyPreference(payload = {}, backgroundState = {}) {
|
||||
if (payload.sub2apiDefaultProxyName !== undefined) {
|
||||
return normalizeSub2ApiProxyPreference(payload.sub2apiDefaultProxyName);
|
||||
}
|
||||
if (backgroundState.sub2apiDefaultProxyName !== undefined) {
|
||||
return normalizeSub2ApiProxyPreference(backgroundState.sub2apiDefaultProxyName);
|
||||
}
|
||||
return SUB2API_DEFAULT_PROXY_NAME;
|
||||
}
|
||||
|
||||
function resolveSub2ApiAccountPriority(payload = {}, backgroundState = {}) {
|
||||
const candidate = payload.sub2apiAccountPriority !== undefined
|
||||
? payload.sub2apiAccountPriority
|
||||
: backgroundState.sub2apiAccountPriority;
|
||||
const rawValue = String(candidate ?? '').trim();
|
||||
if (!rawValue) {
|
||||
return SUB2API_DEFAULT_PRIORITY;
|
||||
}
|
||||
const numeric = Number(rawValue);
|
||||
if (!Number.isSafeInteger(numeric) || numeric < 1) {
|
||||
throw new Error('SUB2API 账号优先级必须是大于等于 1 的整数。');
|
||||
}
|
||||
return numeric;
|
||||
}
|
||||
|
||||
function normalizeProxyId(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return null;
|
||||
}
|
||||
const normalized = Number(value);
|
||||
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildProxyDisplayName(proxy = {}) {
|
||||
const id = normalizeProxyId(proxy.id);
|
||||
const name = String(proxy.name || '').trim();
|
||||
const protocol = String(proxy.protocol || '').trim();
|
||||
const host = String(proxy.host || '').trim();
|
||||
const port = proxy.port === undefined || proxy.port === null ? '' : String(proxy.port).trim();
|
||||
const address = protocol && host && port ? `${protocol}://${host}:${port}` : '';
|
||||
const parts = [
|
||||
name || '(未命名代理)',
|
||||
id ? `#${id}` : '',
|
||||
address,
|
||||
].filter(Boolean);
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function buildProxySearchText(proxy = {}) {
|
||||
return [
|
||||
proxy.id,
|
||||
proxy.name,
|
||||
proxy.protocol,
|
||||
proxy.host,
|
||||
proxy.port,
|
||||
buildProxyDisplayName(proxy),
|
||||
]
|
||||
.filter((value) => value !== undefined && value !== null && value !== '')
|
||||
.map((value) => String(value).trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function isActiveProxy(proxy = {}) {
|
||||
const status = String(proxy.status || '').trim().toLowerCase();
|
||||
return !status || status === 'active';
|
||||
}
|
||||
|
||||
function findSub2ApiProxy(proxies = [], preference = '') {
|
||||
const activeProxies = (Array.isArray(proxies) ? proxies : [])
|
||||
.filter(isActiveProxy)
|
||||
.filter((proxy) => normalizeProxyId(proxy.id));
|
||||
const normalizedPreference = normalizeSub2ApiProxyPreference(preference).toLowerCase();
|
||||
const preferredId = normalizeProxyId(normalizedPreference);
|
||||
|
||||
if (preferredId) {
|
||||
const matchedById = activeProxies.find((proxy) => normalizeProxyId(proxy.id) === preferredId);
|
||||
return {
|
||||
proxy: matchedById || null,
|
||||
reason: matchedById ? 'id' : 'missing-id',
|
||||
candidates: activeProxies,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedPreference) {
|
||||
const exactMatches = activeProxies.filter((proxy) => {
|
||||
const name = String(proxy.name || '').trim().toLowerCase();
|
||||
return name === normalizedPreference;
|
||||
});
|
||||
if (exactMatches.length === 1) {
|
||||
return { proxy: exactMatches[0], reason: 'name', candidates: activeProxies };
|
||||
}
|
||||
if (exactMatches.length > 1) {
|
||||
return { proxy: null, reason: 'ambiguous-name', candidates: exactMatches };
|
||||
}
|
||||
|
||||
const fuzzyMatches = activeProxies.filter((proxy) => buildProxySearchText(proxy).includes(normalizedPreference));
|
||||
if (fuzzyMatches.length === 1) {
|
||||
return { proxy: fuzzyMatches[0], reason: 'fuzzy', candidates: activeProxies };
|
||||
}
|
||||
if (fuzzyMatches.length > 1) {
|
||||
return { proxy: null, reason: 'ambiguous-fuzzy', candidates: fuzzyMatches };
|
||||
}
|
||||
|
||||
return { proxy: null, reason: 'missing-name', candidates: activeProxies };
|
||||
}
|
||||
|
||||
if (activeProxies.length === 1) {
|
||||
return { proxy: activeProxies[0], reason: 'single-active', candidates: activeProxies };
|
||||
}
|
||||
return {
|
||||
proxy: null,
|
||||
reason: activeProxies.length ? 'no-preference' : 'none-active',
|
||||
candidates: activeProxies,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveSub2ApiProxy(origin, token, preference = '') {
|
||||
const proxies = await requestJson(origin, '/api/v1/admin/proxies/all?with_count=true', {
|
||||
method: 'GET',
|
||||
token,
|
||||
});
|
||||
if (!Array.isArray(proxies)) {
|
||||
throw new Error('SUB2API 代理列表返回格式异常,无法自动选择代理。');
|
||||
}
|
||||
|
||||
const { proxy, reason, candidates } = findSub2ApiProxy(proxies, preference);
|
||||
if (proxy) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
const configured = normalizeSub2ApiProxyPreference(preference) || '(未配置)';
|
||||
const available = (candidates || [])
|
||||
.slice(0, 8)
|
||||
.map(buildProxyDisplayName)
|
||||
.join(',') || '无可用代理';
|
||||
if (reason === 'ambiguous-name' || reason === 'ambiguous-fuzzy') {
|
||||
throw new Error(`SUB2API 默认代理“${configured}”匹配到多个代理,请改填代理 ID。候选:${available}`);
|
||||
}
|
||||
if (reason === 'missing-id') {
|
||||
throw new Error(`SUB2API 默认代理 ID “${configured}”不存在或未启用。可用代理:${available}`);
|
||||
}
|
||||
if (reason === 'missing-name') {
|
||||
throw new Error(`SUB2API 默认代理“${configured}”不存在或未启用。可用代理:${available}`);
|
||||
}
|
||||
if (reason === 'no-preference') {
|
||||
throw new Error(`SUB2API 存在多个可用代理,请在侧边栏填写默认代理名称或 ID;留空则不使用代理。可用代理:${available}`);
|
||||
}
|
||||
throw new Error('SUB2API 没有可用代理;请检查默认代理配置,或将其留空以禁用代理。');
|
||||
}
|
||||
|
||||
function buildDraftAccountName(groupName) {
|
||||
const prefix = (groupName || SUB2API_DEFAULT_GROUP_NAME)
|
||||
.trim()
|
||||
.replace(/[^\w\u4e00-\u9fa5-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '') || SUB2API_DEFAULT_GROUP_NAME;
|
||||
const stamp = new Date().toISOString().replace(/\D/g, '').slice(2, 14);
|
||||
const random = Math.floor(Math.random() * 9000 + 1000);
|
||||
return `${prefix}-${stamp}-${random}`;
|
||||
}
|
||||
|
||||
function extractStateFromAuthUrl(authUrl) {
|
||||
try {
|
||||
return new URL(authUrl).searchParams.get('state') || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function parseLocalhostCallback(rawUrl, visibleStep = 10) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error('提供的回调 URL 不是合法链接。');
|
||||
}
|
||||
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
throw new Error('回调 URL 协议不正确。');
|
||||
}
|
||||
if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) {
|
||||
throw new Error(`步骤 ${visibleStep} 只接受 localhost / 127.0.0.1 回调地址。`);
|
||||
}
|
||||
if (parsed.pathname !== '/auth/callback') {
|
||||
throw new Error('回调 URL 路径必须是 /auth/callback。');
|
||||
}
|
||||
|
||||
const code = (parsed.searchParams.get('code') || '').trim();
|
||||
const state = (parsed.searchParams.get('state') || '').trim();
|
||||
if (!code || !state) {
|
||||
throw new Error('回调 URL 中缺少 code 或 state。');
|
||||
}
|
||||
|
||||
return {
|
||||
url: parsed.toString(),
|
||||
code,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
function buildOpenAiCredentials(exchangeData) {
|
||||
const credentials = {};
|
||||
const allowedKeys = [
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'id_token',
|
||||
'expires_at',
|
||||
'email',
|
||||
'chatgpt_account_id',
|
||||
'chatgpt_user_id',
|
||||
'organization_id',
|
||||
'plan_type',
|
||||
'client_id',
|
||||
];
|
||||
|
||||
for (const key of allowedKeys) {
|
||||
if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
|
||||
credentials[key] = exchangeData[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!credentials.access_token) {
|
||||
throw new Error('SUB2API 交换授权码后未返回 access_token。');
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
function buildOpenAiExtra(exchangeData) {
|
||||
const extra = {};
|
||||
const allowedKeys = ['email', 'name', 'privacy_mode'];
|
||||
|
||||
for (const key of allowedKeys) {
|
||||
if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
|
||||
extra[key] = exchangeData[key];
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(extra).length ? extra : undefined;
|
||||
}
|
||||
|
||||
async function getBackgroundState() {
|
||||
try {
|
||||
return await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sub2api-panel' });
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function openAccountsPageSoon(origin) {
|
||||
const accountsUrl = `${origin}/admin/accounts`;
|
||||
if (location.href === accountsUrl || location.pathname.startsWith('/admin/accounts')) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
location.replace(accountsUrl);
|
||||
} catch { }
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function step1_generateOpenAiAuthUrl(payload = {}, options = {}) {
|
||||
const { report = true } = options;
|
||||
const logStep = Number.isInteger(payload?.logStep) ? payload.logStep : 1;
|
||||
const redirectUri = normalizeRedirectUri();
|
||||
const groupNames = normalizeSub2ApiGroupNames(payload.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME);
|
||||
const groupName = groupNames[0] || SUB2API_DEFAULT_GROUP_NAME;
|
||||
|
||||
const { origin, token } = await loginSub2Api(payload);
|
||||
const groups = await getGroupsByNames(origin, token, groupNames);
|
||||
const group = groups[0];
|
||||
const proxyPreference = resolveSub2ApiProxyPreference(payload);
|
||||
const proxy = proxyPreference ? await resolveSub2ApiProxy(origin, token, proxyPreference) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const draftName = buildDraftAccountName(group.name || groupName);
|
||||
const groupLabel = groups.map((item) => `${item.name}(#${item.id})`).join('、');
|
||||
|
||||
log(`步骤 ${logStep}:已登录 SUB2API,使用分组 ${groupLabel}。`);
|
||||
if (proxy) {
|
||||
log(`步骤 ${logStep}:已选择 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`);
|
||||
} else {
|
||||
log(`步骤 ${logStep}:未配置 SUB2API 默认代理,本次将不使用代理。`);
|
||||
}
|
||||
log(`步骤 ${logStep}:正在向 SUB2API 生成 OpenAI Auth 链接,回调地址为 ${redirectUri}。`);
|
||||
|
||||
const authRequestBody = {
|
||||
redirect_uri: redirectUri,
|
||||
};
|
||||
if (proxyId) {
|
||||
authRequestBody.proxy_id = proxyId;
|
||||
}
|
||||
|
||||
const authData = await requestJson(origin, '/api/v1/admin/openai/generate-auth-url', {
|
||||
method: 'POST',
|
||||
token,
|
||||
body: authRequestBody,
|
||||
});
|
||||
|
||||
const oauthUrl = String(authData?.auth_url || '').trim();
|
||||
const sessionId = String(authData?.session_id || '').trim();
|
||||
const oauthState = String(authData?.state || extractStateFromAuthUrl(oauthUrl)).trim();
|
||||
|
||||
if (!oauthUrl || !sessionId) {
|
||||
throw new Error('SUB2API 未返回完整的 auth_url / session_id。');
|
||||
}
|
||||
|
||||
log(`步骤 ${logStep}:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok');
|
||||
const result = {
|
||||
oauthUrl,
|
||||
sub2apiSessionId: sessionId,
|
||||
sub2apiOAuthState: oauthState,
|
||||
sub2apiGroupId: group.id,
|
||||
sub2apiGroupIds: groups.map((item) => item.id),
|
||||
sub2apiDraftName: draftName,
|
||||
sub2apiProxyId: proxyId,
|
||||
};
|
||||
if (report) {
|
||||
reportComplete(1, result);
|
||||
}
|
||||
openAccountsPageSoon(origin);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function step9_submitOpenAiCallback(payload = {}) {
|
||||
const visibleStep = Number(payload?.visibleStep) || 10;
|
||||
const callback = parseLocalhostCallback(payload.localhostUrl || '', visibleStep);
|
||||
const backgroundState = await getBackgroundState();
|
||||
const flowEmail = String(backgroundState.email || '').trim();
|
||||
|
||||
const sessionId = String(payload.sub2apiSessionId || backgroundState.sub2apiSessionId || '').trim();
|
||||
const expectedState = String(payload.sub2apiOAuthState || backgroundState.sub2apiOAuthState || '').trim();
|
||||
|
||||
const { origin, token } = await loginSub2Api(payload);
|
||||
const proxyPreference = resolveSub2ApiProxyPreference(payload, backgroundState);
|
||||
const preferredProxyId = normalizeProxyId(payload.sub2apiProxyId || backgroundState.sub2apiProxyId);
|
||||
const proxySelector = preferredProxyId || proxyPreference;
|
||||
const proxy = proxySelector ? await resolveSub2ApiProxy(origin, token, proxySelector) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const accountPriority = resolveSub2ApiAccountPriority(payload, backgroundState);
|
||||
const storedGroupIds = Array.isArray(payload.sub2apiGroupIds)
|
||||
? payload.sub2apiGroupIds
|
||||
: (Array.isArray(backgroundState.sub2apiGroupIds) ? backgroundState.sub2apiGroupIds : []);
|
||||
const groupIdsFromState = storedGroupIds
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
const groups = groupIdsFromState.length
|
||||
? groupIdsFromState.map((id) => ({ id }))
|
||||
: (payload.sub2apiGroupId
|
||||
? [{ id: payload.sub2apiGroupId, name: payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME }]
|
||||
: await getGroupsByNames(origin, token, payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME));
|
||||
|
||||
if (!sessionId) {
|
||||
throw new Error('缺少 SUB2API session_id,请重新执行步骤 1。');
|
||||
}
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error('本次 localhost 回调中的 state 与步骤 1 生成的 state 不一致,请重新执行步骤 1。');
|
||||
}
|
||||
|
||||
log('正在向 SUB2API 交换 OpenAI 授权码...', 'info', { step: visibleStep, stepKey: 'platform-verify' });
|
||||
if (proxy) {
|
||||
log(`使用 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`, 'info', { step: visibleStep, stepKey: 'platform-verify' });
|
||||
} else {
|
||||
log('未配置 SUB2API 默认代理,本次将不使用代理。', 'info', { step: visibleStep, stepKey: 'platform-verify' });
|
||||
}
|
||||
const exchangeRequestBody = {
|
||||
session_id: sessionId,
|
||||
code: callback.code,
|
||||
state: callback.state,
|
||||
};
|
||||
if (proxyId) {
|
||||
exchangeRequestBody.proxy_id = proxyId;
|
||||
}
|
||||
const exchangeData = await requestJson(origin, '/api/v1/admin/openai/exchange-code', {
|
||||
method: 'POST',
|
||||
token,
|
||||
body: exchangeRequestBody,
|
||||
});
|
||||
|
||||
const credentials = buildOpenAiCredentials(exchangeData);
|
||||
const extra = buildOpenAiExtra(exchangeData);
|
||||
const resolvedEmail = String(exchangeData?.email || credentials?.email || '').trim();
|
||||
const groupIds = groups
|
||||
.map((group) => Number(group.id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
if (!groupIds.length) {
|
||||
throw new Error('SUB2API 返回的目标分组 ID 无效。');
|
||||
}
|
||||
const accountName = resolvedEmail
|
||||
|| flowEmail
|
||||
|| String(payload.sub2apiDraftName || backgroundState.sub2apiDraftName || '').trim()
|
||||
|| buildDraftAccountName(payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME);
|
||||
const createPayload = {
|
||||
name: accountName,
|
||||
notes: '',
|
||||
platform: 'openai',
|
||||
type: 'oauth',
|
||||
credentials,
|
||||
concurrency: SUB2API_DEFAULT_CONCURRENCY,
|
||||
priority: accountPriority,
|
||||
rate_multiplier: SUB2API_DEFAULT_RATE_MULTIPLIER,
|
||||
group_ids: groupIds,
|
||||
auto_pause_on_expired: true,
|
||||
};
|
||||
if (proxyId) {
|
||||
createPayload.proxy_id = proxyId;
|
||||
}
|
||||
|
||||
if (extra) {
|
||||
createPayload.extra = extra;
|
||||
}
|
||||
|
||||
log(`授权码交换成功,正在创建 SUB2API 账号(名称:${accountName})...`, 'info', { step: visibleStep, stepKey: 'platform-verify' });
|
||||
const createdAccount = await requestJson(origin, '/api/v1/admin/accounts', {
|
||||
method: 'POST',
|
||||
token,
|
||||
body: createPayload,
|
||||
});
|
||||
|
||||
const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
|
||||
log(verifiedStatus, 'ok', { step: visibleStep, stepKey: 'platform-verify' });
|
||||
reportComplete('platform-verify', {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
visibleStep,
|
||||
});
|
||||
openAccountsPageSoon(origin);
|
||||
}
|
||||
|
||||
reportReady();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
||||
// flows/openai/content/whatsapp-flow.js — WhatsApp Web code reader for GoPay.
|
||||
|
||||
console.log('[MultiPage:whatsapp-flow] Content script loaded on', location.href);
|
||||
|
||||
const WHATSAPP_FLOW_LISTENER_SENTINEL = 'data-multipage-whatsapp-flow-listener';
|
||||
|
||||
if (document.documentElement.getAttribute(WHATSAPP_FLOW_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(WHATSAPP_FLOW_LISTENER_SENTINEL, '1');
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (
|
||||
message.type === 'WHATSAPP_GET_STATE'
|
||||
|| message.type === 'WHATSAPP_FIND_CODE'
|
||||
) {
|
||||
resetStopState();
|
||||
handleWhatsAppCommand(message).then((result) => {
|
||||
sendResponse({ ok: true, ...(result || {}) });
|
||||
}).catch((err) => {
|
||||
if (isStopError(err)) {
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('[MultiPage:whatsapp-flow] 消息监听已存在,跳过重复注册');
|
||||
}
|
||||
|
||||
async function handleWhatsAppCommand(message) {
|
||||
switch (message.type) {
|
||||
case 'WHATSAPP_GET_STATE':
|
||||
return inspectWhatsAppState();
|
||||
case 'WHATSAPP_FIND_CODE':
|
||||
return findWhatsAppCode(message.payload || {});
|
||||
default:
|
||||
throw new Error(`whatsapp-flow.js 不处理消息:${message.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeText(text = '') {
|
||||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getBodyText() {
|
||||
return normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||||
}
|
||||
|
||||
function extractVerificationCode(text = '') {
|
||||
const normalized = normalizeText(text);
|
||||
const preferredPatterns = [
|
||||
/(?:gopay|gojek|openai|chatgpt|kode|code|otp|verification|verifikasi|whatsapp|验证码|驗證碼|代码|代碼)[^\d]{0,40}(\d{4,8})/i,
|
||||
/(\d{4,8})[^\d]{0,40}(?:gopay|gojek|openai|chatgpt|kode|code|otp|verification|verifikasi|验证码|驗證碼|代码|代碼)/i,
|
||||
];
|
||||
for (const pattern of preferredPatterns) {
|
||||
const match = normalized.match(pattern);
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
const genericMatch = normalized.match(/(?<!\d)(\d{4,8})(?!\d)/);
|
||||
return genericMatch?.[1] || '';
|
||||
}
|
||||
|
||||
function getMessageCandidates() {
|
||||
const selectors = [
|
||||
'[data-testid*="msg" i]',
|
||||
'[data-pre-plain-text]',
|
||||
'.message-in',
|
||||
'[role="row"]',
|
||||
'div',
|
||||
'span',
|
||||
];
|
||||
const seen = new Set();
|
||||
const messages = [];
|
||||
for (const selector of selectors) {
|
||||
for (const el of Array.from(document.querySelectorAll(selector))) {
|
||||
const text = normalizeText(el.innerText || el.textContent || '');
|
||||
if (!text || text.length < 4 || seen.has(text)) continue;
|
||||
seen.add(text);
|
||||
messages.push(text);
|
||||
}
|
||||
}
|
||||
return messages.slice(-80);
|
||||
}
|
||||
|
||||
async function waitUntil(predicate, options = {}) {
|
||||
const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 1000));
|
||||
const timeoutMs = Math.max(0, Math.floor(Number(options.timeoutMs) || 0));
|
||||
const startedAt = Date.now();
|
||||
while (true) {
|
||||
throwIfStopped();
|
||||
const value = await predicate();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) {
|
||||
return null;
|
||||
}
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function findWhatsAppCode(payload = {}) {
|
||||
const timeoutMs = Math.max(0, Math.floor(Number(payload.timeoutMs) || 0));
|
||||
const result = await waitUntil(() => {
|
||||
const messages = getMessageCandidates();
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const text = messages[index];
|
||||
const code = extractVerificationCode(text);
|
||||
if (code) {
|
||||
return {
|
||||
code,
|
||||
messageText: text.slice(0, 500),
|
||||
};
|
||||
}
|
||||
}
|
||||
const code = extractVerificationCode(getBodyText());
|
||||
return code ? { code, messageText: getBodyText().slice(0, 500) } : null;
|
||||
}, {
|
||||
intervalMs: 1000,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
return result || {
|
||||
code: '',
|
||||
messageText: '',
|
||||
};
|
||||
}
|
||||
|
||||
function inspectWhatsAppState() {
|
||||
const bodyText = getBodyText();
|
||||
const code = extractVerificationCode(bodyText);
|
||||
return {
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
loggedIn: !/use whatsapp on your computer|link a device|scan|qr|使用 WhatsApp|扫码|掃碼/i.test(bodyText),
|
||||
code,
|
||||
textPreview: bodyText.slice(0, 500),
|
||||
messagePreview: getMessageCandidates().slice(-8),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
(function attachMultiPageOpenAiFlowDefinition(root, factory) {
|
||||
root.MultiPageOpenAiFlowDefinition = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createMultiPageOpenAiFlowDefinition() {
|
||||
function freezeDeep(entry) {
|
||||
if (!entry || typeof entry !== 'object' || Object.isFrozen(entry)) {
|
||||
return entry;
|
||||
}
|
||||
Object.getOwnPropertyNames(entry).forEach((key) => {
|
||||
freezeDeep(entry[key]);
|
||||
});
|
||||
return Object.freeze(entry);
|
||||
}
|
||||
|
||||
const VALUE = freezeDeep({
|
||||
"id": "openai",
|
||||
"label": "Codex / OpenAI",
|
||||
"services": [
|
||||
"account",
|
||||
"email",
|
||||
"proxy"
|
||||
],
|
||||
"capabilities": {
|
||||
"supportsEmailSignup": true,
|
||||
"supportsPhoneSignup": true,
|
||||
"supportsPhoneVerificationSettings": true,
|
||||
"supportsPlusMode": true,
|
||||
"supportsContributionMode": true,
|
||||
"supportsAccountContribution": true,
|
||||
"supportsOpenAiOAuthContribution": true,
|
||||
"contributionAdapterIds": [
|
||||
"openai-oauth",
|
||||
"openai-codex-file",
|
||||
"openai-sub2api-file"
|
||||
],
|
||||
"supportedTargetIds": [
|
||||
"cpa",
|
||||
"sub2api",
|
||||
"codex2api"
|
||||
],
|
||||
"supportsLuckmail": true,
|
||||
"supportsOauthTimeoutBudget": true,
|
||||
"canSwitchFlow": true,
|
||||
"stepDefinitionMode": "openai-dynamic",
|
||||
"targetSelectorLabel": "来源"
|
||||
},
|
||||
"baseGroups": [
|
||||
"openai-plus",
|
||||
"openai-phone",
|
||||
"openai-oauth",
|
||||
"openai-step6"
|
||||
],
|
||||
"targets": {
|
||||
"cpa": {
|
||||
"id": "cpa",
|
||||
"label": "CPA 面板",
|
||||
"groups": [
|
||||
"openai-target-cpa"
|
||||
]
|
||||
},
|
||||
"sub2api": {
|
||||
"id": "sub2api",
|
||||
"label": "SUB2API",
|
||||
"groups": [
|
||||
"openai-target-sub2api"
|
||||
]
|
||||
},
|
||||
"codex2api": {
|
||||
"id": "codex2api",
|
||||
"label": "Codex2API",
|
||||
"groups": [
|
||||
"openai-target-codex2api"
|
||||
]
|
||||
}
|
||||
},
|
||||
"runtimeSources": {
|
||||
"openai-auth": {
|
||||
"flowId": "openai",
|
||||
"kind": "flow-page",
|
||||
"label": "认证页",
|
||||
"readyPolicy": "allow-child-frame",
|
||||
"family": "openai-auth-family",
|
||||
"driverId": "flows/openai/content/openai-auth",
|
||||
"cleanupScopes": [
|
||||
"oauth-localhost-callback"
|
||||
],
|
||||
"detectionMatchers": [
|
||||
{
|
||||
"hostnames": [
|
||||
"auth0.openai.com",
|
||||
"auth.openai.com",
|
||||
"accounts.openai.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"familyMatchers": [
|
||||
{
|
||||
"hostnames": [
|
||||
"auth0.openai.com",
|
||||
"auth.openai.com",
|
||||
"accounts.openai.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hostnames": [
|
||||
"chatgpt.com",
|
||||
"www.chatgpt.com",
|
||||
"chat.openai.com"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"chatgpt": {
|
||||
"flowId": "openai",
|
||||
"kind": "flow-entry",
|
||||
"label": "ChatGPT 首页",
|
||||
"readyPolicy": "allow-child-frame",
|
||||
"family": "chatgpt-entry-family",
|
||||
"driverId": null,
|
||||
"cleanupScopes": [],
|
||||
"detectionMatchers": [
|
||||
{
|
||||
"hostnames": [
|
||||
"chatgpt.com",
|
||||
"www.chatgpt.com",
|
||||
"chat.openai.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"familyMatchers": [
|
||||
{
|
||||
"hostnames": [
|
||||
"chatgpt.com",
|
||||
"www.chatgpt.com",
|
||||
"chat.openai.com"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"vps-panel": {
|
||||
"flowId": "openai",
|
||||
"kind": "panel-page",
|
||||
"label": "CPA 面板",
|
||||
"readyPolicy": "allow-child-frame",
|
||||
"family": "vps-panel-family",
|
||||
"driverId": "flows/openai/content/vps-panel",
|
||||
"cleanupScopes": [],
|
||||
"familyMatchers": [
|
||||
{
|
||||
"originEqualsReference": true,
|
||||
"pathEqualsReference": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"platform-panel": {
|
||||
"flowId": "openai",
|
||||
"kind": "virtual-page",
|
||||
"label": "平台回调面板",
|
||||
"readyPolicy": "disabled",
|
||||
"family": "platform-panel-family",
|
||||
"driverId": "content/platform-panel",
|
||||
"cleanupScopes": [],
|
||||
"familyMatchers": []
|
||||
},
|
||||
"sub2api-panel": {
|
||||
"flowId": "openai",
|
||||
"kind": "panel-page",
|
||||
"label": "SUB2API 后台",
|
||||
"readyPolicy": "allow-child-frame",
|
||||
"family": "sub2api-panel-family",
|
||||
"driverId": "flows/openai/content/sub2api-panel",
|
||||
"cleanupScopes": [],
|
||||
"familyMatchers": [
|
||||
{
|
||||
"originEqualsReference": true,
|
||||
"pathPrefixes": [
|
||||
"/admin/accounts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"originEqualsReference": true,
|
||||
"pathPrefixes": [
|
||||
"/login"
|
||||
]
|
||||
},
|
||||
{
|
||||
"originEqualsReference": true,
|
||||
"pathEqualsOneOf": [
|
||||
"/"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"codex2api-panel": {
|
||||
"flowId": "openai",
|
||||
"kind": "panel-page",
|
||||
"label": "Codex2API 后台",
|
||||
"readyPolicy": "allow-child-frame",
|
||||
"family": "codex2api-panel-family",
|
||||
"driverId": "flows/openai/content/sub2api-panel",
|
||||
"cleanupScopes": [],
|
||||
"familyMatchers": [
|
||||
{
|
||||
"originEqualsReference": true,
|
||||
"pathPrefixes": [
|
||||
"/admin/accounts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"originEqualsReference": true,
|
||||
"pathEqualsOneOf": [
|
||||
"/admin",
|
||||
"/"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"plus-checkout": {
|
||||
"flowId": "openai",
|
||||
"kind": "flow-page",
|
||||
"label": "Plus Checkout",
|
||||
"readyPolicy": "top-frame-only",
|
||||
"family": "plus-checkout-family",
|
||||
"driverId": "flows/openai/content/plus-checkout",
|
||||
"cleanupScopes": [],
|
||||
"familyMatchers": [
|
||||
{
|
||||
"hostnames": [
|
||||
"chatgpt.com"
|
||||
],
|
||||
"pathPrefixes": [
|
||||
"/checkout/"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"paypal-flow": {
|
||||
"flowId": "openai",
|
||||
"kind": "flow-page",
|
||||
"label": "PayPal 授权页",
|
||||
"readyPolicy": "allow-child-frame",
|
||||
"family": "paypal-flow-family",
|
||||
"driverId": "flows/openai/content/paypal-flow",
|
||||
"cleanupScopes": [],
|
||||
"familyMatchers": [
|
||||
{
|
||||
"hostnameEndsWith": [
|
||||
"paypal.com"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gopay-flow": {
|
||||
"flowId": "openai",
|
||||
"kind": "flow-page",
|
||||
"label": "GoPay 授权页",
|
||||
"readyPolicy": "allow-child-frame",
|
||||
"family": "gopay-flow-family",
|
||||
"driverId": "flows/openai/content/gopay-flow",
|
||||
"cleanupScopes": [],
|
||||
"familyMatchers": [
|
||||
{
|
||||
"hostnameRegex": "gopay|gojek",
|
||||
"hostnameRegexFlags": "i"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"driverDefinitions": {
|
||||
"flows/openai/content/openai-auth": {
|
||||
"sourceId": "openai-auth",
|
||||
"commands": [
|
||||
"submit-signup-email",
|
||||
"fill-password",
|
||||
"fill-profile",
|
||||
"oauth-login",
|
||||
"submit-verification-code",
|
||||
"post-login-phone-verification",
|
||||
"bind-email",
|
||||
"fetch-bind-email-code",
|
||||
"confirm-oauth",
|
||||
"detect-auth-state"
|
||||
]
|
||||
},
|
||||
"flows/openai/content/sub2api-panel": {
|
||||
"sourceId": "sub2api-panel",
|
||||
"commands": [
|
||||
"open-panel",
|
||||
"fetch-oauth-url",
|
||||
"platform-verify"
|
||||
]
|
||||
},
|
||||
"flows/openai/content/vps-panel": {
|
||||
"sourceId": "vps-panel",
|
||||
"commands": [
|
||||
"open-panel",
|
||||
"fetch-oauth-url",
|
||||
"platform-verify"
|
||||
]
|
||||
},
|
||||
"content/platform-panel": {
|
||||
"sourceId": "platform-panel",
|
||||
"commands": [
|
||||
"platform-verify",
|
||||
"fetch-oauth-url"
|
||||
]
|
||||
},
|
||||
"flows/openai/content/plus-checkout": {
|
||||
"sourceId": "plus-checkout",
|
||||
"commands": [
|
||||
"plus-checkout-create",
|
||||
"paypal-hosted-openai-checkout",
|
||||
"plus-checkout-billing",
|
||||
"plus-checkout-return"
|
||||
]
|
||||
},
|
||||
"flows/openai/content/paypal-flow": {
|
||||
"sourceId": "paypal-flow",
|
||||
"commands": [
|
||||
"paypal-approve",
|
||||
"paypal-hosted-email",
|
||||
"paypal-hosted-card",
|
||||
"paypal-hosted-create-account",
|
||||
"paypal-hosted-review"
|
||||
]
|
||||
},
|
||||
"flows/openai/content/gopay-flow": {
|
||||
"sourceId": "gopay-flow",
|
||||
"commands": [
|
||||
"gopay-subscription-confirm"
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultTargetId": "cpa",
|
||||
"settingsGroups": {
|
||||
"openai-target-cpa": {
|
||||
"id": "openai-target-cpa",
|
||||
"label": "CPA 来源",
|
||||
"rowIds": [
|
||||
"row-vps-url",
|
||||
"row-vps-password",
|
||||
"row-local-cpa-step9-mode"
|
||||
]
|
||||
},
|
||||
"openai-target-sub2api": {
|
||||
"id": "openai-target-sub2api",
|
||||
"label": "SUB2API 来源",
|
||||
"rowIds": [
|
||||
"row-sub2api-url",
|
||||
"row-sub2api-email",
|
||||
"row-sub2api-password",
|
||||
"row-sub2api-group",
|
||||
"row-sub2api-account-priority",
|
||||
"row-sub2api-default-proxy"
|
||||
]
|
||||
},
|
||||
"openai-target-codex2api": {
|
||||
"id": "openai-target-codex2api",
|
||||
"label": "Codex2API 来源",
|
||||
"rowIds": [
|
||||
"row-codex2api-url",
|
||||
"row-codex2api-admin-key"
|
||||
]
|
||||
},
|
||||
"openai-plus": {
|
||||
"id": "openai-plus",
|
||||
"label": "Plus",
|
||||
"rowIds": [
|
||||
"row-plus-mode",
|
||||
"row-plus-account-access-strategy",
|
||||
"row-plus-payment-method"
|
||||
]
|
||||
},
|
||||
"openai-phone": {
|
||||
"id": "openai-phone",
|
||||
"label": "接码设置",
|
||||
"sectionIds": [
|
||||
"phone-verification-section"
|
||||
],
|
||||
"rowIds": []
|
||||
},
|
||||
"openai-oauth": {
|
||||
"id": "openai-oauth",
|
||||
"label": "OAuth",
|
||||
"rowIds": [
|
||||
"row-oauth-flow-timeout",
|
||||
"row-oauth-display"
|
||||
]
|
||||
},
|
||||
"openai-step6": {
|
||||
"id": "openai-step6",
|
||||
"label": "第六步",
|
||||
"rowIds": [
|
||||
"row-step6-cookie-settings"
|
||||
]
|
||||
}
|
||||
},
|
||||
"targetCapabilities": {
|
||||
"cpa": {
|
||||
"supportsPhoneSignup": true,
|
||||
"requiresPhoneSignupWarning": true,
|
||||
"supportedPlusAccountAccessStrategies": [
|
||||
"oauth",
|
||||
"cpa_codex_session"
|
||||
]
|
||||
},
|
||||
"sub2api": {
|
||||
"supportsPhoneSignup": true,
|
||||
"requiresPhoneSignupWarning": false,
|
||||
"supportedPlusAccountAccessStrategies": [
|
||||
"oauth",
|
||||
"sub2api_codex_session"
|
||||
]
|
||||
},
|
||||
"codex2api": {
|
||||
"supportsPhoneSignup": true,
|
||||
"requiresPhoneSignupWarning": false,
|
||||
"supportedPlusAccountAccessStrategies": [
|
||||
"oauth"
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return VALUE;
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user