diff --git a/background/contribution-oauth.js b/background/contribution-oauth.js index 1a59f7f..a734307 100644 --- a/background/contribution-oauth.js +++ b/background/contribution-oauth.js @@ -4,7 +4,7 @@ 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_FINAL_STATUSES = new Set(['submitted']); const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']); const RUNTIME_DEFAULTS = { @@ -93,10 +93,6 @@ 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'; @@ -114,13 +110,13 @@ case 'started': return '已生成登录地址'; case 'waiting': - return '等待授权完成'; + return '等待提交回调'; case 'processing': - return '已授权,正在自动审核'; + return '已提交回调,等待 CPA 确认'; case 'auto_approved': - return '贡献成功,已自动导入'; + return '贡献成功,CPA 已确认'; case 'auto_rejected': - return '贡献未通过自动审核'; + return '贡献未通过确认'; case 'manual_review_required': return '已提交,等待人工处理'; case 'expired': @@ -143,8 +139,6 @@ return '正在提交回调'; case 'submitted': return '已提交回调'; - case 'not_required': - return '当前流程无需手动回调'; case 'failed': return '回调提交失败'; default: @@ -294,22 +288,6 @@ 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( @@ -338,14 +316,6 @@ }; } - if (looksLikeNoNeedCallback(payload)) { - return { - status: 'not_required', - message: buildCallbackMessage('not_required', payload), - callbackUrl, - }; - } - if (callbackUrl) { return { status: CALLBACK_FINAL_STATUSES.has(existingStatus) ? existingStatus : 'captured', @@ -387,8 +357,10 @@ } const code = normalizeString(parsed.searchParams.get('code')); + const errorText = normalizeString(parsed.searchParams.get('error')) + || normalizeString(parsed.searchParams.get('error_description')); const authState = normalizeString(parsed.searchParams.get('state')); - if (!code || !authState) { + if ((!code && !errorText) || !authState) { return false; } @@ -472,7 +444,7 @@ }, }); - const nextStatus = looksLikeNoNeedCallback(payload) ? 'not_required' : 'submitted'; + const nextStatus = 'submitted'; await applyRuntimeUpdates({ contributionCallbackUrl: normalizedUrl, contributionCallbackStatus: nextStatus, @@ -485,20 +457,6 @@ 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', diff --git a/sidepanel/contribution-mode.js b/sidepanel/contribution-mode.js index d0b9b35..7fe9086 100644 --- a/sidepanel/contribution-mode.js +++ b/sidepanel/contribution-mode.js @@ -1,7 +1,7 @@ (function attachSidepanelContributionMode(globalScope) { const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']); const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']); - const DEFAULT_COPY = '当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,无需手动复制。'; + const DEFAULT_COPY = '当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,并继续等待 CPA 最终确认。'; function createContributionModeManager(context = {}) { const { @@ -56,7 +56,6 @@ case 'captured': case 'submitting': case 'submitted': - case 'not_required': case 'failed': case 'idle': return normalized; @@ -137,10 +136,10 @@ return '未生成登录地址'; } if (status === 'waiting') { - return '等待授权完成'; + return '等待提交回调'; } if (status === 'processing' || status === 'auto_approved' || status === 'auto_rejected' || status === 'manual_review_required') { - return '授权已完成'; + return status === 'processing' ? '已提交回调' : '授权已结束'; } if (status === 'expired' || status === 'error') { return '授权失败'; @@ -160,8 +159,6 @@ return '正在提交回调'; case 'submitted': return '已提交回调'; - case 'not_required': - return '当前流程无需手动回调'; case 'failed': return '回调提交失败'; case 'waiting': diff --git a/tests/background-contribution-mode.test.js b/tests/background-contribution-mode.test.js index aab3403..ef736fe 100644 --- a/tests/background-contribution-mode.test.js +++ b/tests/background-contribution-mode.test.js @@ -316,12 +316,13 @@ test('account run history snapshot sync is disabled in contribution mode', () => ); }); -test('contribution oauth manager starts session, opens auth url, polls real statuses, and treats callback submit no-op as success', async () => { +test('contribution oauth manager starts session, opens auth url, submits callback, and continues polling final status', async () => { const source = fs.readFileSync('background/contribution-oauth.js', 'utf8'); const globalScope = {}; const fetchCalls = []; const tabCalls = []; const closeCallbackCalls = []; + let statusPollCount = 0; let currentState = { contributionMode: true, email: 'user@example.com', @@ -345,26 +346,31 @@ test('contribution oauth manager starts session, opens auth url, polls real stat }); } if (String(url).includes('/status?')) { + statusPollCount += 1; + if (statusPollCount === 1) { + return createMockResponse(true, 200, { + ok: true, + session_id: 'session-001', + status: 'waiting', + message: '等待提交回调。', + }); + } return createMockResponse(true, 200, { ok: true, session_id: 'session-001', - status: 'waiting', - message: '等待 OAuth 回调完成', + status: 'processing', + message: '回调地址已提交给 CPA,正在等待结果确认。', }); } if (String(url).endsWith('/submit-callback')) { - return createMockResponse(false, 400, { - ok: false, - message: '当前已启用自动回调转发,无需手动粘贴回调 URL。', - callback_url_received: true, + return createMockResponse(true, 200, { + ok: true, + session_id: 'session-001', + status: 'processing', + message: '回调地址已提交给 CPA,正在等待结果确认。', }); } - return createMockResponse(true, 200, { - ok: true, - session_id: 'session-001', - status: 'processing', - message: '授权已完成,正在自动审核并导入。', - }); + return createMockResponse(true, 200, { ok: true }); } ); @@ -414,11 +420,43 @@ test('contribution oauth manager starts session, opens auth url, polls real stat ); assert.equal(callbackState.contributionCallbackUrl, 'http://localhost:1455/auth/callback?code=abc123&state=oauth-state-001'); - assert.equal(callbackState.contributionCallbackStatus, 'not_required'); + assert.equal(callbackState.contributionCallbackStatus, 'submitted'); + assert.equal(callbackState.contributionStatus, 'processing'); assert.equal(closeCallbackCalls[0], 'http://localhost:1455/auth/callback?code=abc123&state=oauth-state-001'); assert.ok(fetchCalls.some((call) => String(call.url).endsWith('/submit-callback'))); assert.ok(broadcasts.some((item) => item.contributionCallbackStatus === 'captured')); - assert.ok(broadcasts.some((item) => item.contributionCallbackStatus === 'not_required')); + assert.ok(broadcasts.some((item) => item.contributionCallbackStatus === 'submitted')); +}); + +test('contribution oauth manager accepts localhost callback urls that contain error and state', async () => { + const source = fs.readFileSync('background/contribution-oauth.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', 'fetch', `${source}; return self.MultiPageBackgroundContributionOAuth;`)( + globalScope, + async () => createMockResponse(true, 200, { ok: true }) + ); + + const manager = api.createContributionOAuthManager({ + chrome: { + tabs: { + onUpdated: { addListener() {} }, + }, + webNavigation: { + onCommitted: { addListener() {} }, + onHistoryStateUpdated: { addListener() {} }, + }, + }, + getState: async () => ({}), + setState: async () => ({}), + }); + + assert.equal( + manager.isContributionCallbackUrl( + 'http://localhost:1455/auth/callback?error=access_denied&state=oauth-state-001', + { contributionAuthState: 'oauth-state-001' } + ), + true + ); }); test('refreshOAuthUrlBeforeStep6 uses contribution oauth session instead of panel bridge in contribution mode', async () => { diff --git a/tests/sidepanel-contribution-mode.test.js b/tests/sidepanel-contribution-mode.test.js index 4053b71..98aa4ff 100644 --- a/tests/sidepanel-contribution-mode.test.js +++ b/tests/sidepanel-contribution-mode.test.js @@ -361,9 +361,9 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri state: { ...latestState, contributionStatus: 'processing', - contributionStatusMessage: '已授权,正在自动审核', - contributionCallbackStatus: 'not_required', - contributionCallbackMessage: '当前流程无需手动回调', + contributionStatusMessage: '已提交回调,等待 CPA 确认', + contributionCallbackStatus: 'submitted', + contributionCallbackMessage: '已提交回调', }, }; } @@ -408,9 +408,9 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri await manager.pollOnce({ reason: 'test_poll' }); assert.equal(statusState.contributionStatus, 'processing'); - assert.equal(dom.contributionOauthStatus.textContent, '\u6388\u6743\u5df2\u5b8c\u6210'); - assert.equal(dom.contributionCallbackStatus.textContent, '\u5f53\u524d\u6d41\u7a0b\u65e0\u9700\u624b\u52a8\u56de\u8c03'); - assert.equal(dom.contributionModeSummary.textContent, '\u5df2\u6388\u6743\uff0c\u6b63\u5728\u81ea\u52a8\u5ba1\u6838'); + assert.equal(dom.contributionOauthStatus.textContent, '\u5df2\u63d0\u4ea4\u56de\u8c03'); + assert.equal(dom.contributionCallbackStatus.textContent, '\u5df2\u63d0\u4ea4\u56de\u8c03'); + assert.equal(dom.contributionModeSummary.textContent, '\u5df2\u63d0\u4ea4\u56de\u8c03\uff0c\u7b49\u5f85 CPA \u786e\u8ba4'); dom.btnOpenContributionUpload.listeners.click(); assert.deepStrictEqual(openedUrls, ['https://apikey.qzz.io/']);