贡献功能开发

This commit is contained in:
QLHazyCoder
2026-04-18 23:51:56 +08:00
parent f13b90b7db
commit c994d7ae12
14 changed files with 2658 additions and 19 deletions
+3
View File
@@ -298,6 +298,9 @@
}
function shouldSyncAccountRunHistorySnapshot(state = {}) {
if (Boolean(state.contributionMode)) {
return false;
}
if (!Boolean(state.accountRunHistoryTextEnabled)) {
return false;
}
+722
View File
@@ -0,0 +1,722 @@
(function attachBackgroundContributionOAuth(root, factory) {
root.MultiPageBackgroundContributionOAuth = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundContributionOAuthModule() {
const API_BASE_URL = 'https://apikey.qzz.io/oauth/api';
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']);
const CALLBACK_FINAL_STATUSES = new Set(['submitted', 'not_required']);
const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']);
const RUNTIME_DEFAULTS = {
contributionMode: false,
contributionSessionId: '',
contributionAuthUrl: '',
contributionAuthState: '',
contributionCallbackUrl: '',
contributionStatus: '',
contributionStatusMessage: '',
contributionLastPollAt: 0,
contributionCallbackStatus: 'idle',
contributionCallbackMessage: '',
contributionAuthOpenedAt: 0,
contributionAuthTabId: 0,
};
const RUNTIME_KEYS = Object.keys(RUNTIME_DEFAULTS);
function createContributionOAuthManager(deps = {}) {
const {
addLog,
broadcastDataUpdate,
chrome,
closeLocalhostCallbackTabs,
getState,
setState,
} = deps;
let listenersBound = false;
function normalizeString(value = '') {
return String(value || '').trim();
}
function normalizePositiveInteger(value, fallback = 0) {
const numeric = Math.floor(Number(value) || 0);
return numeric > 0 ? numeric : fallback;
}
function normalizeContributionStatus(value = '') {
const normalized = normalizeString(value).toLowerCase();
switch (normalized) {
case 'started':
return 'started';
case 'waiting':
case 'wait':
return 'waiting';
case 'processing':
return 'processing';
case 'auto_approved':
case 'approved':
return 'auto_approved';
case 'auto_rejected':
case 'rejected':
return 'auto_rejected';
case 'manual_review_required':
case 'manual_review':
return 'manual_review_required';
case 'expired':
case 'timeout':
return 'expired';
case 'error':
case 'failed':
return 'error';
default:
return '';
}
}
function normalizeContributionCallbackStatus(value = '') {
const normalized = normalizeString(value).toLowerCase();
switch (normalized) {
case 'idle':
return 'idle';
case 'waiting':
case 'pending':
return 'waiting';
case 'captured':
return 'captured';
case 'submitting':
case 'processing':
return 'submitting';
case 'submitted':
case 'success':
case 'done':
return 'submitted';
case 'not_required':
case 'no_need':
case 'no_callback_required':
return 'not_required';
case 'failed':
case 'error':
return 'failed';
default:
return '';
}
}
function isContributionFinalStatus(status = '') {
return FINAL_STATUSES.has(normalizeContributionStatus(status));
}
function getStatusLabel(status = '') {
switch (normalizeContributionStatus(status)) {
case 'started':
return '已生成登录地址';
case 'waiting':
return '等待授权完成';
case 'processing':
return '已授权,正在自动审核';
case 'auto_approved':
return '贡献成功,已自动导入';
case 'auto_rejected':
return '贡献未通过自动审核';
case 'manual_review_required':
return '已提交,等待人工处理';
case 'expired':
return '贡献会话已超时';
case 'error':
return '贡献流程失败';
default:
return '等待开始贡献';
}
}
function getCallbackLabel(status = '') {
switch (normalizeContributionCallbackStatus(status)) {
case 'waiting':
case 'idle':
return '等待回调';
case 'captured':
return '已捕获回调地址';
case 'submitting':
return '正在提交回调';
case 'submitted':
return '已提交回调';
case 'not_required':
return '当前流程无需手动回调';
case 'failed':
return '回调提交失败';
default:
return '等待回调';
}
}
function unwrapPayload(payload) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
return {};
}
if (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
return { ...payload.data, ...payload };
}
return payload;
}
function getErrorMessage(payload, responseStatus = 500) {
const details = [
payload?.message,
payload?.detail,
payload?.error,
payload?.reason,
]
.map((item) => normalizeString(item))
.find(Boolean);
if (details) {
return details;
}
return `贡献服务请求失败(HTTP ${responseStatus})。`;
}
async function fetchContributionJson(endpoint, options = {}) {
const controller = new AbortController();
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 15000));
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: options.method || 'GET',
headers: {
Accept: 'application/json',
...(options.body ? { 'Content-Type': 'application/json' } : {}),
},
body: options.body ? JSON.stringify(options.body) : undefined,
signal: controller.signal,
});
let rawPayload = {};
try {
rawPayload = await response.json();
} catch {
rawPayload = {};
}
const payload = unwrapPayload(rawPayload);
if (!response.ok || payload.ok === false) {
const error = new Error(getErrorMessage(payload, response.status));
error.payload = payload;
error.responseStatus = response.status;
throw error;
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error('贡献服务请求超时,请稍后重试。');
}
throw error;
} finally {
clearTimeout(timer);
}
}
function pickContributionState(state = {}) {
const picked = {};
for (const key of RUNTIME_KEYS) {
picked[key] = state[key] !== undefined ? state[key] : RUNTIME_DEFAULTS[key];
}
return picked;
}
async function applyRuntimeUpdates(updates = {}) {
if (!updates || typeof updates !== 'object' || Array.isArray(updates) || Object.keys(updates).length === 0) {
return getState();
}
await setState(updates);
broadcastDataUpdate(updates);
return getState();
}
function extractAuthStateFromUrl(authUrl = '') {
try {
return new URL(authUrl).searchParams.get('state') || '';
} catch {
return '';
}
}
function buildNickname(state = {}, preferredNickname = '') {
const nickname = normalizeString(preferredNickname)
|| normalizeString(state.email)
|| normalizeString(state.contributionNickname);
return nickname || 'codex-extension-user';
}
function buildStatusMessage(status, payload = {}) {
const label = getStatusLabel(status);
const details = [
payload.status_message,
payload.statusMessage,
payload.message,
payload.detail,
payload.reason,
]
.map((item) => normalizeString(item))
.find(Boolean);
if (!details || details === label) {
return label;
}
return `${label}${details}`;
}
function buildCallbackMessage(status, payload = {}) {
const label = getCallbackLabel(status);
const details = [
payload.callback_message,
payload.callbackMessage,
payload.message,
payload.detail,
payload.reason,
]
.map((item) => normalizeString(item))
.find(Boolean);
if (!details || details === label) {
return label;
}
return `${label}${details}`;
}
function looksLikeNoNeedCallback(payload = {}) {
const combined = [
payload.message,
payload.detail,
payload.reason,
payload.error,
]
.map((item) => normalizeString(item).toLowerCase())
.filter(Boolean)
.join(' ');
return Boolean(payload.callback_required === false
|| payload.callbackRequired === false
|| /无需手动|鏃犻渶鎵嬪姩|not required|already enabled/i.test(combined));
}
function deriveCallbackState(payload = {}, state = {}) {
const existingStatus = normalizeContributionCallbackStatus(state.contributionCallbackStatus);
const callbackUrl = normalizeString(
payload.callback_url
|| payload.callbackUrl
|| state.contributionCallbackUrl
);
const explicitStatus = normalizeContributionCallbackStatus(
payload.callback_status
|| payload.callbackStatus
);
if (explicitStatus) {
return {
status: explicitStatus,
message: buildCallbackMessage(explicitStatus, payload),
callbackUrl,
};
}
if (payload.callback_submitted === true || payload.callbackSubmitted === true) {
return {
status: 'submitted',
message: buildCallbackMessage('submitted', payload),
callbackUrl,
};
}
if (looksLikeNoNeedCallback(payload)) {
return {
status: 'not_required',
message: buildCallbackMessage('not_required', payload),
callbackUrl,
};
}
if (callbackUrl) {
return {
status: CALLBACK_FINAL_STATUSES.has(existingStatus) ? existingStatus : 'captured',
message: buildCallbackMessage(CALLBACK_FINAL_STATUSES.has(existingStatus) ? existingStatus : 'captured', payload),
callbackUrl,
};
}
if (CALLBACK_FINAL_STATUSES.has(existingStatus) || existingStatus === 'failed') {
return {
status: existingStatus,
message: normalizeString(state.contributionCallbackMessage) || buildCallbackMessage(existingStatus),
callbackUrl: normalizeString(state.contributionCallbackUrl),
};
}
return {
status: 'waiting',
message: buildCallbackMessage('waiting', payload),
callbackUrl: '',
};
}
function isContributionCallbackUrl(rawUrl, state = {}) {
const urlText = normalizeString(rawUrl);
if (!urlText) {
return false;
}
let parsed;
try {
parsed = new URL(urlText);
} catch {
return false;
}
if (!['http:', 'https:'].includes(parsed.protocol)) {
return false;
}
const code = normalizeString(parsed.searchParams.get('code'));
const authState = normalizeString(parsed.searchParams.get('state'));
if (!code || !authState) {
return false;
}
const hostLooksLocal = ['localhost', '127.0.0.1'].includes(parsed.hostname);
const pathLooksLikeCallback = /callback/i.test(parsed.pathname || '');
if (!hostLooksLocal && !pathLooksLikeCallback) {
return false;
}
const expectedState = normalizeString(state.contributionAuthState);
return !expectedState || expectedState === authState;
}
async function openContributionAuthUrl(authUrl, options = {}) {
const normalizedUrl = normalizeString(authUrl);
if (!normalizedUrl) {
throw new Error('贡献服务未返回有效的登录地址。');
}
const currentState = options.stateOverride || await getState();
const preferredTabId = normalizePositiveInteger(options.tabId || currentState.contributionAuthTabId, 0);
let tab = null;
if (preferredTabId) {
tab = await chrome.tabs.update(preferredTabId, {
url: normalizedUrl,
active: true,
}).catch(() => null);
}
if (!tab) {
tab = await chrome.tabs.create({ url: normalizedUrl, active: true });
}
await applyRuntimeUpdates({
contributionAuthUrl: normalizedUrl,
contributionAuthOpenedAt: Date.now(),
contributionAuthTabId: normalizePositiveInteger(tab?.id, 0),
});
return tab;
}
async function fetchContributionResult(sessionId) {
try {
return await fetchContributionJson(`/result?session_id=${encodeURIComponent(sessionId)}`);
} catch (error) {
if (typeof addLog === 'function') {
await addLog(`贡献模式:获取最终结果失败:${error.message}`, 'warn');
}
return null;
}
}
async function submitContributionCallback(callbackUrl, options = {}) {
const currentState = options.stateOverride || await getState();
const sessionId = normalizeString(currentState.contributionSessionId);
const normalizedUrl = normalizeString(callbackUrl);
if (!sessionId || !normalizedUrl) {
return currentState;
}
const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
if (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting') {
return currentState;
}
await applyRuntimeUpdates({
contributionCallbackUrl: normalizedUrl,
contributionCallbackStatus: 'submitting',
contributionCallbackMessage: buildCallbackMessage('submitting'),
});
try {
const payload = await fetchContributionJson('/submit-callback', {
method: 'POST',
body: {
session_id: sessionId,
callback_url: normalizedUrl,
},
});
const nextStatus = looksLikeNoNeedCallback(payload) ? 'not_required' : 'submitted';
await applyRuntimeUpdates({
contributionCallbackUrl: normalizedUrl,
contributionCallbackStatus: nextStatus,
contributionCallbackMessage: buildCallbackMessage(nextStatus, payload),
});
if (typeof closeLocalhostCallbackTabs === 'function') {
await closeLocalhostCallbackTabs(normalizedUrl).catch(() => {});
}
return await pollContributionStatus({ reason: options.reason || 'submit_callback' });
} catch (error) {
if (looksLikeNoNeedCallback(error?.payload || {})) {
await applyRuntimeUpdates({
contributionCallbackUrl: normalizedUrl,
contributionCallbackStatus: 'not_required',
contributionCallbackMessage: buildCallbackMessage('not_required', error.payload || {}),
});
if (typeof closeLocalhostCallbackTabs === 'function') {
await closeLocalhostCallbackTabs(normalizedUrl).catch(() => {});
}
return pollContributionStatus({ reason: options.reason || 'submit_callback_not_required' }).catch(() => getState());
}
await applyRuntimeUpdates({
contributionCallbackUrl: normalizedUrl,
contributionCallbackStatus: 'failed',
contributionCallbackMessage: `回调提交失败:${error.message}`,
});
if (typeof addLog === 'function') {
await addLog(`贡献模式:回调提交失败:${error.message}`, 'warn');
}
throw error;
}
}
async function handleCapturedCallback(rawUrl, metadata = {}) {
const currentState = await getState();
if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) {
return currentState;
}
if (!isContributionCallbackUrl(rawUrl, currentState)) {
return currentState;
}
const normalizedUrl = normalizeString(rawUrl);
const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
if (
normalizedUrl
&& normalizeString(currentState.contributionCallbackUrl) === normalizedUrl
&& (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting')
) {
return currentState;
}
await applyRuntimeUpdates({
contributionCallbackUrl: normalizedUrl,
contributionCallbackStatus: 'captured',
contributionCallbackMessage: buildCallbackMessage('captured'),
});
if (typeof addLog === 'function') {
await addLog(`贡献模式:已捕获回调地址(${metadata.source || 'unknown'})。`, 'info');
}
try {
return await submitContributionCallback(normalizedUrl, {
reason: metadata.source || 'navigation',
stateOverride: await getState(),
});
} catch {
return getState();
}
}
async function pollContributionStatus(options = {}) {
const currentState = options.stateOverride || await getState();
const sessionId = normalizeString(currentState.contributionSessionId);
if (!sessionId) {
return currentState;
}
const payload = await fetchContributionJson(`/status?session_id=${encodeURIComponent(sessionId)}`);
const nextStatus = normalizeContributionStatus(payload.status || payload.state || payload.phase) || currentState.contributionStatus || 'waiting';
let finalPayload = null;
if (isContributionFinalStatus(nextStatus)) {
finalPayload = await fetchContributionResult(sessionId);
}
const mergedPayload = finalPayload ? { ...payload, ...finalPayload } : payload;
const normalizedStatus = normalizeContributionStatus(mergedPayload.status || mergedPayload.state || mergedPayload.phase) || nextStatus;
const callbackState = deriveCallbackState(mergedPayload, currentState);
const updates = {
contributionLastPollAt: Date.now(),
contributionStatus: normalizedStatus,
contributionStatusMessage: buildStatusMessage(normalizedStatus, mergedPayload),
contributionCallbackUrl: callbackState.callbackUrl,
contributionCallbackStatus: callbackState.status,
contributionCallbackMessage: callbackState.message,
};
const authUrl = normalizeString(mergedPayload.auth_url || mergedPayload.authUrl);
if (authUrl) {
updates.contributionAuthUrl = authUrl;
}
const authState = normalizeString(mergedPayload.state || mergedPayload.auth_state || mergedPayload.authState)
|| (authUrl ? extractAuthStateFromUrl(authUrl) : '');
if (authState) {
updates.contributionAuthState = authState;
}
await applyRuntimeUpdates(updates);
const nextState = await getState();
if (
normalizeString(nextState.contributionCallbackUrl)
&& CALLBACK_WAITING_STATUSES.has(normalizeContributionCallbackStatus(nextState.contributionCallbackStatus))
) {
try {
return await submitContributionCallback(nextState.contributionCallbackUrl, {
reason: options.reason || 'status_poll',
stateOverride: nextState,
});
} catch {
return getState();
}
}
return nextState;
}
async function startContributionFlow(options = {}) {
const currentState = options.stateOverride || await getState();
const shouldOpenAuthTab = options.openAuthTab !== false;
if (!currentState.contributionMode) {
throw new Error('请先进入贡献模式。');
}
const currentSessionId = normalizeString(currentState.contributionSessionId);
const currentStatus = normalizeContributionStatus(currentState.contributionStatus);
if (currentSessionId && ACTIVE_STATUSES.has(currentStatus)) {
if (normalizeString(currentState.contributionAuthUrl)) {
if (shouldOpenAuthTab) {
await openContributionAuthUrl(currentState.contributionAuthUrl, {
stateOverride: currentState,
}).catch(() => null);
}
}
return pollContributionStatus({ reason: 'resume_existing' });
}
const payload = await fetchContributionJson('/start', {
method: 'POST',
body: {
nickname: buildNickname(currentState, options.nickname),
source: 'cpa',
channel: 'codex-extension',
},
});
const sessionId = normalizeString(payload.session_id || payload.sessionId);
const authUrl = normalizeString(payload.auth_url || payload.authUrl);
const authState = normalizeString(payload.state || payload.auth_state || payload.authState) || extractAuthStateFromUrl(authUrl);
if (!sessionId || !authUrl) {
throw new Error('贡献服务未返回有效的 session_id 或 auth_url。');
}
await applyRuntimeUpdates({
contributionSessionId: sessionId,
contributionAuthUrl: authUrl,
contributionAuthState: authState,
contributionCallbackUrl: '',
contributionStatus: normalizeContributionStatus(payload.status) || 'started',
contributionStatusMessage: buildStatusMessage(normalizeContributionStatus(payload.status) || 'started', payload),
contributionLastPollAt: 0,
contributionCallbackStatus: 'waiting',
contributionCallbackMessage: buildCallbackMessage('waiting'),
contributionAuthOpenedAt: 0,
contributionAuthTabId: 0,
});
if (shouldOpenAuthTab) {
await openContributionAuthUrl(authUrl);
}
return pollContributionStatus({ reason: 'after_start' });
}
function onNavigationEvent(details = {}, source) {
if (details?.frameId !== undefined && Number(details.frameId) !== 0) {
return;
}
handleCapturedCallback(details?.url || '', {
source,
tabId: normalizePositiveInteger(details?.tabId, 0),
}).catch(() => {});
}
function onTabUpdated(tabId, changeInfo, tab) {
const candidateUrl = normalizeString(changeInfo?.url || tab?.url);
if (!candidateUrl) {
return;
}
handleCapturedCallback(candidateUrl, {
source: 'tabs.onUpdated',
tabId: normalizePositiveInteger(tabId, 0),
}).catch(() => {});
}
function ensureCallbackListeners() {
if (listenersBound) {
return;
}
chrome.webNavigation.onCommitted.addListener((details) => {
onNavigationEvent(details, 'webNavigation.onCommitted');
});
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
onNavigationEvent(details, 'webNavigation.onHistoryStateUpdated');
});
chrome.tabs.onUpdated.addListener(onTabUpdated);
listenersBound = true;
}
return {
ensureCallbackListeners,
handleCapturedCallback,
isContributionCallbackUrl,
isContributionFinalStatus,
pollContributionStatus,
startContributionFlow,
submitContributionCallback,
};
}
return {
ACTIVE_STATUSES,
FINAL_STATUSES,
RUNTIME_DEFAULTS,
RUNTIME_KEYS,
createContributionOAuthManager,
};
});
+46
View File
@@ -56,6 +56,7 @@
notifyStepComplete,
notifyStepError,
patchHotmailAccount,
pollContributionStatus,
registerTab,
requestStop,
handleCloudflareSecurityBlocked,
@@ -64,6 +65,7 @@
scheduleAutoRun,
selectLuckmailPurchase,
setCurrentHotmailAccount,
setContributionMode,
setEmailState,
setEmailStateSilently,
setIcloudAliasPreservedState,
@@ -76,6 +78,7 @@
setStepStatus,
skipAutoRunCountdown,
skipStep,
startContributionFlow,
startAutoRunLoop,
syncHotmailAccounts,
testHotmailAccountMailAccess,
@@ -288,6 +291,49 @@
return { ok: true };
}
case 'SET_CONTRIBUTION_MODE': {
const enabled = Boolean(message.payload?.enabled);
const state = await ensureManualInteractionAllowed(enabled ? '进入贡献模式' : '退出贡献模式');
if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) {
throw new Error(enabled ? '当前有步骤正在执行,无法进入贡献模式。' : '当前有步骤正在执行,无法退出贡献模式。');
}
if (typeof setContributionMode !== 'function') {
throw new Error('贡献模式切换能力未接入。');
}
return {
ok: true,
state: await setContributionMode(enabled),
};
}
case 'START_CONTRIBUTION_FLOW': {
const state = await ensureManualInteractionAllowed('开始贡献');
if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) {
throw new Error('当前有步骤正在执行,无法开始贡献流程。');
}
if (typeof startContributionFlow !== 'function') {
throw new Error('贡献 OAuth 流程尚未接入。');
}
return {
ok: true,
state: await startContributionFlow({
nickname: message.payload?.nickname,
}),
};
}
case 'POLL_CONTRIBUTION_STATUS': {
if (typeof pollContributionStatus !== 'function') {
throw new Error('贡献状态轮询能力尚未接入。');
}
return {
ok: true,
state: await pollContributionStatus({
reason: message.payload?.reason || 'sidepanel_poll',
}),
};
}
case 'CLEAR_ACCOUNT_RUN_HISTORY': {
const state = await getState();
if (isAutoRunLockedState(state)) {