From 8a16d81e01fcdac9d507ae9dd986bf94552fe9b5 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Mon, 20 Apr 2026 13:15:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=B4=A1=E7=8C=AE?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=98=B5=E7=A7=B0=E5=92=8CQQ=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=92=8C=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 2 + background/contribution-oauth.js | 8 +++ background/message-router.js | 37 +++++++++++++ sidepanel/contribution-mode.js | 64 ++++++++++++++++++++++ sidepanel/sidepanel.html | 8 +++ sidepanel/sidepanel.js | 18 ++++++ tests/background-contribution-mode.test.js | 29 +++++++--- tests/sidepanel-contribution-mode.test.js | 31 ++++++++++- 8 files changed, 187 insertions(+), 10 deletions(-) diff --git a/background.js b/background.js index 76e20b9..cc44a84 100644 --- a/background.js +++ b/background.js @@ -179,6 +179,8 @@ const ACCOUNT_RUN_HISTORY_STORAGE_KEY = 'accountRunHistory'; const CONTRIBUTION_RUNTIME_DEFAULTS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_DEFAULTS || { contributionMode: false, contributionModeExpected: false, + contributionNickname: '', + contributionQq: '', contributionSessionId: '', contributionAuthUrl: '', contributionAuthState: '', diff --git a/background/contribution-oauth.js b/background/contribution-oauth.js index a734307..d5fc80d 100644 --- a/background/contribution-oauth.js +++ b/background/contribution-oauth.js @@ -10,6 +10,8 @@ const RUNTIME_DEFAULTS = { contributionMode: false, contributionModeExpected: false, + contributionNickname: '', + contributionQq: '', contributionSessionId: '', contributionAuthUrl: '', contributionAuthState: '', @@ -250,6 +252,11 @@ return nickname || 'codex-extension-user'; } + function buildContributionQq(state = {}, preferredQq = '') { + const qq = normalizeString(preferredQq) || normalizeString(state.contributionQq); + return qq; + } + function buildStatusMessage(status, payload = {}) { const label = getStatusLabel(status); const details = [ @@ -592,6 +599,7 @@ method: 'POST', body: { nickname: buildNickname(currentState, options.nickname), + qq: buildContributionQq(currentState, options.qq), source: 'cpa', channel: 'codex-extension', }, diff --git a/background/message-router.js b/background/message-router.js index 0755ad5..af4c479 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -319,10 +319,31 @@ ok: true, state: await startContributionFlow({ nickname: message.payload?.nickname, + qq: message.payload?.qq, }), }; } + case 'SET_CONTRIBUTION_PROFILE': { + const state = await getState(); + if (!state?.contributionMode) { + throw new Error('请先进入贡献模式。'); + } + const nickname = String(message.payload?.nickname || '').trim(); + const qq = String(message.payload?.qq || '').trim(); + if (qq && !/^\d{1,20}$/.test(qq)) { + throw new Error('QQ 只能填写数字,且长度不能超过 20 位。'); + } + await setState({ + contributionNickname: nickname, + contributionQq: qq, + }); + return { + ok: true, + state: await getState(), + }; + } + case 'POLL_CONTRIBUTION_STATUS': { if (typeof pollContributionStatus !== 'function') { throw new Error('贡献状态轮询能力尚未接入。'); @@ -388,6 +409,14 @@ clearStopRequest(); if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') { await setContributionMode(true); + if (typeof setState === 'function') { + const contributionNickname = String(message.payload?.contributionNickname || '').trim(); + const contributionQq = String(message.payload?.contributionQq || '').trim(); + await setState({ + contributionNickname, + contributionQq, + }); + } } const state = await getState(); if (getPendingAutoRunTimerPlan(state)) { @@ -405,6 +434,14 @@ clearStopRequest(); if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') { await setContributionMode(true); + if (typeof setState === 'function') { + const contributionNickname = String(message.payload?.contributionNickname || '').trim(); + const contributionQq = String(message.payload?.contributionQq || '').trim(); + await setState({ + contributionNickname, + contributionQq, + }); + } } const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1); return await scheduleAutoRun(totalRuns, { diff --git a/sidepanel/contribution-mode.js b/sidepanel/contribution-mode.js index 7fe9086..0655a00 100644 --- a/sidepanel/contribution-mode.js +++ b/sidepanel/contribution-mode.js @@ -174,6 +174,24 @@ return normalizeString(currentState.contributionStatusMessage) || DEFAULT_COPY; } + async function syncContributionProfile(partial = {}) { + const payload = { + nickname: normalizeString(partial.nickname), + qq: normalizeString(partial.qq), + }; + const response = await runtime.sendMessage({ + type: 'SET_CONTRIBUTION_PROFILE', + source: 'sidepanel', + payload, + }); + if (response?.error) { + throw new Error(response.error); + } + if (response?.state) { + helpers.applySettingsState?.(response.state); + } + } + async function requestContributionMode(enabled) { const response = await runtime.sendMessage({ type: 'SET_CONTRIBUTION_MODE', @@ -234,6 +252,13 @@ throw new Error('贡献模式尚未接入主自动流程启动能力。'); } + const profile = helpers.getContributionProfile?.() || {}; + const qq = normalizeString(profile.qq); + if (qq && !/^\d{1,20}$/.test(qq)) { + throw new Error('QQ 只能填写数字,且长度不能超过 20 位。'); + } + await syncContributionProfile(profile); + const started = await helpers.startContributionAutoRun(); if (!started) { return; @@ -258,6 +283,7 @@ const currentState = getLatestState(); const enabled = isContributionModeEnabled(currentState); const blocked = isModeSwitchBlocked(); + const activeElement = typeof document !== 'undefined' ? document.activeElement : null; if (enabled && dom.selectPanelMode) { dom.selectPanelMode.value = 'cpa'; @@ -272,6 +298,18 @@ if (dom.contributionModeText) { dom.contributionModeText.textContent = DEFAULT_COPY; } + if (dom.inputContributionNickname && activeElement !== dom.inputContributionNickname) { + const nextNickname = normalizeString(currentState.contributionNickname); + if (nextNickname || !normalizeString(dom.inputContributionNickname.value)) { + dom.inputContributionNickname.value = nextNickname; + } + } + if (dom.inputContributionQq && activeElement !== dom.inputContributionQq) { + const nextQq = normalizeString(currentState.contributionQq); + if (nextQq || !normalizeString(dom.inputContributionQq.value)) { + dom.inputContributionQq.value = nextQq; + } + } if (dom.contributionOauthStatus) { dom.contributionOauthStatus.textContent = getOauthStatusText(currentState); } @@ -350,6 +388,32 @@ } }); + dom.inputContributionNickname?.addEventListener('change', async () => { + try { + await syncContributionProfile({ + nickname: dom.inputContributionNickname?.value, + qq: dom.inputContributionQq?.value, + }); + } catch (error) { + helpers.showToast?.(error.message, 'error'); + } finally { + render(); + } + }); + + dom.inputContributionQq?.addEventListener('change', async () => { + try { + await syncContributionProfile({ + nickname: dom.inputContributionNickname?.value, + qq: dom.inputContributionQq?.value, + }); + } catch (error) { + helpers.showToast?.(error.message, 'error'); + } finally { + render(); + } + }); + dom.btnOpenContributionUpload?.addEventListener('click', () => { try { helpers.openExternalUrl?.(contributionUploadUrl); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index f851304..5f73343 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -110,6 +110,14 @@ CPA

当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,无需手动复制。

+
+ 贡献昵称 + +
+
+ QQ + +
OAUTH diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 351b7f9..bd31724 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -35,6 +35,8 @@ const btnOpenRelease = document.getElementById('btn-open-release'); const settingsCard = document.getElementById('settings-card'); const contributionModePanel = document.getElementById('contribution-mode-panel'); const contributionModeText = document.getElementById('contribution-mode-text'); +const inputContributionNickname = document.getElementById('input-contribution-nickname'); +const inputContributionQq = document.getElementById('input-contribution-qq'); const contributionOauthStatus = document.getElementById('contribution-oauth-status'); const contributionCallbackStatus = document.getElementById('contribution-callback-status'); const contributionModeSummary = document.getElementById('contribution-mode-summary'); @@ -1763,6 +1765,12 @@ function applySettingsState(state) { if (inputAccountRunHistoryHelperBaseUrl) { inputAccountRunHistoryHelperBaseUrl.value = normalizeAccountRunHistoryHelperBaseUrlValue(state?.accountRunHistoryHelperBaseUrl); } + if (inputContributionNickname) { + inputContributionNickname.value = state?.contributionNickname || ''; + } + if (inputContributionQq) { + inputContributionQq.value = state?.contributionQq || ''; + } setManagedAliasBaseEmailInputForProvider(restoredMailProvider, state); inputInbucketHost.value = state?.inbucketHost || ''; inputInbucketMailbox.value = state?.inbucketMailbox || ''; @@ -3041,6 +3049,8 @@ const contributionModeManager = window.SidepanelContributionMode?.createContribu dom: { btnConfigMenu, btnContributionMode, + inputContributionNickname, + inputContributionQq, contributionCallbackStatus, btnExitContributionMode, btnOpenAccountRecords, @@ -3068,6 +3078,10 @@ const contributionModeManager = window.SidepanelContributionMode?.createContribu closeAccountRecordsPanel, closeConfigMenu, getContributionNickname: () => latestState?.email || '', + getContributionProfile: () => ({ + nickname: String(inputContributionNickname?.value || '').trim(), + qq: String(inputContributionQq?.value || '').trim(), + }), isModeSwitchBlocked: isContributionModeSwitchBlocked, openConfirmModal, openExternalUrl, @@ -3446,6 +3460,8 @@ async function startAutoRunFromCurrentSettings() { const totalRuns = getRunCountValue(); let mode = 'restart'; const autoRunSkipFailures = inputAutoSkipFailures.checked; + const contributionNickname = String(inputContributionNickname?.value || '').trim(); + const contributionQq = String(inputContributionQq?.value || '').trim(); const fallbackThreadIntervalMinutes = normalizeAutoRunThreadIntervalMinutes( inputAutoSkipFailuresThreadIntervalMinutes.value ); @@ -3488,6 +3504,8 @@ async function startAutoRunFromCurrentSettings() { delayMinutes, autoRunSkipFailures, contributionMode: Boolean(latestState?.contributionMode), + contributionNickname, + contributionQq, mode, }, }); diff --git a/tests/background-contribution-mode.test.js b/tests/background-contribution-mode.test.js index ef736fe..e30c482 100644 --- a/tests/background-contribution-mode.test.js +++ b/tests/background-contribution-mode.test.js @@ -92,6 +92,8 @@ const DEFAULT_STATE = { panelMode: 'cpa' }; const CONTRIBUTION_RUNTIME_DEFAULTS = { contributionMode: false, contributionModeExpected: false, + contributionNickname: '', + contributionQq: '', contributionSessionId: '', contributionAuthUrl: '', contributionAuthState: '', @@ -123,6 +125,8 @@ return { buildContributionModeState }; { contributionMode: true, contributionModeExpected: true, + contributionNickname: '', + contributionQq: '', contributionSessionId: 'session-001', contributionAuthUrl: 'https://auth.example.com', contributionAuthState: '', @@ -153,6 +157,8 @@ return { buildContributionModeState }; { contributionMode: false, contributionModeExpected: false, + contributionNickname: '', + contributionQq: '', contributionSessionId: '', contributionAuthUrl: '', contributionAuthState: '', @@ -215,7 +221,7 @@ test('message router handles contribution mode, start flow, and status polling m }); const startResponse = await router.handleMessage({ type: 'START_CONTRIBUTION_FLOW', - payload: { nickname: '阿青' }, + payload: { nickname: '阿青', qq: '123456' }, }); const pollResponse = await router.handleMessage({ type: 'POLL_CONTRIBUTION_STATUS', @@ -227,7 +233,7 @@ test('message router handles contribution mode, start flow, and status polling m assert.equal(pollResponse.ok, true); assert.deepStrictEqual(calls, [ { type: 'toggle', enabled: true }, - { type: 'start', options: { nickname: '阿青' } }, + { type: 'start', options: { nickname: '阿青', qq: '123456' } }, { type: 'poll', options: { reason: 'test_poll' } }, ]); }); @@ -260,17 +266,20 @@ test('message router re-syncs contribution mode before AUTO_RUN when sidepanel p const response = await router.handleMessage({ type: 'AUTO_RUN', - payload: { - totalRuns: 2, - autoRunSkipFailures: true, - mode: 'restart', - contributionMode: true, - }, - }); + payload: { + totalRuns: 2, + autoRunSkipFailures: true, + mode: 'restart', + contributionMode: true, + contributionNickname: '阿青', + contributionQq: '123456', + }, + }); assert.equal(response.ok, true); assert.deepStrictEqual(calls, [ { type: 'toggle', enabled: true }, + { type: 'setState', updates: { contributionNickname: '阿青', contributionQq: '123456' } }, { type: 'setState', updates: { autoRunSkipFailures: true } }, { type: 'startAutoRunLoop', totalRuns: 2, options: { autoRunSkipFailures: true, mode: 'restart' } }, ]); @@ -412,6 +421,8 @@ test('contribution oauth manager starts session, opens auth url, submits callbac assert.equal(startedState.contributionAuthTabId, 88); assert.equal(tabCalls.length, 1); assert.match(fetchCalls[0].url, /\/start$/); + assert.match(String(fetchCalls[0].options.body || ''), /"nickname":"user@example\.com"/); + assert.match(String(fetchCalls[0].options.body || ''), /"qq":""/); assert.match(fetchCalls[1].url, /\/status\?/); const callbackState = await manager.handleCapturedCallback( diff --git a/tests/sidepanel-contribution-mode.test.js b/tests/sidepanel-contribution-mode.test.js index 98aa4ff..d41a214 100644 --- a/tests/sidepanel-contribution-mode.test.js +++ b/tests/sidepanel-contribution-mode.test.js @@ -111,6 +111,8 @@ test('sidepanel html contains contribution mode runtime UI and loads the module assert.match(html, /id="btn-start-contribution"/); assert.match(html, /id="btn-open-contribution-upload"/); assert.match(html, /id="btn-exit-contribution-mode"/); + assert.match(html, /id="input-contribution-nickname"/); + assert.match(html, /id="input-contribution-qq"/); assert.notEqual(moduleIndex, -1); assert.notEqual(sidepanelIndex, -1); assert.ok(moduleIndex < sidepanelIndex); @@ -246,6 +248,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri btnOpenAccountRecords: createElement(), btnOpenContributionUpload: createElement(), btnStartContribution: createElement(), + inputContributionNickname: createElement({ value: '贡献者昵称' }), + inputContributionQq: createElement({ value: '123456' }), contributionCallbackStatus: createElement(), contributionModePanel: createElement({ hidden: true }), contributionModeSummary: createElement(), @@ -284,6 +288,12 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri getContributionNickname() { return latestState.email; }, + getContributionProfile() { + return { + nickname: dom.inputContributionNickname.value, + qq: dom.inputContributionQq.value, + }; + }, isModeSwitchBlocked() { return blocked; }, @@ -332,6 +342,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri ? { contributionMode: true, panelMode: 'cpa', + contributionNickname: '', + contributionQq: '', contributionSessionId: '', contributionAuthUrl: '', contributionAuthState: '', @@ -344,6 +356,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri : { contributionMode: false, panelMode: 'cpa', + contributionNickname: '', + contributionQq: '', contributionSessionId: '', contributionAuthUrl: '', contributionAuthState: '', @@ -367,6 +381,14 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri }, }; } + if (message.type === 'SET_CONTRIBUTION_PROFILE') { + latestState = { + ...latestState, + contributionNickname: message.payload.nickname, + contributionQq: message.payload.qq, + }; + return { state: latestState }; + } return {}; }, }, @@ -399,11 +421,16 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri assert.ok(updateConfigMenuCount >= 1); assert.equal(timers.length, 0); + dom.inputContributionNickname.value = '贡献者昵称'; + dom.inputContributionQq.value = '123456'; + await dom.btnStartContribution.listeners.click(); assert.equal(contributionAutoRunStartCount, 1); assert.equal(appliedState.contributionSessionId, ''); assert.equal(latestState.contributionSessionId, 'session-002'); assert.equal(latestState.contributionStatus, 'started'); + const contributionProfileMessage = sentMessages.find((message) => message.type === 'SET_CONTRIBUTION_PROFILE'); + assert.deepStrictEqual(contributionProfileMessage?.payload, { nickname: '贡献者昵称', qq: '123456' }); assert.equal(timers.length > 0, true); await manager.pollOnce({ reason: 'test_poll' }); @@ -423,7 +450,7 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri assert.equal(dom.rowVpsUrl.classList.contains('is-contribution-hidden'), false); assert.deepStrictEqual( sentMessages.map((message) => message.type), - ['SET_CONTRIBUTION_MODE', 'POLL_CONTRIBUTION_STATUS', 'SET_CONTRIBUTION_MODE'] + ['SET_CONTRIBUTION_MODE', 'SET_CONTRIBUTION_PROFILE', 'POLL_CONTRIBUTION_STATUS', 'SET_CONTRIBUTION_MODE'] ); assert.deepStrictEqual( toasts.map((item) => item.message), @@ -434,6 +461,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri latestState = { contributionMode: true, panelMode: 'cpa', + contributionNickname: '贡献者昵称', + contributionQq: '123456', contributionSessionId: 'session-002', contributionAuthUrl: 'https://auth.example.com/oauth?state=oauth-state-002', contributionStatus: 'waiting',