feat: release SUB2API OAuth flow on master
- bring the SUB2API OAuth generation and callback account-creation workflow from dev into master - include sidepanel source switching and background state handling for CPA and SUB2API modes - keep the dev-side timeout fix that avoids duplicate SUB2API step execution when content-script responses are slow
This commit is contained in:
+204
-9
@@ -31,6 +31,11 @@ const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
|||||||
const HUMAN_STEP_DELAY_MIN = 700;
|
const HUMAN_STEP_DELAY_MIN = 700;
|
||||||
const HUMAN_STEP_DELAY_MAX = 2200;
|
const HUMAN_STEP_DELAY_MAX = 2200;
|
||||||
const STEP7_RESTART_MAX_ROUNDS = 8;
|
const STEP7_RESTART_MAX_ROUNDS = 8;
|
||||||
|
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_GROUP_NAME = 'codex';
|
||||||
|
const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
||||||
const AUTO_RUN_ALARM_NAME = 'scheduled-auto-run';
|
const AUTO_RUN_ALARM_NAME = 'scheduled-auto-run';
|
||||||
const AUTO_RUN_DELAY_MIN_MINUTES = 1;
|
const AUTO_RUN_DELAY_MIN_MINUTES = 1;
|
||||||
const AUTO_RUN_DELAY_MAX_MINUTES = 1440;
|
const AUTO_RUN_DELAY_MAX_MINUTES = 1440;
|
||||||
@@ -42,8 +47,13 @@ initializeSessionStorageAccess();
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const PERSISTED_SETTING_DEFAULTS = {
|
const PERSISTED_SETTING_DEFAULTS = {
|
||||||
|
panelMode: 'cpa', // Step 1 / Step 9 的来源模式:cpa | sub2api。
|
||||||
vpsUrl: '', // VPS 面板地址,可手动填写。
|
vpsUrl: '', // VPS 面板地址,可手动填写。
|
||||||
vpsPassword: '', // VPS 面板登录密码,可手动填写。
|
vpsPassword: '', // VPS 面板登录密码,可手动填写。
|
||||||
|
sub2apiUrl: DEFAULT_SUB2API_URL, // SUB2API 管理后台地址。
|
||||||
|
sub2apiEmail: '', // SUB2API 登录邮箱。
|
||||||
|
sub2apiPassword: '', // SUB2API 登录密码。
|
||||||
|
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME, // SUB2API 创建账号时绑定的分组名。
|
||||||
customPassword: '', // 自定义账号密码;留空时由程序自动生成随机密码。
|
customPassword: '', // 自定义账号密码;留空时由程序自动生成随机密码。
|
||||||
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
|
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
|
||||||
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
|
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
|
||||||
@@ -73,6 +83,10 @@ const DEFAULT_STATE = {
|
|||||||
lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。
|
lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。
|
||||||
lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。
|
lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。
|
||||||
localhostUrl: null, // 运行时捕获到的 localhost 回调地址,不要手动预填。
|
localhostUrl: null, // 运行时捕获到的 localhost 回调地址,不要手动预填。
|
||||||
|
sub2apiSessionId: null, // SUB2API OpenAI Auth 会话 ID。
|
||||||
|
sub2apiOAuthState: null, // SUB2API OpenAI Auth state。
|
||||||
|
sub2apiGroupId: null, // SUB2API 目标分组 ID。
|
||||||
|
sub2apiDraftName: null, // SUB2API 本轮预生成的账号名称。
|
||||||
flowStartTime: null, // 当前流程开始时间。
|
flowStartTime: null, // 当前流程开始时间。
|
||||||
tabRegistry: {}, // 程序维护的标签页注册表。
|
tabRegistry: {}, // 程序维护的标签页注册表。
|
||||||
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
|
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
|
||||||
@@ -919,6 +933,26 @@ function parseUrlSafely(rawUrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSub2ApiUrl(rawUrl) {
|
||||||
|
const input = (rawUrl || '').trim() || DEFAULT_SUB2API_URL;
|
||||||
|
const withProtocol = /^https?:\/\//i.test(input) ? input : `https://${input}`;
|
||||||
|
const parsed = new URL(withProtocol);
|
||||||
|
if (!parsed.pathname || parsed.pathname === '/') {
|
||||||
|
parsed.pathname = '/admin/accounts';
|
||||||
|
}
|
||||||
|
parsed.hash = '';
|
||||||
|
return parsed.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPanelMode(state = {}) {
|
||||||
|
return state.panelMode === 'sub2api' ? 'sub2api' : 'cpa';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPanelModeLabel(modeOrState) {
|
||||||
|
const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState);
|
||||||
|
return mode === 'sub2api' ? 'SUB2API' : 'CPA';
|
||||||
|
}
|
||||||
|
|
||||||
function isSignupPageHost(hostname = '') {
|
function isSignupPageHost(hostname = '') {
|
||||||
return ['auth0.openai.com', 'auth.openai.com', 'accounts.openai.com'].includes(hostname);
|
return ['auth0.openai.com', 'auth.openai.com', 'accounts.openai.com'].includes(hostname);
|
||||||
}
|
}
|
||||||
@@ -975,6 +1009,14 @@ function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
|
|||||||
return Boolean(reference)
|
return Boolean(reference)
|
||||||
&& candidate.origin === reference.origin
|
&& candidate.origin === reference.origin
|
||||||
&& candidate.pathname === reference.pathname;
|
&& candidate.pathname === reference.pathname;
|
||||||
|
case 'sub2api-panel':
|
||||||
|
return Boolean(reference)
|
||||||
|
&& candidate.origin === reference.origin
|
||||||
|
&& (
|
||||||
|
candidate.pathname.startsWith('/admin/accounts')
|
||||||
|
|| candidate.pathname.startsWith('/login')
|
||||||
|
|| candidate.pathname === '/'
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1622,6 +1664,7 @@ function getSourceLabel(source) {
|
|||||||
'sidepanel': '侧边栏',
|
'sidepanel': '侧边栏',
|
||||||
'signup-page': '认证页',
|
'signup-page': '认证页',
|
||||||
'vps-panel': 'CPA 面板',
|
'vps-panel': 'CPA 面板',
|
||||||
|
'sub2api-panel': 'SUB2API 后台',
|
||||||
'qq-mail': 'QQ 邮箱',
|
'qq-mail': 'QQ 邮箱',
|
||||||
'mail-163': '163 邮箱',
|
'mail-163': '163 邮箱',
|
||||||
'inbucket-mail': 'Inbucket 邮箱',
|
'inbucket-mail': 'Inbucket 邮箱',
|
||||||
@@ -1753,6 +1796,10 @@ function getDownstreamStateResets(step) {
|
|||||||
if (step <= 1) {
|
if (step <= 1) {
|
||||||
return {
|
return {
|
||||||
oauthUrl: null,
|
oauthUrl: null,
|
||||||
|
sub2apiSessionId: null,
|
||||||
|
sub2apiOAuthState: null,
|
||||||
|
sub2apiGroupId: null,
|
||||||
|
sub2apiDraftName: null,
|
||||||
flowStartTime: null,
|
flowStartTime: null,
|
||||||
password: null,
|
password: null,
|
||||||
lastEmailTimestamp: null,
|
lastEmailTimestamp: null,
|
||||||
@@ -2384,8 +2431,13 @@ async function handleMessage(message, sender) {
|
|||||||
|
|
||||||
case 'SAVE_SETTING': {
|
case 'SAVE_SETTING': {
|
||||||
const updates = {};
|
const updates = {};
|
||||||
|
if (message.payload.panelMode !== undefined) updates.panelMode = message.payload.panelMode;
|
||||||
if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
|
if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
|
||||||
if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
|
if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
|
||||||
|
if (message.payload.sub2apiUrl !== undefined) updates.sub2apiUrl = message.payload.sub2apiUrl;
|
||||||
|
if (message.payload.sub2apiEmail !== undefined) updates.sub2apiEmail = message.payload.sub2apiEmail;
|
||||||
|
if (message.payload.sub2apiPassword !== undefined) updates.sub2apiPassword = message.payload.sub2apiPassword;
|
||||||
|
if (message.payload.sub2apiGroupName !== undefined) updates.sub2apiGroupName = message.payload.sub2apiGroupName;
|
||||||
if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
|
if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
|
||||||
if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures);
|
if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures);
|
||||||
if (message.payload.autoRunDelayEnabled !== undefined) updates.autoRunDelayEnabled = Boolean(message.payload.autoRunDelayEnabled);
|
if (message.payload.autoRunDelayEnabled !== undefined) updates.autoRunDelayEnabled = Boolean(message.payload.autoRunDelayEnabled);
|
||||||
@@ -2510,12 +2562,21 @@ async function handleMessage(message, sender) {
|
|||||||
|
|
||||||
async function handleStepData(step, payload) {
|
async function handleStepData(step, payload) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 1:
|
case 1: {
|
||||||
|
const updates = {};
|
||||||
if (payload.oauthUrl) {
|
if (payload.oauthUrl) {
|
||||||
await setState({ oauthUrl: payload.oauthUrl });
|
updates.oauthUrl = payload.oauthUrl;
|
||||||
broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
|
broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
|
||||||
}
|
}
|
||||||
|
if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
|
||||||
|
if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
|
||||||
|
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||||
|
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||||
|
if (Object.keys(updates).length) {
|
||||||
|
await setState(updates);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 3:
|
case 3:
|
||||||
if (payload.email) await setEmailState(payload.email);
|
if (payload.email) await setEmailState(payload.email);
|
||||||
if (payload.signupVerificationRequestedAt) {
|
if (payload.signupVerificationRequestedAt) {
|
||||||
@@ -3244,10 +3305,17 @@ async function resumeAutoRun() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Step 1: Get OAuth Link (via vps-panel.js)
|
// Step 1: Get OAuth Link
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function executeStep1(state) {
|
async function executeStep1(state) {
|
||||||
|
if (getPanelMode(state) === 'sub2api') {
|
||||||
|
return executeSub2ApiStep1(state);
|
||||||
|
}
|
||||||
|
return executeCpaStep1(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeCpaStep1(state) {
|
||||||
if (!state.vpsUrl) {
|
if (!state.vpsUrl) {
|
||||||
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
||||||
}
|
}
|
||||||
@@ -3293,6 +3361,63 @@ async function executeStep1(state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeSub2ApiStep1(state) {
|
||||||
|
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||||
|
const groupName = (state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME).trim() || DEFAULT_SUB2API_GROUP_NAME;
|
||||||
|
|
||||||
|
if (!state.sub2apiEmail) {
|
||||||
|
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
||||||
|
}
|
||||||
|
if (!state.sub2apiPassword) {
|
||||||
|
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog('步骤 1:正在打开 SUB2API 后台...');
|
||||||
|
|
||||||
|
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||||
|
|
||||||
|
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl);
|
||||||
|
|
||||||
|
const tab = await chrome.tabs.create({ url: sub2apiUrl, active: true });
|
||||||
|
const tabId = tab.id;
|
||||||
|
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
||||||
|
|
||||||
|
await addLog('步骤 1:SUB2API 页面已打开,正在等待页面进入目标地址...');
|
||||||
|
const matchedTab = await waitForTabUrlFamily('sub2api-panel', tabId, sub2apiUrl, {
|
||||||
|
timeoutMs: 15000,
|
||||||
|
retryDelayMs: 400,
|
||||||
|
});
|
||||||
|
if (!matchedTab) {
|
||||||
|
await addLog('步骤 1:SUB2API 页面尚未稳定,继续尝试连接内容脚本...', 'warn');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
|
||||||
|
inject: injectFiles,
|
||||||
|
injectSource: 'sub2api-panel',
|
||||||
|
timeoutMs: 45000,
|
||||||
|
retryDelayMs: 900,
|
||||||
|
logMessage: '步骤 1:SUB2API 页面仍在加载,正在重试连接内容脚本...',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sendToContentScript('sub2api-panel', {
|
||||||
|
type: 'EXECUTE_STEP',
|
||||||
|
step: 1,
|
||||||
|
source: 'background',
|
||||||
|
payload: {
|
||||||
|
sub2apiUrl,
|
||||||
|
sub2apiEmail: state.sub2apiEmail,
|
||||||
|
sub2apiPassword: state.sub2apiPassword,
|
||||||
|
sub2apiGroupName: groupName,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
responseTimeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register)
|
// Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -3731,11 +3856,7 @@ async function executeStep5(state) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function refreshOAuthUrlBeforeStep6(state) {
|
async function refreshOAuthUrlBeforeStep6(state) {
|
||||||
if (!state.vpsUrl) {
|
await addLog(`步骤 6:正在刷新登录用的 ${getPanelModeLabel(state)} OAuth 链接...`);
|
||||||
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
|
||||||
}
|
|
||||||
|
|
||||||
await addLog('步骤 6:正在刷新登录用的 CPA OAuth 链接...');
|
|
||||||
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] preparing fresh OAuth via step 1');
|
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] preparing fresh OAuth via step 1');
|
||||||
const waitForFreshOAuth = waitForStepComplete(1, 120000);
|
const waitForFreshOAuth = waitForStepComplete(1, 120000);
|
||||||
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] executing step 1 for fresh OAuth');
|
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] executing step 1 for fresh OAuth');
|
||||||
@@ -4272,10 +4393,17 @@ async function executeStep8(state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Step 9: CPA 回调验证(通过 vps-panel.js)
|
// Step 9: 平台回调验证
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function executeStep9(state) {
|
async function executeStep9(state) {
|
||||||
|
if (getPanelMode(state) === 'sub2api') {
|
||||||
|
return executeSub2ApiStep9(state);
|
||||||
|
}
|
||||||
|
return executeCpaStep9(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeCpaStep9(state) {
|
||||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||||
throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。');
|
throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。');
|
||||||
}
|
}
|
||||||
@@ -4336,6 +4464,73 @@ async function executeStep9(state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeSub2ApiStep9(state) {
|
||||||
|
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||||
|
throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。');
|
||||||
|
}
|
||||||
|
if (!state.localhostUrl) {
|
||||||
|
throw new Error('缺少 localhost 回调地址,请先完成步骤 8。');
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||||
|
|
||||||
|
await addLog('步骤 9:正在打开 SUB2API 后台...');
|
||||||
|
|
||||||
|
let tabId = await getTabId('sub2api-panel');
|
||||||
|
const alive = tabId && await isTabAlive('sub2api-panel');
|
||||||
|
|
||||||
|
if (!alive) {
|
||||||
|
tabId = await reuseOrCreateTab('sub2api-panel', sub2apiUrl, {
|
||||||
|
inject: injectFiles,
|
||||||
|
injectSource: 'sub2api-panel',
|
||||||
|
reloadIfSameUrl: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl, { excludeTabIds: [tabId] });
|
||||||
|
await chrome.tabs.update(tabId, { active: true });
|
||||||
|
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
|
||||||
|
inject: injectFiles,
|
||||||
|
injectSource: 'sub2api-panel',
|
||||||
|
});
|
||||||
|
|
||||||
|
await addLog('步骤 9:正在向 SUB2API 提交回调并创建账号...');
|
||||||
|
const result = await sendToContentScript('sub2api-panel', {
|
||||||
|
type: 'EXECUTE_STEP',
|
||||||
|
step: 9,
|
||||||
|
source: 'background',
|
||||||
|
payload: {
|
||||||
|
localhostUrl: state.localhostUrl,
|
||||||
|
sub2apiUrl,
|
||||||
|
sub2apiEmail: state.sub2apiEmail,
|
||||||
|
sub2apiPassword: state.sub2apiPassword,
|
||||||
|
sub2apiGroupName: state.sub2apiGroupName,
|
||||||
|
sub2apiSessionId: state.sub2apiSessionId,
|
||||||
|
sub2apiOAuthState: state.sub2apiOAuthState,
|
||||||
|
sub2apiGroupId: state.sub2apiGroupId,
|
||||||
|
sub2apiDraftName: state.sub2apiDraftName,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
responseTimeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Open Side Panel on extension icon click
|
// Open Side Panel on extension icon click
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -0,0 +1,401 @@
|
|||||||
|
// content/sub2api-panel.js — 页内脚本:SUB2API 后台(步骤 1、9)
|
||||||
|
|
||||||
|
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_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_STEP') {
|
||||||
|
resetStopState();
|
||||||
|
handleStep(message.step, message.payload).then(() => {
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
}).catch((err) => {
|
||||||
|
if (isStopError(err)) {
|
||||||
|
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
|
||||||
|
sendResponse({ stopped: true, error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reportError(message.step, 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 9:
|
||||||
|
return step9_submitOpenAiCallback(payload);
|
||||||
|
default:
|
||||||
|
throw new Error(`sub2api-panel.js 不处理步骤 ${step}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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) {
|
||||||
|
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('步骤 9 只接受 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 = {}) {
|
||||||
|
const redirectUri = normalizeRedirectUri();
|
||||||
|
const groupName = (payload.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME).trim() || SUB2API_DEFAULT_GROUP_NAME;
|
||||||
|
|
||||||
|
const { origin, token } = await loginSub2Api(payload);
|
||||||
|
const group = await getGroupByName(origin, token, groupName);
|
||||||
|
const draftName = buildDraftAccountName(group.name || groupName);
|
||||||
|
|
||||||
|
log(`步骤 1:已登录 SUB2API,使用分组 ${group.name}(#${group.id})。`);
|
||||||
|
log(`步骤 1:正在向 SUB2API 生成 OpenAI Auth 链接,回调地址为 ${redirectUri}。`);
|
||||||
|
|
||||||
|
const authData = await requestJson(origin, '/api/v1/admin/openai/generate-auth-url', {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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(`步骤 1:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok');
|
||||||
|
reportComplete(1, {
|
||||||
|
oauthUrl,
|
||||||
|
sub2apiSessionId: sessionId,
|
||||||
|
sub2apiOAuthState: oauthState,
|
||||||
|
sub2apiGroupId: group.id,
|
||||||
|
sub2apiDraftName: draftName,
|
||||||
|
});
|
||||||
|
openAccountsPageSoon(origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function step9_submitOpenAiCallback(payload = {}) {
|
||||||
|
const callback = parseLocalhostCallback(payload.localhostUrl || '');
|
||||||
|
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 accountName = flowEmail
|
||||||
|
|| String(payload.sub2apiDraftName || backgroundState.sub2apiDraftName || '').trim()
|
||||||
|
|| buildDraftAccountName(payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME);
|
||||||
|
|
||||||
|
const { origin, token } = await loginSub2Api(payload);
|
||||||
|
const group = payload.sub2apiGroupId
|
||||||
|
? { id: payload.sub2apiGroupId, name: payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME }
|
||||||
|
: await getGroupByName(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('步骤 9:正在向 SUB2API 交换 OpenAI 授权码...');
|
||||||
|
const exchangeData = await requestJson(origin, '/api/v1/admin/openai/exchange-code', {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
session_id: sessionId,
|
||||||
|
code: callback.code,
|
||||||
|
state: callback.state,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const credentials = buildOpenAiCredentials(exchangeData);
|
||||||
|
const extra = buildOpenAiExtra(exchangeData);
|
||||||
|
const groupId = Number(group.id);
|
||||||
|
if (!Number.isFinite(groupId) || groupId <= 0) {
|
||||||
|
throw new Error('SUB2API 返回的目标分组 ID 无效。');
|
||||||
|
}
|
||||||
|
const createPayload = {
|
||||||
|
name: accountName,
|
||||||
|
notes: '',
|
||||||
|
platform: 'openai',
|
||||||
|
type: 'oauth',
|
||||||
|
credentials,
|
||||||
|
concurrency: SUB2API_DEFAULT_CONCURRENCY,
|
||||||
|
priority: SUB2API_DEFAULT_PRIORITY,
|
||||||
|
rate_multiplier: SUB2API_DEFAULT_RATE_MULTIPLIER,
|
||||||
|
group_ids: [groupId],
|
||||||
|
auto_pause_on_expired: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (extra) {
|
||||||
|
createPayload.extra = extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`步骤 9:授权码交换成功,正在创建 SUB2API 账号(名称:${accountName})...`);
|
||||||
|
const createdAccount = await requestJson(origin, '/api/v1/admin/accounts', {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: createPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
|
||||||
|
log(`步骤 9:${verifiedStatus}`, 'ok');
|
||||||
|
reportComplete(9, {
|
||||||
|
localhostUrl: callback.url,
|
||||||
|
verifiedStatus,
|
||||||
|
});
|
||||||
|
openAccountsPageSoon(origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
reportReady();
|
||||||
@@ -64,6 +64,13 @@
|
|||||||
<section id="data-section">
|
<section id="data-section">
|
||||||
<div class="data-card">
|
<div class="data-card">
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
|
<span class="data-label">来源</span>
|
||||||
|
<select id="select-panel-mode" class="data-select">
|
||||||
|
<option value="cpa">CPA 面板</option>
|
||||||
|
<option value="sub2api">SUB2API</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-vps-url">
|
||||||
<span class="data-label">CPA</span>
|
<span class="data-label">CPA</span>
|
||||||
<div class="input-with-icon">
|
<div class="input-with-icon">
|
||||||
<input type="password" id="input-vps-url" class="data-input data-input-with-icon"
|
<input type="password" id="input-vps-url" class="data-input data-input-with-icon"
|
||||||
@@ -72,10 +79,26 @@
|
|||||||
title="显示 CPA 地址"></button>
|
title="显示 CPA 地址"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row" id="row-vps-password">
|
||||||
<span class="data-label">管理密钥</span>
|
<span class="data-label">管理密钥</span>
|
||||||
<input type="password" id="input-vps-password" class="data-input" placeholder="请输入 CPA 管理密钥" />
|
<input type="password" id="input-vps-password" class="data-input" placeholder="请输入 CPA 管理密钥" />
|
||||||
</div>
|
</div>
|
||||||
|
<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" />
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-sub2api-email" style="display:none;">
|
||||||
|
<span class="data-label">账号</span>
|
||||||
|
<input type="text" id="input-sub2api-email" class="data-input" placeholder="请输入 SUB2API 登录邮箱" />
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-sub2api-password" style="display:none;">
|
||||||
|
<span class="data-label">密码</span>
|
||||||
|
<input type="password" id="input-sub2api-password" class="data-input" placeholder="请输入 SUB2API 登录密码" />
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-sub2api-group" style="display:none;">
|
||||||
|
<span class="data-label">分组</span>
|
||||||
|
<input type="text" id="input-sub2api-group" class="data-input" placeholder="默认 codex" />
|
||||||
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">codex密码</span>
|
<span class="data-label">codex密码</span>
|
||||||
<div class="data-inline">
|
<div class="data-inline">
|
||||||
@@ -279,7 +302,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="step-row" data-step="9">
|
<div class="step-row" data-step="9">
|
||||||
<div class="step-indicator" data-step="9"><span class="step-num">9</span></div>
|
<div class="step-indicator" data-step="9"><span class="step-num">9</span></div>
|
||||||
<button class="step-btn" data-step="9">CPA 回调验证</button>
|
<button class="step-btn" data-step="9">平台回调验证</button>
|
||||||
<span class="step-status" data-step="9"></span>
|
<span class="step-status" data-step="9"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,8 +33,19 @@ const autoScheduleMeta = document.getElementById('auto-schedule-meta');
|
|||||||
const btnAutoRunNow = document.getElementById('btn-auto-run-now');
|
const btnAutoRunNow = document.getElementById('btn-auto-run-now');
|
||||||
const btnAutoCancelSchedule = document.getElementById('btn-auto-cancel-schedule');
|
const btnAutoCancelSchedule = document.getElementById('btn-auto-cancel-schedule');
|
||||||
const btnClearLog = document.getElementById('btn-clear-log');
|
const btnClearLog = document.getElementById('btn-clear-log');
|
||||||
|
const selectPanelMode = document.getElementById('select-panel-mode');
|
||||||
|
const rowVpsUrl = document.getElementById('row-vps-url');
|
||||||
const inputVpsUrl = document.getElementById('input-vps-url');
|
const inputVpsUrl = document.getElementById('input-vps-url');
|
||||||
|
const rowVpsPassword = document.getElementById('row-vps-password');
|
||||||
const inputVpsPassword = document.getElementById('input-vps-password');
|
const inputVpsPassword = document.getElementById('input-vps-password');
|
||||||
|
const rowSub2ApiUrl = document.getElementById('row-sub2api-url');
|
||||||
|
const inputSub2ApiUrl = document.getElementById('input-sub2api-url');
|
||||||
|
const rowSub2ApiEmail = document.getElementById('row-sub2api-email');
|
||||||
|
const inputSub2ApiEmail = document.getElementById('input-sub2api-email');
|
||||||
|
const rowSub2ApiPassword = document.getElementById('row-sub2api-password');
|
||||||
|
const inputSub2ApiPassword = document.getElementById('input-sub2api-password');
|
||||||
|
const rowSub2ApiGroup = document.getElementById('row-sub2api-group');
|
||||||
|
const inputSub2ApiGroup = document.getElementById('input-sub2api-group');
|
||||||
const selectMailProvider = document.getElementById('select-mail-provider');
|
const selectMailProvider = document.getElementById('select-mail-provider');
|
||||||
const selectEmailGenerator = document.getElementById('select-email-generator');
|
const selectEmailGenerator = document.getElementById('select-email-generator');
|
||||||
const hotmailSection = document.getElementById('hotmail-section');
|
const hotmailSection = document.getElementById('hotmail-section');
|
||||||
@@ -478,8 +489,13 @@ function collectSettingsPayload() {
|
|||||||
!cloudflareDomainEditMode ? selectCfDomain.value : activeDomain
|
!cloudflareDomainEditMode ? selectCfDomain.value : activeDomain
|
||||||
) || activeDomain;
|
) || activeDomain;
|
||||||
return {
|
return {
|
||||||
|
panelMode: selectPanelMode.value,
|
||||||
vpsUrl: inputVpsUrl.value.trim(),
|
vpsUrl: inputVpsUrl.value.trim(),
|
||||||
vpsPassword: inputVpsPassword.value,
|
vpsPassword: inputVpsPassword.value,
|
||||||
|
sub2apiUrl: inputSub2ApiUrl.value.trim(),
|
||||||
|
sub2apiEmail: inputSub2ApiEmail.value.trim(),
|
||||||
|
sub2apiPassword: inputSub2ApiPassword.value,
|
||||||
|
sub2apiGroupName: inputSub2ApiGroup.value.trim(),
|
||||||
customPassword: inputPassword.value,
|
customPassword: inputPassword.value,
|
||||||
mailProvider: selectMailProvider.value,
|
mailProvider: selectMailProvider.value,
|
||||||
emailGenerator: selectEmailGenerator.value,
|
emailGenerator: selectEmailGenerator.value,
|
||||||
@@ -535,6 +551,7 @@ async function saveSettings(options = {}) {
|
|||||||
|
|
||||||
syncLatestState(payload);
|
syncLatestState(payload);
|
||||||
markSettingsDirty(false);
|
markSettingsDirty(false);
|
||||||
|
updatePanelModeUI();
|
||||||
updateMailProviderUI();
|
updateMailProviderUI();
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
@@ -660,6 +677,21 @@ async function restoreState() {
|
|||||||
if (state.vpsPassword) {
|
if (state.vpsPassword) {
|
||||||
inputVpsPassword.value = state.vpsPassword;
|
inputVpsPassword.value = state.vpsPassword;
|
||||||
}
|
}
|
||||||
|
if (state.panelMode) {
|
||||||
|
selectPanelMode.value = state.panelMode;
|
||||||
|
}
|
||||||
|
if (state.sub2apiUrl) {
|
||||||
|
inputSub2ApiUrl.value = state.sub2apiUrl;
|
||||||
|
}
|
||||||
|
if (state.sub2apiEmail) {
|
||||||
|
inputSub2ApiEmail.value = state.sub2apiEmail;
|
||||||
|
}
|
||||||
|
if (state.sub2apiPassword) {
|
||||||
|
inputSub2ApiPassword.value = state.sub2apiPassword;
|
||||||
|
}
|
||||||
|
if (state.sub2apiGroupName) {
|
||||||
|
inputSub2ApiGroup.value = state.sub2apiGroupName;
|
||||||
|
}
|
||||||
if (state.mailProvider) {
|
if (state.mailProvider) {
|
||||||
selectMailProvider.value = state.mailProvider;
|
selectMailProvider.value = state.mailProvider;
|
||||||
}
|
}
|
||||||
@@ -698,6 +730,7 @@ async function restoreState() {
|
|||||||
updateAutoDelayInputState();
|
updateAutoDelayInputState();
|
||||||
updateStatusDisplay(latestState);
|
updateStatusDisplay(latestState);
|
||||||
updateProgressCounter();
|
updateProgressCounter();
|
||||||
|
updatePanelModeUI();
|
||||||
updateMailProviderUI();
|
updateMailProviderUI();
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1014,6 +1047,21 @@ async function saveCloudflareDomainSettings(domains, activeDomain, options = {})
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePanelModeUI() {
|
||||||
|
const useSub2Api = selectPanelMode.value === 'sub2api';
|
||||||
|
rowVpsUrl.style.display = useSub2Api ? 'none' : '';
|
||||||
|
rowVpsPassword.style.display = useSub2Api ? 'none' : '';
|
||||||
|
rowSub2ApiUrl.style.display = useSub2Api ? '' : 'none';
|
||||||
|
rowSub2ApiEmail.style.display = useSub2Api ? '' : 'none';
|
||||||
|
rowSub2ApiPassword.style.display = useSub2Api ? '' : 'none';
|
||||||
|
rowSub2ApiGroup.style.display = useSub2Api ? '' : 'none';
|
||||||
|
|
||||||
|
const step9Btn = document.querySelector('.step-btn[data-step="9"]');
|
||||||
|
if (step9Btn) {
|
||||||
|
step9Btn.textContent = useSub2Api ? 'SUB2API 回调验证' : 'CPA 回调验证';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// UI Updates
|
// UI Updates
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -1841,6 +1889,12 @@ selectEmailGenerator.addEventListener('change', () => {
|
|||||||
saveSettings({ silent: true }).catch(() => { });
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
selectPanelMode.addEventListener('change', () => {
|
||||||
|
updatePanelModeUI();
|
||||||
|
markSettingsDirty(true);
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
selectCfDomain.addEventListener('change', () => {
|
selectCfDomain.addEventListener('change', () => {
|
||||||
if (selectCfDomain.disabled) {
|
if (selectCfDomain.disabled) {
|
||||||
return;
|
return;
|
||||||
@@ -1877,6 +1931,38 @@ inputCfDomain.addEventListener('keydown', (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
inputSub2ApiUrl.addEventListener('input', () => {
|
||||||
|
markSettingsDirty(true);
|
||||||
|
scheduleSettingsAutoSave();
|
||||||
|
});
|
||||||
|
inputSub2ApiUrl.addEventListener('blur', () => {
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
inputSub2ApiEmail.addEventListener('input', () => {
|
||||||
|
markSettingsDirty(true);
|
||||||
|
scheduleSettingsAutoSave();
|
||||||
|
});
|
||||||
|
inputSub2ApiEmail.addEventListener('blur', () => {
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
inputSub2ApiPassword.addEventListener('input', () => {
|
||||||
|
markSettingsDirty(true);
|
||||||
|
scheduleSettingsAutoSave();
|
||||||
|
});
|
||||||
|
inputSub2ApiPassword.addEventListener('blur', () => {
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
inputSub2ApiGroup.addEventListener('input', () => {
|
||||||
|
markSettingsDirty(true);
|
||||||
|
scheduleSettingsAutoSave();
|
||||||
|
});
|
||||||
|
inputSub2ApiGroup.addEventListener('blur', () => {
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
inputInbucketMailbox.addEventListener('input', () => {
|
inputInbucketMailbox.addEventListener('input', () => {
|
||||||
markSettingsDirty(true);
|
markSettingsDirty(true);
|
||||||
scheduleSettingsAutoSave();
|
scheduleSettingsAutoSave();
|
||||||
@@ -2064,6 +2150,7 @@ updateSaveButtonState();
|
|||||||
restoreState().then(() => {
|
restoreState().then(() => {
|
||||||
syncPasswordToggleLabel();
|
syncPasswordToggleLabel();
|
||||||
syncVpsUrlToggleLabel();
|
syncVpsUrlToggleLabel();
|
||||||
|
updatePanelModeUI();
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
updateStatusDisplay(latestState);
|
updateStatusDisplay(latestState);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user