This commit is contained in:
QLHazyCoder
2026-04-23 01:00:44 +08:00
22 changed files with 723 additions and 33 deletions
+24 -2
View File
@@ -132,7 +132,15 @@
4. Step 1 会直接在 SUB2API 后台生成 OAuth 链接
5. Step 10 会把 localhost 回调提交回 SUB2API,并直接创建 OpenAI 账号
### 方案 C`Hotmail 账号池`
### 方案 C`Codex2API + QQ / 163 / 163 VIP`
1. `来源` 选择 `Codex2API`
2. 填好 `Codex2API` 后台地址、管理密钥
3. `Mail``邮箱生成` 的配置方式同方案 A
4. Step 7 会直接通过 Codex2API 协议 `/api/admin/oauth/generate-auth-url` 生成 OAuth 链接
5. Step 10 会把 localhost 回调中的 `code / state` 通过 `/api/admin/oauth/exchange-code` 直接提交给 Codex2API
### 方案 D`Hotmail 账号池`
1. `Mail` 选择 `Hotmail`
2.`Hotmail 账号池` 中添加 `邮箱 / Client ID / Refresh Token`
@@ -140,7 +148,7 @@
4. 通过后再执行步骤或 `Auto`
5. 当前项目中,`Mail = Hotmail` 时会直接使用账号池里的邮箱作为注册邮箱,不再走 `Duck / Cloudflare` 自动生成
### 方案 D`2925 账号池`
### 方案 E`2925 账号池`
1. `Mail` 选择 `2925`
2.`2925 账号池` 中添加 `邮箱 / 密码`
@@ -177,6 +185,20 @@ Step 1 和 Step 10 都依赖这个地址。
插件会在 Step 1 和 Step 10 自动从 `/api/v1/admin/proxies/all` 解析这个代理,并在 OAuth 链接生成、授权码交换和账号创建请求中附带 `proxy_id`。如果名称匹配到多个代理,请改填代理 ID;留空则不会发送 `proxy_id`
### `Codex2API`
`来源 = Codex2API` 时,需要配置:
- `Codex2API`:后台账号管理页地址,默认 `http://localhost:8080/admin/accounts`
- `管理密钥`Codex2API 的 `Admin Secret`
插件会在:
- Step 7 调用 `POST /api/admin/oauth/generate-auth-url` 生成授权链接
- Step 10 调用 `POST /api/admin/oauth/exchange-code` 完成 localhost callback 的授权码交换并创建账号
这条来源是协议直连,不依赖 Codex2API 后台页面的“添加账号 / OAuth 授权 / 生成授权链接”按钮 DOM。
### `Mail`
支持五种验证码来源:
+55 -6
View File
@@ -156,6 +156,7 @@ 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_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
const DEFAULT_SUB2API_PROXY_NAME = '';
const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback';
@@ -252,6 +253,8 @@ const PERSISTED_SETTING_DEFAULTS = {
sub2apiPassword: '',
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME,
sub2apiDefaultProxyName: DEFAULT_SUB2API_PROXY_NAME,
codex2apiUrl: DEFAULT_CODEX2API_URL,
codex2apiAdminKey: '',
customPassword: '',
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
@@ -330,6 +333,8 @@ const DEFAULT_STATE = {
sub2apiGroupId: null, // SUB2API 目标分组 ID。
sub2apiDraftName: null, // SUB2API 本轮预生成的账号名称。
sub2apiProxyId: null, // SUB2API 本轮使用的代理 ID。
codex2apiSessionId: null, // Codex2API OAuth 会话 ID。
codex2apiOAuthState: null, // Codex2API OAuth state。
flowStartTime: null, // 当前流程开始时间。
tabRegistry: {}, // 程序维护的标签页注册表。
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
@@ -654,7 +659,14 @@ function normalizeEmailGenerator(value = '') {
}
function normalizePanelMode(value = '') {
return String(value || '').trim().toLowerCase() === 'sub2api' ? 'sub2api' : 'cpa';
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'sub2api') {
return 'sub2api';
}
if (normalized === 'codex2api') {
return 'codex2api';
}
return 'cpa';
}
function normalizeMailProvider(value = '') {
@@ -876,6 +888,10 @@ function normalizePersistentSettingValue(key, value) {
return String(value || '').trim();
case 'sub2apiDefaultProxyName':
return String(value || '').trim();
case 'codex2apiUrl':
return normalizeCodex2ApiUrl(value);
case 'codex2apiAdminKey':
return String(value || '').trim();
case 'customPassword':
return String(value || '');
case 'autoRunSkipFailures':
@@ -3626,11 +3642,31 @@ function normalizeSub2ApiUrl(rawUrl) {
return parsed.toString();
}
function normalizeCodex2ApiUrl(rawUrl) {
if (typeof navigationUtils !== 'undefined' && navigationUtils?.normalizeCodex2ApiUrl) {
return navigationUtils.normalizeCodex2ApiUrl(rawUrl);
}
const input = (rawUrl || '').trim() || DEFAULT_CODEX2API_URL;
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
const parsed = new URL(withProtocol);
if (!parsed.pathname || parsed.pathname === '/' || parsed.pathname === '/admin') {
parsed.pathname = '/admin/accounts';
}
parsed.hash = '';
return parsed.toString();
}
function getPanelMode(state = {}) {
if (typeof navigationUtils !== 'undefined' && navigationUtils?.getPanelMode) {
return navigationUtils.getPanelMode(state);
}
return state.panelMode === 'sub2api' ? 'sub2api' : 'cpa';
if (state.panelMode === 'sub2api') {
return 'sub2api';
}
if (state.panelMode === 'codex2api') {
return 'codex2api';
}
return 'cpa';
}
function getPanelModeLabel(modeOrState) {
@@ -3638,7 +3674,13 @@ function getPanelModeLabel(modeOrState) {
return navigationUtils.getPanelModeLabel(modeOrState);
}
const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState);
return mode === 'sub2api' ? 'SUB2API' : 'CPA';
if (mode === 'sub2api') {
return 'SUB2API';
}
if (mode === 'codex2api') {
return 'Codex2API';
}
return 'CPA';
}
function isSignupPageHost(hostname = '') {
@@ -3942,6 +3984,7 @@ function isRetryableContentScriptTransportError(error) {
}
const navigationUtils = self.MultiPageBackgroundNavigationUtils?.createNavigationUtils({
DEFAULT_CODEX2API_URL,
DEFAULT_SUB2API_URL,
normalizeLocalCpaStep9Mode,
});
@@ -4135,6 +4178,8 @@ function getDownstreamStateResets(step) {
sub2apiOAuthState: null,
sub2apiGroupId: null,
sub2apiDraftName: null,
codex2apiSessionId: null,
codex2apiOAuthState: null,
flowStartTime: null,
password: null,
lastEmailTimestamp: null,
@@ -4920,6 +4965,8 @@ async function handleStepData(step, payload) {
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
if (Object.keys(updates).length) {
await setState(updates);
}
@@ -6187,6 +6234,7 @@ const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({
closeConflictingTabsForSource,
ensureContentScriptReadyOnTab,
getPanelMode,
normalizeCodex2ApiUrl,
normalizeSub2ApiUrl,
rememberSourceLastUrl,
sendToContentScript,
@@ -6362,6 +6410,7 @@ const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
getTabId,
isLocalhostOAuthCallbackUrl,
isTabAlive,
normalizeCodex2ApiUrl,
normalizeSub2ApiUrl,
rememberSourceLastUrl,
reuseOrCreateTab,
@@ -6812,7 +6861,7 @@ async function runPreStep6CookieCleanup() {
async function refreshOAuthUrlBeforeStep6(state) {
if (state?.contributionModeExpected && !state?.contributionMode) {
throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API 链路。请重新进入贡献模式后再点击自动。');
throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。');
}
if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) {
await addLog('步骤 7contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', 'info');
@@ -6828,7 +6877,7 @@ async function refreshOAuthUrlBeforeStep6(state) {
await handleStepData(1, { oauthUrl });
return oauthUrl;
}
await addLog(`步骤 7contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info');
await addLog(`步骤 7contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info');
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel');
const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' });
await handleStepData(1, refreshResult);
@@ -7529,7 +7578,7 @@ async function executeContributionStep10(state) {
async function executeStep10(state) {
if (state?.contributionModeExpected && !state?.contributionMode) {
throw new Error('步骤 10:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API 提交。请重新进入贡献模式后再点击自动。');
throw new Error('步骤 10:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 提交。请重新进入贡献模式后再点击自动。');
}
if (state?.contributionMode) {
return executeContributionStep10(state);
+2
View File
@@ -140,6 +140,8 @@
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
if (Object.keys(updates).length) {
await setState(updates);
}
+35 -2
View File
@@ -3,6 +3,7 @@
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundNavigationUtilsModule() {
function createNavigationUtils(deps = {}) {
const {
DEFAULT_CODEX2API_URL,
DEFAULT_SUB2API_URL,
normalizeLocalCpaStep9Mode,
} = deps;
@@ -27,13 +28,36 @@
return parsed.toString();
}
function normalizeCodex2ApiUrl(rawUrl) {
const input = (rawUrl || '').trim() || DEFAULT_CODEX2API_URL;
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
const parsed = new URL(withProtocol);
if (!parsed.pathname || parsed.pathname === '/' || parsed.pathname === '/admin') {
parsed.pathname = '/admin/accounts';
}
parsed.hash = '';
return parsed.toString();
}
function getPanelMode(state = {}) {
return state.panelMode === 'sub2api' ? 'sub2api' : 'cpa';
if (state.panelMode === 'sub2api') {
return 'sub2api';
}
if (state.panelMode === 'codex2api') {
return 'codex2api';
}
return 'cpa';
}
function getPanelModeLabel(modeOrState) {
const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState);
return mode === 'sub2api' ? 'SUB2API' : 'CPA';
if (mode === 'sub2api') {
return 'SUB2API';
}
if (mode === 'codex2api') {
return 'Codex2API';
}
return 'CPA';
}
function isSignupPageHost(hostname = '') {
@@ -124,6 +148,14 @@
|| candidate.pathname.startsWith('/login')
|| candidate.pathname === '/'
);
case 'codex2api-panel':
return Boolean(reference)
&& candidate.origin === reference.origin
&& (
candidate.pathname.startsWith('/admin/accounts')
|| candidate.pathname === '/admin'
|| candidate.pathname === '/'
);
default:
return false;
}
@@ -160,6 +192,7 @@
isSignupPageHost,
isSignupPasswordPageUrl,
matchesSourceUrlFamily,
normalizeCodex2ApiUrl,
normalizeSub2ApiUrl,
parseUrlSafely,
shouldBypassStep9ForLocalCpa,
+102
View File
@@ -8,6 +8,7 @@
closeConflictingTabsForSource,
ensureContentScriptReadyOnTab,
getPanelMode,
normalizeCodex2ApiUrl,
normalizeSub2ApiUrl,
rememberSourceLastUrl,
sendToContentScript,
@@ -17,7 +18,74 @@
SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
} = deps;
function normalizeAdminKey(value = '') {
return String(value || '').trim();
}
function extractStateFromAuthUrl(authUrl = '') {
try {
return new URL(authUrl).searchParams.get('state') || '';
} catch {
return '';
}
}
function getCodex2ApiErrorMessage(payload, responseStatus = 500) {
const candidates = [
payload?.error,
payload?.message,
payload?.detail,
payload?.reason,
];
const message = candidates
.map((value) => String(value || '').trim())
.find(Boolean);
return message || `Codex2API 请求失败(HTTP ${responseStatus})。`;
}
async function fetchCodex2ApiJson(origin, path, options = {}) {
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`${origin}${path}`, {
method: options.method || 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Admin-Key': normalizeAdminKey(options.adminKey),
},
body: options.body === undefined ? undefined : JSON.stringify(options.body),
signal: controller.signal,
});
let payload = {};
try {
payload = await response.json();
} catch {
payload = {};
}
if (!response.ok) {
throw new Error(getCodex2ApiErrorMessage(payload, response.status));
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error('Codex2API 请求超时,请稍后重试。');
}
throw error;
} finally {
clearTimeout(timer);
}
}
async function requestOAuthUrlFromPanel(state, options = {}) {
if (getPanelMode(state) === 'codex2api') {
return requestCodex2ApiOAuthUrl(state, options);
}
if (getPanelMode(state) === 'sub2api') {
return requestSub2ApiOAuthUrl(state, options);
}
@@ -74,6 +142,39 @@
return result || {};
}
async function requestCodex2ApiOAuthUrl(state, options = {}) {
const { logLabel = 'OAuth 刷新' } = options;
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
const adminKey = normalizeAdminKey(state.codex2apiAdminKey);
if (!adminKey) {
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
}
const origin = new URL(codex2apiUrl).origin;
await addLog(`${logLabel}:正在通过 Codex2API 协议生成 OAuth 授权链接...`);
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/generate-auth-url', {
adminKey,
method: 'POST',
body: {},
});
const oauthUrl = String(result?.auth_url || result?.authUrl || '').trim();
const sessionId = String(result?.session_id || result?.sessionId || '').trim();
const oauthState = extractStateFromAuthUrl(oauthUrl);
if (!oauthUrl || !sessionId) {
throw new Error('Codex2API 未返回有效的 auth_url 或 session_id。');
}
return {
oauthUrl,
codex2apiSessionId: sessionId,
codex2apiOAuthState: oauthState || null,
};
}
async function requestSub2ApiOAuthUrl(state, options = {}) {
const { logLabel = 'OAuth 刷新' } = options;
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
@@ -135,6 +236,7 @@
return {
requestOAuthUrlFromPanel,
requestCodex2ApiOAuthUrl,
requestCpaOAuthUrl,
requestSub2ApiOAuthUrl,
};
+21
View File
@@ -23,6 +23,20 @@
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);
}
async function executeStep7(state) {
if (!state.email) {
throw new Error('缺少邮箱地址,请先完成步骤 3。');
@@ -99,6 +113,13 @@
if (isAddPhoneAuthFailure(err)) {
throw err;
}
if (isManagementSecretConfigError(err)) {
await addLog(
`步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
'error'
);
throw err;
}
lastError = err;
if (attempt >= STEP6_MAX_ATTEMPTS) {
break;
+123
View File
@@ -12,6 +12,7 @@
getTabId,
isLocalhostOAuthCallbackUrl,
isTabAlive,
normalizeCodex2ApiUrl,
normalizeSub2ApiUrl,
rememberSourceLastUrl,
reuseOrCreateTab,
@@ -21,7 +22,86 @@
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
} = deps;
function normalizeString(value = '') {
return String(value || '').trim();
}
function parseLocalhostCallback(rawUrl) {
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。');
}
const code = normalizeString(parsed.searchParams.get('code'));
const state = normalizeString(parsed.searchParams.get('state'));
if (!code || !state) {
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。');
}
return {
url: parsed.toString(),
code,
state,
};
}
function getCodex2ApiErrorMessage(payload, responseStatus = 500) {
const details = [
payload?.error,
payload?.message,
payload?.detail,
payload?.reason,
]
.map((value) => normalizeString(value))
.find(Boolean);
return details || `Codex2API 请求失败(HTTP ${responseStatus})。`;
}
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);
}
@@ -90,6 +170,48 @@
}
}
async function executeCodex2ApiStep10(state) {
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
}
if (!state.localhostUrl) {
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
}
if (!state.codex2apiSessionId) {
throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。');
}
if (!normalizeString(state.codex2apiAdminKey)) {
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
}
const callback = parseLocalhostCallback(state.localhostUrl);
const expectedState = normalizeString(state.codex2apiOAuthState);
if (expectedState && expectedState !== callback.state) {
throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
}
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
const origin = new URL(codex2apiUrl).origin;
await addLog('步骤 10:正在向 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 addLog(`步骤 10${verifiedStatus}`, 'ok');
await completeStepFromBackground(10, {
localhostUrl: callback.url,
verifiedStatus,
});
}
async function executeSub2ApiStep10(state) {
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
@@ -161,6 +283,7 @@
return {
executeCpaStep10,
executeCodex2ApiStep10,
executeStep10,
executeSub2ApiStep10,
};
+2
View File
@@ -24,6 +24,8 @@
dom.rowSub2ApiPassword,
dom.rowSub2ApiGroup,
dom.rowSub2ApiDefaultProxy,
dom.rowCodex2ApiUrl,
dom.rowCodex2ApiAdminKey,
dom.rowCustomPassword,
dom.rowAccountRunHistoryHelperBaseUrl,
].filter(Boolean);
+11
View File
@@ -806,6 +806,17 @@ header {
flex-shrink: 0;
}
#row-codex2api-url .data-label,
#row-codex2api-admin-key .data-label {
width: 76px;
}
#row-codex2api-url .data-label {
font-size: 10px;
letter-spacing: 0.02em;
text-transform: none;
}
.data-value {
font-size: 13px;
color: var(--text-muted);
+11
View File
@@ -112,6 +112,7 @@
<select id="select-panel-mode" class="data-select">
<option value="cpa">CPA 面板</option>
<option value="sub2api">SUB2API</option>
<option value="codex2api">Codex2API</option>
</select>
</div>
<div id="contribution-mode-panel" class="contribution-mode-panel" hidden>
@@ -192,6 +193,16 @@
<input type="text" id="input-sub2api-default-proxy" class="data-input"
placeholder="留空则不使用代理;或填写代理名称 / ID" />
</div>
<div class="data-row" id="row-codex2api-url" style="display:none;">
<span class="data-label">Codex2API</span>
<input type="text" id="input-codex2api-url" class="data-input"
placeholder="http://localhost:8080/admin/accounts" />
</div>
<div class="data-row" id="row-codex2api-admin-key" style="display:none;">
<span class="data-label">管理密钥</span>
<input type="password" id="input-codex2api-admin-key" class="data-input"
placeholder="请输入 Codex2API Admin Secret" />
</div>
<div class="data-row" id="row-custom-password">
<span class="data-label">账户密码</span>
<div class="input-with-icon">
+34 -4
View File
@@ -94,6 +94,10 @@ const rowSub2ApiGroup = document.getElementById('row-sub2api-group');
const inputSub2ApiGroup = document.getElementById('input-sub2api-group');
const rowSub2ApiDefaultProxy = document.getElementById('row-sub2api-default-proxy');
const inputSub2ApiDefaultProxy = document.getElementById('input-sub2api-default-proxy');
const rowCodex2ApiUrl = document.getElementById('row-codex2api-url');
const inputCodex2ApiUrl = document.getElementById('input-codex2api-url');
const rowCodex2ApiAdminKey = document.getElementById('row-codex2api-admin-key');
const inputCodex2ApiAdminKey = document.getElementById('input-codex2api-admin-key');
const rowCustomPassword = document.getElementById('row-custom-password');
const selectMailProvider = document.getElementById('select-mail-provider');
const btnMailLogin = document.getElementById('btn-mail-login');
@@ -1513,6 +1517,8 @@ function collectSettingsPayload() {
sub2apiPassword: inputSub2ApiPassword.value,
sub2apiGroupName: inputSub2ApiGroup.value.trim(),
sub2apiDefaultProxyName: inputSub2ApiDefaultProxy.value.trim(),
codex2apiUrl: inputCodex2ApiUrl.value.trim(),
codex2apiAdminKey: inputCodex2ApiAdminKey.value.trim(),
...(contributionModeEnabled ? {} : {
customPassword: inputPassword.value,
}),
@@ -1894,6 +1900,8 @@ function applySettingsState(state) {
inputSub2ApiPassword.value = state?.sub2apiPassword || '';
inputSub2ApiGroup.value = state?.sub2apiGroupName || '';
inputSub2ApiDefaultProxy.value = state?.sub2apiDefaultProxyName || '';
inputCodex2ApiUrl.value = state?.codex2apiUrl || '';
inputCodex2ApiAdminKey.value = state?.codex2apiAdminKey || '';
const restoredMailProvider = isCustomMailProvider(state?.mailProvider)
|| [ICLOUD_PROVIDER, 'hotmail-api', GMAIL_PROVIDER, 'luckmail-api', '163', '163-vip', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim())
? String(state?.mailProvider || '163').trim()
@@ -2950,18 +2958,24 @@ async function saveCloudflareTempEmailDomainSettings(domains, activeDomain, opti
function updatePanelModeUI() {
const useSub2Api = selectPanelMode.value === 'sub2api';
rowVpsUrl.style.display = useSub2Api ? 'none' : '';
rowVpsPassword.style.display = useSub2Api ? 'none' : '';
rowLocalCpaStep9Mode.style.display = useSub2Api ? 'none' : '';
const useCodex2Api = selectPanelMode.value === 'codex2api';
const useCpa = !useSub2Api && !useCodex2Api;
rowVpsUrl.style.display = useCpa ? '' : 'none';
rowVpsPassword.style.display = useCpa ? '' : 'none';
rowLocalCpaStep9Mode.style.display = useCpa ? '' : 'none';
rowSub2ApiUrl.style.display = useSub2Api ? '' : 'none';
rowSub2ApiEmail.style.display = useSub2Api ? '' : 'none';
rowSub2ApiPassword.style.display = useSub2Api ? '' : 'none';
rowSub2ApiGroup.style.display = useSub2Api ? '' : 'none';
rowSub2ApiDefaultProxy.style.display = useSub2Api ? '' : 'none';
rowCodex2ApiUrl.style.display = useCodex2Api ? '' : 'none';
rowCodex2ApiAdminKey.style.display = useCodex2Api ? '' : 'none';
const step9Btn = document.querySelector('.step-btn[data-step-key="platform-verify"]');
if (step9Btn) {
step9Btn.textContent = useSub2Api ? 'SUB2API 回调验证' : 'CPA 回调验证';
step9Btn.textContent = useSub2Api
? 'SUB2API 回调验证'
: (useCodex2Api ? 'Codex2API 回调验证' : 'CPA 回调验证');
}
}
@@ -4364,6 +4378,22 @@ inputSub2ApiDefaultProxy.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
inputCodex2ApiUrl.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
inputCodex2ApiUrl.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
inputCodex2ApiAdminKey.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
inputCodex2ApiAdminKey.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
inputEmailPrefix.addEventListener('input', () => {
maybeClearGeneratedAliasAfterEmailPrefixChange().catch(() => { });
syncManagedAliasBaseEmailDraftFromInput();
@@ -50,6 +50,7 @@ function extractFunction(name) {
test('background account history settings are normalized independently from hotmail service mode', () => {
const bundle = [
extractFunction('normalizeCodex2ApiUrl'),
extractFunction('normalizeHotmailLocalBaseUrl'),
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
extractFunction('normalizeVerificationResendCount'),
@@ -60,6 +61,7 @@ test('background account history settings are normalized independently from hotm
const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_URL;
const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_SUB2API_PROXY_NAME = '';
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
@@ -70,7 +72,7 @@ const PERSISTED_SETTING_DEFAULTS = {
autoStepDelaySeconds: null,
mailProvider: '163',
};
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : (value === 'codex2api' ? 'codex2api' : 'cpa'); }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
@@ -118,4 +120,16 @@ return {
api.normalizePersistentSettingValue('sub2apiDefaultProxyName', ' proxy-a '),
'proxy-a'
);
assert.equal(
api.normalizePersistentSettingValue('codex2apiUrl', 'localhost:8080/admin'),
'http://localhost:8080/admin/accounts'
);
assert.equal(
api.normalizePersistentSettingValue('codex2apiUrl', 'https://codex-admin.example.com/'),
'https://codex-admin.example.com/admin/accounts'
);
assert.equal(
api.normalizePersistentSettingValue('codex2apiAdminKey', ' secret-key '),
'secret-key'
);
});
+2 -2
View File
@@ -536,7 +536,7 @@ return { refreshOAuthUrlBeforeStep6 };
delete globalThis.LOG_PREFIX;
});
test('refreshOAuthUrlBeforeStep6 logs the normal CPA/SUB2API path explicitly when contributionMode=false', async () => {
test('refreshOAuthUrlBeforeStep6 logs the normal CPA/SUB2API/Codex2API path explicitly when contributionMode=false', async () => {
const bundle = extractFunction(backgroundSource, 'refreshOAuthUrlBeforeStep6');
const calls = [];
@@ -574,7 +574,7 @@ return { refreshOAuthUrlBeforeStep6 };
assert.equal(oauthUrl, 'https://panel.example.com/oauth');
assert.deepStrictEqual(calls, [
{ type: 'log', message: '步骤 7contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...' },
{ type: 'log', message: '步骤 7contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...' },
{ type: 'panel' },
{
type: 'step',
@@ -15,3 +15,23 @@ test('navigation utils module exposes a factory', () => {
assert.equal(typeof api?.createNavigationUtils, 'function');
});
test('navigation utils support codex2api mode and url normalization', () => {
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: 'https://sub.example.com/admin/accounts',
normalizeLocalCpaStep9Mode: (value) => value,
});
assert.equal(utils.normalizeCodex2ApiUrl('localhost:8080/admin'), 'http://localhost:8080/admin/accounts');
assert.equal(
utils.normalizeCodex2ApiUrl('https://codex-admin.example.com/'),
'https://codex-admin.example.com/admin/accounts'
);
assert.equal(utils.getPanelMode({ panelMode: 'codex2api' }), 'codex2api');
assert.equal(utils.getPanelModeLabel('codex2api'), 'Codex2API');
});
@@ -21,3 +21,53 @@ test('panel bridge requests oauth url with step 7 log label payload', () => {
assert.match(source, /logStep:\s*7/);
assert.doesNotMatch(source, /logStep:\s*6/);
});
test('panel bridge can request codex2api oauth url via protocol', async () => {
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
assert.equal(url, 'http://localhost:8080/api/admin/oauth/generate-auth-url');
assert.equal(options.method, 'POST');
assert.equal(options.headers['X-Admin-Key'], 'admin-secret');
return {
ok: true,
json: async () => ({
auth_url: 'https://auth.openai.com/authorize?state=oauth-state',
session_id: 'session-123',
}),
};
};
try {
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
const bridge = api.createPanelBridge({
addLog: async () => {},
chrome: {},
closeConflictingTabsForSource: async () => {},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'codex2api',
normalizeCodex2ApiUrl: (value) => value ? `http://${value.replace(/^https?:\/\//, '')}`.replace(/\/admin$/, '/admin/accounts') : 'http://localhost:8080/admin/accounts',
normalizeSub2ApiUrl: (value) => value,
rememberSourceLastUrl: async () => {},
sendToContentScript: async () => ({}),
sendToContentScriptResilient: async () => ({}),
waitForTabUrlFamily: async () => null,
DEFAULT_SUB2API_GROUP_NAME: 'codex',
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
});
const result = await bridge.requestOAuthUrlFromPanel({
panelMode: 'codex2api',
codex2apiUrl: 'localhost:8080/admin',
codex2apiAdminKey: 'admin-secret',
}, { logLabel: '步骤 7' });
assert.deepStrictEqual(result, {
oauthUrl: 'https://auth.openai.com/authorize?state=oauth-state',
codex2apiSessionId: 'session-123',
codex2apiOAuthState: 'oauth-state',
});
} finally {
globalThis.fetch = originalFetch;
}
});
@@ -0,0 +1,81 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const test = require('node:test');
test('platform verify module supports codex2api protocol callback exchange', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
assert.equal(url, 'http://localhost:8080/api/admin/oauth/exchange-code');
assert.equal(options.method, 'POST');
assert.equal(options.headers['X-Admin-Key'], 'admin-secret');
assert.deepStrictEqual(JSON.parse(options.body), {
session_id: 'session-123',
code: 'callback-code',
state: 'oauth-state',
});
return {
ok: true,
json: async () => ({
message: 'OAuth 账号 flow@example.com 添加成功',
id: 42,
email: 'flow@example.com',
plan_type: 'pro',
}),
};
};
try {
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const completed = [];
const logs = [];
const executor = api.createStep10Executor({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
chrome: {},
closeConflictingTabsForSource: async () => {},
completeStepFromBackground: async (step, payload) => {
completed.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'codex2api',
getTabId: async () => 0,
isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
isTabAlive: async () => false,
normalizeCodex2ApiUrl: () => 'http://localhost:8080/admin/accounts',
normalizeSub2ApiUrl: (value) => value,
rememberSourceLastUrl: async () => {},
reuseOrCreateTab: async () => 0,
sendToContentScript: async () => ({}),
sendToContentScriptResilient: async () => ({}),
shouldBypassStep9ForLocalCpa: () => false,
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
});
await executor.executeStep10({
panelMode: 'codex2api',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
codex2apiUrl: 'http://localhost:8080/admin/accounts',
codex2apiAdminKey: 'admin-secret',
codex2apiSessionId: 'session-123',
codex2apiOAuthState: 'oauth-state',
});
assert.deepStrictEqual(logs, [
{ message: '步骤 10:正在向 Codex2API 提交回调并创建账号...', level: 'info' },
{ message: '步骤 10OAuth 账号 flow@example.com 添加成功', level: 'ok' },
]);
assert.deepStrictEqual(completed, [
{
step: 10,
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
verifiedStatus: 'OAuth 账号 flow@example.com 添加成功',
},
},
]);
} finally {
globalThis.fetch = originalFetch;
}
});
@@ -177,3 +177,88 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as
},
]);
});
test('step 7 stops immediately when management secret is missing', 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 = {
refreshCalls: 0,
sendCalls: 0,
logs: [],
};
const executor = api.createStep7Executor({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
completeStepFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => {
events.refreshCalls += 1;
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
},
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => {
events.sendCalls += 1;
return { step6Outcome: 'success' };
},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await assert.rejects(
() => executor.executeStep7({ email: 'user@example.com', password: 'secret' }),
/管理密钥/
);
assert.equal(events.refreshCalls, 1);
assert.equal(events.sendCalls, 0);
assert.ok(events.logs.some(({ message }) => /管理密钥缺失或错误不再重试当前流程停止/.test(message)));
assert.ok(!events.logs.some(({ message }) => /准备重试/.test(message)));
});
test('step 7 stops immediately when management secret is invalid', 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 = {
refreshCalls: 0,
logs: [],
};
const executor = api.createStep7Executor({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
completeStepFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => {
events.refreshCalls += 1;
throw new Error('Codex2API 请求失败(HTTP 401)。X-Admin-Key 无效或未授权。');
},
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => ({ step6Outcome: 'success' }),
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await assert.rejects(
() => executor.executeStep7({ email: 'user@example.com', password: 'secret' }),
/401|未授权|无效/
);
assert.equal(events.refreshCalls, 1);
assert.ok(events.logs.some(({ message }) => /管理密钥缺失或错误不再重试当前流程停止/.test(message)));
assert.ok(!events.logs.some(({ message }) => /准备重试/.test(message)));
});
@@ -135,6 +135,8 @@ const inputSub2ApiEmail = { value: 'user@example.com' };
const inputSub2ApiPassword = { value: 'sub-secret' };
const inputSub2ApiGroup = { value: ' codex ' };
const inputSub2ApiDefaultProxy = { value: ' proxy-a ' };
const inputCodex2ApiUrl = { value: 'http://localhost:8080/admin/accounts' };
const inputCodex2ApiAdminKey = { value: 'codex-admin-secret' };
const inputPassword = { value: 'Secret123!' };
const selectMailProvider = { value: '163' };
const selectEmailGenerator = { value: 'duck' };
@@ -198,6 +200,8 @@ return {
assert.equal(normalPayload.customPassword, 'Secret123!');
assert.equal(normalPayload.accountRunHistoryTextEnabled, true);
assert.equal(normalPayload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(normalPayload.codex2apiUrl, 'http://localhost:8080/admin/accounts');
assert.equal(normalPayload.codex2apiAdminKey, 'codex-admin-secret');
assert.equal(normalPayload.cloudflareTempEmailUseRandomSubdomain, true);
});
@@ -267,6 +271,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
rowSub2ApiGroup: createElement(),
rowSub2ApiPassword: createElement(),
rowSub2ApiUrl: createElement(),
rowCodex2ApiUrl: createElement(),
rowCodex2ApiAdminKey: createElement(),
rowVpsPassword: createElement(),
rowVpsUrl: createElement(),
selectPanelMode: createElement({ value: 'sub2api' }),
@@ -417,6 +423,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
assert.equal(dom.contributionModeSummary.textContent.length > 0, true);
assert.equal(dom.btnContributionMode.classList.contains('is-active'), true);
assert.equal(dom.rowVpsUrl.classList.contains('is-contribution-hidden'), true);
assert.equal(dom.rowCodex2ApiUrl.classList.contains('is-contribution-hidden'), true);
assert.equal(dom.rowCodex2ApiAdminKey.classList.contains('is-contribution-hidden'), true);
assert.ok(closeConfigMenuCount >= 1);
assert.ok(closeAccountRecordsCount >= 1);
assert.ok(updatePanelModeCount >= 1);
@@ -166,6 +166,8 @@ const inputSub2ApiEmail = { value: '' };
const inputSub2ApiPassword = { value: '' };
const inputSub2ApiGroup = { value: '' };
const inputSub2ApiDefaultProxy = { value: '' };
const inputCodex2ApiUrl = { value: '' };
const inputCodex2ApiAdminKey = { value: '' };
const inputPassword = { value: '' };
const selectMailProvider = { value: '2925' };
const selectEmailGenerator = { value: 'duck' };
+24 -9
View File
@@ -22,7 +22,7 @@
- 刷新 OAuth 链接并登录
- 轮询登录验证码
- 自动确认 OAuth 同意页
- 把 localhost 回调提交到 CPA 或 SUB2API
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
## 2. 核心运行参与者
@@ -152,6 +152,7 @@
保存持久配置与账号运行历史:
- CPA / SUB2API 配置
- Codex2API 配置
- 邮箱 provider 配置
- Hotmail 账号池
- 2925 账号池
@@ -167,7 +168,7 @@
注意:
- `contributionMode` 不属于持久配置,也不参与导入/导出;它只存在于运行态
- `panelMode` 仍然只表示 `cpa | sub2api` 来源,不能把贡献模式实现成新的来源枚举
- `panelMode` 当前表示 `cpa | sub2api | codex2api` 三个来源;贡献模式仍不是新的来源枚举
当启用了独立的账号运行历史本地同步配置时,账号运行历史会通过 [scripts/hotmail_helper.py](c:/Users/projectf/Downloads/codex注册扩展/scripts/hotmail_helper.py) 整体同步写入 `data/account-run-history.json` 快照文件,便于开发者直接查看完整记录。
这条配置链路独立于 `mailProvider` 和 Hotmail 的接码模式。
@@ -190,10 +191,10 @@
1. 用户点击顶部 `贡献`
2. sidepanel 直接通过 `SET_CONTRIBUTION_MODE` 切换运行态,不再弹确认窗口
3. 进入贡献模式后会强制 `panelMode = cpa`,并临时清空运行态 `customPassword`、禁用运行态账号记录快照同步
4. sidepanel 隐藏 CPA 管理地址、管理密钥、SUB2API 配置、自定义密码、本地同步等普通模式配置,并禁用来源选择、配置菜单和记录入口
4. sidepanel 隐藏 CPA 管理地址、管理密钥、SUB2API 配置、Codex2API 配置、自定义密码、本地同步等普通模式配置,并禁用来源选择、配置菜单和记录入口
5. 用户点击 `开始贡献` 后,不再单独走一条旁路 OAuth 流程,而是直接复用顶部 `自动` 的同一套主自动流
6. 步骤 1~6 仍按原来的注册自动化执行
7. 当主流程进入步骤 7 时,后台改为调用公开接口 `POST https://apikey.qzz.io/oauth/api/start` 申请贡献登录地址,而不是去 CPA / SUB2API 面板刷新 OAuth
7. 当主流程进入步骤 7 时,后台改为调用公开接口 `POST https://apikey.qzz.io/oauth/api/start` 申请贡献登录地址,而不是去 CPA / SUB2API / Codex2API 来源刷新 OAuth
8. 步骤 7 拿到 `session_id / auth_url / state` 后,继续沿用原有登录链路进入授权页
9. 步骤 9 仍负责捕获 localhost callback;贡献模式下后台也会持续监听导航变化,必要时提前兼容处理 callback
10. 步骤 10 在贡献模式下不再打开 CPA 管理页,而是围绕公开贡献会话做 callback 提交兼容和最终状态确认;当状态进入 `auto_approved / auto_rejected / manual_review_required / expired / error` 时结束
@@ -380,7 +381,10 @@
流程:
1. 通过 CPA / SUB2API 刷新 OAuth 地址
1. 按当前来源刷新 OAuth 地址
- CPA:打开管理页并读取 OAuth 地址
- SUB2API:打开后台并生成 OAuth 地址
- Codex2API:直接调用后台协议 `/api/admin/oauth/generate-auth-url`
2. 打开最新 OAuth 链接
3. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录
4. 确保真正进入验证码页
@@ -390,7 +394,7 @@
贡献模式补充:
- 贡献模式下,步骤 7 不再从 CPA / SUB2API 面板刷新 OAuth,而是直接调用公开贡献接口 `/oauth/api/start`
- 贡献模式下,步骤 7 不再从 CPA / SUB2API / Codex2API 来源刷新 OAuth,而是直接调用公开贡献接口 `/oauth/api/start`
- 贡献接口返回的 `auth_url` 会写回运行态 `oauthUrl`,后续步骤 7 / 8 / 9 继续复用现有授权链路
### Step 8
@@ -437,15 +441,22 @@
流程:
1. 校验 localhost callback 是否有效
2. 判断是 CPA 还是 SUB2API
3. 打开相应后台
4. 提交回调地址
2. 判断当前来源是 CPA、SUB2API 还是 Codex2API
3. CPA / SUB2API 打开相应后台;Codex2API 直接走协议分支,不打开后台页面
4. 提交回调地址或等价的授权码交换请求
5. 仅当出现精确成功徽标,且该徽标不是红色/错误态、页面上也没有同时可见的失败提示时,才判定成功
6. 识别 `认证失败:*``认证失败: timeout of 30000ms exceeded``回调 URL 提交失败: oauth flow is not pending` 等失败提示并立即报错
7. 完成平台侧验证
8. 追加账号运行历史成功记录
9. 做成功后的清理与标记
Codex2API 补充:
- 步骤 7 直接调用 `POST /api/admin/oauth/generate-auth-url` 获取 `auth_url / session_id`
- 授权页仍沿用现有 OpenAI 登录、验证码、OAuth 同意页与 localhost callback 主链
- 步骤 10 直接调用 `POST /api/admin/oauth/exchange-code`,用 callback 中的 `code / state` 完成账号创建
- Codex2API 这条来源不新增 panel content script,也不依赖“添加账号 -> OAuth 授权 -> 生成授权链接”页面按钮 DOM
贡献模式补充:
- 贡献模式下,步骤 10 不再打开 CPA 管理页
@@ -679,6 +690,10 @@
5. Step 4 / 7 的验证码流
6. 成功收尾逻辑
如果来源本身提供稳定协议接口,还必须额外判断:
7. 是否可以不打开来源后台页面,直接把步骤 7 / 10 收敛到协议分支
### 新增配置项
必须同时检查:
+9 -1
View File
@@ -82,6 +82,14 @@
5. 是否需要成功收尾逻辑
6. 是否需要 README 与完整链路文档更新
补充约定:
- 如果新增来源本身已经提供稳定的后台协议接口,可以直接走协议分支接入:
- 步骤 7 通过 `background/panel-bridge.js` 生成 `auth_url`
- 步骤 10 通过 `background/steps/platform-verify.js` 直接提交 localhost callback
- 这类来源优先复用现有 OpenAI 授权页与 localhost callback 主链,不要为了“看起来统一”再额外新增一套页面 DOM 自动点击内容脚本。
- 只有当目标来源没有可用协议接口、必须依赖后台页面按钮时,才新增对应的 panel content script。
### 3.2.1 共享别名邮箱逻辑补充
当 Gmail / 2925 这类“既影响注册邮箱生成,又影响 sidepanel 表单行为”的 provider 发生变化时,必须优先检查是否应落入共享层,而不是继续把规则分散写在:
@@ -119,7 +127,7 @@
当前约定示例:
- `contributionMode` 是 sidepanel 的运行态 UI 模式,不是新的 `panelMode`
- `panelMode` 仍然只允许 `cpa | sub2api`
- `panelMode` 当前允许 `cpa | sub2api | codex2api`
- 运行态模式不能混进 `PERSISTED_SETTING_DEFAULTS`
- 运行态模式不能混进配置导入/导出
- 如果运行态模式会临时覆盖某些持久配置的显示值,必须同时处理好“退出模式后恢复”和“自动保存不能误覆盖原配置”这两个问题
+7 -6
View File
@@ -48,8 +48,8 @@
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息。
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API 地址、步骤跳转相关判断。
- `background/panel-bridge.js`CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
- `background/panel-bridge.js`来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
@@ -64,7 +64,7 @@
- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
- `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
- `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证。
- `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证,以及 Codex2API 的协议式 callback code/state 交换
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与提交后落地页分支判断。
@@ -120,10 +120,10 @@
- `sidepanel/mail-2925-manager.js`:侧边栏 2925 账号池管理器,负责 2925 账号的新增、导入、切换、手动登录、启停、清冷却与删除。
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行的禁用与隐藏。
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行(包括 Codex2API 配置)的禁用与隐藏。
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式。
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免 receive 模式把账号池开关一起隐藏;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显示并同时服务于 provide / receive 两种模式;Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免 receive 模式把账号池开关一起隐藏;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密钥配置行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显示并同时服务于 provide / receive 两种模式;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑;Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。
## `tests/`
@@ -151,6 +151,7 @@
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑。
- `tests/background-navigation-utils-module.test.js`:测试导航工具模块已接入且导出工厂。
- `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。
- `tests/background-platform-verify-codex2api.test.js`:测试 Codex2API 新来源在步骤 10 走协议式 callback 交换,不依赖后台页面注入。
- `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。
- `tests/background-skip-step-linking.test.js`:测试手动跳过步骤 1 时,会级联跳过步骤 2~5,并仅跳过其中未完成且未运行的步骤。
- `tests/background-signup-step2-branching.test.js`:测试在 Gmail / 2925 模式下,已有兼容别名邮箱时应直接复用,不应再次重生成。