feat: upgrade multi-flow account contributions

This commit is contained in:
QLHazycoder
2026-05-19 13:20:06 +00:00
parent 68c1f387e1
commit 4687d07e2d
36 changed files with 2960 additions and 442 deletions
+1 -1
View File
@@ -559,7 +559,7 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc
stepStatuses: { 3: 'completed' },
stepsVersion: 'ultra2.0',
visibleStep: 10,
contributionMode: false,
accountContributionEnabled: false,
},
});
@@ -102,7 +102,7 @@ test('account run history helper upgrades old records, keeps stopped items and s
attemptRun: 3,
},
plusModeEnabled: false,
contributionMode: false,
accountContributionEnabled: false,
});
const appended = await helpers.appendAccountRunRecord('node:fetch-login-code:failed', null, '步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
@@ -213,7 +213,7 @@ test('account run history helper accepts phone-only records without forcing emai
source: 'manual',
autoRunContext: null,
plusModeEnabled: false,
contributionMode: false,
accountContributionEnabled: false,
});
const normalized = helpers.normalizeAccountRunHistoryRecord({
@@ -426,22 +426,22 @@ test('account run history records preserve Plus and contribution mode flags', ()
email: 'plus@example.com',
password: 'secret',
plusModeEnabled: true,
contributionMode: true,
accountContributionEnabled: true,
}, 'success');
assert.equal(record.plusModeEnabled, true);
assert.equal(record.contributionMode, true);
assert.equal(record.accountContributionEnabled, true);
const normalized = helpers.normalizeAccountRunHistoryRecord({
email: 'plus@example.com',
password: 'secret',
finalStatus: 'success',
plusModeEnabled: true,
contributionMode: true,
accountContributionEnabled: true,
});
assert.equal(normalized.plusModeEnabled, true);
assert.equal(normalized.contributionMode, true);
assert.equal(normalized.accountContributionEnabled, true);
});
test('account run history helper clears persisted records and syncs full snapshot payload to local helper', async () => {
+187 -166
View File
@@ -69,7 +69,7 @@ test('background imports contribution oauth module and keeps contribution runtim
assert.match(backgroundSource, /background\/contribution-oauth\.js/);
assert.doesNotMatch(persistedBlock, /contributionSessionId|contributionAuthUrl|contributionCallbackUrl|contributionStatus/);
assert.match(defaultStateBlock, /contributionMode:\s*false|CONTRIBUTION_RUNTIME_DEFAULTS/);
assert.match(defaultStateBlock, /accountContributionEnabled:\s*false|CONTRIBUTION_RUNTIME_DEFAULTS/);
});
test('contribution oauth module exposes a factory', () => {
@@ -84,14 +84,48 @@ test('contribution oauth module exposes a factory', () => {
assert.equal(Array.isArray(api?.RUNTIME_KEYS), true);
});
test('buildContributionModeState preserves active contribution runtime while keeping contribution on sub2api', () => {
const bundle = extractFunction(backgroundSource, 'buildContributionModeState');
test('buildAccountContributionState preserves active contribution runtime while keeping contribution on sub2api', () => {
const bundle = extractFunction(backgroundSource, 'buildAccountContributionState');
const helperBundle = [
'normalizeAccountContributionFlowId',
'normalizeAccountContributionAdapterId',
'assertAccountContributionAdapterAvailable',
'buildFlowContributionRuntimePatch',
'normalizeOpenAiContributionSource',
'resolveOpenAiContributionRoutingState',
].map((name) => extractFunction(backgroundSource, name)).join('\n');
const api = new Function(`
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const CONTRIBUTION_SOURCE_CPA = 'cpa';
const CONTRIBUTION_SOURCE_SUB2API = 'sub2api';
const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池';
const CONTRIBUTION_SUB2API_PLUS_GROUP_NAME = 'openai-plus';
const self = {
MultiPageFlowRegistry: {
normalizeFlowId(value = '', fallback = 'openai') {
const normalized = String(value || '').trim().toLowerCase();
return normalized || fallback || 'openai';
},
},
MultiPageContributionRegistry: {
normalizeAdapterId(value = '') {
return String(value || '').trim().toLowerCase();
},
hasContributionAdapter(flowId, adapterId) {
return flowId === 'openai' && adapterId === 'openai-oauth';
},
getDefaultContributionAdapterId(flowId) {
return flowId === 'openai' ? 'openai-oauth' : '';
},
},
};
const DEFAULT_STATE = { panelMode: 'cpa' };
const CONTRIBUTION_RUNTIME_DEFAULTS = {
contributionMode: false,
contributionModeExpected: false,
accountContributionEnabled: false,
accountContributionExpected: false,
contributionAdapterId: '',
flowContributionRuntime: {},
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
contributionNickname: '',
@@ -110,143 +144,130 @@ const CONTRIBUTION_RUNTIME_DEFAULTS = {
};
const CONTRIBUTION_RUNTIME_KEYS = Object.keys(CONTRIBUTION_RUNTIME_DEFAULTS);
function isPlusModeState(state = {}) { return Boolean(state?.plusModeEnabled); }
function normalizeContributionModeSource(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'sub2api' ? 'sub2api' : 'cpa';
}
function resolveContributionModeRoutingState(state = {}) {
const currentStatus = String(state?.contributionStatus || '').trim().toLowerCase();
const currentSource = normalizeContributionModeSource(state?.contributionSource);
const hasActiveSession = Boolean(
String(state?.contributionSessionId || '').trim()
&& currentStatus
&& !['auto_approved', 'auto_rejected', 'expired', 'error'].includes(currentStatus)
);
if (hasActiveSession) {
return {
source: currentSource,
targetGroupName: currentSource === 'sub2api'
? (String(state?.contributionTargetGroupName || '').trim() || 'codex号池')
: '',
};
}
const source = 'sub2api';
return {
source,
targetGroupName: isPlusModeState(state)
? 'openai-plus'
: (String(state?.contributionTargetGroupName || '').trim() || 'codex号池'),
};
}
${helperBundle}
${bundle}
return { buildContributionModeState };
return { buildAccountContributionState };
`)();
assert.deepStrictEqual(
api.buildContributionModeState(true, {
panelMode: 'sub2api',
customPassword: 'Secret123!',
accountRunHistoryTextEnabled: true,
}, {
contributionSessionId: 'session-001',
contributionAuthUrl: 'https://auth.example.com',
contributionStatus: 'waiting',
contributionCallbackStatus: 'waiting',
}),
{
contributionMode: true,
contributionModeExpected: true,
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
contributionNickname: '',
contributionQq: '',
contributionSessionId: 'session-001',
contributionAuthUrl: 'https://auth.example.com',
contributionAuthState: '',
contributionCallbackUrl: '',
contributionStatus: 'waiting',
contributionStatusMessage: '',
contributionLastPollAt: 0,
contributionCallbackStatus: 'waiting',
contributionCallbackMessage: '',
contributionAuthOpenedAt: 0,
contributionAuthTabId: 0,
panelMode: 'sub2api',
customPassword: '',
accountRunHistoryTextEnabled: false,
}
);
const enabledState = api.buildAccountContributionState(true, {
panelMode: 'sub2api',
customPassword: 'Secret123!',
accountRunHistoryTextEnabled: true,
}, {
contributionSessionId: 'session-001',
contributionAuthUrl: 'https://auth.example.com',
contributionStatus: 'waiting',
contributionCallbackStatus: 'waiting',
});
assert.equal(enabledState.accountContributionEnabled, true);
assert.equal(enabledState.accountContributionExpected, true);
assert.equal(enabledState.contributionAdapterId, 'openai-oauth');
assert.deepStrictEqual(enabledState.flowContributionRuntime, {
openai: { enabled: true, adapterId: 'openai-oauth' },
});
assert.equal(enabledState.contributionSessionId, 'session-001');
assert.equal(enabledState.panelMode, 'sub2api');
assert.equal(enabledState.customPassword, '');
assert.equal(enabledState.accountRunHistoryTextEnabled, false);
assert.deepStrictEqual(
api.buildContributionModeState(false, {
panelMode: 'sub2api',
customPassword: 'Secret123!',
accountRunHistoryTextEnabled: true,
}, {
contributionSessionId: 'session-001',
contributionAuthUrl: 'https://auth.example.com',
contributionStatus: 'waiting',
}),
{
contributionMode: false,
contributionModeExpected: false,
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
contributionNickname: '',
contributionQq: '',
contributionSessionId: '',
contributionAuthUrl: '',
contributionAuthState: '',
contributionCallbackUrl: '',
contributionStatus: '',
contributionStatusMessage: '',
contributionLastPollAt: 0,
contributionCallbackStatus: 'idle',
contributionCallbackMessage: '',
contributionAuthOpenedAt: 0,
contributionAuthTabId: 0,
panelMode: 'sub2api',
customPassword: 'Secret123!',
accountRunHistoryTextEnabled: true,
}
);
const disabledState = api.buildAccountContributionState(false, {
panelMode: 'sub2api',
customPassword: 'Secret123!',
accountRunHistoryTextEnabled: true,
}, {
contributionSessionId: 'session-001',
contributionAuthUrl: 'https://auth.example.com',
contributionStatus: 'waiting',
});
assert.equal(disabledState.accountContributionEnabled, false);
assert.equal(disabledState.accountContributionExpected, false);
assert.equal(disabledState.contributionAdapterId, '');
assert.deepStrictEqual(disabledState.flowContributionRuntime, {});
assert.equal(disabledState.contributionSessionId, '');
assert.equal(disabledState.panelMode, 'sub2api');
assert.equal(disabledState.customPassword, 'Secret123!');
assert.deepStrictEqual(
api.buildContributionModeState(true, {
panelMode: 'cpa',
plusModeEnabled: true,
customPassword: 'Secret123!',
accountRunHistoryTextEnabled: true,
}, {}),
{
contributionMode: true,
contributionModeExpected: true,
contributionSource: 'sub2api',
contributionTargetGroupName: 'openai-plus',
contributionNickname: '',
contributionQq: '',
contributionSessionId: '',
contributionAuthUrl: '',
contributionAuthState: '',
contributionCallbackUrl: '',
contributionStatus: '',
contributionStatusMessage: '',
contributionLastPollAt: 0,
contributionCallbackStatus: 'idle',
contributionCallbackMessage: '',
contributionAuthOpenedAt: 0,
contributionAuthTabId: 0,
panelMode: 'sub2api',
customPassword: '',
accountRunHistoryTextEnabled: false,
}
);
const plusContributionState = api.buildAccountContributionState(true, {
panelMode: 'cpa',
plusModeEnabled: true,
customPassword: 'Secret123!',
accountRunHistoryTextEnabled: true,
}, {});
assert.equal(plusContributionState.contributionTargetGroupName, 'openai-plus');
assert.equal(plusContributionState.panelMode, 'sub2api');
});
test('resetState preserves contribution runtime across reset', () => {
assert.match(backgroundSource, /CONTRIBUTION_RUNTIME_KEYS/);
assert.match(backgroundSource, /const contributionModeState = buildContributionModeState/);
assert.match(backgroundSource, /\.\.\.contributionModeState/);
assert.match(backgroundSource, /const accountContributionState = buildAccountContributionState/);
assert.match(backgroundSource, /\.\.\.accountContributionState/);
});
test('storage migration upgrades legacy contribution mode into unified account contribution state', async () => {
const helperBundle = [
'normalizeAccountContributionFlowId',
'normalizeAccountContributionAdapterId',
'buildFlowContributionRuntimePatch',
].map((name) => extractFunction(backgroundSource, name)).join('\n');
const migrationBundle = extractFunction(backgroundSource, 'migrateLegacyAccountContributionState');
const api = new Function(`
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const self = {
MultiPageFlowRegistry: {
normalizeFlowId(value = '', fallback = 'openai') {
const normalized = String(value || '').trim().toLowerCase();
return normalized || fallback || 'openai';
},
},
MultiPageContributionRegistry: {
normalizeAdapterId(value = '') {
return String(value || '').trim().toLowerCase();
},
hasContributionAdapter(flowId, adapterId) {
return (flowId === 'openai' && adapterId === 'openai-oauth')
|| (flowId === 'kiro' && adapterId === 'kiro-builder-id');
},
getDefaultContributionAdapterId(flowId) {
return flowId === 'kiro' ? 'kiro-builder-id' : 'openai-oauth';
},
},
};
const sessionStore = {
contributionMode: true,
contributionModeExpected: true,
activeFlowId: 'kiro',
};
const removed = [];
const chrome = {
storage: {
session: {
async get() { return { ...sessionStore }; },
async set(updates) { Object.assign(sessionStore, updates); },
async remove(keys) { removed.push(['session', ...keys]); keys.forEach((key) => { delete sessionStore[key]; }); },
},
local: {
async remove(keys) { removed.push(['local', ...keys]); },
},
},
};
${helperBundle}
${migrationBundle}
return { migrateLegacyAccountContributionState, sessionStore, removed };
`)();
await api.migrateLegacyAccountContributionState();
assert.equal(api.sessionStore.accountContributionEnabled, true);
assert.equal(api.sessionStore.accountContributionExpected, true);
assert.equal(api.sessionStore.contributionAdapterId, 'kiro-builder-id');
assert.deepStrictEqual(api.sessionStore.flowContributionRuntime, {
kiro: { enabled: true, adapterId: 'kiro-builder-id' },
});
assert.equal(Object.prototype.hasOwnProperty.call(api.sessionStore, 'contributionMode'), false);
assert.deepStrictEqual(api.removed, [
['session', 'contributionMode', 'contributionModeExpected'],
['local', 'contributionMode', 'contributionModeExpected'],
]);
});
test('message router handles contribution mode, start flow, and status polling messages', async () => {
@@ -258,23 +279,23 @@ test('message router handles contribution mode, start flow, and status polling m
const router = api.createMessageRouter({
ensureManualInteractionAllowed: async () => ({
stepStatuses: { 1: 'pending', 2: 'completed' },
contributionMode: true,
accountContributionEnabled: true,
}),
pollContributionStatus: async (options) => {
calls.push({ type: 'poll', options });
return { contributionStatus: 'waiting' };
},
setContributionMode: async (enabled) => {
setAccountContributionMode: async (enabled) => {
calls.push({ type: 'toggle', enabled });
return {
contributionMode: Boolean(enabled),
accountContributionEnabled: Boolean(enabled),
panelMode: 'cpa',
};
},
startContributionFlow: async (options) => {
startFlowContribution: async (options) => {
calls.push({ type: 'start', options });
return {
contributionMode: true,
accountContributionEnabled: true,
contributionSessionId: 'session-001',
contributionStatus: 'started',
};
@@ -282,15 +303,15 @@ test('message router handles contribution mode, start flow, and status polling m
});
const enableResponse = await router.handleMessage({
type: 'SET_CONTRIBUTION_MODE',
type: 'SET_ACCOUNT_CONTRIBUTION_MODE',
payload: { enabled: true },
});
const startResponse = await router.handleMessage({
type: 'START_CONTRIBUTION_FLOW',
type: 'START_FLOW_CONTRIBUTION',
payload: { nickname: '阿青', qq: '123456' },
});
const pollResponse = await router.handleMessage({
type: 'POLL_CONTRIBUTION_STATUS',
type: 'POLL_FLOW_CONTRIBUTION_STATUS',
payload: { reason: 'test_poll' },
});
@@ -304,7 +325,7 @@ test('message router handles contribution mode, start flow, and status polling m
]);
});
test('message router re-syncs contribution mode before AUTO_RUN when sidepanel payload marks contributionMode=true', async () => {
test('message router re-syncs contribution mode before AUTO_RUN when sidepanel payload marks accountContributionEnabled=true', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
@@ -314,13 +335,13 @@ test('message router re-syncs contribution mode before AUTO_RUN when sidepanel p
clearStopRequest: () => {},
getPendingAutoRunTimerPlan: () => null,
getState: async () => ({
contributionMode: false,
accountContributionEnabled: false,
stepStatuses: {},
}),
normalizeRunCount: (value) => Number(value) || 1,
setContributionMode: async (enabled) => {
setAccountContributionMode: async (enabled) => {
calls.push({ type: 'toggle', enabled });
return { contributionMode: true };
return { accountContributionEnabled: true };
},
setState: async (updates) => {
calls.push({ type: 'setState', updates });
@@ -336,7 +357,7 @@ test('message router re-syncs contribution mode before AUTO_RUN when sidepanel p
totalRuns: 2,
autoRunSkipFailures: true,
mode: 'restart',
contributionMode: true,
accountContributionEnabled: true,
contributionNickname: '阿青',
contributionQq: '123456',
},
@@ -433,7 +454,7 @@ test('account run history snapshot sync is disabled in contribution mode', () =>
assert.equal(
helpers.shouldSyncAccountRunHistorySnapshot({
contributionMode: true,
accountContributionEnabled: true,
accountRunHistoryTextEnabled: true,
accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373',
}),
@@ -442,7 +463,7 @@ test('account run history snapshot sync is disabled in contribution mode', () =>
assert.equal(
helpers.shouldSyncAccountRunHistorySnapshot({
contributionMode: false,
accountContributionEnabled: false,
accountRunHistoryTextEnabled: true,
accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373',
}),
@@ -458,7 +479,7 @@ test('contribution oauth manager starts session, opens auth url, submits callbac
const closeCallbackCalls = [];
let statusPollCount = 0;
let currentState = {
contributionMode: true,
accountContributionEnabled: true,
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
email: 'user@example.com',
@@ -541,7 +562,7 @@ test('contribution oauth manager starts session, opens auth url, submits callbac
},
});
const startedState = await manager.startContributionFlow();
const startedState = await manager.startFlowContribution();
assert.equal(startedState.contributionSessionId, 'session-001');
assert.equal(startedState.contributionAuthState, 'oauth-state-001');
assert.equal(startedState.contributionStatus, 'waiting');
@@ -577,7 +598,7 @@ test('contribution oauth manager deduplicates concurrent callback captures for t
let submitCallCount = 0;
let resolveSubmitRequest = null;
let currentState = {
contributionMode: true,
accountContributionEnabled: true,
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
contributionSessionId: 'session-001',
@@ -664,7 +685,7 @@ test('contribution oauth manager ignores tabs.onUpdated events without a real ur
const fetchCalls = [];
const addedListeners = {};
let currentState = {
contributionMode: true,
accountContributionEnabled: true,
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
contributionSessionId: 'session-001',
@@ -756,7 +777,7 @@ test('contribution oauth manager switches Plus contribution traffic to sub2api o
const globalScope = {};
const fetchCalls = [];
let currentState = {
contributionMode: true,
accountContributionEnabled: true,
plusModeEnabled: true,
contributionSource: 'sub2api',
contributionTargetGroupName: 'openai-plus',
@@ -817,7 +838,7 @@ test('contribution oauth manager switches Plus contribution traffic to sub2api o
},
});
await manager.startContributionFlow();
await manager.startFlowContribution();
assert.match(String(fetchCalls[0].options.body || ''), /"source":"sub2api"/);
assert.match(String(fetchCalls[0].options.body || ''), /"target_group_name":"openai-plus"/);
@@ -836,7 +857,7 @@ return { refreshOAuthUrlBeforeStep6 };
calls.push({ type: 'log', message, level, options });
};
globalThis.contributionOAuthManager = {
async startContributionFlow(options) {
async startFlowContribution(options) {
calls.push({ type: 'contribution', options });
return {
contributionAuthUrl: 'https://auth.example.com/oauth?state=oauth-state-001',
@@ -854,7 +875,7 @@ return { refreshOAuthUrlBeforeStep6 };
globalThis.LOG_PREFIX = '[test]';
const oauthUrl = await api.refreshOAuthUrlBeforeStep6({
contributionMode: true,
accountContributionEnabled: true,
email: 'user@example.com',
});
@@ -862,7 +883,7 @@ return { refreshOAuthUrlBeforeStep6 };
assert.deepStrictEqual(calls, [
{
type: 'log',
message: 'contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...',
message: '账号贡献已开启,走公开贡献接口,正在申请 OAuth 登录地址...',
level: 'info',
options: { step: 7, stepKey: 'oauth-login' },
},
@@ -872,7 +893,7 @@ return { refreshOAuthUrlBeforeStep6 };
nickname: '',
openAuthTab: false,
stateOverride: {
contributionMode: true,
accountContributionEnabled: true,
email: 'user@example.com',
},
},
@@ -894,7 +915,7 @@ return { refreshOAuthUrlBeforeStep6 };
delete globalThis.LOG_PREFIX;
});
test('refreshOAuthUrlBeforeStep6 logs the normal CPA/SUB2API/Codex2API path explicitly when contributionMode=false', async () => {
test('refreshOAuthUrlBeforeStep6 logs the normal CPA/SUB2API/Codex2API path explicitly when accountContributionEnabled=false', async () => {
const bundle = extractFunction(backgroundSource, 'refreshOAuthUrlBeforeStep6');
const calls = [];
@@ -907,7 +928,7 @@ return { refreshOAuthUrlBeforeStep6 };
calls.push({ type: 'log', message, level, options });
};
globalThis.contributionOAuthManager = {
async startContributionFlow() {
async startFlowContribution() {
calls.push({ type: 'contribution' });
return {
contributionAuthUrl: 'https://auth.example.com/oauth?state=unexpected',
@@ -925,7 +946,7 @@ return { refreshOAuthUrlBeforeStep6 };
globalThis.LOG_PREFIX = '[test]';
const oauthUrl = await api.refreshOAuthUrlBeforeStep6({
contributionMode: false,
accountContributionEnabled: false,
panelMode: 'sub2api',
email: 'user@example.com',
});
@@ -934,7 +955,7 @@ return { refreshOAuthUrlBeforeStep6 };
assert.deepStrictEqual(calls, [
{
type: 'log',
message: 'contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...',
message: '账号贡献未开启,走普通 CPA / SUB2API / Codex2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...',
level: 'info',
options: { step: 7, stepKey: 'oauth-login' },
},
@@ -956,7 +977,7 @@ return { refreshOAuthUrlBeforeStep6 };
delete globalThis.LOG_PREFIX;
});
test('executeStep10 blocks silent fallback when contributionModeExpected=true but contributionMode=false', async () => {
test('executeStep10 blocks silent fallback when accountContributionExpected=true but accountContributionEnabled=false', async () => {
const bundle = extractFunction(backgroundSource, 'executeStep10');
const api = new Function(`
@@ -973,10 +994,10 @@ return { executeStep10 };
await assert.rejects(
() => api.executeStep10({
contributionModeExpected: true,
contributionMode: false,
accountContributionExpected: true,
accountContributionEnabled: false,
}),
/步骤 10:当前自动流程预期使用贡献模式/
/步骤 10:当前自动流程预期使用账号贡献/
);
delete globalThis.executeContributionStep10;
+2 -2
View File
@@ -674,7 +674,7 @@ return {
test('resetState preserves LuckMail session config, used map, and preserve tag cache while clearing runtime purchase state', async () => {
const bundle = [
extractFunction('buildContributionModeState'),
extractFunction('buildAccountContributionState'),
extractFunction('resetState'),
].join('\n');
@@ -702,7 +702,7 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
' email: null,',
'};',
'const CONTRIBUTION_RUNTIME_DEFAULTS = {',
' contributionMode: false,',
' accountContributionEnabled: false,',
" contributionSessionId: '',",
" contributionAuthUrl: '',",
" contributionAuthState: '',",
@@ -100,7 +100,7 @@ function createRouterWithFinalNode(options = {}) {
selectLuckmailPurchase: async () => {},
setCurrentHotmailAccount: async () => {},
setCurrentMail2925Account: async () => {},
setContributionMode: async () => {},
setAccountContributionMode: async () => {},
setEmailState: async () => {},
setEmailStateSilently: async () => {},
setIcloudAliasPreservedState: async () => {},
@@ -58,7 +58,7 @@ let state = {
signupMethod: 'phone',
phoneVerificationEnabled: true,
plusModeEnabled: false,
contributionMode: false,
accountContributionEnabled: false,
resolvedSignupMethod: null,
};
async function getState() { return { ...state }; }
+1 -1
View File
@@ -61,7 +61,7 @@ function getErrorMessage(error) {
}
async function getState() {
events.push({ type: 'getState' });
return { nodeStatuses: {}, contributionMode: true };
return { nodeStatuses: {}, accountContributionEnabled: true };
}
function getLastNodeIdForState() {
return lastNodeId;
@@ -8,6 +8,7 @@ function createContributionContentService(options = {}) {
const cache = new Map();
const windowObject = {};
let fetchCalls = 0;
const fetchUrls = [];
const localStorage = {
getItem(key) {
@@ -23,7 +24,7 @@ function createContributionContentService(options = {}) {
if (options.cachedSnapshot) {
cache.set(
'multipage-contribution-content-summary-v1',
options.cacheKey || 'multipage-contribution-content-summary-v2:openai:cpa',
JSON.stringify(options.cachedSnapshot)
);
}
@@ -44,6 +45,7 @@ function createContributionContentService(options = {}) {
const wrappedFetch = async (...args) => {
fetchCalls += 1;
fetchUrls.push(String(args[0] || ''));
return fetchImpl(...args);
};
@@ -69,11 +71,14 @@ function createContributionContentService(options = {}) {
getFetchCalls() {
return fetchCalls;
},
getFetchUrls() {
return fetchUrls.slice();
},
};
}
test('getContentUpdateSnapshot returns a prompt version for visible contribution content updates', async () => {
const { api } = createContributionContentService({
const { api, getFetchUrls } = createContributionContentService({
fetchImpl: async () => ({
ok: true,
async json() {
@@ -100,10 +105,13 @@ test('getContentUpdateSnapshot returns a prompt version for visible contribution
}),
});
const snapshot = await api.getContentUpdateSnapshot();
const snapshot = await api.getContentUpdateSnapshot({ flowId: 'kiro', targetId: 'kiro-rs' });
assert.equal(snapshot.status, 'update-available');
assert.equal(snapshot.promptVersion, 'auto_run_notice:2026-04-21T12:05:00Z');
assert.equal(snapshot.flowId, 'kiro');
assert.equal(snapshot.targetId, 'kiro-rs');
assert.equal(getFetchUrls()[0], 'https://flowpilot.qlhazycoder.top/api/content-summary?flow=kiro&target=kiro-rs');
assert.equal(snapshot.hasVisibleUpdates, true);
assert.equal(snapshot.latestUpdatedAt, '2026-04-21T12:05:00Z');
assert.equal(snapshot.items.length, 1);
@@ -132,7 +140,7 @@ test('getContentUpdateSnapshot falls back to cached snapshot when the live reque
},
],
portalUrl: 'https://flowpilot.qlhazycoder.top',
apiUrl: 'https://flowpilot.qlhazycoder.top/api/content-summary',
apiUrl: 'https://flowpilot.qlhazycoder.top/api/content-summary?flow=openai&target=cpa',
checkedAt: Date.now() - 1000,
};
@@ -151,3 +159,35 @@ test('getContentUpdateSnapshot falls back to cached snapshot when the live reque
assert.equal(snapshot.errorMessage, 'offline');
assert.equal(snapshot.items[0].slug, 'announcement');
});
test('getContentUpdateSnapshot keeps flow caches isolated', async () => {
const cachedSnapshot = {
status: 'update-available',
promptVersion: 'flow:kiro|target:kiro-rs|auto_run_notice:2026-04-22T00:00:00Z',
hasVisibleUpdates: true,
latestUpdatedAt: '2026-04-22T00:00:00Z',
latestUpdatedAtDisplay: '2026-04-22 08:00',
flowId: 'kiro',
targetId: 'kiro-rs',
items: [{ slug: 'auto_run_notice', isVisible: true, text: 'Kiro 提示' }],
checkedAt: Date.now() - 1000,
};
const { api } = createContributionContentService({
cachedSnapshot,
cacheKey: 'multipage-contribution-content-summary-v2:kiro:kiro-rs',
fetchImpl: async () => {
throw new Error('offline');
},
});
const openAiSnapshot = await api.getContentUpdateSnapshot({ flowId: 'openai', targetId: 'cpa' });
const kiroSnapshot = await api.getContentUpdateSnapshot({ flowId: 'kiro', targetId: 'kiro-rs' });
assert.equal(openAiSnapshot.status, 'error');
assert.equal(openAiSnapshot.fromCache, undefined);
assert.equal(openAiSnapshot.flowId, 'openai');
assert.equal(kiroSnapshot.fromCache, true);
assert.equal(kiroSnapshot.flowId, 'kiro');
assert.equal(kiroSnapshot.promptVersion, cachedSnapshot.promptVersion);
});
+79
View File
@@ -0,0 +1,79 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
const contributionRegistrySource = fs.readFileSync('shared/contribution-registry.js', 'utf8');
function loadApi() {
const scope = {};
return new Function(
'self',
`${flowRegistrySource}; ${contributionRegistrySource}; return self.MultiPageContributionRegistry;`
)(scope);
}
test('contribution registry exposes OpenAI and Kiro adapters through one contract', () => {
const api = loadApi();
assert.deepEqual(
api.getContributionAdapterIds('openai'),
['openai-oauth', 'openai-codex-file', 'openai-sub2api-file']
);
assert.deepEqual(api.getContributionAdapterIds('kiro'), ['kiro-builder-id']);
assert.equal(api.getDefaultContributionAdapterId('openai'), 'openai-oauth');
assert.equal(api.getDefaultContributionAdapterId('kiro'), 'kiro-builder-id');
assert.equal(api.getAdapterDefinition('kiro-builder-id')?.artifactKind, 'kiro-builder-id');
assert.equal(api.getAdapterDefinition('openai-oauth')?.flowId, 'openai');
assert.equal(api.hasContributionAdapter('kiro', 'kiro-builder-id'), true);
assert.equal(api.hasContributionAdapter('kiro', 'openai-oauth'), false);
});
test('contribution registry resolves the combined tutorial entry per flow', () => {
const api = loadApi();
assert.deepEqual(
api.getContributionTutorialEntry('openai', {
portalBaseUrl: 'https://flowpilot.example/root/',
targetId: 'sub2api',
}),
{
id: 'openai-contribution-tutorial',
flowId: 'openai',
label: '贡献/使用教程',
portalPath: '/tutorial',
defaultTargetId: 'cpa',
contributionAdapterId: 'openai-oauth',
action: 'open-portal-and-enable-contribution',
targetId: 'sub2api',
portalUrl: 'https://flowpilot.example/root/tutorial?flow=openai&target=sub2api',
}
);
assert.deepEqual(
api.getContributionTutorialEntry('kiro', {
portalBaseUrl: 'https://flowpilot.example',
}),
{
id: 'kiro-contribution-tutorial',
flowId: 'kiro',
label: '贡献/使用教程',
portalPath: '/tutorial',
defaultTargetId: 'kiro-rs',
contributionAdapterId: 'kiro-builder-id',
action: 'open-portal-and-enable-contribution',
targetId: 'kiro-rs',
portalUrl: 'https://flowpilot.example/tutorial?flow=kiro&target=kiro-rs',
}
);
});
test('contribution registry fails fast when a published flow has no adapter', () => {
const api = loadApi();
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(['openai', 'kiro']), true);
assert.throws(
() => api.assertPublishedFlowsHaveContributionAdapters(['openai', 'missing-flow']),
/缺少账号贡献适配器:missing-flow/
);
});
+12 -8
View File
@@ -3,6 +3,7 @@ const assert = require('node:assert/strict');
const fs = require('node:fs');
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
const contributionRegistrySource = fs.readFileSync('shared/contribution-registry.js', 'utf8');
const settingsSchemaSource = fs.readFileSync('shared/settings-schema.js', 'utf8');
const source = fs.readFileSync('shared/flow-capabilities.js', 'utf8');
@@ -10,7 +11,7 @@ function loadApi() {
const scope = {};
return new Function(
'self',
`${flowRegistrySource}; ${settingsSchemaSource}; ${source}; return self.MultiPageFlowCapabilities;`
`${flowRegistrySource}; ${contributionRegistrySource}; ${settingsSchemaSource}; ${source}; return self.MultiPageFlowCapabilities;`
)(scope);
}
@@ -24,7 +25,7 @@ test('flow capability registry keeps OpenAI phone signup available only when run
openaiIntegrationTargetId: 'cpa',
phoneVerificationEnabled: true,
plusModeEnabled: false,
contributionMode: false,
accountContributionEnabled: false,
signupMethod: 'phone',
},
});
@@ -40,7 +41,7 @@ test('flow capability registry keeps OpenAI phone signup available only when run
openaiIntegrationTargetId: 'sub2api',
phoneVerificationEnabled: true,
plusModeEnabled: true,
contributionMode: false,
accountContributionEnabled: false,
signupMethod: 'phone',
},
});
@@ -61,7 +62,7 @@ test('flow capability registry defaults unknown flows to minimal non-phone capab
openaiIntegrationTargetId: 'codex2api',
phoneVerificationEnabled: true,
plusModeEnabled: true,
contributionMode: true,
accountContributionEnabled: true,
signupMethod: 'phone',
},
});
@@ -94,8 +95,10 @@ test('flow capability registry exposes Kiro as an independent flow with its own
assert.equal(capabilityState.activeFlowId, 'kiro');
assert.equal(capabilityState.canShowPhoneSettings, false);
assert.equal(capabilityState.canShowPlusSettings, false);
assert.equal(capabilityState.canShowContributionMode, true);
assert.equal(capabilityState.effectiveSignupMethod, 'email');
assert.equal(capabilityState.effectiveTargetId, 'kiro-rs');
assert.deepEqual(capabilityState.flowCapabilities.contributionAdapterIds, ['kiro-builder-id']);
assert.deepEqual(
capabilityState.visibleGroupIds,
['kiro-runtime-status', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
@@ -121,7 +124,7 @@ test('flow capability registry exposes shared auto-run validation for phone lock
signupMethod: 'phone',
phoneVerificationEnabled: true,
plusModeEnabled: true,
contributionMode: false,
accountContributionEnabled: false,
},
});
@@ -159,14 +162,14 @@ test('flow capability registry normalizes unsupported mode switches back to the
signupMethod: 'phone',
phoneVerificationEnabled: true,
plusModeEnabled: true,
contributionMode: true,
accountContributionEnabled: true,
},
changedKeys: [
'openaiIntegrationTargetId',
'signupMethod',
'phoneVerificationEnabled',
'plusModeEnabled',
'contributionMode',
'accountContributionEnabled',
],
});
@@ -178,7 +181,8 @@ test('flow capability registry normalizes unsupported mode switches back to the
signupMethod: 'email',
phoneVerificationEnabled: false,
plusModeEnabled: false,
contributionMode: false,
accountContributionEnabled: false,
accountContributionEnabled: false,
});
assert.deepEqual(
validation.errors.map((entry) => entry.code),
@@ -39,6 +39,16 @@ test('flow registry exposes canonical flow and target metadata', () => {
['row-plus-mode', 'row-plus-account-access-strategy', 'row-plus-payment-method']
);
assert.equal(flowRegistry.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs');
assert.equal(flowRegistry.getFlowCapabilities('openai').supportsAccountContribution, true);
assert.equal(flowRegistry.getFlowCapabilities('kiro').supportsAccountContribution, true);
assert.deepEqual(
flowRegistry.getFlowCapabilities('openai').contributionAdapterIds,
['openai-oauth', 'openai-codex-file', 'openai-sub2api-file']
);
assert.deepEqual(
flowRegistry.getFlowCapabilities('kiro').contributionAdapterIds,
['kiro-builder-id']
);
});
test('settings schema normalizes view input into canonical nested namespaces', () => {
+217
View File
@@ -0,0 +1,217 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadKiroContributionModules() {
const stateSource = fs.readFileSync('background/kiro/state.js', 'utf8');
const artifactSource = fs.readFileSync('background/kiro/credential-artifact.js', 'utf8');
const adapterSource = fs.readFileSync('background/contribution/adapters/kiro-builder-id.js', 'utf8');
const globalScope = {};
new Function('self', `${stateSource}; ${artifactSource}; ${adapterSource}; return self;`)(globalScope);
return globalScope;
}
function buildAuthorizedKiroState(overrides = {}) {
return {
activeFlowId: 'kiro',
flowId: 'kiro',
accountContributionEnabled: true,
contributionAdapterId: 'kiro-builder-id',
contributionNickname: '贡献者',
contributionQq: '123456',
kiroRuntime: {
register: {
email: 'kiro-user@example.com',
},
desktopAuth: {
region: 'us-east-1',
clientId: 'client-id-001',
clientSecret: 'client-secret-super-long',
refreshToken: 'refresh-token-super-secret',
tokenSource: 'desktop_authorization_code_pkce',
authorizedAt: 1760000000000,
},
upload: {
targetId: 'kiro-rs',
},
},
...overrides,
};
}
test('Kiro Builder ID artifact builder validates and builds unified contribution artifact', () => {
const scope = loadKiroContributionModules();
const api = scope.MultiPageBackgroundKiroCredentialArtifact;
const artifact = api.buildKiroBuilderIdArtifact(buildAuthorizedKiroState());
assert.equal(artifact.flow, 'kiro');
assert.equal(artifact.adapter, 'kiro-builder-id');
assert.equal(artifact.artifact, 'kiro-builder-id');
assert.equal(artifact.account.email, 'kiro-user@example.com');
assert.equal(artifact.credentials.refreshToken, 'refresh-token-super-secret');
assert.equal(artifact.credentials.clientId, 'client-id-001');
assert.equal(artifact.credentials.clientSecret, 'client-secret-super-long');
assert.equal(artifact.credentials.region, 'us-east-1');
assert.equal(artifact.metadata.targetId, 'kiro-rs');
const safeSummary = api.buildSafeArtifactSummary(artifact);
assert.equal(safeSummary.refreshToken.includes('refresh-token-super-secret'), false);
assert.equal(safeSummary.clientSecret.includes('client-secret-super-long'), false);
});
test('Kiro Builder ID artifact builder rejects missing required fields', () => {
const scope = loadKiroContributionModules();
const api = scope.MultiPageBackgroundKiroCredentialArtifact;
assert.throws(
() => api.buildKiroBuilderIdArtifact(buildAuthorizedKiroState({
kiroRuntime: {
...buildAuthorizedKiroState().kiroRuntime,
desktopAuth: {
...buildAuthorizedKiroState().kiroRuntime.desktopAuth,
refreshToken: '',
},
},
})),
/refreshToken/
);
assert.throws(
() => api.buildKiroBuilderIdArtifact(buildAuthorizedKiroState({
kiroRuntime: {
...buildAuthorizedKiroState().kiroRuntime,
desktopAuth: {
...buildAuthorizedKiroState().kiroRuntime.desktopAuth,
clientId: '',
},
},
})),
/clientId/
);
assert.throws(
() => api.buildKiroBuilderIdArtifact(buildAuthorizedKiroState({
kiroRuntime: {
...buildAuthorizedKiroState().kiroRuntime,
desktopAuth: {
...buildAuthorizedKiroState().kiroRuntime.desktopAuth,
clientSecret: '',
},
},
})),
/clientSecret/
);
assert.throws(
() => api.buildKiroBuilderIdArtifact(buildAuthorizedKiroState({
kiroRuntime: {
...buildAuthorizedKiroState().kiroRuntime,
register: { email: '' },
},
email: '',
accountIdentifier: '',
})),
/注册邮箱/
);
});
test('Kiro contribution adapter submits public contribution without requiring kiro.rs config and redacts logs', async () => {
const scope = loadKiroContributionModules();
const adapterApi = scope.MultiPageBackgroundKiroBuilderIdContributionAdapter;
const fetchCalls = [];
const logs = [];
const statePatches = [];
const adapter = adapterApi.createKiroBuilderIdContributionAdapter({
addLog: async (message, level, options) => logs.push({ message, level, options }),
fetchImpl: async (url, options) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
async text() {
return JSON.stringify({ ok: true, contribution_id: 'kiro-contribution-001', message: '已接收' });
},
};
},
setState: async (patch) => statePatches.push(patch),
});
const result = await adapter.maybeSubmitFlowContribution(buildAuthorizedKiroState({
kiroRsUrl: '',
kiroRsKey: '',
}), { nodeId: 'kiro-complete-desktop-authorize' });
assert.equal(result.ok, true);
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, 'https://flowpilot.qlhazycoder.top/api/contributions');
const requestBody = JSON.parse(fetchCalls[0].options.body);
assert.equal(requestBody.flow, 'kiro');
assert.equal(requestBody.adapter_id, 'kiro-builder-id');
assert.equal(requestBody.artifact_kind, 'kiro-builder-id');
assert.equal(requestBody.artifact.credentials.refreshToken, 'refresh-token-super-secret');
assert.equal(statePatches.at(-1).flowContributionRuntime.kiro.status, 'submitted');
const logText = logs.map((entry) => entry.message).join('\n');
assert.equal(logText.includes('refresh-token-super-secret'), false);
assert.equal(logText.includes('client-secret-super-long'), false);
});
test('Kiro contribution adapter skips invalid artifacts without sending secrets', async () => {
const scope = loadKiroContributionModules();
const adapterApi = scope.MultiPageBackgroundKiroBuilderIdContributionAdapter;
const fetchCalls = [];
const logs = [];
const adapter = adapterApi.createKiroBuilderIdContributionAdapter({
addLog: async (message) => logs.push(message),
fetchImpl: async () => {
fetchCalls.push(true);
throw new Error('should not be called');
},
setState: async () => {},
});
const result = await adapter.maybeSubmitFlowContribution(buildAuthorizedKiroState({
kiroRuntime: {
...buildAuthorizedKiroState().kiroRuntime,
desktopAuth: {
...buildAuthorizedKiroState().kiroRuntime.desktopAuth,
refreshToken: '',
},
},
}));
assert.equal(result.skipped, true);
assert.equal(result.reason, 'missing_refreshToken');
assert.equal(fetchCalls.length, 0);
assert.equal(logs.join('\n').includes('refresh-token-super-secret'), false);
});
test('Kiro contribution adapter redacts server errors that echo submitted secrets', async () => {
const scope = loadKiroContributionModules();
const adapterApi = scope.MultiPageBackgroundKiroBuilderIdContributionAdapter;
const logs = [];
const statePatches = [];
const adapter = adapterApi.createKiroBuilderIdContributionAdapter({
addLog: async (message) => logs.push(message),
fetchImpl: async () => ({
ok: false,
status: 400,
async text() {
return JSON.stringify({
ok: false,
message: 'bad refresh-token-super-secret and client-secret-super-long',
});
},
}),
setState: async (patch) => statePatches.push(patch),
});
const result = await adapter.maybeSubmitFlowContribution(buildAuthorizedKiroState());
assert.equal(result.ok, false);
const combined = `${logs.join('\n')}\n${JSON.stringify(statePatches)}`;
assert.equal(combined.includes('refresh-token-super-secret'), false);
assert.equal(combined.includes('client-secret-super-long'), false);
});
test('Kiro desktop authorization runner is wired to submit public contribution after step 8', () => {
const source = fs.readFileSync('background/kiro/desktop-authorize-runner.js', 'utf8');
assert.match(source, /maybeSubmitFlowContribution/);
assert.match(source, /trigger:\s*'kiro-step-8'/);
});
@@ -64,7 +64,7 @@ return new Function(`
const events = [];
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const latestState = {
contributionMode: false,
accountContributionEnabled: false,
activeFlowId: 'openai',
flowId: 'openai',
panelMode: 'cpa',
@@ -266,7 +266,7 @@ const latestState = {
activeFlowId: 'site-a',
panelMode: 'cpa',
signupMethod: 'phone',
contributionMode: false,
accountContributionEnabled: false,
phoneVerificationEnabled: true,
};
const inputAutoSkipFailures = { checked: false };
@@ -4,6 +4,8 @@ const fs = require('node:fs');
const vm = require('node:vm');
const source = fs.readFileSync('sidepanel/contribution-mode.js', 'utf8');
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
const contributionRegistrySource = fs.readFileSync('shared/contribution-registry.js', 'utf8');
function createElement() {
return {
@@ -19,7 +21,10 @@ function createElement() {
},
},
setAttribute() {},
addEventListener() {},
listeners: {},
addEventListener(type, handler) {
this.listeners[type] = handler;
},
};
}
@@ -37,12 +42,14 @@ test('contribution mode manager does not project openai-only ui state into kiro
const rowVpsUrl = createElement();
const dom = {
btnContributionMode: createElement(),
contributionModePanel: createElement(),
contributionModeText: createElement(),
contributionModeBadge: createElement(),
accountContributionPanel: createElement(),
accountContributionText: createElement(),
accountContributionBadge: createElement(),
contributionPrimaryStatusLabel: createElement(),
contributionSecondaryStatusLabel: createElement(),
contributionOauthStatus: createElement(),
contributionCallbackStatus: createElement(),
contributionModeSummary: createElement(),
accountContributionSummary: createElement(),
inputContributionNickname: createElement(),
inputContributionQq: createElement(),
btnStartContribution: createElement(),
@@ -57,7 +64,9 @@ test('contribution mode manager does not project openai-only ui state into kiro
getLatestState: () => ({
activeFlowId: 'kiro',
flowId: 'kiro',
contributionMode: true,
accountContributionEnabled: true,
supportsAccountContribution: true,
contributionAdapterId: 'kiro-builder-id',
contributionSource: 'cpa',
}),
},
@@ -80,11 +89,107 @@ test('contribution mode manager does not project openai-only ui state into kiro
manager.render();
assert.equal(dom.contributionModePanel.hidden, true);
assert.equal(dom.accountContributionPanel.hidden, false);
assert.equal(dom.contributionPrimaryStatusLabel.textContent, '账号产物');
assert.equal(dom.contributionSecondaryStatusLabel.textContent, '提交');
assert.equal(dom.contributionOauthStatus.textContent, '等待账号产物');
assert.equal(dom.selectPanelMode.disabled, false);
assert.equal(dom.btnContributionMode.disabled, true);
assert.equal(dom.btnContributionMode.title, '当前 flow 不支持贡献模式');
assert.equal(dom.btnStartContribution.disabled, true);
assert.equal(dom.btnOpenContributionUpload.disabled, true);
assert.equal(dom.selectPanelMode.value, '');
assert.equal(dom.btnContributionMode.disabled, false);
assert.equal(dom.btnContributionMode.title, '打开当前 flow 教程;当前已在贡献模式');
assert.equal(dom.btnStartContribution.disabled, false);
assert.equal(dom.btnOpenContributionUpload.disabled, false);
assert.equal(dom.btnOpenContributionUpload.textContent, '查看当前 flow 贡献说明');
assert.equal(rowVpsUrl.classList.hiddenState, false);
});
test('combined contribution tutorial button opens current flow page and enables current flow adapter', async () => {
const windowObject = {};
const api = new Function(
'self',
'window',
'setTimeout',
'clearTimeout',
`${flowRegistrySource}; ${contributionRegistrySource}; ${source}; return window.SidepanelContributionMode;`
)(windowObject, windowObject, setTimeout, clearTimeout);
let latestState = {
activeFlowId: 'kiro',
flowId: 'kiro',
accountContributionEnabled: false,
supportsAccountContribution: true,
contributionAdapterId: '',
kiroTargetId: 'kiro-rs',
};
const openedUrls = [];
const sentMessages = [];
const dom = {
btnContributionMode: createElement(),
accountContributionPanel: createElement(),
accountContributionText: createElement(),
accountContributionBadge: createElement(),
contributionPrimaryStatusLabel: createElement(),
contributionSecondaryStatusLabel: createElement(),
contributionOauthStatus: createElement(),
contributionCallbackStatus: createElement(),
accountContributionSummary: createElement(),
inputContributionNickname: createElement(),
inputContributionQq: createElement(),
btnStartContribution: createElement(),
btnOpenContributionUpload: createElement(),
btnExitContributionMode: createElement(),
btnOpenAccountRecords: createElement(),
selectPanelMode: createElement(),
};
const manager = api.createContributionModeManager({
state: {
getLatestState: () => latestState,
},
dom,
helpers: {
applySettingsState(nextState) {
latestState = nextState;
},
updatePanelModeUI() {},
updateAccountRunHistorySettingsUI() {},
updateConfigMenuControls() {},
closeConfigMenu() {},
closeAccountRecordsPanel() {},
isModeSwitchBlocked() {
return false;
},
openExternalUrl(url) {
openedUrls.push(url);
},
showToast() {},
},
runtime: {
sendMessage: async (message) => {
sentMessages.push(message);
return {
state: {
...latestState,
accountContributionEnabled: true,
contributionAdapterId: message.payload.adapterId,
},
};
},
},
constants: {
contributionPortalUrl: 'https://flowpilot.qlhazycoder.top',
},
});
manager.bindEvents();
await dom.btnContributionMode.listeners.click();
assert.deepEqual(openedUrls, ['https://flowpilot.qlhazycoder.top/tutorial?flow=kiro&target=kiro-rs']);
assert.equal(sentMessages[0].type, 'SET_ACCOUNT_CONTRIBUTION_MODE');
assert.deepEqual(sentMessages[0].payload, {
enabled: true,
flowId: 'kiro',
adapterId: 'kiro-builder-id',
});
assert.equal(latestState.accountContributionEnabled, true);
assert.equal(latestState.contributionAdapterId, 'kiro-builder-id');
});
+23 -31
View File
@@ -193,7 +193,7 @@ test('collectSettingsPayload omits custom password and local sync settings in co
const bundle = extractFunction('collectSettingsPayload');
const api = new Function('normalizeIcloudTargetMailboxType', 'normalizeIcloudForwardMailProvider', `
let latestState = { contributionMode: true };
let latestState = { accountContributionEnabled: true };
const window = {};
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
@@ -337,7 +337,7 @@ return {
assert.equal(contributionPayload.phoneVerificationEnabled, true);
assert.equal(contributionPayload.cloudflareTempEmailUseRandomSubdomain, true);
api.setLatestState({ contributionMode: false });
api.setLatestState({ accountContributionEnabled: false });
const normalPayload = api.collectSettingsPayload();
assert.equal(normalPayload.panelMode, 'cpa');
assert.equal(normalPayload.customPassword, 'Secret123!');
@@ -370,7 +370,7 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
assert.equal(typeof api?.createContributionModeManager, 'function');
let latestState = {
contributionMode: false,
accountContributionEnabled: false,
panelMode: 'sub2api',
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
@@ -401,13 +401,13 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
btnOpenAccountRecords: createElement(),
btnOpenContributionUpload: createElement(),
btnStartContribution: createElement(),
contributionModeBadge: createElement(),
accountContributionBadge: createElement(),
inputContributionNickname: createElement({ value: '贡献者昵称' }),
inputContributionQq: createElement({ value: '123456' }),
contributionCallbackStatus: createElement(),
contributionModePanel: createElement({ hidden: true }),
contributionModeSummary: createElement(),
contributionModeText: createElement(),
accountContributionPanel: createElement({ hidden: true }),
accountContributionSummary: createElement(),
accountContributionText: createElement(),
contributionOauthStatus: createElement(),
rowAccountRunHistoryHelperBaseUrl: createElement(),
rowAccountRunHistoryTextEnabled: createElement(),
@@ -492,11 +492,11 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
runtime: {
sendMessage: async (message) => {
sentMessages.push(message);
if (message.type === 'SET_CONTRIBUTION_MODE') {
if (message.type === 'SET_ACCOUNT_CONTRIBUTION_MODE') {
return {
state: message.payload.enabled
? {
contributionMode: true,
accountContributionEnabled: true,
panelMode: 'sub2api',
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
@@ -512,7 +512,7 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
email: latestState.email,
}
: {
contributionMode: false,
accountContributionEnabled: false,
panelMode: 'cpa',
contributionSource: 'cpa',
contributionTargetGroupName: '',
@@ -530,7 +530,7 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
},
};
}
if (message.type === 'POLL_CONTRIBUTION_STATUS') {
if (message.type === 'POLL_FLOW_CONTRIBUTION_STATUS') {
return {
state: {
...latestState,
@@ -541,14 +541,6 @@ 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 {};
},
},
@@ -560,21 +552,21 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
});
manager.render();
assert.equal(dom.contributionModePanel.hidden, true);
assert.equal(dom.accountContributionPanel.hidden, true);
assert.equal(dom.btnContributionMode.disabled, false);
assert.equal(dom.contributionModeBadge.textContent, '');
assert.equal(dom.accountContributionBadge.textContent, '');
manager.bindEvents();
await dom.btnContributionMode.listeners.click();
assert.equal(dom.contributionModePanel.hidden, false);
assert.equal(dom.accountContributionPanel.hidden, false);
assert.equal(dom.selectPanelMode.value, 'sub2api');
assert.equal(dom.selectPanelMode.disabled, true);
assert.equal(dom.contributionModeBadge.textContent, 'SUB2API');
assert.equal(dom.accountContributionBadge.textContent, 'SUB2API');
assert.equal(dom.btnOpenAccountRecords.disabled, true);
assert.equal(dom.contributionOauthStatus.textContent, '\u672a\u751f\u6210\u767b\u5f55\u5730\u5740');
assert.equal(dom.contributionCallbackStatus.textContent, '\u7b49\u5f85\u56de\u8c03');
assert.equal(dom.contributionModeSummary.textContent.length > 0, true);
assert.equal(dom.accountContributionSummary.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);
@@ -595,28 +587,28 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
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(latestState.contributionNickname, '贡献者昵称');
assert.equal(latestState.contributionQq, '123456');
assert.equal(timers.length > 0, true);
await manager.pollOnce({ reason: 'test_poll' });
assert.equal(statusState.contributionStatus, 'processing');
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\u670d\u52a1\u7aef\u786e\u8ba4');
assert.equal(dom.accountContributionSummary.textContent, '\u5df2\u63d0\u4ea4\u56de\u8c03\uff0c\u7b49\u5f85\u670d\u52a1\u7aef\u786e\u8ba4');
dom.btnOpenContributionUpload.listeners.click();
assert.deepStrictEqual(openedUrls, ['https://flowpilot.qlhazycoder.top', 'https://flowpilot.qlhazycoder.top/upload']);
await dom.btnExitContributionMode.listeners.click();
manager.render();
assert.equal(dom.contributionModePanel.hidden, true);
assert.equal(dom.accountContributionPanel.hidden, true);
assert.equal(dom.btnContributionMode.classList.contains('is-active'), false);
assert.equal(dom.selectPanelMode.disabled, false);
assert.equal(dom.rowVpsUrl.classList.contains('is-contribution-hidden'), false);
assert.deepStrictEqual(
sentMessages.map((message) => message.type),
['SET_CONTRIBUTION_MODE', 'SET_CONTRIBUTION_PROFILE', 'POLL_CONTRIBUTION_STATUS', 'SET_CONTRIBUTION_MODE']
['SET_ACCOUNT_CONTRIBUTION_MODE', 'POLL_FLOW_CONTRIBUTION_STATUS', 'SET_ACCOUNT_CONTRIBUTION_MODE']
);
assert.deepStrictEqual(
toasts.map((item) => item.message),
@@ -625,7 +617,7 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
blocked = true;
latestState = {
contributionMode: true,
accountContributionEnabled: true,
panelMode: 'sub2api',
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
@@ -640,7 +632,7 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
};
manager.render();
assert.equal(dom.selectPanelMode.value, 'sub2api');
assert.equal(dom.contributionModeBadge.textContent, 'SUB2API');
assert.equal(dom.accountContributionBadge.textContent, 'SUB2API');
assert.equal(dom.btnExitContributionMode.disabled, true);
manager.stopPolling();
});
+1 -1
View File
@@ -84,7 +84,7 @@ test('collectSettingsPayload persists icloud target mailbox settings', () => {
const bundle = extractFunction('collectSettingsPayload');
const api = new Function(`
let latestState = { contributionMode: false };
let latestState = { accountContributionEnabled: false };
const window = {};
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+1 -1
View File
@@ -154,7 +154,7 @@ test('collectSettingsPayload persists currentMail2925AccountId for 2925 account
const api = new Function('normalizeIcloudTargetMailboxType', 'normalizeIcloudForwardMailProvider', `
let latestState = {
contributionMode: false,
accountContributionEnabled: false,
mail2925UseAccountPool: true,
currentMail2925AccountId: 'acc-2',
};
+5 -5
View File
@@ -72,7 +72,7 @@ const localStorage = {
},
};
const btnContributionMode = { disabled: false };
let latestState = { contributionMode: false };
let latestState = { accountContributionEnabled: false };
${bundle}
return {
shouldPromptNewUserGuide,
@@ -82,8 +82,8 @@ return {
setButtonDisabled(value) {
btnContributionMode.disabled = Boolean(value);
},
setContributionMode(value) {
latestState = { contributionMode: Boolean(value) };
setAccountContributionMode(value) {
latestState = { accountContributionEnabled: Boolean(value) };
},
};
`)();
@@ -98,7 +98,7 @@ return {
assert.equal(api.shouldPromptNewUserGuide(), false);
api.setButtonDisabled(false);
api.setContributionMode(true);
api.setAccountContributionMode(true);
assert.equal(api.shouldPromptNewUserGuide(), false);
});
@@ -129,7 +129,7 @@ const localStorage = {
},
};
const btnContributionMode = { disabled: false };
const latestState = { contributionMode: false };
const latestState = { accountContributionEnabled: false };
const contributionContentService = { portalUrl: 'https://flowpilot.qlhazycoder.top' };
const openedUrls = [];
let modalOptions = null;
@@ -337,7 +337,7 @@ const window = {
};
let latestState = {
activeFlowId: 'site-a',
contributionMode: false,
accountContributionEnabled: false,
panelMode: 'cpa',
};
const inputPhoneVerificationEnabled = { checked: true };
@@ -877,7 +877,7 @@ const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
let latestState = {
contributionMode: false,
accountContributionEnabled: false,
mail2925UseAccountPool: false,
currentMail2925AccountId: '',
fiveSimCountryOrder: ['thailand', 'england'],