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
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',