Merge branch 'QLHazyCoder:master' into master
This commit is contained in:
+14
-5
@@ -251,7 +251,7 @@ const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8;
|
||||
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000;
|
||||
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
|
||||
const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts';
|
||||
const DEFAULT_SUB2API_URL = '';
|
||||
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
|
||||
@@ -2364,15 +2364,23 @@ function normalizePersistentSettingValue(key, value) {
|
||||
);
|
||||
case 'gopayHelperApiUrl':
|
||||
{
|
||||
const legacyGpcHelperApiUrl = 'https://gpc.leftcode.xyz';
|
||||
const defaultGpcHelperApiUrl = PERSISTED_SETTING_DEFAULTS.gopayHelperApiUrl
|
||||
|| (typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined' ? DEFAULT_GPC_HELPER_API_URL : 'https://gpc.qlhazycoder.top');
|
||||
const normalizedGpcHelperApiUrl = self.GoPayUtils?.normalizeGpcHelperBaseUrl
|
||||
? self.GoPayUtils.normalizeGpcHelperBaseUrl(value || defaultGpcHelperApiUrl)
|
||||
: String(value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, '');
|
||||
return normalizedGpcHelperApiUrl === legacyGpcHelperApiUrl
|
||||
? defaultGpcHelperApiUrl
|
||||
: normalizedGpcHelperApiUrl;
|
||||
if (!self.GoPayUtils?.normalizeGpcHelperBaseUrl) {
|
||||
try {
|
||||
const parsed = new URL(normalizedGpcHelperApiUrl);
|
||||
const hostname = parsed.hostname.toLowerCase();
|
||||
if (hostname !== 'gpc.qlhazycoder.top' && hostname !== 'localhost' && hostname !== '127.0.0.1') {
|
||||
return defaultGpcHelperApiUrl;
|
||||
}
|
||||
} catch {
|
||||
return defaultGpcHelperApiUrl;
|
||||
}
|
||||
}
|
||||
return normalizedGpcHelperApiUrl;
|
||||
}
|
||||
case 'gopayHelperApiKey':
|
||||
case 'gopayHelperCardKey':
|
||||
@@ -7102,6 +7110,7 @@ function normalizeSub2ApiUrl(rawUrl) {
|
||||
return navigationUtils.normalizeSub2ApiUrl(rawUrl);
|
||||
}
|
||||
const input = (rawUrl || '').trim() || DEFAULT_SUB2API_URL;
|
||||
if (!input) return '';
|
||||
const withProtocol = /^https?:\/\//i.test(input) ? input : `https://${input}`;
|
||||
const parsed = new URL(withProtocol);
|
||||
if (!parsed.pathname || parsed.pathname === '/') {
|
||||
|
||||
@@ -37,12 +37,10 @@
|
||||
const MAIL2925_COOKIE_DOMAINS = [
|
||||
'2925.com',
|
||||
'www.2925.com',
|
||||
'mail2.xiyouji.com',
|
||||
];
|
||||
const MAIL2925_COOKIE_ORIGINS = [
|
||||
'https://2925.com',
|
||||
'https://www.2925.com',
|
||||
'https://mail2.xiyouji.com',
|
||||
];
|
||||
const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::';
|
||||
const MAIL2925_THREAD_TERMINATED_ERROR_PREFIX = 'MAIL2925_THREAD_TERMINATED::';
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
function normalizeSub2ApiUrl(rawUrl) {
|
||||
const input = (rawUrl || '').trim() || DEFAULT_SUB2API_URL;
|
||||
if (!input) return '';
|
||||
const withProtocol = /^https?:\/\//i.test(input) ? input : `https://${input}`;
|
||||
const parsed = new URL(withProtocol);
|
||||
if (!parsed.pathname || parsed.pathname === '/') {
|
||||
|
||||
@@ -251,6 +251,9 @@
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||
const groupName = (state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME).trim() || DEFAULT_SUB2API_GROUP_NAME;
|
||||
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
}
|
||||
if (!state.sub2apiEmail) {
|
||||
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
@@ -40,29 +40,64 @@
|
||||
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 canUseConfiguredPhoneSignup(state = {}) {
|
||||
return normalizeStep7SignupMethod(state?.signupMethod) === 'phone'
|
||||
&& Boolean(state?.phoneVerificationEnabled)
|
||||
&& !Boolean(state?.plusModeEnabled)
|
||||
&& !Boolean(state?.contributionMode);
|
||||
}
|
||||
|
||||
function resolveStep7LoginIdentifierType(state = {}, fallbackType = '') {
|
||||
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';
|
||||
}
|
||||
|
||||
async function executeStep7(state) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
const completionStep = visibleStep > 0 ? visibleStep : 7;
|
||||
const resolvedIdentifierType = String(
|
||||
state?.accountIdentifierType
|
||||
|| (state?.signupPhoneNumber ? 'phone' : '')
|
||||
|| ''
|
||||
).trim().toLowerCase() === 'phone'
|
||||
? 'phone'
|
||||
: 'email';
|
||||
const phoneNumber = String(
|
||||
state?.signupPhoneNumber
|
||||
|| (resolvedIdentifierType === 'phone' ? state?.accountIdentifier : '')
|
||||
|| state?.signupPhoneCompletedActivation?.phoneNumber
|
||||
|| state?.signupPhoneActivation?.phoneNumber
|
||||
|| ''
|
||||
).trim();
|
||||
const email = String(
|
||||
state?.email
|
||||
|| (resolvedIdentifierType === 'email' ? state?.accountIdentifier : '')
|
||||
|| ''
|
||||
).trim();
|
||||
if (!email && !phoneNumber) {
|
||||
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,或在侧栏“注册邮箱/注册手机号”中手动填写账号后再执行当前步骤。');
|
||||
}
|
||||
|
||||
@@ -75,25 +110,23 @@
|
||||
try {
|
||||
const currentState = attempt === 1 ? state : await getState();
|
||||
const password = currentState.password || currentState.customPassword || '';
|
||||
const currentIdentifierType = String(
|
||||
currentState?.accountIdentifierType
|
||||
|| (currentState?.signupPhoneNumber ? 'phone' : '')
|
||||
|| resolvedIdentifierType
|
||||
).trim().toLowerCase() === 'phone'
|
||||
? 'phone'
|
||||
: 'email';
|
||||
const currentPhoneNumber = String(
|
||||
currentState?.signupPhoneNumber
|
||||
|| (currentIdentifierType === 'phone' ? currentState?.accountIdentifier : '')
|
||||
|| currentState?.signupPhoneCompletedActivation?.phoneNumber
|
||||
|| currentState?.signupPhoneActivation?.phoneNumber
|
||||
|| phoneNumber
|
||||
).trim();
|
||||
const currentEmail = String(
|
||||
currentState?.email
|
||||
|| (currentIdentifierType === 'email' ? currentState?.accountIdentifier : '')
|
||||
|| email
|
||||
).trim();
|
||||
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;
|
||||
|
||||
@@ -343,6 +343,9 @@
|
||||
}
|
||||
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
}
|
||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||
|
||||
await addStepLog(visibleStep, '正在打开 SUB2API 后台...');
|
||||
|
||||
+48
-8
@@ -3484,6 +3484,10 @@ function summarizePhoneInputCandidate(element, options = {}) {
|
||||
const normalizedName = summary.name.toLowerCase();
|
||||
const normalizedId = summary.id.toLowerCase();
|
||||
const combinedText = `${normalizedName} ${normalizedId} ${summary.placeholder} ${summary.ariaLabel}`;
|
||||
if (isLoginEmailLikeInput(element)) {
|
||||
summary.skipReason = 'email_like';
|
||||
return summary;
|
||||
}
|
||||
if (
|
||||
summary.type === 'tel'
|
||||
|| summary.autocomplete === 'tel'
|
||||
@@ -3517,14 +3521,50 @@ function findUsablePhoneInput(selector, options = {}) {
|
||||
.find((element) => isUsablePhoneInputElement(element, options)) || null;
|
||||
}
|
||||
|
||||
function getLoginInputAttributeText(input) {
|
||||
return {
|
||||
type: String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase(),
|
||||
autocomplete: String(input?.getAttribute?.('autocomplete') || '').trim().toLowerCase(),
|
||||
name: String(input?.getAttribute?.('name') || input?.name || '').trim(),
|
||||
id: String(input?.getAttribute?.('id') || input?.id || '').trim(),
|
||||
placeholder: String(input?.getAttribute?.('placeholder') || '').trim(),
|
||||
ariaLabel: String(input?.getAttribute?.('aria-label') || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function isLoginEmailLikeInput(input) {
|
||||
const summary = getLoginInputAttributeText(input);
|
||||
const nameId = `${summary.name} ${summary.id}`;
|
||||
const labelText = `${summary.placeholder} ${summary.ariaLabel}`;
|
||||
return summary.type === 'email'
|
||||
|| summary.autocomplete === 'email'
|
||||
|| /email/i.test(nameId)
|
||||
|| /email|电子邮件|邮箱/i.test(labelText);
|
||||
}
|
||||
|
||||
function getLoginEmailInput() {
|
||||
const input = document.querySelector(
|
||||
'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]'
|
||||
);
|
||||
if (isLoginPhoneUsernameKind() || isLoginPhoneEntryPageText()) {
|
||||
const input = Array.from(document.querySelectorAll([
|
||||
'input[type="email"]',
|
||||
'input[autocomplete="email"]',
|
||||
'input[name="email"]',
|
||||
'input[name="username"]',
|
||||
'input[autocomplete="username"]',
|
||||
'input[id*="email" i]',
|
||||
'input[placeholder*="email" i]',
|
||||
'input[placeholder*="Email"]',
|
||||
'input[placeholder*="电子邮件"]',
|
||||
'input[placeholder*="邮箱"]',
|
||||
'input[aria-label*="email" i]',
|
||||
'input[aria-label*="电子邮件"]',
|
||||
'input[aria-label*="邮箱"]',
|
||||
].join(', '))).find((candidate) => isVisibleElement(candidate)) || null;
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
return input && isVisibleElement(input) ? input : null;
|
||||
if ((isLoginPhoneUsernameKind() || isLoginPhoneEntryPageText()) && !isLoginEmailLikeInput(input)) {
|
||||
return null;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
function getLoginPhoneInput() {
|
||||
@@ -5162,9 +5202,9 @@ async function step6OpenLoginEntry(payload, snapshot) {
|
||||
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
|
||||
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
|
||||
const preferPhoneLogin = String(payload?.loginIdentifierType || '').trim() === 'phone' || (!payload?.email && payload?.phoneNumber);
|
||||
const trigger = preferPhoneLogin
|
||||
? (currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger())
|
||||
: (currentSnapshot.loginEntryTrigger || findLoginEntryTrigger());
|
||||
const genericEntryTrigger = currentSnapshot.loginEntryTrigger || findLoginEntryTrigger();
|
||||
const phoneEntryTrigger = currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger();
|
||||
const trigger = genericEntryTrigger || (preferPhoneLogin ? phoneEntryTrigger : null);
|
||||
if (!trigger || !isActionEnabled(trigger)) {
|
||||
return createStep6RecoverableResult('missing_login_entry_trigger', currentSnapshot, {
|
||||
message: preferPhoneLogin
|
||||
|
||||
+10
-3
@@ -5,7 +5,7 @@
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const LEGACY_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz';
|
||||
const ALLOWED_GPC_HELPER_REMOTE_HOST = 'gpc.qlhazycoder.top';
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
@@ -67,10 +67,17 @@
|
||||
normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, '');
|
||||
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
|
||||
normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, '');
|
||||
if (normalized === LEGACY_GPC_HELPER_API_URL) {
|
||||
|
||||
try {
|
||||
const parsed = new URL(normalized);
|
||||
const hostname = parsed.hostname.toLowerCase();
|
||||
if (hostname === ALLOWED_GPC_HELPER_REMOTE_HOST || hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
||||
}
|
||||
return DEFAULT_GPC_HELPER_API_URL;
|
||||
} catch {
|
||||
return DEFAULT_GPC_HELPER_API_URL;
|
||||
}
|
||||
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
||||
}
|
||||
|
||||
function buildGpcHelperApiUrl(apiUrl = '', path = '') {
|
||||
|
||||
+5
-3
@@ -6,7 +6,6 @@
|
||||
|
||||
root.HotmailUtils = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createHotmailUtils() {
|
||||
const HOTMAIL_MAIL_API_URL = 'https://apple.882263.xyz/api/mail-new';
|
||||
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
||||
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
|
||||
|
||||
@@ -287,8 +286,11 @@
|
||||
return list.map((message) => normalizeHotmailMailApiMessage(message));
|
||||
}
|
||||
|
||||
function buildHotmailMailApiLatestUrl(options) {
|
||||
const apiUrl = String(options?.apiUrl || '').trim() || HOTMAIL_MAIL_API_URL;
|
||||
function buildHotmailMailApiLatestUrl(options = {}) {
|
||||
const apiUrl = String(options?.apiUrl || '').trim();
|
||||
if (!apiUrl) {
|
||||
throw new Error('Hotmail mail API URL is required.');
|
||||
}
|
||||
const url = new URL(apiUrl);
|
||||
url.searchParams.set('refresh_token', String(options?.refreshToken || ''));
|
||||
url.searchParams.set('client_id', String(options?.clientId || ''));
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "codex-oauth-automation-extension",
|
||||
"version": "7.0",
|
||||
"version_name": "Ultra7.0",
|
||||
"version": "7.3",
|
||||
"version_name": "Ultra7.3",
|
||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
<div class="data-row" id="row-sub2api-url" style="display:none;">
|
||||
<span class="data-label">SUB2API</span>
|
||||
<input type="text" id="input-sub2api-url" class="data-input"
|
||||
placeholder="https://sub2api.hisence.fun/admin/accounts" />
|
||||
placeholder="https://your-sub2api-domain/admin/accounts" />
|
||||
</div>
|
||||
<div class="data-row" id="row-sub2api-email" style="display:none;">
|
||||
<span class="data-label">账号</span>
|
||||
@@ -406,10 +406,10 @@
|
||||
<option value="hotmail-api">Hotmail(账号池)</option>
|
||||
<option value="luckmail-api">LuckMail(API 购邮)</option>
|
||||
<option value="icloud">iCloud 邮箱</option>
|
||||
<option value="163">163 邮箱 (mail.163.com)</option>
|
||||
<option value="163-vip">163 VIP 邮箱 (vip.163.com)</option>
|
||||
<option value="126">126 邮箱 (mail.126.com)</option>
|
||||
<option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
|
||||
<option value="163">163 邮箱 (mail.163.com) 下个版本删除</option>
|
||||
<option value="163-vip">163 VIP 邮箱 (vip.163.com) 下个版本删除</option>
|
||||
<option value="126">126 邮箱 (mail.126.com) 下个版本删除</option>
|
||||
<option value="qq">QQ 邮箱 (wx.mail.qq.com) 下个版本删除</option>
|
||||
<option value="inbucket">Inbucket(自定义主机)</option>
|
||||
<option value="2925">2925 邮箱 (2925.com)</option>
|
||||
<option value="gmail">Gmail 邮箱 (mail.google.com)</option>
|
||||
|
||||
@@ -71,6 +71,20 @@ test('navigation utils support codex2api mode and url normalization', () => {
|
||||
assert.equal(utils.getPanelModeLabel('codex2api'), 'Codex2API');
|
||||
});
|
||||
|
||||
test('navigation utils leaves SUB2API url empty when no default is configured', () => {
|
||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundNavigationUtils;`)(globalScope);
|
||||
const utils = api.createNavigationUtils({
|
||||
DEFAULT_CODEX2API_URL: 'http://localhost:8080/admin/accounts',
|
||||
DEFAULT_SUB2API_URL: '',
|
||||
normalizeLocalCpaStep9Mode: (value) => value,
|
||||
});
|
||||
|
||||
assert.equal(utils.normalizeSub2ApiUrl(''), '');
|
||||
});
|
||||
|
||||
test('navigation utils recognize the iCloud mail tab family on both supported hosts', () => {
|
||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -358,6 +358,106 @@ test('step 7 forwards phone login identity payload when account identifier is ph
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 7 keeps Plus email login even when phone sms runtime exists', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
const events = {
|
||||
payloads: [],
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
email: 'plus.user@example.com',
|
||||
password: 'secret',
|
||||
signupPhoneNumber: '+441111111111',
|
||||
}),
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async (_sourceName, message) => {
|
||||
events.payloads.push(message.payload);
|
||||
return {
|
||||
step6Outcome: 'success',
|
||||
state: 'verification_page',
|
||||
loginVerificationRequestedAt: 123456,
|
||||
};
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
email: 'plus.user@example.com',
|
||||
password: 'secret',
|
||||
signupPhoneNumber: '+441111111111',
|
||||
visibleStep: 10,
|
||||
});
|
||||
|
||||
assert.equal(events.payloads[0].loginIdentifierType, 'email');
|
||||
assert.equal(events.payloads[0].email, 'plus.user@example.com');
|
||||
assert.equal(events.payloads[0].phoneNumber, '');
|
||||
assert.equal(events.payloads[0].accountIdentifier, 'plus.user@example.com');
|
||||
});
|
||||
|
||||
test('step 7 can infer phone login from an available phone signup configuration before step 2 finishes', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
const events = {
|
||||
payloads: [],
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
signupPhoneNumber: '+447780579093',
|
||||
}),
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async (_sourceName, message) => {
|
||||
events.payloads.push(message.payload);
|
||||
return {
|
||||
step6Outcome: 'success',
|
||||
state: 'phone_verification_page',
|
||||
loginVerificationRequestedAt: 987654,
|
||||
};
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
signupPhoneNumber: '+447780579093',
|
||||
});
|
||||
|
||||
assert.equal(events.payloads[0].loginIdentifierType, 'phone');
|
||||
assert.equal(events.payloads[0].phoneNumber, '+447780579093');
|
||||
assert.equal(events.payloads[0].email, '');
|
||||
});
|
||||
|
||||
test('step 7 can start from a manually filled signup phone without completed step 2 or step 3 state', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -28,6 +28,7 @@ test('GoPay utils builds GPC queue task and balance URLs from helper endpoints',
|
||||
const api = loadGoPayUtils();
|
||||
assert.equal(api.DEFAULT_GPC_HELPER_API_URL, 'https://gpc.qlhazycoder.top');
|
||||
assert.equal(api.normalizeGpcHelperBaseUrl(''), 'https://gpc.qlhazycoder.top');
|
||||
assert.equal(api.normalizeGpcHelperBaseUrl('https://example.com/api/gp/tasks'), 'https://gpc.qlhazycoder.top');
|
||||
assert.equal(
|
||||
api.buildGpcHelperApiUrl('', '/api/checkout/start'),
|
||||
'https://gpc.qlhazycoder.top/api/checkout/start'
|
||||
|
||||
@@ -358,13 +358,14 @@ test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp while ke
|
||||
|
||||
test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and mailbox', () => {
|
||||
const url = new URL(buildHotmailMailApiLatestUrl({
|
||||
apiUrl: 'https://example.com/api/mail-new',
|
||||
clientId: 'client-123',
|
||||
email: 'user@hotmail.com',
|
||||
refreshToken: 'refresh-token-xyz',
|
||||
mailbox: 'Junk',
|
||||
}));
|
||||
|
||||
assert.equal(url.origin + url.pathname, 'https://apple.882263.xyz/api/mail-new');
|
||||
assert.equal(url.origin + url.pathname, 'https://example.com/api/mail-new');
|
||||
assert.equal(url.searchParams.get('client_id'), 'client-123');
|
||||
assert.equal(url.searchParams.get('email'), 'user@hotmail.com');
|
||||
assert.equal(url.searchParams.get('refresh_token'), 'refresh-token-xyz');
|
||||
@@ -372,6 +373,13 @@ test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and
|
||||
assert.equal(url.searchParams.get('response_type'), 'json');
|
||||
});
|
||||
|
||||
test('buildHotmailMailApiLatestUrl requires an explicit api url', () => {
|
||||
assert.throws(
|
||||
() => buildHotmailMailApiLatestUrl({ email: 'user@hotmail.com' }),
|
||||
/Hotmail mail API URL is required/
|
||||
);
|
||||
});
|
||||
|
||||
test('buildHotmailMailApiLatestUrl supports custom api url and can omit response_type', () => {
|
||||
const url = new URL(buildHotmailMailApiLatestUrl({
|
||||
apiUrl: 'https://example.com/custom-mail-api',
|
||||
|
||||
@@ -135,14 +135,18 @@ ${extractFunction('getPageTextSnapshot')}
|
||||
${extractFunction('isLoginPhoneUsernameKind')}
|
||||
${extractFunction('isLoginPhoneEntryPageText')}
|
||||
${extractFunction('isInsideHiddenPhoneControl')}
|
||||
${extractFunction('getLoginInputAttributeText')}
|
||||
${extractFunction('isLoginEmailLikeInput')}
|
||||
${extractFunction('summarizePhoneInputCandidate')}
|
||||
${extractFunction('isUsablePhoneInputElement')}
|
||||
${extractFunction('collectPhoneInputCandidates')}
|
||||
${extractFunction('findUsablePhoneInput')}
|
||||
${extractFunction('getLoginEmailInput')}
|
||||
${extractFunction('getLoginPhoneInput')}
|
||||
${extractFunction('isAddPhonePageReady')}
|
||||
|
||||
return {
|
||||
getLoginEmailInput,
|
||||
getLoginPhoneInput,
|
||||
isAddPhonePageReady,
|
||||
};
|
||||
@@ -168,6 +172,62 @@ test('step 7 does not mistake email entry with a phone switch action for phone i
|
||||
assert.equal(api.isAddPhonePageReady(), false);
|
||||
});
|
||||
|
||||
test('step 7 treats unified OpenAI login page as email input despite phone option text', () => {
|
||||
const api = createPhoneLoginEntryApi({
|
||||
href: 'https://auth.openai.com/log-in-or-create-account',
|
||||
pathname: '/log-in-or-create-account',
|
||||
pageText: '\u767b\u5f55\u6216\u6ce8\u518c \u7535\u5b50\u90ae\u4ef6\u5730\u5740 \u7ee7\u7eed \u4f7f\u7528\u7535\u8bdd\u53f7\u7801\u7ee7\u7eed',
|
||||
inputAttributes: { type: 'text', placeholder: '\u7535\u5b50\u90ae\u4ef6\u5730\u5740' },
|
||||
});
|
||||
|
||||
assert.ok(api.getLoginEmailInput(), 'unified login email input should be detected');
|
||||
assert.equal(api.getLoginPhoneInput(), null, 'phone option button must not turn email input into phone input');
|
||||
});
|
||||
|
||||
test('step 7 clicks the generic other-account entry before phone entry', async () => {
|
||||
const api = new Function(`
|
||||
const clicks = [];
|
||||
const genericEntry = { id: 'generic', textContent: '\\u767b\\u5f55\\u81f3\\u53e6\\u4e00\\u4e2a\\u5e10\\u6237' };
|
||||
const phoneEntry = { id: 'phone', textContent: '\\u4f7f\\u7528\\u7535\\u8bdd\\u53f7\\u7801\\u7ee7\\u7eed' };
|
||||
|
||||
function normalizeStep6Snapshot(snapshot) { return snapshot; }
|
||||
function inspectLoginAuthState() {
|
||||
return { state: 'entry_page', loginEntryTrigger: genericEntry, phoneEntryTrigger: phoneEntry };
|
||||
}
|
||||
function findLoginEntryTrigger() { return genericEntry; }
|
||||
function findLoginPhoneEntryTrigger() { return phoneEntry; }
|
||||
function isActionEnabled() { return true; }
|
||||
function getActionText(el) { return el.textContent || ''; }
|
||||
function log() {}
|
||||
async function humanPause() {}
|
||||
function simulateClick(el) { clicks.push(el.id); }
|
||||
async function waitForLoginEntryOpenTransition() { return { state: 'phone_entry_page' }; }
|
||||
async function switchFromEmailPageToPhoneLogin() { return { routed: 'switch-phone' }; }
|
||||
async function step6LoginFromEmailPage() { return { routed: 'email' }; }
|
||||
async function step6LoginFromPasswordPage() { return { routed: 'password' }; }
|
||||
async function step6LoginFromPhonePage() { return { routed: 'phone' }; }
|
||||
async function finalizeStep6VerificationReady() { return { routed: 'verification' }; }
|
||||
function createStep6OAuthConsentSuccessResult() { return { routed: 'oauth' }; }
|
||||
function createStep6AddEmailSuccessResult() { return { routed: 'add-email' }; }
|
||||
async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'recoverable' } }; }
|
||||
function createStep6RecoverableResult(reason, snapshot, options = {}) {
|
||||
return { step6Outcome: 'recoverable', reason, state: snapshot?.state, message: options.message || '' };
|
||||
}
|
||||
|
||||
${extractFunction('step6OpenLoginEntry')}
|
||||
|
||||
return { clicks, step6OpenLoginEntry };
|
||||
`)();
|
||||
|
||||
const result = await api.step6OpenLoginEntry(
|
||||
{ loginIdentifierType: 'phone', phoneNumber: '+441111111111' },
|
||||
{ state: 'entry_page', loginEntryTrigger: { id: 'generic', textContent: '\u767b\u5f55\u81f3\u53e6\u4e00\u4e2a\u5e10\u6237' }, phoneEntryTrigger: { id: 'phone', textContent: '\u4f7f\u7528\u7535\u8bdd\u53f7\u7801\u7ee7\u7eed' } }
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(api.clicks, ['generic']);
|
||||
assert.equal(result.routed, 'phone');
|
||||
});
|
||||
|
||||
test('step 7 detects username text input when usernameKind is phone_number', () => {
|
||||
const api = createPhoneLoginEntryApi({
|
||||
phoneUsernameKind: true,
|
||||
|
||||
+1
-1
@@ -549,7 +549,7 @@ Plus 模式通过 `plusModeEnabled` 开启,目标是在普通注册资料完
|
||||
Plus 模式可见步骤:
|
||||
|
||||
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
|
||||
2. 第 6 步 `创建 Plus Checkout`:PayPal / GoPay 打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR` 与 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`;GoPay 使用 `ID / IDR` 与 `https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`;GPC helper 模式改为把 accessToken、手机号、国家区号、`otp_channel` 等提交到 `gopayHelperApiUrl` 的 `/api/gp/tasks`,并通过 `gopayHelperApiKey` 发送 `X-API-Key` 认证,创建后保存 `task_id`;当前默认 GPC API 地址为 `https://gpc.qlhazycoder.top`,旧的 `https://gpc.leftcode.xyz` 配置会兼容归一到新地址。
|
||||
2. 第 6 步 `创建 Plus Checkout`:PayPal / GoPay 打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR` 与 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`;GoPay 使用 `ID / IDR` 与 `https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`;GPC helper 模式改为把 accessToken、手机号、国家区号、`otp_channel` 等提交到 `gopayHelperApiUrl` 的 `/api/gp/tasks`,并通过 `gopayHelperApiKey` 发送 `X-API-Key` 认证,创建后保存 `task_id`;当前默认 GPC API 地址为 `https://gpc.qlhazycoder.top`。
|
||||
3. 第 7 步 `填写账单并提交订阅`:PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是轮询 `/api/gp/tasks/{task_id}`,根据远端 `api_waiting_for` 依次向 `/otp` 和 `/pin` 提交验证码与 PIN。若侧栏启用本地 OTP helper,后台会轮询 `gopayHelperLocalSmsHelperUrl` 的 `/latest-otp?phone=...&consume=1` 接口,按当前 GPC 手机号读取并消费当前 OTP 通道的验证码;未开启本地 helper 时才弹出手动 OTP 输入框。仓库内置的 `scripts/gpc_sms_helper_macos.py` 是 macOS Messages/本地通知兼容读取实现,其他通道需要提供兼容的本地 `/latest-otp` 或 `/otp` 接口。
|
||||
4. 仅 PayPal / GoPay 会继续显示第 8 步:该步按当前支付方式显示为 `PayPal 登录与授权` 或 `GoPay 手机验证与授权`,底层 step key 仍为 `paypal-approve`。
|
||||
5. 第 9 步 `订阅回跳确认` 仅在 PayPal / GoPay 模式下等待授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。GPC helper 模式在第 7 步任务完成后会直接进入 Plus 可见第 10 步 OAuth 登录。
|
||||
|
||||
Reference in New Issue
Block a user