From 4687d07e2dd8e44468ec11b30d19d24301756764 Mon Sep 17 00:00:00 2001
From: QLHazycoder <2825305047@qq.com>
Date: Tue, 19 May 2026 13:20:06 +0000
Subject: [PATCH] feat: upgrade multi-flow account contributions
---
background.js | 233 +++-
background/account-run-history.js | 6 +-
background/contribution-oauth.js | 22 +-
.../contribution/adapters/kiro-builder-id.js | 245 ++++
background/kiro/credential-artifact.js | 138 +++
background/kiro/desktop-authorize-runner.js | 10 +
background/message-router.js | 76 +-
background/steps/oauth-login.js | 2 +-
docs/多Flow资源入口与贡献体系升级开发方案.md | 1054 +++++++++++++++++
docs/多注册流程侧边栏能力矩阵.md | 3 +-
shared/contribution-registry.js | 224 ++++
shared/flow-capabilities.js | 34 +-
shared/flow-registry.js | 8 +
.../contribution-content-update-service.js | 70 +-
sidepanel/contribution-mode.js | 180 ++-
sidepanel/sidepanel.html | 5 +-
sidepanel/sidepanel.js | 138 ++-
tests/auto-run-step6-restart.test.js | 2 +-
...kground-account-run-history-module.test.js | 12 +-
tests/background-contribution-mode.test.js | 353 +++---
tests/background-luckmail.test.js | 4 +-
...und-message-router-plus-final-step.test.js | 2 +-
.../background-signup-method-settings.test.js | 2 +-
tests/background-step-completion.test.js | 2 +-
...ontribution-content-update-service.test.js | 48 +-
tests/contribution-registry.test.js | 79 ++
tests/flow-capabilities-module.test.js | 20 +-
tests/flow-registry-settings-schema.test.js | 10 +
tests/kiro-contribution-artifact.test.js | 217 ++++
...sidepanel-auto-run-content-refresh.test.js | 4 +-
...panel-contribution-mode-flow-scope.test.js | 127 +-
tests/sidepanel-contribution-mode.test.js | 54 +-
tests/sidepanel-icloud-provider.test.js | 2 +-
tests/sidepanel-mail2925-base-email.test.js | 2 +-
tests/sidepanel-new-user-guide.test.js | 10 +-
...epanel-phone-verification-settings.test.js | 4 +-
36 files changed, 2960 insertions(+), 442 deletions(-)
create mode 100644 background/contribution/adapters/kiro-builder-id.js
create mode 100644 background/kiro/credential-artifact.js
create mode 100644 docs/多Flow资源入口与贡献体系升级开发方案.md
create mode 100644 shared/contribution-registry.js
create mode 100644 tests/contribution-registry.test.js
create mode 100644 tests/kiro-contribution-artifact.test.js
diff --git a/background.js b/background.js
index f6000d9..c37acba 100644
--- a/background.js
+++ b/background.js
@@ -2,6 +2,7 @@
importScripts(
'shared/flow-registry.js',
+ 'shared/contribution-registry.js',
'shared/settings-schema.js',
'shared/source-registry.js',
'shared/flow-capabilities.js',
@@ -27,6 +28,8 @@ importScripts(
'background/workflow-engine.js',
'background/runtime-state.js',
'background/kiro/state.js',
+ 'background/kiro/credential-artifact.js',
+ 'background/contribution/adapters/kiro-builder-id.js',
'background/kiro/register-runner.js',
'background/kiro/desktop-client.js',
'background/kiro/desktop-authorize-runner.js',
@@ -694,8 +697,10 @@ const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
const CONTRIBUTION_RUNTIME_DEFAULTS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_DEFAULTS || {
- contributionMode: false,
- contributionModeExpected: false,
+ accountContributionEnabled: false,
+ accountContributionExpected: false,
+ contributionAdapterId: '',
+ flowContributionRuntime: {},
contributionSource: CONTRIBUTION_SOURCE_SUB2API,
contributionTargetGroupName: CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME,
contributionNickname: '',
@@ -715,6 +720,61 @@ const CONTRIBUTION_RUNTIME_DEFAULTS = self.MultiPageBackgroundContributionOAuth?
const CONTRIBUTION_RUNTIME_KEYS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_KEYS
|| Object.keys(CONTRIBUTION_RUNTIME_DEFAULTS);
+function normalizeAccountContributionFlowId(value = '', fallback = DEFAULT_ACTIVE_FLOW_ID) {
+ return self.MultiPageFlowRegistry?.normalizeFlowId
+ ? self.MultiPageFlowRegistry.normalizeFlowId(value, fallback)
+ : (String(value || fallback || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID);
+}
+
+function normalizeAccountContributionAdapterId(flowId = DEFAULT_ACTIVE_FLOW_ID, adapterId = '') {
+ const normalizedFlowId = normalizeAccountContributionFlowId(flowId);
+ const contributionRegistry = self.MultiPageContributionRegistry || {};
+ if (typeof contributionRegistry.normalizeAdapterId === 'function') {
+ const normalizedAdapterId = contributionRegistry.normalizeAdapterId(adapterId);
+ if (normalizedAdapterId && contributionRegistry.hasContributionAdapter?.(normalizedFlowId, normalizedAdapterId)) {
+ return normalizedAdapterId;
+ }
+ }
+ if (typeof contributionRegistry.getDefaultContributionAdapterId === 'function') {
+ return contributionRegistry.getDefaultContributionAdapterId(normalizedFlowId) || '';
+ }
+ return normalizedFlowId === DEFAULT_ACTIVE_FLOW_ID ? 'openai-oauth' : '';
+}
+
+function assertAccountContributionAdapterAvailable(flowId = DEFAULT_ACTIVE_FLOW_ID, adapterId = '') {
+ const normalizedFlowId = normalizeAccountContributionFlowId(flowId);
+ const normalizedAdapterId = normalizeAccountContributionAdapterId(normalizedFlowId, adapterId);
+ const contributionRegistry = self.MultiPageContributionRegistry || {};
+ const hasAdapter = typeof contributionRegistry.hasContributionAdapter === 'function'
+ ? contributionRegistry.hasContributionAdapter(normalizedFlowId, normalizedAdapterId)
+ : (normalizedFlowId === DEFAULT_ACTIVE_FLOW_ID && normalizedAdapterId === 'openai-oauth');
+ if (!normalizedAdapterId || !hasAdapter) {
+ throw new Error('当前 flow 尚未接入账号贡献适配器。');
+ }
+ return normalizedAdapterId;
+}
+
+function buildFlowContributionRuntimePatch(currentRuntime = {}, flowId = DEFAULT_ACTIVE_FLOW_ID, adapterId = '', enabled = false) {
+ const normalizedFlowId = normalizeAccountContributionFlowId(flowId);
+ const normalizedAdapterId = normalizeAccountContributionAdapterId(normalizedFlowId, adapterId);
+ const current = currentRuntime && typeof currentRuntime === 'object' && !Array.isArray(currentRuntime)
+ ? currentRuntime
+ : {};
+ if (!enabled) {
+ return {};
+ }
+ return {
+ ...current,
+ [normalizedFlowId]: {
+ ...(current[normalizedFlowId] && typeof current[normalizedFlowId] === 'object' && !Array.isArray(current[normalizedFlowId])
+ ? current[normalizedFlowId]
+ : {}),
+ enabled: true,
+ adapterId: normalizedAdapterId,
+ },
+ };
+}
+
function isPlusModeState(state = {}) {
return Boolean(state?.plusModeEnabled);
}
@@ -732,16 +792,16 @@ function normalizeGpcHelperPhoneMode(value = '') {
return normalized === 'auto' || normalized === 'builtin' ? 'auto' : 'manual';
}
-function normalizeContributionModeSource(value = '') {
+function normalizeOpenAiContributionSource(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === CONTRIBUTION_SOURCE_SUB2API
? CONTRIBUTION_SOURCE_SUB2API
: CONTRIBUTION_SOURCE_CPA;
}
-function resolveContributionModeRoutingState(state = {}) {
+function resolveOpenAiContributionRoutingState(state = {}) {
const currentStatus = String(state?.contributionStatus || '').trim().toLowerCase();
- const currentSource = normalizeContributionModeSource(state?.contributionSource);
+ const currentSource = normalizeOpenAiContributionSource(state?.contributionSource);
const hasActiveSession = Boolean(
String(state?.contributionSessionId || '').trim()
&& currentStatus
@@ -1748,7 +1808,7 @@ function canUsePhoneSignup(state = {}) {
}
return Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
- && !Boolean(state?.contributionMode);
+ && !Boolean(state?.accountContributionEnabled);
}
function resolveSignupMethod(state = {}) {
@@ -3753,6 +3813,56 @@ async function initializeSessionStorageAccess() {
}
}
+async function migrateLegacyAccountContributionState() {
+ const legacyKeys = ['contributionMode', 'contributionModeExpected'];
+ const sessionKeys = [
+ ...legacyKeys,
+ 'accountContributionEnabled',
+ 'accountContributionExpected',
+ 'contributionAdapterId',
+ 'flowContributionRuntime',
+ 'activeFlowId',
+ 'flowId',
+ ];
+ const legacySessionState = await chrome.storage.session.get(sessionKeys).catch(() => ({}));
+ const updates = {};
+ const shouldEnable = legacySessionState.accountContributionEnabled === undefined
+ && legacySessionState.contributionMode === true;
+ if (shouldEnable) {
+ const flowId = normalizeAccountContributionFlowId(legacySessionState.activeFlowId || legacySessionState.flowId);
+ const adapterId = normalizeAccountContributionAdapterId(flowId, legacySessionState.contributionAdapterId);
+ updates.accountContributionEnabled = true;
+ updates.accountContributionExpected = legacySessionState.accountContributionExpected !== undefined
+ ? Boolean(legacySessionState.accountContributionExpected)
+ : Boolean(legacySessionState.contributionModeExpected);
+ updates.contributionAdapterId = adapterId;
+ updates.flowContributionRuntime = buildFlowContributionRuntimePatch(
+ legacySessionState.flowContributionRuntime,
+ flowId,
+ adapterId,
+ true
+ );
+ } else if (
+ legacySessionState.contributionMode !== undefined
+ && legacySessionState.accountContributionEnabled === undefined
+ ) {
+ updates.accountContributionEnabled = false;
+ updates.accountContributionExpected = false;
+ } else if (
+ legacySessionState.contributionModeExpected !== undefined
+ && legacySessionState.accountContributionExpected === undefined
+ ) {
+ updates.accountContributionExpected = Boolean(legacySessionState.contributionModeExpected);
+ }
+ if (Object.keys(updates).length > 0) {
+ await chrome.storage.session.set(updates);
+ }
+ await Promise.all([
+ chrome.storage.session.remove?.(legacyKeys),
+ chrome.storage.local.remove?.(legacyKeys),
+ ].filter(Boolean)).catch(() => {});
+}
+
async function setState(updates) {
console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
if (Object.keys(updates || {}).length > 0) {
@@ -3902,7 +4012,7 @@ async function importSettingsBundle(configBundle) {
|| Object.prototype.hasOwnProperty.call(importedSettings, 'signupMethod')
|| Object.prototype.hasOwnProperty.call(importedSettings, 'panelMode')
|| Object.prototype.hasOwnProperty.call(importedSettings, 'activeFlowId')
- || Object.prototype.hasOwnProperty.call(importedSettings, 'contributionMode')
+ || Object.prototype.hasOwnProperty.call(importedSettings, 'accountContributionEnabled')
) {
importedSettings.signupMethod = resolveSignupMethod({
...state,
@@ -4112,27 +4222,42 @@ async function setPasswordState(password) {
broadcastDataUpdate({ password });
}
-function buildContributionModeState(enabled, persistedSettings = {}, currentState = {}) {
+function buildAccountContributionState(enabled, persistedSettings = {}, currentState = {}, options = {}) {
const currentContributionState = {};
for (const key of CONTRIBUTION_RUNTIME_KEYS) {
currentContributionState[key] = currentState[key] !== undefined
? currentState[key]
: CONTRIBUTION_RUNTIME_DEFAULTS[key];
}
-
if (enabled) {
- const routing = resolveContributionModeRoutingState({
+ const activeFlowId = normalizeAccountContributionFlowId(
+ options.flowId
+ || currentState.activeFlowId
+ || currentState.flowId
+ || persistedSettings.activeFlowId
+ || persistedSettings.flowId
+ || DEFAULT_ACTIVE_FLOW_ID
+ );
+ const adapterId = assertAccountContributionAdapterAvailable(
+ activeFlowId,
+ options.adapterId || currentState.contributionAdapterId
+ );
+ const routing = activeFlowId === DEFAULT_ACTIVE_FLOW_ID ? resolveOpenAiContributionRoutingState({
...persistedSettings,
...currentState,
...currentContributionState,
- });
+ }) : null;
return {
...currentContributionState,
- contributionMode: true,
- contributionModeExpected: true,
- contributionSource: routing.source,
- contributionTargetGroupName: routing.targetGroupName,
- panelMode: routing.source,
+ accountContributionEnabled: true,
+ accountContributionExpected: true,
+ contributionAdapterId: adapterId,
+ flowContributionRuntime: buildFlowContributionRuntimePatch(currentContributionState.flowContributionRuntime, activeFlowId, adapterId, true),
+ ...(routing ? {
+ contributionSource: routing.source,
+ contributionTargetGroupName: routing.targetGroupName,
+ panelMode: routing.source,
+ } : {}),
customPassword: '',
accountRunHistoryTextEnabled: false,
};
@@ -4140,22 +4265,24 @@ function buildContributionModeState(enabled, persistedSettings = {}, currentStat
return {
...CONTRIBUTION_RUNTIME_DEFAULTS,
- contributionMode: false,
- contributionModeExpected: false,
+ accountContributionEnabled: false,
+ accountContributionExpected: false,
+ contributionAdapterId: '',
+ flowContributionRuntime: {},
panelMode: persistedSettings.panelMode || DEFAULT_STATE.panelMode,
customPassword: persistedSettings.customPassword || '',
accountRunHistoryTextEnabled: Boolean(persistedSettings.accountRunHistoryTextEnabled),
};
}
-async function setContributionMode(enabled) {
+async function setAccountContributionMode(enabled, options = {}) {
const normalizedEnabled = Boolean(enabled);
const [persistedSettings, currentState] = await Promise.all([
getPersistedSettings(),
getState(),
]);
- const updates = buildContributionModeState(normalizedEnabled, persistedSettings, currentState);
+ const updates = buildAccountContributionState(normalizedEnabled, persistedSettings, currentState, options);
await setState(updates);
const nextState = await getState();
@@ -4410,7 +4537,10 @@ async function resetState() {
getPersistedSettings(),
getPersistedAliasState(),
]);
- const contributionModeState = buildContributionModeState(Boolean(prev.contributionMode), persistedSettings, prev);
+ const accountContributionState = buildAccountContributionState(Boolean(prev.accountContributionEnabled), persistedSettings, prev, {
+ adapterId: prev.contributionAdapterId,
+ flowId: prev.activeFlowId || prev.flowId,
+ });
const reusablePhoneActivation = (
prev.reusablePhoneActivation
&& typeof prev.reusablePhoneActivation === 'object'
@@ -4453,7 +4583,7 @@ async function resetState() {
...DEFAULT_STATE,
...persistedSettings,
...persistedAliasState,
- ...contributionModeState,
+ ...accountContributionState,
seenCodes: prev.seenCodes || [],
seenInbucketMailIds: prev.seenInbucketMailIds || [],
accounts: prev.accounts || [],
@@ -13378,6 +13508,32 @@ const kiroRegisterRunner = self.MultiPageBackgroundKiroRegisterRunner?.createKir
waitForTabStableComplete,
KIRO_REGISTER_INJECT_FILES,
});
+const kiroBuilderIdContributionAdapter = self.MultiPageBackgroundKiroBuilderIdContributionAdapter?.createKiroBuilderIdContributionAdapter?.({
+ addLog,
+ fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
+ getState,
+ setState,
+});
+async function maybeSubmitFlowContribution(state = {}, options = {}) {
+ const currentState = state && typeof state === 'object' && !Array.isArray(state) && Object.keys(state).length
+ ? state
+ : await getState();
+ const activeFlowId = normalizeAccountContributionFlowId(currentState.activeFlowId || currentState.flowId);
+ const adapterId = normalizeAccountContributionAdapterId(activeFlowId, currentState.contributionAdapterId);
+ if (!currentState.accountContributionEnabled) {
+ return { ok: true, skipped: true, reason: 'account_contribution_disabled' };
+ }
+ if (activeFlowId === 'kiro' && adapterId === 'kiro-builder-id') {
+ if (!kiroBuilderIdContributionAdapter?.maybeSubmitFlowContribution) {
+ return { ok: false, skipped: true, reason: 'kiro_builder_id_adapter_missing' };
+ }
+ return kiroBuilderIdContributionAdapter.maybeSubmitFlowContribution({
+ ...currentState,
+ contributionAdapterId: adapterId,
+ }, options);
+ }
+ return { ok: true, skipped: true, reason: 'adapter_not_handled_by_flow_submission' };
+}
const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeRunner?.createKiroDesktopAuthorizeRunner({
addLog,
chrome,
@@ -13398,6 +13554,7 @@ const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeR
YYDS_MAIL_PROVIDER,
MAIL_2925_VERIFICATION_INTERVAL_MS,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
+ maybeSubmitFlowContribution,
pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode,
pollHotmailVerificationCode,
@@ -13626,7 +13783,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
setCurrentPayPalAccount,
setCurrentHotmailAccount,
setCurrentMail2925Account,
- setContributionMode,
+ setAccountContributionMode,
setEmailState,
setEmailStateSilently,
persistRegistrationEmailState,
@@ -13643,9 +13800,10 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
setNodeStatus,
skipAutoRunCountdown,
skipNode,
- startContributionFlow: (...args) => contributionOAuthManager?.startContributionFlow?.(...args),
+ startFlowContribution: (...args) => contributionOAuthManager?.startFlowContribution?.(...args),
startAutoRunLoop,
pollContributionStatus: (...args) => contributionOAuthManager?.pollContributionStatus?.(...args),
+ submitFlowContribution: (...args) => contributionOAuthManager?.submitContributionCallback?.(...args),
syncHotmailAccounts,
syncPayPalAccounts,
deleteMail2925Account,
@@ -13958,15 +14116,15 @@ async function executeStep5(state) {
async function refreshOAuthUrlBeforeStep6(state, options = {}) {
const visibleStep = Number(options.visibleStep) || Number(state?.visibleStep) || 7;
- if (state?.contributionModeExpected && !state?.contributionMode) {
- throw new Error(`步骤 ${visibleStep}:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。`);
+ if (state?.accountContributionExpected && !state?.accountContributionEnabled) {
+ throw new Error(`步骤 ${visibleStep}:当前自动流程预期使用账号贡献,但运行态 accountContributionEnabled 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入账号贡献后再点击自动。`);
}
- if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) {
- await addLog('contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', 'info', {
+ if (state?.accountContributionEnabled && contributionOAuthManager?.startFlowContribution) {
+ await addLog('账号贡献已开启,走公开贡献接口,正在申请 OAuth 登录地址...', 'info', {
step: visibleStep,
stepKey: 'oauth-login',
});
- const contributionState = await contributionOAuthManager.startContributionFlow({
+ const contributionState = await contributionOAuthManager.startFlowContribution({
nickname: state.contributionNickname || '',
openAuthTab: false,
stateOverride: state,
@@ -13978,7 +14136,7 @@ async function refreshOAuthUrlBeforeStep6(state, options = {}) {
await handleStepData(1, { oauthUrl });
return oauthUrl;
}
- await addLog(`contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info', {
+ await addLog(`账号贡献未开启,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info', {
step: visibleStep,
stepKey: 'oauth-login',
});
@@ -15337,10 +15495,10 @@ async function executeStep10(state) {
const platformVerifyStep = typeof getStepIdByKeyForState === 'function'
? (getStepIdByKeyForState('platform-verify', state || {}) || 10)
: 10;
- if (state?.contributionModeExpected && !state?.contributionMode) {
- throw new Error(`步骤 ${platformVerifyStep}:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 提交。请重新进入贡献模式后再点击自动。`);
+ if (state?.accountContributionExpected && !state?.accountContributionEnabled) {
+ throw new Error(`步骤 ${platformVerifyStep}:当前自动流程预期使用账号贡献,但运行态 accountContributionEnabled 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 提交。请重新进入账号贡献后再点击自动。`);
}
- if (state?.contributionMode) {
+ if (state?.accountContributionEnabled) {
return executeContributionStep10(state);
}
return step10Executor.executeStep10(state);
@@ -15367,6 +15525,9 @@ chrome.alarms.onAlarm.addListener((alarm) => {
});
chrome.runtime.onStartup.addListener(() => {
+ migrateLegacyAccountContributionState().catch((err) => {
+ console.error(LOG_PREFIX, 'Failed to migrate legacy account contribution state on startup:', err);
+ });
restoreAutoRunTimerIfNeeded().catch((err) => {
console.error(LOG_PREFIX, 'Failed to restore auto run timer on startup:', err);
});
@@ -15384,6 +15545,9 @@ chrome.runtime.onStartup.addListener(() => {
});
chrome.runtime.onInstalled.addListener(() => {
+ migrateLegacyAccountContributionState().catch((err) => {
+ console.error(LOG_PREFIX, 'Failed to migrate legacy account contribution state on install/update:', err);
+ });
restoreAutoRunTimerIfNeeded().catch((err) => {
console.error(LOG_PREFIX, 'Failed to restore auto run timer on install/update:', err);
});
@@ -15400,6 +15564,9 @@ chrome.runtime.onInstalled.addListener(() => {
});
});
+migrateLegacyAccountContributionState().catch((err) => {
+ console.error(LOG_PREFIX, 'Failed to migrate legacy account contribution state:', err);
+});
restoreAutoRunTimerIfNeeded().catch((err) => {
console.error(LOG_PREFIX, 'Failed to restore auto run timer:', err);
});
diff --git a/background/account-run-history.js b/background/account-run-history.js
index c695338..c647eae 100644
--- a/background/account-run-history.js
+++ b/background/account-run-history.js
@@ -431,7 +431,7 @@
source,
autoRunContext: source === 'auto' ? autoRunContext : null,
plusModeEnabled: Boolean(record.plusModeEnabled),
- contributionMode: Boolean(record.contributionMode),
+ accountContributionEnabled: Boolean(record.accountContributionEnabled),
};
}
@@ -526,7 +526,7 @@
source,
autoRunContext,
plusModeEnabled: Boolean(state.plusModeEnabled),
- contributionMode: Boolean(state.contributionMode),
+ accountContributionEnabled: Boolean(state.accountContributionEnabled),
};
}
@@ -637,7 +637,7 @@
}
function shouldSyncAccountRunHistorySnapshot(state = {}) {
- if (Boolean(state.contributionMode)) {
+ if (Boolean(state.accountContributionEnabled)) {
return false;
}
diff --git a/background/contribution-oauth.js b/background/contribution-oauth.js
index 749e2d0..92135fc 100644
--- a/background/contribution-oauth.js
+++ b/background/contribution-oauth.js
@@ -8,8 +8,10 @@
const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']);
const RUNTIME_DEFAULTS = {
- contributionMode: false,
- contributionModeExpected: false,
+ accountContributionEnabled: false,
+ accountContributionExpected: false,
+ contributionAdapterId: '',
+ flowContributionRuntime: {},
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
contributionNickname: '',
@@ -265,14 +267,14 @@
return Boolean(state?.plusModeEnabled);
}
- function normalizeContributionModeSource(value = '') {
+ function normalizeOpenAiContributionSource(value = '') {
const normalized = normalizeString(value).toLowerCase();
return normalized === 'sub2api' ? 'sub2api' : 'cpa';
}
- function resolveContributionModeRoutingState(state = {}) {
+ function resolveOpenAiContributionRoutingState(state = {}) {
const currentStatus = normalizeString(state?.contributionStatus).toLowerCase();
- const currentSource = normalizeContributionModeSource(state?.contributionSource);
+ const currentSource = normalizeOpenAiContributionSource(state?.contributionSource);
const hasActiveSession = Boolean(
normalizeString(state?.contributionSessionId)
&& currentStatus
@@ -533,7 +535,7 @@
async function handleCapturedCallback(rawUrl, metadata = {}) {
const currentState = await getState();
- if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) {
+ if (!normalizeString(currentState.contributionSessionId) || !currentState.accountContributionEnabled) {
return currentState;
}
if (!isContributionCallbackUrl(rawUrl, currentState)) {
@@ -642,10 +644,10 @@
return nextState;
}
- async function startContributionFlow(options = {}) {
+ async function startFlowContribution(options = {}) {
const currentState = options.stateOverride || await getState();
const shouldOpenAuthTab = options.openAuthTab !== false;
- if (!currentState.contributionMode) {
+ if (!currentState.accountContributionEnabled) {
throw new Error('请先进入贡献模式。');
}
@@ -662,7 +664,7 @@
return pollContributionStatus({ reason: 'resume_existing' });
}
- const routingState = resolveContributionModeRoutingState(currentState);
+ const routingState = resolveOpenAiContributionRoutingState(currentState);
const payload = await fetchContributionJson('/start', {
method: 'POST',
body: {
@@ -744,7 +746,7 @@
isContributionCallbackUrl,
isContributionFinalStatus,
pollContributionStatus,
- startContributionFlow,
+ startFlowContribution,
submitContributionCallback,
};
}
diff --git a/background/contribution/adapters/kiro-builder-id.js b/background/contribution/adapters/kiro-builder-id.js
new file mode 100644
index 0000000..2d31fc5
--- /dev/null
+++ b/background/contribution/adapters/kiro-builder-id.js
@@ -0,0 +1,245 @@
+(function attachBackgroundKiroBuilderIdContributionAdapter(root, factory) {
+ root.MultiPageBackgroundKiroBuilderIdContributionAdapter = factory(root);
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroBuilderIdContributionAdapterModule(root) {
+ const artifactApi = root?.MultiPageBackgroundKiroCredentialArtifact || {};
+ const FLOW_ID = artifactApi.FLOW_ID || 'kiro';
+ const ADAPTER_ID = artifactApi.ADAPTER_ID || 'kiro-builder-id';
+ const ARTIFACT_KIND = artifactApi.ARTIFACT_KIND || 'kiro-builder-id';
+ const DEFAULT_CONTRIBUTION_API_URL = 'https://flowpilot.qlhazycoder.top/api/contributions';
+
+ function isPlainObject(value) {
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
+ }
+
+ function cleanString(value = '') {
+ return String(value ?? '').trim();
+ }
+
+ function getErrorMessage(error) {
+ return error instanceof Error ? error.message : String(error ?? '未知错误');
+ }
+
+ function normalizeFlowId(state = {}) {
+ return cleanString(state.activeFlowId || state.flowId).toLowerCase() || 'openai';
+ }
+
+ function normalizeAdapterId(state = {}) {
+ return cleanString(state.contributionAdapterId || ADAPTER_ID).toLowerCase() || ADAPTER_ID;
+ }
+
+ function shouldSubmitKiroBuilderIdContribution(state = {}) {
+ return Boolean(state?.accountContributionEnabled)
+ && normalizeFlowId(state) === FLOW_ID
+ && normalizeAdapterId(state) === ADAPTER_ID;
+ }
+
+ function normalizeContributionApiUrl(value = '') {
+ const normalized = cleanString(value).replace(/\/+$/, '');
+ if (!normalized) {
+ return DEFAULT_CONTRIBUTION_API_URL;
+ }
+ if (/\/api\/contributions$/i.test(normalized)) {
+ return normalized;
+ }
+ return `${normalized}/api/contributions`;
+ }
+
+ function mergeFlowContributionRuntime(currentRuntime = {}, flowId = FLOW_ID, patch = {}) {
+ const runtime = isPlainObject(currentRuntime) ? currentRuntime : {};
+ const currentFlowRuntime = isPlainObject(runtime[flowId]) ? runtime[flowId] : {};
+ return {
+ ...runtime,
+ [flowId]: {
+ ...currentFlowRuntime,
+ ...patch,
+ },
+ };
+ }
+
+ function redactKnownArtifactSecrets(message = '', artifact = {}) {
+ let result = cleanString(message);
+ const secrets = [
+ artifact?.credentials?.refreshToken,
+ artifact?.credentials?.clientSecret,
+ ]
+ .map((value) => cleanString(value))
+ .filter(Boolean);
+ secrets.forEach((secret) => {
+ result = result.split(secret).join(artifactApi.redactSecret?.(secret) || '[redacted]');
+ });
+ return result;
+ }
+
+ async function readJsonResponse(response) {
+ const text = await response.text();
+ try {
+ return text ? JSON.parse(text) : {};
+ } catch (_error) {
+ return { message: text };
+ }
+ }
+
+ async function submitContributionArtifact(artifact = {}, options = {}) {
+ const fetchImpl = options.fetchImpl;
+ if (typeof fetchImpl !== 'function') {
+ throw new Error('账号贡献提交能力缺少 fetch。');
+ }
+ const endpoint = normalizeContributionApiUrl(options.apiUrl);
+ const payload = {
+ flow: FLOW_ID,
+ adapter_id: ADAPTER_ID,
+ artifact_kind: ARTIFACT_KIND,
+ source: 'flowpilot-extension',
+ contributor: {
+ nickname: cleanString(options.nickname),
+ qq: cleanString(options.qq),
+ },
+ artifact,
+ };
+ const response = await fetchImpl(endpoint, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+ const body = await readJsonResponse(response);
+ if (!response.ok || body?.ok === false) {
+ throw new Error(cleanString(body?.message || body?.detail || body?.error) || `贡献服务请求失败(HTTP ${response.status})。`);
+ }
+ return {
+ ok: true,
+ status: response.status,
+ contributionId: cleanString(body?.contribution_id || body?.contributionId || body?.id),
+ message: cleanString(body?.message) || '贡献提交成功',
+ raw: body,
+ };
+ }
+
+ function createKiroBuilderIdContributionAdapter(deps = {}) {
+ const {
+ addLog = async () => {},
+ contributionApiUrl = DEFAULT_CONTRIBUTION_API_URL,
+ fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
+ getState = async () => ({}),
+ setState = async () => {},
+ } = deps;
+
+ async function updateFlowContributionRuntime(currentState = {}, patch = {}) {
+ const runtime = mergeFlowContributionRuntime(currentState.flowContributionRuntime, FLOW_ID, {
+ adapterId: ADAPTER_ID,
+ ...patch,
+ });
+ await setState({
+ flowContributionRuntime: runtime,
+ });
+ return runtime;
+ }
+
+ async function log(message, level = 'info', nodeId = '') {
+ await addLog(message, level, nodeId ? { nodeId } : {});
+ }
+
+ async function maybeSubmitFlowContribution(state = {}, options = {}) {
+ const currentState = Object.keys(state || {}).length ? state : await getState();
+ const nodeId = cleanString(options.nodeId || 'kiro-complete-desktop-authorize');
+ if (!shouldSubmitKiroBuilderIdContribution(currentState)) {
+ return {
+ ok: true,
+ skipped: true,
+ reason: 'not_enabled_for_kiro_builder_id',
+ };
+ }
+
+ let artifact = null;
+ try {
+ artifact = artifactApi.buildKiroBuilderIdArtifact(currentState);
+ } catch (error) {
+ const message = redactKnownArtifactSecrets(getErrorMessage(error), artifact);
+ await updateFlowContributionRuntime(currentState, {
+ enabled: true,
+ status: 'skipped',
+ error: message,
+ lastMessage: message,
+ updatedAt: Date.now(),
+ });
+ await log(`Kiro 账号贡献已跳过:${message}`, 'warn', nodeId);
+ return {
+ ok: false,
+ skipped: true,
+ reason: error?.code || 'artifact_invalid',
+ message,
+ };
+ }
+
+ await updateFlowContributionRuntime(currentState, {
+ enabled: true,
+ status: 'submitting',
+ error: '',
+ lastMessage: '正在提交 Kiro Builder ID 贡献',
+ updatedAt: Date.now(),
+ });
+
+ const safeSummary = artifactApi.buildSafeArtifactSummary?.(artifact) || {};
+ await log(`Kiro 账号贡献:正在提交 Builder ID,邮箱 ${safeSummary.email || '未知'}。`, 'info', nodeId);
+
+ try {
+ const result = await submitContributionArtifact(artifact, {
+ apiUrl: options.contributionApiUrl || contributionApiUrl,
+ fetchImpl,
+ nickname: currentState.contributionNickname,
+ qq: currentState.contributionQq,
+ });
+ await updateFlowContributionRuntime(currentState, {
+ enabled: true,
+ status: 'submitted',
+ error: '',
+ contributionId: result.contributionId,
+ lastMessage: result.message,
+ submittedAt: Date.now(),
+ updatedAt: Date.now(),
+ });
+ await log(`Kiro 账号贡献:提交完成,${result.message}。`, 'ok', nodeId);
+ return result;
+ } catch (error) {
+ const message = redactKnownArtifactSecrets(getErrorMessage(error), artifact);
+ await updateFlowContributionRuntime(currentState, {
+ enabled: true,
+ status: 'error',
+ error: message,
+ lastMessage: message,
+ updatedAt: Date.now(),
+ });
+ await log(`Kiro 账号贡献提交失败:${message}`, 'warn', nodeId);
+ return {
+ ok: false,
+ skipped: false,
+ reason: 'submit_failed',
+ message,
+ };
+ }
+ }
+
+ return {
+ maybeSubmitFlowContribution,
+ shouldSubmitKiroBuilderIdContribution,
+ submitContributionArtifact: (artifact, options = {}) => submitContributionArtifact(artifact, {
+ ...options,
+ apiUrl: options.apiUrl || contributionApiUrl,
+ fetchImpl: options.fetchImpl || fetchImpl,
+ }),
+ };
+ }
+
+ return {
+ ADAPTER_ID,
+ ARTIFACT_KIND,
+ DEFAULT_CONTRIBUTION_API_URL,
+ FLOW_ID,
+ createKiroBuilderIdContributionAdapter,
+ normalizeContributionApiUrl,
+ shouldSubmitKiroBuilderIdContribution,
+ submitContributionArtifact,
+ };
+});
diff --git a/background/kiro/credential-artifact.js b/background/kiro/credential-artifact.js
new file mode 100644
index 0000000..11da827
--- /dev/null
+++ b/background/kiro/credential-artifact.js
@@ -0,0 +1,138 @@
+(function attachBackgroundKiroCredentialArtifact(root, factory) {
+ root.MultiPageBackgroundKiroCredentialArtifact = factory(root);
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroCredentialArtifactModule(root) {
+ const kiroStateApi = root?.MultiPageBackgroundKiroState || null;
+ const FLOW_ID = 'kiro';
+ const ADAPTER_ID = 'kiro-builder-id';
+ const ARTIFACT_KIND = 'kiro-builder-id';
+ const DEFAULT_REGION = kiroStateApi?.DEFAULT_REGION || 'us-east-1';
+ const DEFAULT_TARGET_ID = kiroStateApi?.DEFAULT_TARGET_ID || 'kiro-rs';
+ const BUILDER_ID_PROFILE_ARN = 'arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX';
+
+ function isPlainObject(value) {
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
+ }
+
+ function cleanString(value = '') {
+ return String(value ?? '').trim();
+ }
+
+ function readKiroRuntime(state = {}) {
+ return kiroStateApi?.ensureRuntimeState
+ ? kiroStateApi.ensureRuntimeState(state)
+ : (isPlainObject(state?.kiroRuntime) ? state.kiroRuntime : {});
+ }
+
+ function resolveKiroTargetId(state = {}, runtimeState = readKiroRuntime(state)) {
+ return cleanString(
+ state?.settingsState?.flows?.kiro?.targetId
+ || state?.flows?.kiro?.targetId
+ || state?.kiroTargetId
+ || runtimeState?.upload?.targetId
+ || DEFAULT_TARGET_ID
+ ) || DEFAULT_TARGET_ID;
+ }
+
+ function resolveEmail(state = {}, runtimeState = readKiroRuntime(state)) {
+ return cleanString(
+ runtimeState?.register?.email
+ || state?.email
+ || state?.registrationEmailState?.current
+ || state?.accountIdentifier
+ );
+ }
+
+ function resolveRegion(state = {}, runtimeState = readKiroRuntime(state), targetId = DEFAULT_TARGET_ID) {
+ return cleanString(
+ runtimeState?.desktopAuth?.region
+ || state?.settingsState?.flows?.kiro?.targets?.[targetId]?.region
+ || state?.flows?.kiro?.targets?.[targetId]?.region
+ || DEFAULT_REGION
+ ) || DEFAULT_REGION;
+ }
+
+ function assertRequiredField(name, value, message) {
+ if (!cleanString(value)) {
+ const error = new Error(message);
+ error.code = `missing_${name}`;
+ throw error;
+ }
+ }
+
+ function redactSecret(value = '') {
+ const normalized = cleanString(value);
+ if (!normalized) {
+ return '';
+ }
+ if (normalized.length <= 10) {
+ return `${normalized.slice(0, 2)}***`;
+ }
+ return `${normalized.slice(0, 6)}...${normalized.slice(-4)}`;
+ }
+
+ function buildKiroBuilderIdArtifact(state = {}, options = {}) {
+ const runtimeState = readKiroRuntime(state);
+ const desktopAuth = runtimeState?.desktopAuth || {};
+ const targetId = resolveKiroTargetId(state, runtimeState);
+ const region = resolveRegion(state, runtimeState, targetId);
+ const email = resolveEmail(state, runtimeState);
+ const refreshToken = String(desktopAuth.refreshToken || '');
+ const clientId = cleanString(desktopAuth.clientId);
+ const clientSecret = String(desktopAuth.clientSecret || '');
+
+ assertRequiredField('refreshToken', refreshToken, '缺少桌面授权 refreshToken,无法提交 Kiro Builder ID 贡献。');
+ assertRequiredField('clientId', clientId, '缺少桌面授权 clientId,无法提交 Kiro Builder ID 贡献。');
+ assertRequiredField('clientSecret', clientSecret, '缺少桌面授权 clientSecret,无法提交 Kiro Builder ID 贡献。');
+ assertRequiredField('email', email, '缺少注册邮箱,无法提交 Kiro Builder ID 贡献。');
+
+ return {
+ schemaVersion: 1,
+ flow: FLOW_ID,
+ adapter: ADAPTER_ID,
+ artifact: ARTIFACT_KIND,
+ account: {
+ email,
+ },
+ credentials: {
+ refreshToken,
+ clientId,
+ clientSecret,
+ profileArn: cleanString(options.profileArn) || BUILDER_ID_PROFILE_ARN,
+ authMethod: 'idc',
+ region,
+ authRegion: region,
+ apiRegion: region,
+ tokenSource: cleanString(desktopAuth.tokenSource) || 'desktop_authorization_code_pkce',
+ },
+ metadata: {
+ targetId,
+ authorizedAt: Math.max(0, Number(desktopAuth.authorizedAt) || 0),
+ generatedAt: new Date().toISOString(),
+ source: 'flowpilot-extension',
+ },
+ };
+ }
+
+ function buildSafeArtifactSummary(artifact = {}) {
+ return {
+ flow: cleanString(artifact.flow),
+ adapter: cleanString(artifact.adapter),
+ artifact: cleanString(artifact.artifact),
+ email: cleanString(artifact.account?.email),
+ clientId: cleanString(artifact.credentials?.clientId),
+ refreshToken: redactSecret(artifact.credentials?.refreshToken),
+ clientSecret: redactSecret(artifact.credentials?.clientSecret),
+ region: cleanString(artifact.credentials?.region),
+ };
+ }
+
+ return {
+ ADAPTER_ID,
+ ARTIFACT_KIND,
+ BUILDER_ID_PROFILE_ARN,
+ FLOW_ID,
+ buildKiroBuilderIdArtifact,
+ buildSafeArtifactSummary,
+ redactSecret,
+ };
+});
diff --git a/background/kiro/desktop-authorize-runner.js b/background/kiro/desktop-authorize-runner.js
index 8bf0bf6..a900e53 100644
--- a/background/kiro/desktop-authorize-runner.js
+++ b/background/kiro/desktop-authorize-runner.js
@@ -365,6 +365,7 @@
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
isTabAlive = async () => false,
+ maybeSubmitFlowContribution = async () => null,
KIRO_REGISTER_INJECT_FILES = null,
KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null,
pollCloudflareTempEmailVerificationCode = null,
@@ -477,6 +478,15 @@
},
});
await log('步骤 8:桌面授权回调已捕获,Token 换取成功。', 'ok', nodeId);
+ await maybeSubmitFlowContribution({
+ ...currentState,
+ ...payload,
+ }, {
+ nodeId,
+ trigger: 'kiro-step-8',
+ }).catch(async (error) => {
+ await log(`步骤 8:Kiro 公共贡献提交异常,已保留桌面授权结果:${getErrorMessage(error)}`, 'warn', nodeId);
+ });
await completeNodeFromBackground(nodeId, payload);
return payload;
}
diff --git a/background/message-router.js b/background/message-router.js
index cebebce..c1b3d79 100644
--- a/background/message-router.js
+++ b/background/message-router.js
@@ -67,7 +67,7 @@
}
return Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
- && !Boolean(state?.contributionMode);
+ && !Boolean(state?.accountContributionEnabled);
},
resolveSignupMethod = (state = {}) => {
const method = normalizeSignupMethod(state?.signupMethod);
@@ -149,6 +149,7 @@
patchMail2925Account,
patchHotmailAccount,
pollContributionStatus,
+ submitFlowContribution,
registerTab,
requestStop,
probeIpProxyExit,
@@ -162,7 +163,7 @@
setCurrentPayPalAccount,
setCurrentMail2925Account,
setCurrentHotmailAccount,
- setContributionMode,
+ setAccountContributionMode,
setEmailState,
setEmailStateSilently,
persistRegistrationEmailState,
@@ -179,7 +180,7 @@
setNodeStatus,
skipAutoRunCountdown,
skipNode,
- startContributionFlow,
+ startFlowContribution,
startAutoRunLoop,
deleteMail2925Account,
deleteMail2925Accounts,
@@ -1154,59 +1155,61 @@
return await setFreeReusablePhoneActivation(message.payload || {});
}
- case 'SET_CONTRIBUTION_MODE': {
+ case 'SET_ACCOUNT_CONTRIBUTION_MODE': {
const enabled = Boolean(message.payload?.enabled);
- const state = await ensureManualInteractionAllowed(enabled ? '进入贡献模式' : '退出贡献模式');
+ const state = await ensureManualInteractionAllowed(enabled ? '进入账号贡献' : '退出账号贡献');
if (Object.values(state.nodeStatuses || {}).some((status) => status === 'running')) {
- throw new Error(enabled ? '当前有步骤正在执行,无法进入贡献模式。' : '当前有步骤正在执行,无法退出贡献模式。');
+ throw new Error(enabled ? '当前有步骤正在执行,无法进入账号贡献。' : '当前有步骤正在执行,无法退出账号贡献。');
}
- if (typeof setContributionMode !== 'function') {
- throw new Error('贡献模式切换能力未接入。');
+ if (typeof setAccountContributionMode !== 'function') {
+ throw new Error('账号贡献切换能力未接入。');
}
return {
ok: true,
- state: await setContributionMode(enabled),
+ state: await setAccountContributionMode(enabled, {
+ adapterId: message.payload?.adapterId,
+ flowId: message.payload?.flowId || state?.activeFlowId || state?.flowId,
+ }),
};
}
- case 'START_CONTRIBUTION_FLOW': {
+ case 'START_FLOW_CONTRIBUTION': {
const state = await ensureManualInteractionAllowed('开始贡献');
if (Object.values(state.nodeStatuses || {}).some((status) => status === 'running')) {
throw new Error('当前有步骤正在执行,无法开始贡献流程。');
}
- if (typeof startContributionFlow !== 'function') {
+ if (!state?.accountContributionEnabled) {
+ throw new Error('请先进入账号贡献。');
+ }
+ if (typeof startFlowContribution !== 'function') {
throw new Error('贡献 OAuth 流程尚未接入。');
}
return {
ok: true,
- state: await startContributionFlow({
+ state: await startFlowContribution({
nickname: message.payload?.nickname,
qq: message.payload?.qq,
}),
};
}
- case 'SET_CONTRIBUTION_PROFILE': {
+ case 'SUBMIT_FLOW_CONTRIBUTION': {
const state = await getState();
- if (!state?.contributionMode) {
- throw new Error('请先进入贡献模式。');
+ if (!state?.accountContributionEnabled) {
+ 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 位。');
+ if (typeof submitFlowContribution !== 'function') {
+ throw new Error('贡献提交能力尚未接入。');
}
- await setState({
- contributionNickname: nickname,
- contributionQq: qq,
- });
return {
ok: true,
- state: await getState(),
+ state: await submitFlowContribution(message.payload?.callbackUrl, {
+ reason: message.payload?.reason || 'sidepanel_submit',
+ }),
};
}
- case 'POLL_CONTRIBUTION_STATUS': {
+ case 'POLL_FLOW_CONTRIBUTION_STATUS': {
if (typeof pollContributionStatus !== 'function') {
throw new Error('贡献状态轮询能力尚未接入。');
}
@@ -1285,8 +1288,11 @@
if (message.source === 'sidepanel') {
await lockAutomationWindowFromMessage(message, sender);
}
- if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
- await setContributionMode(true);
+ if (Boolean(message.payload?.accountContributionEnabled) && typeof setAccountContributionMode === 'function') {
+ await setAccountContributionMode(true, {
+ adapterId: message.payload?.contributionAdapterId,
+ flowId: message.payload?.activeFlowId || message.payload?.flowId,
+ });
if (typeof setState === 'function') {
const contributionNickname = String(message.payload?.contributionNickname || '').trim();
const contributionQq = String(message.payload?.contributionQq || '').trim();
@@ -1325,8 +1331,11 @@
if (message.source === 'sidepanel') {
await lockAutomationWindowFromMessage(message, sender);
}
- if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
- await setContributionMode(true);
+ if (Boolean(message.payload?.accountContributionEnabled) && typeof setAccountContributionMode === 'function') {
+ await setAccountContributionMode(true, {
+ adapterId: message.payload?.contributionAdapterId,
+ flowId: message.payload?.activeFlowId || message.payload?.flowId,
+ });
if (typeof setState === 'function') {
const contributionNickname = String(message.payload?.contributionNickname || '').trim();
const contributionQq = String(message.payload?.contributionQq || '').trim();
@@ -1444,7 +1453,7 @@
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
|| Object.prototype.hasOwnProperty.call(updates, 'panelMode')
|| Object.prototype.hasOwnProperty.call(updates, 'activeFlowId')
- || Object.prototype.hasOwnProperty.call(updates, 'contributionMode')
+ || Object.prototype.hasOwnProperty.call(updates, 'accountContributionEnabled')
) {
updates.signupMethod = resolveSignupMethod(nextSignupState);
}
@@ -1576,8 +1585,11 @@
error: error?.message || String(error || '代理应用失败'),
}));
}
- if (Boolean(currentState?.contributionMode) && typeof setContributionMode === 'function') {
- await setContributionMode(true);
+ if (Boolean(currentState?.accountContributionEnabled) && typeof setAccountContributionMode === 'function') {
+ await setAccountContributionMode(true, {
+ adapterId: currentState?.contributionAdapterId,
+ flowId: currentState?.activeFlowId || currentState?.flowId,
+ });
}
if (Object.keys(stateUpdates).length > 0 && typeof broadcastDataUpdate === 'function') {
broadcastDataUpdate(stateUpdates);
diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js
index 5c2747d..746f126 100644
--- a/background/steps/oauth-login.js
+++ b/background/steps/oauth-login.js
@@ -59,7 +59,7 @@
return normalizeStep7SignupMethod(state?.signupMethod) === 'phone'
&& Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
- && !Boolean(state?.contributionMode);
+ && !Boolean(state?.accountContributionEnabled);
}
function hasStep7PhoneSignupIdentity(state = {}) {
diff --git a/docs/多Flow资源入口与贡献体系升级开发方案.md b/docs/多Flow资源入口与贡献体系升级开发方案.md
new file mode 100644
index 0000000..104a99e
--- /dev/null
+++ b/docs/多Flow资源入口与贡献体系升级开发方案.md
@@ -0,0 +1,1054 @@
+# 多 Flow 贡献/使用教程组合入口与账号贡献体系开发方案
+
+> 本文是唯一开发方案。目标固定为:用户在任意已发布 flow 中都能贡献当前 flow 产出的账号。OpenAI、Kiro、新增 flow 均通过统一账号贡献体系接入,不再把贡献能力绑定到 OpenAI OAuth。
+
+## 实施状态
+
+截至 2026-05-19,本方案已按开发清单落地到扩展与账号贡献门户:
+
+1. 扩展端已统一使用 `accountContributionEnabled`、`contributionAdapterId`、`flowContributionRuntime` 与 `SET_ACCOUNT_CONTRIBUTION_MODE` / `START_FLOW_CONTRIBUTION` / `POLL_FLOW_CONTRIBUTION_STATUS`。
+2. 扩展端不保留旧贡献 message 兼容入口;`contributionMode` 仅允许出现在一次性 storage migration 与迁移测试中。
+3. 顶部“贡献/使用教程”是组合入口:点击后按当前 `activeFlowId` 打开当前 flow 教程 URL,并在可切换时开启当前 flow 的账号贡献模式。
+4. OpenAI 与 Kiro 均已注册贡献 adapter;Kiro 使用 `kiro-builder-id`,步骤 8 产出 Builder ID 后自动提交公共贡献。
+5. 账号贡献门户已新增统一 `POST /api/contributions`,可接收 OpenAI `openai-oauth`、`openai-codex-file`、`openai-sub2api-file` 与 Kiro `kiro-builder-id` artifact;旧 `/api/upload` 仅作为历史 HTTP 入口保留,并委托统一 intake。
+6. 门户数据库、管理后台、内容摘要均补齐 `flow_id`、`adapter_id`、`artifact_kind` 或 flow/target 作用域,后台返回逐账号详情时会脱敏凭据。
+
+## 0. 硬性目标
+
+1. 任意已发布 flow 必须具备账号贡献入口。
+2. 任意已发布 flow 必须注册 `ContributionAdapter`。
+3. 任意已发布 flow 必须在门户端注册对应 `IntakeSpec`。
+4. “贡献账号”和“使用教程”共用同一个顶部组合入口,点击后同时打开当前 flow 的教程/门户并开启当前 flow 的贡献模式。
+5. OpenAI 历史贡献格式必须升级到统一 `ContributionArtifact + IntakeSpec` 体系。
+6. OpenAI OAuth 贡献实现迁入 `openai-oauth` adapter,不作为其他 flow 的通用实现。
+7. Kiro 账号贡献使用 `kiro-builder-id` 凭据包实现。
+8. 新增 flow 没有 contribution adapter 时不得发布。
+9. 扩展端不保留历史接口、历史 message、历史兼容入口,目的是让后续扩展开发只面对一套统一协议。
+10. 扩展端只允许保留一次性 storage migration,用于把历史存储字段迁到新字段,迁移后运行时代码不得继续读取旧字段。
+11. 服务器后端允许保留历史 HTTP 入口,目的是兼容仍在使用旧版本扩展、旧上传页面或外部旧请求的用户。
+12. 服务器后端历史 HTTP 入口必须加注释说明,并且只能委托统一 intake,不能承载新业务逻辑。
+
+开发结论:
+
+1. 侧边栏顶部“贡献/使用教程”保持为单个组合按钮。
+2. 点击组合按钮后按当前 `activeFlowId` 解析贡献 adapter。
+3. 点击组合按钮后打开当前 flow 对应的教程/门户页面。
+4. 点击组合按钮后开启当前 flow 的账号贡献模式。
+5. 自动流程在账号产物 ready 后调用当前 flow 的 contribution adapter。
+6. `codex`、`sub2api`、`openai-oauth`、`kiro-builder-id` 全部走统一 artifact envelope。
+7. 贡献门户通过统一 API 接收不同 flow 的 artifact。
+8. 贡献门户通过 intake registry 校验、去重、记录和展示不同 flow 的贡献。
+
+## 1. 当前代码结论
+
+### 1.1 侧边栏按钮
+
+文件:`sidepanel/sidepanel.html`
+
+当前按钮:
+
+```html
+
+```
+
+现状问题:
+
+1. 按钮文案包含“贡献”和“教程”。
+2. 点击行为被 `sidepanel/contribution-mode.js` 绑定到 OpenAI 贡献模式。
+3. 非 OpenAI flow 会被禁用并显示“当前 flow 不支持贡献模式”。
+4. 内容更新提示绑定到 contribution 命名,但实际包含教程、公告。
+
+开发处理:
+
+1. 沿用顶部入口。
+2. 顶部入口继续是“贡献/使用教程”组合动作。
+3. 点击后打开当前 flow 对应教程/门户页面。
+4. 点击后开启当前 flow 的账号贡献模式。
+5. 贡献模式由 `activeFlowId` 选择 adapter,不再写死 OpenAI。
+
+### 1.2 OpenAI 贡献逻辑
+
+文件:
+
+1. `sidepanel/contribution-mode.js`
+2. `background/contribution-oauth.js`
+3. `background/message-router.js`
+
+当前事实:
+
+1. `contributionMode` 是全局布尔字段。
+2. `isContributionModeAvailable()` 只允许 OpenAI。
+3. `contribution-oauth.js` 是 OpenAI OAuth donation 实现。
+4. message router 中的贡献消息是 OpenAI 语义。
+
+开发处理:
+
+1. `contributionMode` 只允许在一次性 storage migration 中读取,迁移完成后扩展运行时代码不再引用该字段。
+2. 新增 canonical 字段 `accountContributionEnabled`。
+3. 新增 canonical 字段 `contributionAdapterId`。
+4. OpenAI OAuth 实现迁入 `openai-oauth` adapter。
+5. OpenAI `codex/sub2api` 文件贡献迁入对应 adapter 和 intake。
+6. 删除扩展端历史 contribution message 入口,所有调用点一次性改为新 flow contribution message。
+
+### 1.3 Kiro 账号产物
+
+文件:
+
+1. `background/kiro/state.js`
+2. `background/kiro/desktop-authorize-runner.js`
+3. `background/kiro/publisher-kiro-rs.js`
+4. `data/step-definitions.js`
+
+当前事实:
+
+1. Kiro 已有步骤 7:启动桌面授权。
+2. Kiro 已有步骤 8:完成桌面授权。
+3. Kiro 已有步骤 9:上传凭据到 `kiro.rs`。
+4. `kiroRuntime.desktopAuth` 保存 `refreshToken`、`clientId`、`clientSecret`、`region`。
+5. `kiroRuntime.register` 保存 `email`。
+6. `publisher-kiro-rs.js` 已能构建 Builder ID 凭据 payload。
+
+开发处理:
+
+1. Kiro contribution adapter 复用 Kiro runtime 中的 Builder ID 授权产物。
+2. Kiro 公共贡献不依赖用户自己的 `kiro.rs` 地址和 API key。
+3. Kiro 步骤 8 完成后提交公共贡献。
+4. Kiro 步骤 9 继续负责用户私有 `kiro.rs` 发布。
+
+### 1.4 贡献门户
+
+目录:`账号贡献/contrib-portal`
+
+当前事实:
+
+1. `validation.py` 只识别 `codex` 和 `sub2api`。
+2. `/api/upload` 只处理当前上传格式。
+3. `ValidatedUpload`、`AccountRecord`、`uploads`、`account_keys` 已具备扩展新 kind 的基础。
+4. `/api/content-summary` 是全局内容摘要,不区分 flow。
+
+开发处理:
+
+1. 将现有 `codex/sub2api` 校验迁入 intake registry。
+2. 新增 `openai-oauth` intake。
+3. 新增 `kiro-builder-id` intake。
+4. 新增统一贡献 API:`POST /api/contributions`。
+5. 服务器端 `/api/upload` 保留为历史 HTTP 入口,用于兼容旧版本扩展、旧上传页面和外部旧请求。
+6. `/api/upload` 内部调用统一 intake registry。
+7. `/api/upload` route 必须写代码注释,声明该入口仅为历史兼容入口,不允许新增业务逻辑。
+8. 数据库记录增加 flow、target、artifact、adapter 字段。
+9. 管理后台按 flow、kind、adapter 展示贡献记录。
+
+## 2. 单一目标架构
+
+### 2.1 ContributionTutorialEntry
+
+`ContributionTutorialEntry` 是顶部“贡献/使用教程”组合入口。它不是单纯教程链接,也不是 OpenAI 专属贡献按钮;它在一次点击中完成两件事:
+
+1. 打开当前 flow 对应的教程/门户页面。
+2. 开启当前 flow 的账号贡献模式。
+
+统一结构:
+
+```js
+{
+ id: "kiro-contribution-tutorial",
+ flowId: "kiro",
+ label: "贡献/使用教程",
+ portalUrl: "https://flowpilot.qlhazycoder.top/tutorial?flow=kiro&target=kiro-rs",
+ contributionAdapterId: "kiro-builder-id",
+ action: "open-portal-and-enable-contribution"
+}
+```
+
+执行规则:
+
+1. 先按 `activeFlowId` 获取当前 flow 的 entry。
+2. 打开 `portalUrl`。
+3. 设置 `accountContributionEnabled = true`。
+4. 设置当前 flow 的 `contributionAdapterId`。
+5. 刷新侧边栏贡献状态。
+
+### 2.2 AccountContributionMode
+
+`AccountContributionMode` 表示本次运行要提交账号贡献。
+
+canonical 状态:
+
+```js
+{
+ accountContributionEnabled: true,
+ contributionAdapterId: "kiro-builder-id",
+ contributionProfile: {
+ contributorName: "",
+ contributorKey: "",
+ visibility: "public"
+ }
+}
+```
+
+一次性 storage migration:
+
+```js
+if (oldState.contributionMode === true) {
+ nextState.accountContributionEnabled = true;
+ nextState.contributionAdapterId = "openai-oauth";
+}
+delete nextState.contributionMode;
+```
+
+规则:
+
+1. storage migration 是扩展端唯一允许读取 `contributionMode` 的位置。
+2. 迁移后扩展运行时代码只读取 `accountContributionEnabled`。
+3. 迁移后扩展运行时代码只读取 `contributionAdapterId`。
+4. OpenAI 历史 `contributionMode: true` 迁移为 `contributionAdapterId = openai-oauth`。
+5. Kiro 贡献模式使用 `contributionAdapterId = kiro-builder-id`。
+6. 新增 flow 贡献模式使用该 flow 的默认 adapter。
+7. 切换 flow 时根据当前 flow 重新解析 `contributionAdapterId`。
+
+### 2.3 ContributionAdapter
+
+`ContributionAdapter` 是 flow 账号贡献实现。
+
+统一接口:
+
+```js
+{
+ id: "kiro-builder-id",
+ flowId: "kiro",
+ artifactKind: "kiro-builder-id",
+ isReady(state) {},
+ buildArtifact(state, context) {},
+ validateArtifact(artifact) {},
+ submitArtifact(artifact, context) {},
+ redactForLog(value) {}
+}
+```
+
+职责:
+
+1. 判断当前 flow 是否已产出可贡献账号。
+2. 构建当前 flow 的账号贡献 artifact。
+3. 本地校验 artifact。
+4. 调用门户贡献 API。
+5. 保存贡献提交结果。
+6. 对日志、错误、UI 状态做敏感信息脱敏。
+
+### 2.4 ContributionArtifact
+
+所有 flow 使用统一 envelope:
+
+```json
+{
+ "schemaVersion": 1,
+ "flowId": "kiro",
+ "targetId": "kiro-rs",
+ "artifactKind": "kiro-builder-id",
+ "adapterId": "kiro-builder-id",
+ "source": "codex-oauth-automation-extension",
+ "createdAt": "2026-05-19T00:00:00.000Z",
+ "contributor": {
+ "key": "name:example",
+ "name": "example",
+ "visibility": "public"
+ },
+ "identity": {
+ "email": "user@example.com",
+ "canonicalId": "kiro-builder-id:"
+ },
+ "credentials": {},
+ "metadata": {}
+}
+```
+
+### 2.5 IntakeSpec
+
+门户端每种 artifact 对应一个 `IntakeSpec`。
+
+统一结构:
+
+```python
+@dataclass(frozen=True)
+class IntakeSpec:
+ kind: str
+ flow_id: str
+ filename_patterns: tuple[Pattern[str], ...]
+ max_bytes: int
+ validate: Callable[[str, bytes], ValidatedUpload]
+```
+
+首批 intake:
+
+1. `codex`:OpenAI Codex artifact。
+2. `sub2api`:OpenAI Sub2API artifact。
+3. `openai-oauth`:OpenAI OAuth artifact。
+4. `kiro-builder-id`:Kiro Builder ID artifact。
+
+统一升级规则:
+
+1. `codex` 不再作为孤立文件格式处理,进入系统后必须转换为 `flowId=openai`、`artifactKind=codex` 的 `ContributionArtifact`。
+2. `sub2api` 不再作为孤立文件格式处理,进入系统后必须转换为 `flowId=openai`、`artifactKind=sub2api` 的 `ContributionArtifact`。
+3. `openai-oauth` 不再作为独立 OAuth 网关记录处理,进入系统后必须转换为 `flowId=openai`、`artifactKind=openai-oauth` 的 `ContributionArtifact`。
+4. `kiro-builder-id` 使用同一 envelope,进入系统后写入 `flowId=kiro`、`artifactKind=kiro-builder-id`。
+5. 所有 artifact 都由 intake registry 输出 `ValidatedUpload` 和 `AccountRecord`。
+6. 所有 artifact 都写入统一 flow、target、artifact、adapter 字段。
+7. 所有 artifact 都使用统一去重、审核、统计、展示链路。
+
+## 3. Flow 贡献实现
+
+### 3.1 OpenAI
+
+OpenAI adapters:
+
+```js
+[
+ {
+ id: "openai-oauth",
+ flowId: "openai",
+ artifactKind: "openai-oauth",
+ trigger: "interactive-oauth"
+ },
+ {
+ id: "openai-codex-file",
+ flowId: "openai",
+ artifactKind: "codex",
+ trigger: "manual-upload"
+ },
+ {
+ id: "openai-sub2api-file",
+ flowId: "openai",
+ artifactKind: "sub2api",
+ trigger: "manual-upload"
+ }
+]
+```
+
+处理规则:
+
+1. `background/contribution-oauth.js` 的可复用逻辑迁入 `background/contribution/adapters/openai-oauth.js`。
+2. `openai-oauth` adapter 输出统一 `ContributionArtifact`。
+3. `openai-codex-file` adapter 将 Codex JSON 转换为统一 `ContributionArtifact`。
+4. `openai-sub2api-file` adapter 将 Sub2API bundle 转换为统一 `ContributionArtifact`。
+5. OpenAI 自动贡献和手动上传最终都提交到 `/api/contributions`。
+6. 扩展端手动上传也调用 `/api/contributions`。
+7. 扩展端删除 `SET_CONTRIBUTION_MODE`、`START_CONTRIBUTION_FLOW` 等历史 message 入口。
+8. 扩展端所有调用点改为 `SET_ACCOUNT_CONTRIBUTION_MODE`、`START_FLOW_CONTRIBUTION`、`SUBMIT_FLOW_CONTRIBUTION`、`POLL_FLOW_CONTRIBUTION_STATUS`。
+9. 扩展端测试必须断言历史 message 字符串不再出现在运行时代码中。
+
+### 3.2 Kiro
+
+Kiro adapter:
+
+```js
+{
+ id: "kiro-builder-id",
+ flowId: "kiro",
+ artifactKind: "kiro-builder-id",
+ trigger: "after-desktop-authorize"
+}
+```
+
+Kiro artifact 必需字段:
+
+```json
+{
+ "schemaVersion": 1,
+ "flowId": "kiro",
+ "targetId": "kiro-rs",
+ "artifactKind": "kiro-builder-id",
+ "adapterId": "kiro-builder-id",
+ "identity": {
+ "email": "user@example.com",
+ "canonicalId": "kiro-builder-id:"
+ },
+ "credentials": {
+ "refreshToken": "...",
+ "clientId": "...",
+ "clientSecret": "...",
+ "profileArn": "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX",
+ "authMethod": "idc",
+ "region": "us-east-1",
+ "authRegion": "us-east-1",
+ "apiRegion": "us-east-1"
+ }
+}
+```
+
+Kiro 本地校验:
+
+1. `identity.email` 必须是合法邮箱。
+2. `credentials.refreshToken` 必须非空。
+3. `credentials.clientId` 必须非空。
+4. `credentials.clientSecret` 必须非空。
+5. `credentials.region`、`authRegion`、`apiRegion` 缺省写入 `us-east-1`。
+6. `identity.canonicalId` 使用 `buildMachineId(refreshToken)` 生成。
+
+Kiro 自动提交点:
+
+1. 步骤 8 完成桌面授权后提交公共贡献。
+2. 步骤 9 继续上传到用户自己的 `kiro.rs`。
+3. 未配置 `kiro.rs` 时公共贡献仍执行。
+4. 配置 `kiro.rs` 时公共贡献和私有上传分别记录状态。
+
+### 3.3 新增 Flow
+
+新增 flow 发布前必须提供:
+
+1. `flowId`。
+2. 默认 `targetId`。
+3. `ContributionAdapter`。
+4. `artifactKind`。
+5. artifact builder。
+6. 本地校验规则。
+7. 门户 `IntakeSpec`。
+8. 敏感字段脱敏规则。
+9. UI 贡献入口测试。
+10. 门户 intake 测试。
+
+缺少任一项时,该 flow 不进入发布列表。
+
+## 4. 扩展端开发内容
+
+### 4.1 新增 `shared/contribution-registry.js`
+
+职责:
+
+1. 注册所有 flow 的 contribution adapters。
+2. 提供 `getContributionAdapters(flowId)`。
+3. 提供 `getDefaultContributionAdapter(flowId)`。
+4. 校验已发布 flow 必须存在 adapter。
+5. 为 sidepanel、background、tests 提供统一事实来源。
+
+注册内容:
+
+```js
+const FLOW_CONTRIBUTION_ADAPTERS = {
+ openai: [
+ { id: "openai-oauth", artifactKind: "openai-oauth", trigger: "interactive-oauth" },
+ { id: "openai-codex-file", artifactKind: "codex", trigger: "manual-upload" },
+ { id: "openai-sub2api-file", artifactKind: "sub2api", trigger: "manual-upload" }
+ ],
+ kiro: [
+ { id: "kiro-builder-id", artifactKind: "kiro-builder-id", trigger: "after-desktop-authorize" }
+ ]
+};
+```
+
+### 4.2 修改 `shared/flow-registry.js`
+
+新增 capability:
+
+```js
+supportsAccountContribution: true
+```
+
+规则:
+
+1. OpenAI 设置 `supportsAccountContribution: true`。
+2. Kiro 设置 `supportsAccountContribution: true`。
+3. `supportsContributionMode` 仅作为迁移字段映射到 `supportsAccountContribution`。
+4. 新增 flow 只有在注册 adapter 后才能设置 `supportsAccountContribution: true`。
+
+### 4.3 修改 `shared/flow-capabilities.js`
+
+处理规则:
+
+1. 删除贡献能力对 `activeFlowId === "openai"` 的通用依赖。
+2. `canShowContributionMode()` 改为读取 `supportsAccountContribution`。
+3. OpenAI OAuth 专属 UI 读取 `supportsOpenAiOAuthContribution`。
+4. Kiro 贡献 UI 读取 `kiro-builder-id` adapter 是否存在。
+5. `validateAutoRunStart()` 校验当前 flow 是否存在 adapter。
+
+### 4.4 新增 background contribution manager
+
+新增文件:
+
+```text
+background/contribution/manager.js
+background/contribution/client.js
+background/contribution/adapters/openai-oauth.js
+background/contribution/adapters/kiro-builder-id.js
+```
+
+manager 职责:
+
+1. 根据 `activeFlowId` 选择 adapter。
+2. 读取 `accountContributionEnabled`。
+3. 在账号产物 ready 后调用 adapter。
+4. 保存 `flowContributionRuntime`。
+5. 广播状态给 sidepanel。
+
+runtime:
+
+```js
+flowContributionRuntime: {
+ active: false,
+ flowId: "",
+ adapterId: "",
+ artifactKind: "",
+ status: "",
+ uploadId: null,
+ lastMessage: "",
+ lastError: "",
+ submittedAt: 0
+}
+```
+
+### 4.5 新增 Kiro artifact helper
+
+新增文件:
+
+```text
+background/kiro/credential-artifact.js
+```
+
+职责:
+
+1. 从 `kiroRuntime.desktopAuth` 读取授权凭据。
+2. 从 `kiroRuntime.register` 读取邮箱。
+3. 调用 `buildMachineId(refreshToken)` 生成 canonical id。
+4. 构建 `kiro-builder-id` artifact。
+5. 对 artifact 做本地校验。
+6. 提供日志脱敏方法。
+
+### 4.6 修改 Kiro 自动流程
+
+修改位置:
+
+1. `background/kiro/desktop-authorize-runner.js`
+2. `background/kiro/publisher-kiro-rs.js`
+3. Kiro runner 调度处
+
+处理规则:
+
+1. 步骤 8 完成后调用 `maybeSubmitFlowContribution()`。
+2. 贡献失败不写入完整 secret。
+3. 贡献失败按照配置决定是否中断本轮。
+4. 步骤 9 私有上传状态与公共贡献状态分开。
+
+默认失败策略:
+
+1. 公共贡献接口不可达:记录 warn,不阻断 Kiro 注册成功。
+2. artifact 本地校验失败:记录 error,阻断贡献提交,不阻断私有上传。
+3. 用户显式要求“贡献失败即停止”时,中断自动流程。
+
+### 4.7 修改 sidepanel
+
+新增文件:
+
+```text
+sidepanel/flow-contribution.js
+sidepanel/contribution-tutorial-entry.js
+```
+
+UI 规则:
+
+1. 顶部按钮保持 `贡献/使用教程`。
+2. 点击按钮后打开当前 flow 对应教程/门户。
+3. 点击按钮后开启当前 flow 的账号贡献模式。
+4. OpenAI 点击后使用 `openai-oauth` adapter。
+5. Kiro 点击后使用 `kiro-builder-id` adapter。
+6. 不再对 Kiro 显示“当前 flow 不支持贡献模式”。
+
+## 5. 门户端开发内容
+
+### 5.1 新增 `POST /api/contributions`
+
+请求:
+
+```json
+{
+ "flow_id": "kiro",
+ "target_id": "kiro-rs",
+ "kind": "kiro-builder-id",
+ "adapter_id": "kiro-builder-id",
+ "contributor_name": "example",
+ "contributor_key": "name:example",
+ "artifact": {}
+}
+```
+
+响应:
+
+```json
+{
+ "ok": true,
+ "upload_id": 123,
+ "new_accounts": 1,
+ "updated_accounts": 0,
+ "message": "贡献已提交"
+}
+```
+
+### 5.2 修改数据库
+
+`uploads` 表新增字段:
+
+```sql
+flow_id TEXT NOT NULL DEFAULT 'openai'
+target_id TEXT NOT NULL DEFAULT ''
+artifact_kind TEXT NOT NULL DEFAULT ''
+contribution_method TEXT NOT NULL DEFAULT 'manual_upload'
+adapter_id TEXT NOT NULL DEFAULT ''
+```
+
+`upload_account_reviews` 增加字段:
+
+```sql
+flow_id TEXT NOT NULL DEFAULT 'openai'
+artifact_kind TEXT NOT NULL DEFAULT ''
+```
+
+迁移规则:
+
+1. 历史 `kind = codex` 记录补齐 `flow_id = openai`、`artifact_kind = codex`、`adapter_id = openai-codex-file`。
+2. 历史 `kind = sub2api` 记录补齐 `flow_id = openai`、`artifact_kind = sub2api`、`adapter_id = openai-sub2api-file`。
+3. 历史 OAuth 记录补齐 `flow_id = openai`、`artifact_kind = openai-oauth`、`adapter_id = openai-oauth`。
+4. 新 Kiro 记录写入 `flow_id = kiro`、`artifact_kind = kiro-builder-id`、`adapter_id = kiro-builder-id`。
+5. 新增记录不得缺失 flow、artifact、adapter 字段。
+
+### 5.3 修改 `validation.py`
+
+新增 intake registry:
+
+```python
+INTAKE_SPECS = {
+ "codex": CodexIntakeSpec(),
+ "sub2api": Sub2ApiIntakeSpec(),
+ "openai-oauth": OpenAiOAuthIntakeSpec(),
+ "kiro-builder-id": KiroBuilderIdIntakeSpec(),
+}
+```
+
+`kiro-builder-id` 校验:
+
+1. payload 必须是 JSON object。
+2. `flowId` 必须等于 `kiro`。
+3. `artifactKind` 必须等于 `kiro-builder-id`。
+4. `identity.email` 必须是合法邮箱。
+5. `identity.canonicalId` 必须非空。
+6. `credentials.refreshToken` 必须非空。
+7. `credentials.clientId` 必须非空。
+8. `credentials.clientSecret` 必须非空。
+9. secret 字段长度必须有上限。
+10. 输出 `ValidatedUpload(kind="kiro-builder-id")`。
+
+OpenAI intake 统一校验:
+
+1. `codex` 校验逻辑迁入 `CodexIntakeSpec`。
+2. `sub2api` 校验逻辑迁入 `Sub2ApiIntakeSpec`。
+3. `openai-oauth` 校验逻辑迁入 `OpenAiOAuthIntakeSpec`。
+4. 三类 OpenAI intake 都必须输出统一 `ValidatedUpload`。
+5. 三类 OpenAI intake 都必须写入 `flow_id = openai`。
+6. 三类 OpenAI intake 都必须写入对应 `artifact_kind` 和 `adapter_id`。
+7. `/api/upload` 与 `/api/contributions` 使用同一套 OpenAI intake 代码。
+8. `/api/upload` 文件顶部或 route 附近必须有注释说明:
+
+```python
+# 历史 HTTP 入口:保留给旧上传页面和外部旧请求。
+# 新开发不得在本 route 内新增贡献业务逻辑。
+# 所有格式校验、去重、入库必须委托 unified contribution intake registry。
+```
+
+### 5.4 管理后台
+
+管理后台必须展示:
+
+1. flow。
+2. target。
+3. artifact kind。
+4. adapter。
+5. contributor。
+6. total accounts。
+7. new accounts。
+8. duplicate accounts。
+9. review status。
+
+敏感信息展示规则:
+
+1. `refreshToken` 不展示完整值。
+2. `clientSecret` 不展示完整值。
+3. proxy password 不展示完整值。
+4. 导出数据按管理员权限控制。
+5. 错误日志不包含完整 secret。
+
+## 6. 自动流程接入点
+
+### 6.1 通用接入函数
+
+新增:
+
+```js
+await maybeSubmitFlowContribution({
+ flowId,
+ targetId,
+ state,
+ nodeId,
+ reason: "artifact-ready"
+});
+```
+
+执行条件:
+
+1. `accountContributionEnabled === true`。
+2. 当前 flow 存在 adapter。
+3. adapter `isReady(state)` 返回 true。
+4. 本次运行未提交同一个 canonical id。
+5. 本地校验通过。
+
+### 6.2 OpenAI 接入点
+
+OpenAI 接入:
+
+1. OAuth callback 完成。
+2. Codex session artifact ready。
+3. Sub2API artifact ready。
+
+### 6.3 Kiro 接入点
+
+Kiro 接入:
+
+1. 步骤 8 完成桌面授权后提交公共贡献。
+2. 步骤 9 上传到 `kiro.rs` 后更新私有发布状态。
+3. 公共贡献状态与私有发布状态分开显示。
+
+## 7. 开发阶段清单
+
+### 阶段 1:贡献 registry 与能力矩阵
+
+开发内容:
+
+1. 新增 `shared/contribution-registry.js`。
+2. 注册 OpenAI adapters。
+3. 注册 Kiro `kiro-builder-id` adapter。
+4. 修改 `flow-registry.js`,加入 `supportsAccountContribution`。
+5. 修改 `flow-capabilities.js`,去掉贡献能力的 OpenAI-only 判断。
+
+自检:
+
+1. `canShowContributionMode()` 对 Kiro 返回 true。
+2. OpenAI OAuth 专属能力仍只对 OpenAI 返回 true。
+3. 未注册 adapter 的 flow 在测试中失败。
+4. 正式 flow 不出现“当前 flow 不支持贡献模式”。
+5. 新增中文文案无乱码。
+
+测试:
+
+1. contribution registry 测试。
+2. flow capability 测试。
+3. Kiro contribution capability 测试。
+4. 未注册 adapter 阻断发布测试。
+
+### 阶段 2:通用贡献状态与消息协议
+
+开发内容:
+
+1. 新增 `accountContributionEnabled`。
+2. 新增 `contributionAdapterId`。
+3. 新增 `flowContributionRuntime`。
+4. 新增并唯一使用 background messages:
+
+```text
+SET_ACCOUNT_CONTRIBUTION_MODE
+START_FLOW_CONTRIBUTION
+SUBMIT_FLOW_CONTRIBUTION
+POLL_FLOW_CONTRIBUTION_STATUS
+```
+
+5. 删除扩展端历史 OpenAI contribution messages。
+6. 更新所有 sidepanel、background、tests 调用点,不保留扩展端旧 message shim。
+7. 新增 storage migration,把历史 `contributionMode` 字段迁移到 `accountContributionEnabled` 后删除。
+
+自检:
+
+1. 扩展运行时代码中不存在 `SET_CONTRIBUTION_MODE`。
+2. 扩展运行时代码中不存在 `START_CONTRIBUTION_FLOW`。
+3. 扩展运行时代码中不存在 `POLL_CONTRIBUTION_STATUS`。
+4. `contributionMode` 只出现在 storage migration 和迁移测试中。
+5. Kiro 能开启贡献模式。
+6. flow 切换后 adapter 不串 flow。
+7. 定时任务 payload 使用统一 contribution payload。
+8. storage 读写无乱码。
+
+测试:
+
+1. 历史 message 字符串删除测试。
+2. storage migration 测试。
+3. Kiro contribution mode message 测试。
+4. flow switch 状态隔离测试。
+5. 定时任务 contribution payload 测试。
+
+### 阶段 3:Kiro adapter 与 artifact builder
+
+开发内容:
+
+1. 新增 `background/kiro/credential-artifact.js`。
+2. 新增 `background/contribution/adapters/kiro-builder-id.js`。
+3. 复用 Kiro runtime 产物构建 artifact。
+4. 增加本地校验。
+5. 增加日志脱敏。
+6. 步骤 8 后调用 `maybeSubmitFlowContribution()`。
+
+自检:
+
+1. 缺 `refreshToken` 时不提交。
+2. 缺 `clientId` 时不提交。
+3. 缺 `clientSecret` 时不提交。
+4. 缺 email 时不提交。
+5. 日志不包含完整 token。
+6. 未配置 `kiro.rs` 时仍提交公共贡献。
+
+测试:
+
+1. Kiro artifact builder 成功测试。
+2. Kiro artifact 缺字段失败测试。
+3. Kiro 步骤 8 自动贡献测试。
+4. Kiro secret redact 测试。
+5. 未配置 `kiro.rs` 仍贡献测试。
+
+### 阶段 4:门户 intake 与贡献 API
+
+开发内容:
+
+1. 新增 `POST /api/contributions`。
+2. 新增 intake registry。
+3. 将 `codex` 校验迁入 `CodexIntakeSpec`。
+4. 将 `sub2api` 校验迁入 `Sub2ApiIntakeSpec`。
+5. 将 OpenAI OAuth 校验迁入 `OpenAiOAuthIntakeSpec`。
+6. 新增 `KiroBuilderIdIntakeSpec`。
+7. 服务器端保留 `/api/upload` 历史 HTTP 入口。
+8. `/api/upload` route 只委托统一 intake registry,不写贡献业务逻辑。
+9. `/api/upload` route 附近写明历史入口注释。
+10. 数据库新增 flow/artifact/adapter 字段。
+11. 管理后台显示 flow 贡献记录。
+
+自检:
+
+1. `codex` 上传进入 `CodexIntakeSpec`。
+2. `sub2api` 上传进入 `Sub2ApiIntakeSpec`。
+3. OpenAI OAuth 进入 `OpenAiOAuthIntakeSpec`。
+4. `kiro-builder-id` 进入 `KiroBuilderIdIntakeSpec`。
+5. 四类 artifact 都写入 flow/artifact/adapter 字段。
+6. 四类 artifact 都使用统一去重记录。
+7. 管理后台不泄露 secret。
+8. `/api/upload` route 有历史入口注释。
+9. `/api/upload` 内没有新增贡献业务逻辑。
+10. 数据库迁移默认值正确。
+
+测试:
+
+1. OpenAI Codex intake 测试。
+2. OpenAI Sub2API intake 测试。
+3. OpenAI OAuth intake 测试。
+4. Kiro contribution API 成功测试。
+5. Kiro 缺字段拒绝测试。
+6. Kiro 重复账号测试。
+7. `/api/upload` 与 `/api/contributions` 共用 intake 测试。
+8. `/api/upload` 历史入口注释检查。
+9. 管理后台 flow 筛选测试。
+
+### 阶段 5:侧边栏贡献/使用教程组合入口
+
+开发内容:
+
+1. 新增 `sidepanel/flow-contribution.js`。
+2. 新增 `sidepanel/contribution-tutorial-entry.js`。
+3. 顶部按钮保持 `贡献/使用教程`。
+4. 点击按钮打开当前 flow 对应教程/门户。
+5. 点击按钮开启当前 flow 的账号贡献模式。
+6. OpenAI 点击后进入 `openai-oauth` 贡献模式。
+7. Kiro 点击后进入 `kiro-builder-id` 贡献模式。
+
+自检:
+
+1. Kiro 下 `贡献/使用教程` 按钮可点击。
+2. Kiro 下点击按钮会打开 Kiro 教程/门户。
+3. Kiro 下点击按钮会开启 Kiro 贡献模式。
+4. Kiro 下不显示“不支持贡献模式”。
+5. OpenAI 组合入口会进入统一 `openai-oauth` adapter。
+6. 中文文案无乱码。
+
+测试:
+
+1. Kiro 组合按钮渲染测试。
+2. Kiro 组合按钮打开教程/门户测试。
+3. Kiro 组合按钮开启贡献模式测试。
+4. OpenAI 组合按钮统一 adapter 测试。
+
+### 阶段 6:贡献/教程内容 summary 按 flow 隔离
+
+开发内容:
+
+1. 将 contribution content service 迁移为 flow contribution content service。
+2. `/api/content-summary` 支持 `flow`、`target`。
+3. 缓存 key 加入 flow/target。
+4. Kiro 只接收 Kiro 相关教程和公告提示。
+5. OpenAI 只接收 OpenAI 相关贡献和教程提示。
+
+自检:
+
+1. Kiro 不收到 OpenAI-only 内容提示。
+2. OpenAI 不丢失原有公告提示。
+3. summary 网络失败不阻塞自动运行。
+4. 无 `flow/target` 的历史 summary 请求会转换为 `flow=openai` 默认 scope。
+5. 中文提示无乱码。
+
+测试:
+
+1. Kiro summary query 测试。
+2. OpenAI summary query 测试。
+3. 缓存隔离测试。
+4. API 失败降级测试。
+
+### 阶段 7:命名清理与版本记录
+
+开发内容:
+
+1. 通用贡献文件统一使用 `account-contribution` 或 `flow-contribution` 命名。
+2. OpenAI OAuth adapter 统一使用 `openai-oauth` 命名。
+3. contribution tutorial entry 文件同时负责打开教程/门户和开启贡献模式。
+4. 更新测试名称。
+5. 更新版本记录。
+
+自检:
+
+1. `contribution` 命名只指真实账号贡献。
+2. `contribution-tutorial-entry` 命名只指顶部组合入口。
+3. 历史 DOM id 映射逻辑有测试覆盖。
+4. 文档与代码命名一致。
+5. 无乱码。
+
+## 8. 正确性分析
+
+### 8.1 是否符合要求
+
+符合。方案将“任意 flow 贡献账号”作为强制能力:
+
+1. OpenAI 通过 `openai-oauth/codex/sub2api` 接入。
+2. Kiro 通过 `kiro-builder-id` 接入。
+3. 新增 flow 必须通过 adapter 和 intake 接入。
+4. `贡献/使用教程` 是组合入口,点击后同时打开当前 flow 教程/门户并开启当前 flow 贡献模式。
+
+### 8.2 是否完善
+
+完善。方案覆盖:
+
+1. UI 入口。
+2. capability。
+3. settings migration。
+4. background manager。
+5. Kiro artifact。
+6. OpenAI 历史格式统一升级。
+7. portal API。
+8. portal validation。
+9. database migration。
+10. admin display。
+11. tests。
+12. self-check。
+
+### 8.3 是否完整
+
+完整。账号贡献链路从用户点击到门户入库闭环:
+
+1. 用户开启贡献。
+2. 当前 flow 产出账号 artifact。
+3. adapter 本地校验。
+4. extension 提交到 portal。
+5. portal intake 校验。
+6. portal 去重入库。
+7. admin 展示审核。
+8. UI 展示贡献结果。
+
+### 8.4 是否正确
+
+正确。方案基于当前代码事实:
+
+1. OpenAI 已有 OAuth 与文件上传贡献能力。
+2. Kiro 已有 Builder ID 凭据产物。
+3. 门户已有 `ValidatedUpload` 和 `AccountRecord` 扩展基础。
+4. 现有 OpenAI-only capability 是必须修改的冲突点。
+
+### 8.5 是否规范一致
+
+一致。方案遵守多 flow 边界:
+
+1. OpenAI 专属逻辑留在 OpenAI adapter。
+2. Kiro 专属逻辑留在 Kiro adapter。
+3. 通用逻辑放在 contribution registry 和 manager。
+4. 新增 flow 通过声明式 adapter 接入。
+5. 不在 sidepanel 堆 flow 特判。
+
+### 8.6 方案自身缺陷
+
+已识别缺陷和处理方式:
+
+1. 跨 extension 与 portal,按阶段开发并逐阶段自检。
+2. `contributionMode` 只存在于 storage migration 和迁移测试,扩展运行时代码不保留历史接口。
+3. Kiro artifact 含 secret,所有日志和 UI 必须走脱敏。
+4. 数据库迁移影响历史记录,迁移规则固定补齐 OpenAI flow/artifact/adapter 字段。
+5. 公共贡献与私有 `kiro.rs` 上传是两条链路,状态分开记录。
+
+### 8.7 上下设计冲突
+
+必须解决的冲突:
+
+1. `supportsContributionMode` 当前只允许 OpenAI,与任意 flow 贡献冲突。
+2. Kiro 贡献 UI 当前被禁用,与 Kiro 必须贡献冲突。
+3. 门户 `infer_kind()` 当前不识别 Kiro,与 Kiro intake 冲突。
+4. 顶部按钮当前直接进入 OpenAI contribution,与 flow-aware 组合入口冲突。
+5. 现有测试断言单一 OpenAI contribution button,测试必须更新。
+
+不允许引入的新冲突:
+
+1. 不允许 Kiro 复用 OpenAI OAuth。
+2. 不允许 OpenAI phone/Plus/LuckMail 自动扩散到 Kiro。
+3. 不允许把 `贡献/使用教程` 拆成只打开教程而不开启贡献模式的入口。
+4. 不允许公共贡献依赖用户私有 `kiro.rs` 配置。
+5. 不允许日志输出完整凭据。
+
+## 9. 验收标准
+
+### 9.1 Kiro
+
+1. Kiro flow 下显示 `贡献/使用教程` 组合按钮。
+2. Kiro 贡献模式能开启。
+3. Kiro 步骤 8 后生成 `kiro-builder-id` artifact。
+4. 未配置 `kiro.rs` 时仍提交公共贡献。
+5. 配置 `kiro.rs` 时公共贡献和私有上传都执行。
+6. 门户接收 Kiro artifact。
+7. 门户按 `kiro-builder-id` 去重。
+8. 管理后台显示 Kiro 贡献记录。
+9. 日志和 UI 不泄露完整 `refreshToken/clientSecret`。
+10. 点击 Kiro `贡献/使用教程` 同时打开 Kiro 教程/门户并开启 Kiro 贡献模式。
+
+### 9.2 OpenAI
+
+1. OpenAI OAuth 贡献进入 `openai-oauth` adapter 和 `OpenAiOAuthIntakeSpec`。
+2. `codex/sub2api` 上传进入对应 OpenAI intake spec。
+3. 扩展运行时代码不接收历史 `contributionMode` payload。
+4. 新通用贡献状态能映射到 OpenAI adapter。
+5. 点击 OpenAI `贡献/使用教程` 同时打开 OpenAI 教程/门户并开启 OpenAI 贡献模式。
+
+### 9.3 新增 Flow
+
+1. 未注册 adapter 的 flow 不能发布。
+2. 注册 adapter 后显示贡献入口。
+3. artifact builder 和 intake spec 都有测试。
+4. 门户能按 flow、kind、adapter 筛选贡献记录。
+5. 不同 flow 的贡献状态互相隔离。
+
+## 10. 禁止事项
+
+1. 禁止只做教程入口不做贡献入口。
+2. 禁止发布没有 contribution adapter 的 flow。
+3. 禁止把 OpenAI OAuth adapter 复用给 Kiro 或其他 flow。
+4. 禁止在 sidepanel 到处新增 flow 特判。
+5. 禁止在日志、toast、错误信息中输出完整 token。
+6. 禁止公共贡献依赖用户私有 target 配置。
+7. 禁止门户新增 kind 但不加去重规则。
+8. 禁止只改 extension 不改 portal intake。
+9. 禁止扩展端保留历史 message shim 或历史接口入口。
+10. 禁止扩展端运行时代码继续读取 `contributionMode`。
+11. 禁止把服务端历史 HTTP 入口当成新开发入口。
+12. 禁止服务器端历史 HTTP 入口承载新业务逻辑。
+13. 禁止服务器端保留历史 HTTP 入口但不写注释说明。
diff --git a/docs/多注册流程侧边栏能力矩阵.md b/docs/多注册流程侧边栏能力矩阵.md
index 088d8e6..82d5011 100644
--- a/docs/多注册流程侧边栏能力矩阵.md
+++ b/docs/多注册流程侧边栏能力矩阵.md
@@ -1,5 +1,7 @@
# 多注册流程侧边栏能力矩阵
+> 历史说明:本文记录的是多 flow 侧边栏能力矩阵的早期设计背景。2026-05-19 起,账号贡献运行态已从历史 `contributionMode` 升级为 `accountContributionEnabled` + `contributionAdapterId`;后续开发请以 `多Flow资源入口与贡献体系升级开发方案.md` 和当前代码为准,不要继续新增或恢复 `contributionMode` 运行时逻辑。
+
本文解决的是一个很容易被低估的问题:sidepanel 现在不只是“渲染 UI”,它还承担了大量业务约束。如果未来要支持多个 flow,不能只做动态渲染,还必须把“哪些能力允许显示、允许切换、允许启动”做成统一能力矩阵。
## 1. 当前源码里的真实问题
@@ -255,4 +257,3 @@ getStepDefinitions({
- 手机号注册能力由多个开关共同决定,当前逻辑分散且重复
- `contributionMode` 与 `panelMode` 容易混淆
- 多 flow 之后,步骤列表也必须纳入能力矩阵,而不是继续全局写死
-
diff --git a/shared/contribution-registry.js b/shared/contribution-registry.js
new file mode 100644
index 0000000..9ea6610
--- /dev/null
+++ b/shared/contribution-registry.js
@@ -0,0 +1,224 @@
+(function attachMultiPageContributionRegistry(root, factory) {
+ root.MultiPageContributionRegistry = factory(root);
+})(typeof self !== 'undefined' ? self : globalThis, function createContributionRegistryModule(root) {
+ const flowRegistryApi = root?.MultiPageFlowRegistry || {};
+ const DEFAULT_FLOW_ID = flowRegistryApi.DEFAULT_FLOW_ID || 'openai';
+ const DEFAULT_KIRO_TARGET_ID = flowRegistryApi.DEFAULT_KIRO_TARGET_ID || 'kiro-rs';
+ const DEFAULT_OPENAI_TARGET_ID = flowRegistryApi.DEFAULT_OPENAI_TARGET_ID || 'cpa';
+
+ const ADAPTER_DEFINITIONS = Object.freeze({
+ 'openai-oauth': Object.freeze({
+ id: 'openai-oauth',
+ flowId: 'openai',
+ artifactKind: 'openai-oauth',
+ trigger: 'interactive-oauth',
+ label: 'OpenAI OAuth 贡献',
+ defaultTargetId: DEFAULT_OPENAI_TARGET_ID,
+ sensitiveFieldPaths: Object.freeze([
+ 'credentials.access_token',
+ 'credentials.refresh_token',
+ 'credentials.id_token',
+ ]),
+ }),
+ 'openai-codex-file': Object.freeze({
+ id: 'openai-codex-file',
+ flowId: 'openai',
+ artifactKind: 'codex',
+ trigger: 'manual-upload',
+ label: 'OpenAI Codex 文件贡献',
+ defaultTargetId: 'codex2api',
+ sensitiveFieldPaths: Object.freeze([
+ 'credentials.access_token',
+ 'credentials.refresh_token',
+ 'credentials.id_token',
+ ]),
+ }),
+ 'openai-sub2api-file': Object.freeze({
+ id: 'openai-sub2api-file',
+ flowId: 'openai',
+ artifactKind: 'sub2api',
+ trigger: 'manual-upload',
+ label: 'OpenAI Sub2API 文件贡献',
+ defaultTargetId: 'sub2api',
+ sensitiveFieldPaths: Object.freeze([
+ 'credentials.accounts[].credentials.access_token',
+ 'credentials.accounts[].credentials.refresh_token',
+ 'credentials.accounts[].credentials.id_token',
+ ]),
+ }),
+ 'kiro-builder-id': Object.freeze({
+ id: 'kiro-builder-id',
+ flowId: 'kiro',
+ artifactKind: 'kiro-builder-id',
+ trigger: 'after-desktop-authorize',
+ label: 'Kiro Builder ID 贡献',
+ defaultTargetId: DEFAULT_KIRO_TARGET_ID,
+ sensitiveFieldPaths: Object.freeze([
+ 'credentials.refreshToken',
+ 'credentials.clientSecret',
+ 'metadata.proxyPassword',
+ ]),
+ }),
+ });
+
+ const FLOW_ADAPTER_IDS = Object.freeze({
+ openai: Object.freeze(['openai-oauth', 'openai-codex-file', 'openai-sub2api-file']),
+ kiro: Object.freeze(['kiro-builder-id']),
+ });
+
+ const CONTRIBUTION_TUTORIAL_ENTRIES = Object.freeze({
+ openai: Object.freeze({
+ id: 'openai-contribution-tutorial',
+ flowId: 'openai',
+ label: '贡献/使用教程',
+ portalPath: '/tutorial',
+ defaultTargetId: DEFAULT_OPENAI_TARGET_ID,
+ contributionAdapterId: 'openai-oauth',
+ action: 'open-portal-and-enable-contribution',
+ }),
+ kiro: Object.freeze({
+ id: 'kiro-contribution-tutorial',
+ flowId: 'kiro',
+ label: '贡献/使用教程',
+ portalPath: '/tutorial',
+ defaultTargetId: DEFAULT_KIRO_TARGET_ID,
+ contributionAdapterId: 'kiro-builder-id',
+ action: 'open-portal-and-enable-contribution',
+ }),
+ });
+
+ function normalizeString(value = '') {
+ return String(value || '').trim();
+ }
+
+ function normalizeFlowId(value = '', fallback = DEFAULT_FLOW_ID) {
+ const normalized = normalizeString(value).toLowerCase();
+ if (normalized && Object.prototype.hasOwnProperty.call(FLOW_ADAPTER_IDS, normalized)) {
+ return normalized;
+ }
+ if (!normalized && typeof flowRegistryApi.normalizeFlowId === 'function') {
+ return flowRegistryApi.normalizeFlowId(value, fallback);
+ }
+ return normalized || normalizeString(fallback).toLowerCase() || DEFAULT_FLOW_ID;
+ }
+
+ function normalizeAdapterId(value = '') {
+ return normalizeString(value).toLowerCase();
+ }
+
+ function normalizeTargetId(flowId = DEFAULT_FLOW_ID, targetId = '') {
+ if (typeof flowRegistryApi.normalizeTargetId === 'function') {
+ return flowRegistryApi.normalizeTargetId(flowId, targetId);
+ }
+ const normalizedFlowId = normalizeFlowId(flowId, DEFAULT_FLOW_ID);
+ const normalizedTargetId = normalizeString(targetId).toLowerCase();
+ if (normalizedTargetId) {
+ return normalizedTargetId;
+ }
+ return normalizedFlowId === 'kiro' ? DEFAULT_KIRO_TARGET_ID : DEFAULT_OPENAI_TARGET_ID;
+ }
+
+ function cloneAdapter(adapter) {
+ if (!adapter) {
+ return null;
+ }
+ return {
+ ...adapter,
+ sensitiveFieldPaths: Array.isArray(adapter.sensitiveFieldPaths)
+ ? adapter.sensitiveFieldPaths.slice()
+ : [],
+ };
+ }
+
+ function getAdapterDefinition(adapterId = '') {
+ return cloneAdapter(ADAPTER_DEFINITIONS[normalizeAdapterId(adapterId)] || null);
+ }
+
+ function getContributionAdapterIds(flowId = DEFAULT_FLOW_ID) {
+ const normalizedFlowId = normalizeFlowId(flowId, DEFAULT_FLOW_ID);
+ return (FLOW_ADAPTER_IDS[normalizedFlowId] || []).slice();
+ }
+
+ function getContributionAdapters(flowId = DEFAULT_FLOW_ID) {
+ return getContributionAdapterIds(flowId)
+ .map((adapterId) => getAdapterDefinition(adapterId))
+ .filter(Boolean);
+ }
+
+ function getDefaultContributionAdapterId(flowId = DEFAULT_FLOW_ID) {
+ return getContributionAdapterIds(flowId)[0] || '';
+ }
+
+ function buildPortalPageUrl(portalBaseUrl = '', portalPath = '/tutorial', params = {}) {
+ const baseUrl = normalizeString(portalBaseUrl).replace(/\/+$/, '') || 'https://flowpilot.qlhazycoder.top';
+ const path = normalizeString(portalPath) || '/tutorial';
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
+ const query = Object.entries(params)
+ .map(([key, value]) => [normalizeString(key), normalizeString(value)])
+ .filter(([key, value]) => key && value)
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
+ .join('&');
+ return `${baseUrl}${normalizedPath}${query ? `?${query}` : ''}`;
+ }
+
+ function getContributionTutorialEntry(flowId = DEFAULT_FLOW_ID, options = {}) {
+ const normalizedFlowId = normalizeFlowId(flowId, DEFAULT_FLOW_ID);
+ const definition = CONTRIBUTION_TUTORIAL_ENTRIES[normalizedFlowId] || null;
+ if (!definition) {
+ return null;
+ }
+ const requestedAdapterId = normalizeAdapterId(options.adapterId);
+ const adapterId = requestedAdapterId && hasContributionAdapter(normalizedFlowId, requestedAdapterId)
+ ? requestedAdapterId
+ : definition.contributionAdapterId;
+ const targetId = normalizeTargetId(
+ normalizedFlowId,
+ options.targetId || definition.defaultTargetId
+ );
+ return {
+ ...definition,
+ targetId,
+ contributionAdapterId: adapterId,
+ portalUrl: buildPortalPageUrl(options.portalBaseUrl, definition.portalPath, {
+ flow: normalizedFlowId,
+ target: targetId,
+ }),
+ };
+ }
+
+ function hasContributionAdapter(flowId = DEFAULT_FLOW_ID, adapterId = '') {
+ const normalizedAdapterId = normalizeAdapterId(adapterId);
+ return Boolean(normalizedAdapterId) && getContributionAdapterIds(flowId).includes(normalizedAdapterId);
+ }
+
+ function assertPublishedFlowsHaveContributionAdapters(flowIds = undefined) {
+ const ids = Array.isArray(flowIds)
+ ? flowIds
+ : (typeof flowRegistryApi.getRegisteredFlowIds === 'function'
+ ? flowRegistryApi.getRegisteredFlowIds()
+ : Object.keys(FLOW_ADAPTER_IDS));
+ const missing = ids
+ .map((flowId) => normalizeString(flowId).toLowerCase())
+ .filter(Boolean)
+ .filter((flowId) => getContributionAdapterIds(flowId).length === 0);
+ if (missing.length) {
+ throw new Error(`缺少账号贡献适配器:${missing.join(', ')}`);
+ }
+ return true;
+ }
+
+ return {
+ ADAPTER_DEFINITIONS,
+ CONTRIBUTION_TUTORIAL_ENTRIES,
+ FLOW_ADAPTER_IDS,
+ assertPublishedFlowsHaveContributionAdapters,
+ getContributionTutorialEntry,
+ getAdapterDefinition,
+ getContributionAdapterIds,
+ getContributionAdapters,
+ getDefaultContributionAdapterId,
+ hasContributionAdapter,
+ normalizeAdapterId,
+ normalizeFlowId,
+ };
+});
diff --git a/shared/flow-capabilities.js b/shared/flow-capabilities.js
index a1fd7d1..d5f430f 100644
--- a/shared/flow-capabilities.js
+++ b/shared/flow-capabilities.js
@@ -3,6 +3,7 @@
})(typeof self !== 'undefined' ? self : globalThis, function createFlowCapabilitiesModule() {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
const flowRegistryApi = rootScope.MultiPageFlowRegistry || {};
+ const contributionRegistryApi = rootScope.MultiPageContributionRegistry || {};
const settingsSchemaApi = rootScope.MultiPageSettingsSchema || {};
const DEFAULT_FLOW_ID = flowRegistryApi.DEFAULT_FLOW_ID || 'openai';
const DEFAULT_OPENAI_TARGET_ID = flowRegistryApi.DEFAULT_OPENAI_TARGET_ID || 'cpa';
@@ -25,6 +26,9 @@
supportsPhoneVerificationSettings: false,
supportsPlusMode: false,
supportsContributionMode: false,
+ supportsAccountContribution: false,
+ supportsOpenAiOAuthContribution: false,
+ contributionAdapterIds: [],
supportedTargetIds: [],
supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
@@ -58,7 +62,7 @@
const MODE_SWITCH_RELEVANT_KEYS = Object.freeze([
'activeFlowId',
- 'contributionMode',
+ 'accountContributionEnabled',
'panelMode',
'phoneVerificationEnabled',
'plusModeEnabled',
@@ -195,6 +199,14 @@
function getFlowCapabilities(flowId) {
const normalizedFlowId = normalizeCapabilityFlowId(flowId, defaultFlowId);
const entry = flowCapabilities[normalizedFlowId] || null;
+ const registryAdapterIds = typeof contributionRegistryApi.getContributionAdapterIds === 'function'
+ ? contributionRegistryApi.getContributionAdapterIds(normalizedFlowId)
+ : [];
+ const contributionAdapterIds = registryAdapterIds.length
+ ? registryAdapterIds
+ : (Array.isArray(entry?.contributionAdapterIds)
+ ? entry.contributionAdapterIds.map((value) => String(value || '').trim()).filter(Boolean)
+ : []);
const supportedTargetIds = normalizedFlowId === 'openai'
? normalizeOpenAiTargetList(
entry?.supportedTargetIds || defaultFlowCapabilities.supportedTargetIds
@@ -206,6 +218,8 @@
...defaultFlowCapabilities,
...(entry || {}),
supportedTargetIds,
+ contributionAdapterIds,
+ supportsAccountContribution: Boolean(entry?.supportsAccountContribution || contributionAdapterIds.length > 0),
};
}
@@ -332,7 +346,7 @@
: defaultTargetCapabilities;
const runtimeLocks = {
autoRunLocked: Boolean(options?.autoRunLocked ?? state?.autoRunLocked),
- contributionMode: activeFlowId === 'openai' && flowState.supportsContributionMode && Boolean(state?.contributionMode),
+ accountContribution: Boolean(flowState.supportsAccountContribution) && Boolean(state?.accountContributionEnabled),
phoneVerificationEnabled: activeFlowId === 'openai' && flowState.supportsPhoneVerificationSettings && Boolean(state?.phoneVerificationEnabled),
plusModeEnabled: activeFlowId === 'openai' && flowState.supportsPlusMode && Boolean(state?.plusModeEnabled),
settingsMenuLocked: Boolean(options?.settingsMenuLocked ?? state?.settingsMenuLocked),
@@ -346,7 +360,7 @@
&& Boolean(targetState.supportsPhoneSignup)
&& runtimeLocks.phoneVerificationEnabled
&& !runtimeLocks.plusModeEnabled
- && !runtimeLocks.contributionMode;
+ && !runtimeLocks.accountContribution;
if (canSelectPhoneSignup) {
effectiveSignupMethods.push(SIGNUP_METHOD_PHONE);
}
@@ -390,7 +404,7 @@
return {
activeFlowId,
- canShowContributionMode: activeFlowId === 'openai' && Boolean(flowState.supportsContributionMode),
+ canShowContributionMode: Boolean(flowState.supportsAccountContribution),
canShowLuckmail: activeFlowId === 'openai' && Boolean(flowState.supportsLuckmail),
canShowPhoneSettings: activeFlowId === 'openai' && Boolean(flowState.supportsPhoneVerificationSettings),
canShowPlusSettings: activeFlowId === 'openai' && Boolean(flowState.supportsPlusMode),
@@ -459,7 +473,7 @@
message: 'Plus 模式开启时不能使用手机号注册。',
};
}
- if (runtimeLocks.contributionMode) {
+ if (runtimeLocks.accountContribution) {
return {
code: 'phone_signup_contribution_mode_locked',
message: '贡献模式开启时不能使用手机号注册。',
@@ -494,7 +508,7 @@
});
}
- if (Boolean(state?.contributionMode) && !capabilityState.flowCapabilities?.supportsContributionMode) {
+ if (Boolean(state?.accountContributionEnabled) && !capabilityState.flowCapabilities?.supportsAccountContribution) {
errors.push({
code: 'contribution_mode_unsupported',
message: '当前 flow 不支持贡献模式。',
@@ -553,8 +567,12 @@
});
}
- if (changedKeySet.has('contributionMode') && Boolean(state?.contributionMode) && !flowState.supportsContributionMode) {
- normalizedUpdates.contributionMode = false;
+ if (
+ changedKeySet.has('accountContributionEnabled')
+ && Boolean(state?.accountContributionEnabled)
+ && !flowState.supportsAccountContribution
+ ) {
+ normalizedUpdates.accountContributionEnabled = false;
errors.push({
code: 'contribution_mode_unsupported',
message: '当前 flow 不支持贡献模式。',
diff --git a/shared/flow-registry.js b/shared/flow-registry.js
index c5674be..a9a46ad 100644
--- a/shared/flow-registry.js
+++ b/shared/flow-registry.js
@@ -15,6 +15,9 @@
supportsPhoneVerificationSettings: false,
supportsPlusMode: false,
supportsContributionMode: false,
+ supportsAccountContribution: false,
+ supportsOpenAiOAuthContribution: false,
+ contributionAdapterIds: [],
supportedTargetIds: [],
supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
@@ -44,6 +47,9 @@
supportsPhoneVerificationSettings: true,
supportsPlusMode: true,
supportsContributionMode: true,
+ supportsAccountContribution: true,
+ supportsOpenAiOAuthContribution: true,
+ contributionAdapterIds: ['openai-oauth', 'openai-codex-file', 'openai-sub2api-file'],
supportedTargetIds: [...OPENAI_TARGET_IDS],
supportsLuckmail: true,
supportsOauthTimeoutBudget: true,
@@ -203,6 +209,8 @@
services: ['account', 'email', 'proxy'],
capabilities: {
...DEFAULT_FLOW_CAPABILITIES,
+ supportsAccountContribution: true,
+ contributionAdapterIds: ['kiro-builder-id'],
supportedTargetIds: [DEFAULT_KIRO_TARGET_ID],
stepDefinitionMode: 'kiro',
},
diff --git a/sidepanel/contribution-content-update-service.js b/sidepanel/contribution-content-update-service.js
index b496b16..386fd7a 100644
--- a/sidepanel/contribution-content-update-service.js
+++ b/sidepanel/contribution-content-update-service.js
@@ -1,9 +1,31 @@
(() => {
const PORTAL_BASE_URL = 'https://flowpilot.qlhazycoder.top';
const CONTENT_SUMMARY_API_URL = `${PORTAL_BASE_URL}/api/content-summary`;
- const CACHE_KEY = 'multipage-contribution-content-summary-v1';
+ const CACHE_KEY_PREFIX = 'multipage-contribution-content-summary-v2';
const FETCH_TIMEOUT_MS = 6000;
+ function normalizeScopeId(value = '', fallback = '') {
+ return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || fallback;
+ }
+
+ function normalizeScope(options = {}) {
+ const flowId = normalizeScopeId(options.flowId || options.flow || options.activeFlowId, 'openai');
+ const targetFallback = flowId === 'kiro' ? 'kiro-rs' : 'cpa';
+ const targetId = normalizeScopeId(options.targetId || options.target || options.activeTargetId, targetFallback);
+ return { flowId, targetId };
+ }
+
+ function getCacheKey(scope = normalizeScope()) {
+ return `${CACHE_KEY_PREFIX}:${scope.flowId}:${scope.targetId}`;
+ }
+
+ function buildSummaryApiUrl(scope = normalizeScope()) {
+ const url = new URL(CONTENT_SUMMARY_API_URL);
+ url.searchParams.set('flow', scope.flowId);
+ url.searchParams.set('target', scope.targetId);
+ return url.toString();
+ }
+
function sanitizeItem(item = {}) {
return {
slug: String(item?.slug || '').trim(),
@@ -14,13 +36,20 @@
isVisible: Boolean(item?.is_visible ?? item?.isVisible),
updatedAt: String(item?.updated_at ?? item?.updatedAt ?? '').trim(),
updatedAtDisplay: String(item?.updated_at_display ?? item?.updatedAtDisplay ?? '').trim(),
+ flowId: normalizeScopeId(item?.flow_id ?? item?.flowId, ''),
+ targetId: normalizeScopeId(item?.target_id ?? item?.targetId, ''),
+ scope: String(item?.scope || '').trim(),
};
}
- function buildSnapshot(payload = {}) {
+ function buildSnapshot(payload = {}, scope = normalizeScope()) {
const items = Array.isArray(payload?.items)
? payload.items.map(sanitizeItem).filter((item) => item.slug)
: [];
+ const responseScope = normalizeScope({
+ flowId: payload?.flow_id || payload?.flowId || scope.flowId,
+ targetId: payload?.target_id || payload?.targetId || scope.targetId,
+ });
const promptVersion = String(payload?.prompt_version || '').trim();
const latestUpdatedAt = String(payload?.latest_updated_at || '').trim();
const latestUpdatedAtDisplay = String(payload?.latest_updated_at_display || '').trim();
@@ -33,15 +62,17 @@
latestUpdatedAt,
latestUpdatedAtDisplay,
items,
+ flowId: responseScope.flowId,
+ targetId: responseScope.targetId,
portalUrl: PORTAL_BASE_URL,
- apiUrl: CONTENT_SUMMARY_API_URL,
+ apiUrl: buildSummaryApiUrl(responseScope),
checkedAt: Date.now(),
};
}
- function readCache() {
+ function readCache(scope = normalizeScope()) {
try {
- const raw = localStorage.getItem(CACHE_KEY);
+ const raw = localStorage.getItem(getCacheKey(scope));
if (!raw) {
return null;
}
@@ -57,7 +88,9 @@
has_visible_updates: parsed.hasVisibleUpdates,
latest_updated_at: parsed.latestUpdatedAt,
latest_updated_at_display: parsed.latestUpdatedAtDisplay,
- });
+ flow_id: parsed.flowId,
+ target_id: parsed.targetId,
+ }, scope);
if (!Number.isFinite(parsed.checkedAt)) {
return snapshot;
}
@@ -68,20 +101,21 @@
}
}
- function writeCache(snapshot) {
+ function writeCache(snapshot, scope = normalizeScope(snapshot)) {
try {
- localStorage.setItem(CACHE_KEY, JSON.stringify(snapshot));
+ localStorage.setItem(getCacheKey(scope), JSON.stringify(snapshot));
} catch (error) {
// Ignore cache write failures.
}
}
- async function fetchContentSummary() {
+ async function fetchContentSummary(options = {}) {
+ const scope = normalizeScope(options);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
- const response = await fetch(CONTENT_SUMMARY_API_URL, {
+ const response = await fetch(buildSummaryApiUrl(scope), {
method: 'GET',
headers: {
Accept: 'application/json',
@@ -99,8 +133,8 @@
throw new Error('内容摘要返回格式异常');
}
- const snapshot = buildSnapshot(payload);
- writeCache(snapshot);
+ const snapshot = buildSnapshot(payload, scope);
+ writeCache(snapshot, scope);
return snapshot;
} catch (error) {
if (error?.name === 'AbortError') {
@@ -112,11 +146,12 @@
}
}
- async function getContentUpdateSnapshot() {
+ async function getContentUpdateSnapshot(options = {}) {
+ const scope = normalizeScope(options);
try {
- return await fetchContentSummary();
+ return await fetchContentSummary(scope);
} catch (error) {
- const cached = readCache();
+ const cached = readCache(scope);
if (cached) {
return {
...cached,
@@ -132,8 +167,10 @@
latestUpdatedAt: '',
latestUpdatedAtDisplay: '',
items: [],
+ flowId: scope.flowId,
+ targetId: scope.targetId,
portalUrl: PORTAL_BASE_URL,
- apiUrl: CONTENT_SUMMARY_API_URL,
+ apiUrl: buildSummaryApiUrl(scope),
checkedAt: Date.now(),
errorMessage: error?.message || '内容摘要获取失败',
};
@@ -141,6 +178,7 @@
}
window.SidepanelContributionContentService = {
+ buildSummaryApiUrl,
getContentUpdateSnapshot,
portalUrl: PORTAL_BASE_URL,
apiUrl: CONTENT_SUMMARY_API_URL,
diff --git a/sidepanel/contribution-mode.js b/sidepanel/contribution-mode.js
index 4ff1029..55efdb7 100644
--- a/sidepanel/contribution-mode.js
+++ b/sidepanel/contribution-mode.js
@@ -81,6 +81,14 @@
}
function getContributionSourceLabel(currentState = getLatestState()) {
+ if (getActiveFlowId(currentState) !== 'openai') {
+ const rootScope = typeof window !== 'undefined' ? window : globalThis;
+ const registry = rootScope.MultiPageContributionRegistry || {};
+ const adapter = typeof registry.getAdapterDefinition === 'function'
+ ? registry.getAdapterDefinition(currentState.contributionAdapterId || '', { flowId: getActiveFlowId(currentState) })
+ : null;
+ return normalizeString(adapter?.label) || '账号贡献';
+ }
return getContributionSource(currentState) === CONTRIBUTION_SOURCE_SUB2API ? 'SUB2API' : 'CPA';
}
@@ -88,12 +96,54 @@
return normalizeString(currentState.activeFlowId || currentState.flowId).toLowerCase() || 'openai';
}
+ function getActiveTargetId(currentState = getLatestState()) {
+ const activeFlowId = getActiveFlowId(currentState);
+ if (activeFlowId === 'kiro') {
+ return normalizeString(currentState.kiroTargetId || currentState.targetId || 'kiro-rs').toLowerCase() || 'kiro-rs';
+ }
+ return normalizeString(currentState.openaiIntegrationTargetId || currentState.panelMode || currentState.targetId || 'cpa').toLowerCase() || 'cpa';
+ }
+
+ function getContributionTutorialEntry(currentState = getLatestState()) {
+ const rootScope = typeof window !== 'undefined' ? window : globalThis;
+ const registry = rootScope.MultiPageContributionRegistry || {};
+ const activeFlowId = getActiveFlowId(currentState);
+ if (typeof registry.getContributionTutorialEntry === 'function') {
+ return registry.getContributionTutorialEntry(activeFlowId, {
+ adapterId: currentState.contributionAdapterId,
+ portalBaseUrl: contributionPortalUrl,
+ targetId: getActiveTargetId(currentState),
+ });
+ }
+ return {
+ flowId: activeFlowId,
+ targetId: getActiveTargetId(currentState),
+ contributionAdapterId: normalizeString(currentState.contributionAdapterId),
+ portalUrl: normalizeString(contributionPortalUrl),
+ };
+ }
+
+ function getContributionEntryAdapterId(currentState = getLatestState()) {
+ return normalizeString(getContributionTutorialEntry(currentState)?.contributionAdapterId);
+ }
+
function isContributionModeAvailable(currentState = getLatestState()) {
- return getActiveFlowId(currentState) === 'openai';
+ const rootScope = typeof window !== 'undefined' ? window : globalThis;
+ const registry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: 'openai',
+ }) || null;
+ if (registry?.resolveSidepanelCapabilities) {
+ return Boolean(registry.resolveSidepanelCapabilities({
+ activeFlowId: getActiveFlowId(currentState),
+ panelMode: currentState?.panelMode,
+ state: currentState,
+ })?.canShowContributionMode);
+ }
+ return Boolean(currentState?.supportsAccountContribution || getActiveFlowId(currentState) === 'openai');
}
function isContributionModeEnabled(currentState = getLatestState()) {
- return isContributionModeAvailable(currentState) && Boolean(currentState.contributionMode);
+ return isContributionModeAvailable(currentState) && Boolean(currentState.accountContributionEnabled);
}
function hasActiveContributionSession(currentState = getLatestState()) {
@@ -127,10 +177,10 @@
dom.btnContributionMode.title = '当前 flow 不支持贡献模式';
return;
}
- dom.btnContributionMode.disabled = enabled || blocked;
- dom.btnContributionMode.title = blocked
- ? '当前流程运行中,暂时不能切换贡献模式'
- : (enabled ? '当前已在贡献模式' : '进入贡献模式');
+ dom.btnContributionMode.disabled = actionInFlight;
+ dom.btnContributionMode.title = enabled
+ ? '打开当前 flow 教程;当前已在贡献模式'
+ : (blocked ? '打开当前 flow 教程;当前流程运行中暂时不能进入贡献模式' : '打开当前 flow 教程并进入贡献模式');
}
function stopPolling() {
@@ -163,6 +213,23 @@
}
function getOauthStatusText(currentState = getLatestState()) {
+ if (getActiveFlowId(currentState) !== 'openai') {
+ const flowRuntime = getCurrentFlowContributionRuntime(currentState);
+ const status = normalizeString(flowRuntime.status).toLowerCase();
+ if (status === 'submitting') {
+ return '正在提交账号产物';
+ }
+ if (status === 'submitted') {
+ return '账号产物已提交';
+ }
+ if (status === 'skipped') {
+ return '账号产物未就绪';
+ }
+ if (status === 'error') {
+ return '账号产物提交失败';
+ }
+ return isContributionModeEnabled(currentState) ? '等待账号产物' : '未开启贡献模式';
+ }
const status = normalizeStatus(currentState.contributionStatus);
const hasAuthUrl = Boolean(normalizeString(currentState.contributionAuthUrl));
if (!normalizeString(currentState.contributionSessionId) || !hasAuthUrl) {
@@ -184,6 +251,10 @@
}
function getCallbackStatusText(currentState = getLatestState()) {
+ if (getActiveFlowId(currentState) !== 'openai') {
+ const flowRuntime = getCurrentFlowContributionRuntime(currentState);
+ return normalizeString(flowRuntime.lastMessage || flowRuntime.error) || '账号产物就绪后会自动提交';
+ }
const status = normalizeCallbackStatus(currentState.contributionCallbackStatus);
switch (status) {
case 'captured':
@@ -208,6 +279,9 @@
if (statusMessage) {
return statusMessage;
}
+ if (getActiveFlowId(currentState) !== 'openai') {
+ return '当前账号将用于支持项目维护。扩展会按当前 flow 的贡献适配器收集并提交账号产物,提交过程不会依赖 OpenAI OAuth 配置。';
+ }
if (getContributionSource(currentState) === CONTRIBUTION_SOURCE_SUB2API) {
const groupName = normalizeString(currentState.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME;
return `当前账号将用于支持项目维护。贡献会通过 SUB2API 完成,并固定写入 ${groupName} 分组;如检测到回调地址,扩展会自动提交并等待服务端确认。`;
@@ -215,11 +289,24 @@
return DEFAULT_COPY;
}
+ function getCurrentFlowContributionRuntime(currentState = getLatestState()) {
+ const runtime = currentState?.flowContributionRuntime;
+ if (!runtime || typeof runtime !== 'object') {
+ return {};
+ }
+ const flowRuntime = runtime[getActiveFlowId(currentState)];
+ return flowRuntime && typeof flowRuntime === 'object' ? flowRuntime : {};
+ }
+
function getContributionPortalPageUrl() {
- return normalizeString(contributionPortalUrl);
+ return normalizeString(getContributionTutorialEntry()?.portalUrl || contributionPortalUrl);
}
function getContributionUploadPageUrl() {
+ const currentState = getLatestState();
+ if (getActiveFlowId(currentState) !== 'openai') {
+ return normalizeString(getContributionTutorialEntry(currentState)?.portalUrl || contributionPortalUrl);
+ }
return normalizeString(contributionUploadUrl);
}
@@ -240,28 +327,27 @@
}
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,
+ const nickname = normalizeString(partial.nickname);
+ const qq = normalizeString(partial.qq);
+ if (qq && !/^\d{1,20}$/.test(qq)) {
+ throw new Error('QQ 只能填写数字,且长度不能超过 20 位。');
+ }
+ helpers.applySettingsState?.({
+ ...getLatestState(),
+ contributionNickname: nickname,
+ contributionQq: qq,
});
- 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',
+ type: 'SET_ACCOUNT_CONTRIBUTION_MODE',
source: 'sidepanel',
- payload: { enabled: Boolean(enabled) },
+ payload: {
+ enabled: Boolean(enabled),
+ flowId: getActiveFlowId(),
+ adapterId: getContributionEntryAdapterId(),
+ },
});
if (response?.error) {
@@ -287,7 +373,7 @@
pollInFlight = true;
try {
const response = await runtime.sendMessage({
- type: 'POLL_CONTRIBUTION_STATUS',
+ type: 'POLL_FLOW_CONTRIBUTION_STATUS',
source: 'sidepanel',
payload: {
reason: options.reason || 'sidepanel_poll',
@@ -312,7 +398,7 @@
}
}
- async function startContributionFlow() {
+ async function startAccountContributionFlow() {
if (typeof helpers.startContributionAutoRun !== 'function') {
throw new Error('贡献模式尚未接入主自动流程启动能力。');
}
@@ -323,7 +409,6 @@
throw new Error('QQ 只能填写数字,且长度不能超过 20 位。');
}
await syncContributionProfile(profile);
-
const started = await helpers.startContributionAutoRun();
if (!started) {
return;
@@ -348,28 +433,26 @@
const currentState = getLatestState();
const available = isContributionModeAvailable(currentState);
const enabled = isContributionModeEnabled(currentState);
+ const activeFlowId = getActiveFlowId(currentState);
const blocked = available ? isModeSwitchBlocked() : false;
const activeElement = typeof document !== 'undefined' ? document.activeElement : null;
const sourceLabel = available ? getContributionSourceLabel(currentState) : '';
- if (enabled && dom.selectPanelMode) {
+ if (enabled && activeFlowId === 'openai' && dom.selectPanelMode) {
dom.selectPanelMode.value = getContributionSource(currentState);
}
helpers.updatePanelModeUI?.();
helpers.updateAccountRunHistorySettingsUI?.();
- if (dom.contributionModePanel) {
- dom.contributionModePanel.hidden = !available || !enabled;
+ if (dom.accountContributionPanel) {
+ dom.accountContributionPanel.hidden = !available || !enabled;
}
- if (dom.contributionModeText) {
- dom.contributionModeText.textContent = getSummaryText({
- contributionSource: currentState.contributionSource,
- contributionTargetGroupName: currentState.contributionTargetGroupName,
- });
+ if (dom.accountContributionText) {
+ dom.accountContributionText.textContent = getSummaryText(currentState);
}
- if (dom.contributionModeBadge) {
- dom.contributionModeBadge.textContent = enabled ? sourceLabel : '';
+ if (dom.accountContributionBadge) {
+ dom.accountContributionBadge.textContent = enabled ? sourceLabel : '';
}
if (dom.inputContributionNickname && activeElement !== dom.inputContributionNickname) {
const nextNickname = normalizeString(currentState.contributionNickname);
@@ -386,18 +469,24 @@
if (dom.contributionOauthStatus) {
dom.contributionOauthStatus.textContent = getOauthStatusText(currentState);
}
+ if (dom.contributionPrimaryStatusLabel) {
+ dom.contributionPrimaryStatusLabel.textContent = activeFlowId === 'openai' ? 'OAUTH' : '账号产物';
+ }
if (dom.contributionCallbackStatus) {
dom.contributionCallbackStatus.textContent = getCallbackStatusText(currentState);
}
- if (dom.contributionModeSummary) {
- dom.contributionModeSummary.textContent = getSummaryText(currentState);
+ if (dom.contributionSecondaryStatusLabel) {
+ dom.contributionSecondaryStatusLabel.textContent = activeFlowId === 'openai' ? '回调' : '提交';
+ }
+ if (dom.accountContributionSummary) {
+ dom.accountContributionSummary.textContent = getSummaryText(currentState);
}
- syncContributionRows(enabled);
+ syncContributionRows(enabled && activeFlowId === 'openai');
syncContributionButton(enabled, blocked, available);
if (dom.selectPanelMode) {
- dom.selectPanelMode.disabled = available && enabled;
+ dom.selectPanelMode.disabled = activeFlowId === 'openai' && available && enabled;
}
if (dom.btnStartContribution) {
@@ -406,6 +495,7 @@
if (dom.btnOpenContributionUpload) {
dom.btnOpenContributionUpload.disabled = !available;
+ dom.btnOpenContributionUpload.textContent = activeFlowId === 'openai' ? '已有认证文件?前往上传' : '查看当前 flow 贡献说明';
}
if (dom.btnExitContributionMode) {
@@ -441,7 +531,13 @@
}
render();
try {
- await enterContributionMode();
+ if (isContributionModeEnabled()) {
+ helpers.showToast?.('已打开当前 flow 教程。', 'info', 1800);
+ } else if (isModeSwitchBlocked()) {
+ helpers.showToast?.('已打开当前 flow 教程;当前流程运行中,暂时不能进入贡献模式。', 'warning', 2200);
+ } else {
+ await enterContributionMode();
+ }
} catch (error) {
helpers.showToast?.(error.message, 'error');
} finally {
@@ -457,7 +553,7 @@
actionInFlight = true;
render();
try {
- await startContributionFlow();
+ await startAccountContributionFlow();
} catch (error) {
helpers.showToast?.(error.message, 'error');
} finally {
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 62fcf77..f7079e5 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -152,11 +152,11 @@
- OAUTH
+ OAUTH
未生成登录地址
- 回调
+ 回调
等待回调
@@ -1857,6 +1857,7 @@
+
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 8ecef92..73daa03 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -36,14 +36,16 @@ const btnIgnoreRelease = document.getElementById('btn-ignore-release');
const btnOpenRelease = document.getElementById('btn-open-release');
const settingsCard = document.getElementById('settings-card');
const selectFlow = document.getElementById('select-flow');
-const contributionModePanel = document.getElementById('contribution-mode-panel');
-const contributionModeBadge = document.getElementById('contribution-mode-badge');
-const contributionModeText = document.getElementById('contribution-mode-text');
+const accountContributionPanel = document.getElementById('contribution-mode-panel');
+const accountContributionBadge = document.getElementById('contribution-mode-badge');
+const accountContributionText = document.getElementById('contribution-mode-text');
const inputContributionNickname = document.getElementById('input-contribution-nickname');
const inputContributionQq = document.getElementById('input-contribution-qq');
+const contributionPrimaryStatusLabel = document.getElementById('contribution-primary-status-label');
+const contributionSecondaryStatusLabel = document.getElementById('contribution-secondary-status-label');
const contributionOauthStatus = document.getElementById('contribution-oauth-status');
const contributionCallbackStatus = document.getElementById('contribution-callback-status');
-const contributionModeSummary = document.getElementById('contribution-mode-summary');
+const accountContributionSummary = document.getElementById('contribution-mode-summary');
const btnStartContribution = document.getElementById('btn-start-contribution');
const btnOpenContributionUpload = document.getElementById('btn-open-contribution-upload');
const btnExitContributionMode = document.getElementById('btn-exit-contribution-mode');
@@ -2074,7 +2076,7 @@ function shouldPromptNewUserGuide() {
}
if (typeof isContributionModeActiveForFlow === 'function'
? isContributionModeActiveForFlow(latestState)
- : Boolean(latestState?.contributionMode)) {
+ : Boolean(latestState?.accountContributionEnabled)) {
return false;
}
return true;
@@ -2084,6 +2086,18 @@ function getContributionPortalUrl() {
return String(contributionContentService?.portalUrl || 'https://flowpilot.qlhazycoder.top').trim();
}
+function getContributionContentFlowId(state = latestState) {
+ return String(state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID;
+}
+
+function getContributionContentTargetId(state = latestState) {
+ const flowId = getContributionContentFlowId(state);
+ if (flowId === 'kiro') {
+ return String(state?.kiroTargetId || state?.targetId || 'kiro-rs').trim().toLowerCase() || 'kiro-rs';
+ }
+ return String(state?.openaiIntegrationTargetId || state?.panelMode || state?.targetId || 'cpa').trim().toLowerCase() || 'cpa';
+}
+
function openNewUserGuidePrompt() {
return openActionModal({
title: '新手引导',
@@ -2112,16 +2126,29 @@ async function maybeShowNewUserGuidePrompt() {
return false;
}
-function getDismissedContributionContentPromptVersion() {
- return String(localStorage.getItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY) || '').trim();
+function getContributionContentPromptScope(snapshot = currentContributionContentSnapshot) {
+ return {
+ flowId: String(snapshot?.flowId || getContributionContentFlowId()).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID,
+ targetId: String(snapshot?.targetId || getContributionContentTargetId()).trim().toLowerCase() || 'cpa',
+ };
}
-function setDismissedContributionContentPromptVersion(version) {
+function getContributionContentPromptDismissedStorageKey(snapshot = currentContributionContentSnapshot) {
+ const scope = getContributionContentPromptScope(snapshot);
+ return `${CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY}:${scope.flowId}:${scope.targetId}`;
+}
+
+function getDismissedContributionContentPromptVersion(snapshot = currentContributionContentSnapshot) {
+ return String(localStorage.getItem(getContributionContentPromptDismissedStorageKey(snapshot)) || '').trim();
+}
+
+function setDismissedContributionContentPromptVersion(version, snapshot = currentContributionContentSnapshot) {
const normalized = String(version || '').trim();
+ const storageKey = getContributionContentPromptDismissedStorageKey(snapshot);
if (normalized) {
- localStorage.setItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY, normalized);
+ localStorage.setItem(storageKey, normalized);
} else {
- localStorage.removeItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY);
+ localStorage.removeItem(storageKey);
}
}
@@ -2301,25 +2328,25 @@ async function openAutoRunFallbackRiskConfirmModal(totalRuns) {
function updateConfigMenuControls() {
const disabled = configActionInFlight || settingsSaveInFlight;
- const contributionModeEnabled = typeof isContributionModeActiveForFlow === 'function'
+ const accountContributionEnabled = typeof isContributionModeActiveForFlow === 'function'
? isContributionModeActiveForFlow(latestState)
- : Boolean(latestState?.contributionMode);
- if (contributionModeEnabled && configMenuOpen) {
+ : Boolean(latestState?.accountContributionEnabled);
+ if (accountContributionEnabled && configMenuOpen) {
configMenuOpen = false;
}
const importLocked = disabled
- || contributionModeEnabled
+ || accountContributionEnabled
|| currentAutoRun.autoRunning
|| Object.values(getStepStatuses()).some((status) => status === 'running');
if (btnConfigMenu) {
- btnConfigMenu.disabled = disabled || contributionModeEnabled;
+ btnConfigMenu.disabled = disabled || accountContributionEnabled;
btnConfigMenu.setAttribute('aria-expanded', String(configMenuOpen));
}
if (configMenu) {
- configMenu.hidden = contributionModeEnabled || !configMenuOpen;
+ configMenu.hidden = accountContributionEnabled || !configMenuOpen;
}
if (btnExportSettings) {
- btnExportSettings.disabled = disabled || contributionModeEnabled;
+ btnExportSettings.disabled = disabled || accountContributionEnabled;
}
if (btnImportSettings) {
btnImportSettings.disabled = importLocked;
@@ -2776,7 +2803,10 @@ function isContributionModeActiveForFlow(state = latestState, flowId = undefined
const normalizedFlowId = typeof normalizeFlowId === 'function'
? normalizeFlowId(rawFlowId, DEFAULT_ACTIVE_FLOW_ID)
: (String(rawFlowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID);
- return normalizedFlowId === DEFAULT_ACTIVE_FLOW_ID && Boolean(state?.contributionMode);
+ const stateFlowId = typeof normalizeFlowId === 'function'
+ ? normalizeFlowId(state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID, DEFAULT_ACTIVE_FLOW_ID)
+ : (String(state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID);
+ return normalizedFlowId === stateFlowId && Boolean(state?.accountContributionEnabled);
}
let accountRunHistoryRefreshTimer = null;
@@ -3775,9 +3805,9 @@ function collectSettingsPayload() {
}
return normalized || defaultFlowId;
})();
- const contributionModeEnabled = typeof isContributionModeActiveForFlow === 'function'
+ const accountContributionEnabled = typeof isContributionModeActiveForFlow === 'function'
? isContributionModeActiveForFlow(latestState, activeFlowId)
- : (activeFlowId === defaultFlowId && Boolean(latestState?.contributionMode));
+ : (activeFlowId === defaultFlowId && Boolean(latestState?.accountContributionEnabled));
const icloudFetchModeRawValue = typeof selectIcloudFetchMode !== 'undefined'
? String(selectIcloudFetchMode?.value || '')
: '';
@@ -4419,7 +4449,7 @@ function collectSettingsPayload() {
: null;
return {
activeFlowId,
- ...(contributionModeEnabled ? {} : {
+ ...(accountContributionEnabled ? {} : {
...(activeFlowId === defaultFlowId ? { panelMode: effectivePanelMode } : {}),
}),
kiroTargetId: normalizeKiroTargetIdSafe(
@@ -4528,7 +4558,7 @@ function collectSettingsPayload() {
? inputGpcHelperLocalSmsUrl.value
: (latestState?.gopayHelperLocalSmsHelperUrl || '')
),
- ...(contributionModeEnabled ? {} : {
+ ...(accountContributionEnabled ? {} : {
customPassword: inputPassword.value,
}),
mailProvider: selectMailProvider.value,
@@ -4548,7 +4578,7 @@ function collectSettingsPayload() {
icloudFetchMode: (icloudFetchModeRawValue.trim().toLowerCase() === 'always_new'
? 'always_new'
: 'reuse_existing'),
- ...(contributionModeEnabled ? {} : {
+ ...(accountContributionEnabled ? {} : {
accountRunHistoryTextEnabled: true,
accountRunHistoryHelperBaseUrl: normalizeAccountRunHistoryHelperBaseUrlValue(inputAccountRunHistoryHelperBaseUrl?.value),
}),
@@ -8695,9 +8725,9 @@ function canSelectPhoneSignupMethod() {
const plusModeEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
? Boolean(inputPlusModeEnabled.checked)
: Boolean(latestState?.plusModeEnabled);
- const contributionModeEnabled = typeof isContributionModeActiveForFlow === 'function'
+ const accountContributionEnabled = typeof isContributionModeActiveForFlow === 'function'
? isContributionModeActiveForFlow(latestState)
- : Boolean(latestState?.contributionMode);
+ : Boolean(latestState?.accountContributionEnabled);
const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
? resolveCurrentSidepanelCapabilities({
panelMode: typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : latestState?.panelMode,
@@ -8705,7 +8735,7 @@ function canSelectPhoneSignupMethod() {
...(typeof latestState !== 'undefined' ? latestState : {}),
phoneVerificationEnabled: phoneEnabled,
plusModeEnabled,
- contributionMode: contributionModeEnabled,
+ accountContributionEnabled,
},
})
: (() => {
@@ -8721,7 +8751,7 @@ function canSelectPhoneSignupMethod() {
...(typeof latestState !== 'undefined' ? latestState : {}),
phoneVerificationEnabled: phoneEnabled,
plusModeEnabled,
- contributionMode: contributionModeEnabled,
+ accountContributionEnabled,
},
})
: null;
@@ -8729,7 +8759,7 @@ function canSelectPhoneSignupMethod() {
if (capabilityState && typeof capabilityState.canSelectPhoneSignup === 'boolean') {
return capabilityState.canSelectPhoneSignup;
}
- return phoneEnabled && !plusModeEnabled && !contributionModeEnabled;
+ return phoneEnabled && !plusModeEnabled && !accountContributionEnabled;
}
function isSignupMethodSwitchLocked() {
@@ -8751,9 +8781,9 @@ function updateSignupMethodUI(options = {}) {
let selectedMethod = normalizeSignupMethod(getSelectedSignupMethod());
const phoneSelectable = canSelectPhoneSignupMethod();
- const contributionModeEnabled = typeof isContributionModeActiveForFlow === 'function'
+ const accountContributionEnabled = typeof isContributionModeActiveForFlow === 'function'
? isContributionModeActiveForFlow(latestState)
- : Boolean(latestState?.contributionMode);
+ : Boolean(latestState?.accountContributionEnabled);
if (!phoneSelectable && selectedMethod === SIGNUP_METHOD_PHONE) {
selectedMethod = setSignupMethod(SIGNUP_METHOD_EMAIL);
if (options.notify && typeof showToast === 'function') {
@@ -8773,9 +8803,9 @@ function updateSignupMethodUI(options = {}) {
if (!Boolean(inputPhoneVerificationEnabled?.checked)) {
button.title = '开启接码后可选择手机号注册';
} else if (typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled?.checked) {
- button.title = 'Plus 模式第一版暂不支持手机号注册';
- } else if (contributionModeEnabled) {
- button.title = '贡献模式第一版暂不支持手机号注册';
+ button.title = 'Plus 模式暂不支持手机号注册';
+ } else if (accountContributionEnabled) {
+ button.title = '账号贡献开启时不能使用手机号注册';
} else if (locked) {
button.title = '自动流程运行中不能切换注册方式';
} else {
@@ -11392,12 +11422,12 @@ function shouldShowContributionUpdateHint(snapshot = currentContributionContentS
if (!getContributionUpdatePromptLines(snapshot).length) {
return false;
}
- if (promptVersion === getDismissedContributionContentPromptVersion()) {
+ if (promptVersion === getDismissedContributionContentPromptVersion(snapshot)) {
return false;
}
if (typeof isContributionModeActiveForFlow === 'function'
? isContributionModeActiveForFlow(latestState)
- : Boolean(latestState?.contributionMode)) {
+ : Boolean(latestState?.accountContributionEnabled)) {
return false;
}
return !btnContributionMode.disabled;
@@ -11522,7 +11552,10 @@ async function refreshContributionContentHint() {
return contributionContentSnapshotRequestInFlight;
}
- contributionContentSnapshotRequestInFlight = contributionContentService.getContentUpdateSnapshot()
+ contributionContentSnapshotRequestInFlight = contributionContentService.getContentUpdateSnapshot({
+ flowId: getContributionContentFlowId(),
+ targetId: getContributionContentTargetId(),
+ })
.then((snapshot) => {
currentContributionContentSnapshot = snapshot;
renderContributionUpdateHint(snapshot);
@@ -11543,10 +11576,10 @@ async function refreshContributionContentHint() {
}
function syncPasswordField(state) {
- const contributionModeEnabled = typeof isContributionModeActiveForFlow === 'function'
+ const accountContributionEnabled = typeof isContributionModeActiveForFlow === 'function'
? isContributionModeActiveForFlow(state)
- : Boolean(state?.contributionMode);
- inputPassword.value = contributionModeEnabled ? '' : (state.customPassword || state.password || '');
+ : Boolean(state?.accountContributionEnabled);
+ inputPassword.value = accountContributionEnabled ? '' : (state.customPassword || state.password || '');
}
function isCustomMailProvider(provider = selectMailProvider.value) {
@@ -13518,7 +13551,7 @@ const bindAccountRecordEvents = accountRecordsManager?.bindEvents
const closeAccountRecordsPanel = accountRecordsManager?.closePanel
|| (() => { });
bindAccountRecordEvents();
-const contributionModeManager = window.SidepanelContributionMode?.createContributionModeManager({
+const accountContributionManager = window.SidepanelContributionMode?.createContributionModeManager({
state: {
getLatestState: () => latestState,
},
@@ -13527,15 +13560,17 @@ const contributionModeManager = window.SidepanelContributionMode?.createContribu
btnContributionMode,
inputContributionNickname,
inputContributionQq,
+ contributionPrimaryStatusLabel,
+ contributionSecondaryStatusLabel,
contributionCallbackStatus,
btnExitContributionMode,
btnOpenAccountRecords,
btnOpenContributionUpload,
btnStartContribution,
- contributionModeBadge,
- contributionModePanel,
- contributionModeSummary,
- contributionModeText,
+ accountContributionBadge,
+ accountContributionPanel,
+ accountContributionSummary,
+ accountContributionText,
contributionOauthStatus,
rowAccountRunHistoryHelperBaseUrl,
rowPhoneVerificationEnabled,
@@ -13579,16 +13614,16 @@ const contributionModeManager = window.SidepanelContributionMode?.createContribu
contributionUploadUrl: `${String(contributionContentService?.portalUrl || 'https://flowpilot.qlhazycoder.top').replace(/\/+$/, '')}/upload`,
},
});
-const baseRenderContributionMode = contributionModeManager?.render
+const baseRenderAccountContribution = accountContributionManager?.render
|| (() => { });
const renderContributionMode = () => {
- baseRenderContributionMode();
+ baseRenderAccountContribution();
renderContributionUpdateHint();
updateSignupMethodUI({ notify: true });
};
-const bindContributionModeEvents = contributionModeManager?.bindEvents
+const bindAccountContributionEvents = accountContributionManager?.bindEvents
|| (() => { });
-bindContributionModeEvents();
+bindAccountContributionEvents();
renderStepsList();
async function exportSettingsFile() {
@@ -14094,7 +14129,7 @@ async function startAutoRunFromCurrentSettings() {
plusModeEnabled: typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
? Boolean(inputPlusModeEnabled.checked)
: Boolean(latestState?.plusModeEnabled),
- contributionMode: Boolean(latestState?.contributionMode),
+ accountContributionEnabled: Boolean(latestState?.accountContributionEnabled),
};
return registry.validateAutoRunStart({
activeFlowId: validationState.activeFlowId,
@@ -14184,7 +14219,8 @@ async function startAutoRunFromCurrentSettings() {
activeFlowId,
targetId,
autoRunSkipFailures,
- contributionMode: Boolean(latestState?.contributionMode),
+ accountContributionEnabled: Boolean(latestState?.accountContributionEnabled),
+ contributionAdapterId: latestState?.contributionAdapterId || '',
contributionNickname,
contributionQq,
mode,
@@ -16533,7 +16569,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (
message.payload.password !== undefined
|| message.payload.customPassword !== undefined
- || message.payload.contributionMode !== undefined
+ || message.payload.accountContributionEnabled !== undefined
) {
syncPasswordField(latestState || {});
}
diff --git a/tests/auto-run-step6-restart.test.js b/tests/auto-run-step6-restart.test.js
index c39f906..e39f613 100644
--- a/tests/auto-run-step6-restart.test.js
+++ b/tests/auto-run-step6-restart.test.js
@@ -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,
},
});
diff --git a/tests/background-account-run-history-module.test.js b/tests/background-account-run-history-module.test.js
index 10cc295..ecdbb75 100644
--- a/tests/background-account-run-history-module.test.js
+++ b/tests/background-account-run-history-module.test.js
@@ -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 () => {
diff --git a/tests/background-contribution-mode.test.js b/tests/background-contribution-mode.test.js
index afe84ee..591a440 100644
--- a/tests/background-contribution-mode.test.js
+++ b/tests/background-contribution-mode.test.js
@@ -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;
diff --git a/tests/background-luckmail.test.js b/tests/background-luckmail.test.js
index 8ae1784..d87d5ce 100644
--- a/tests/background-luckmail.test.js
+++ b/tests/background-luckmail.test.js
@@ -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: '',",
diff --git a/tests/background-message-router-plus-final-step.test.js b/tests/background-message-router-plus-final-step.test.js
index f0d0afb..2ad6b6e 100644
--- a/tests/background-message-router-plus-final-step.test.js
+++ b/tests/background-message-router-plus-final-step.test.js
@@ -100,7 +100,7 @@ function createRouterWithFinalNode(options = {}) {
selectLuckmailPurchase: async () => {},
setCurrentHotmailAccount: async () => {},
setCurrentMail2925Account: async () => {},
- setContributionMode: async () => {},
+ setAccountContributionMode: async () => {},
setEmailState: async () => {},
setEmailStateSilently: async () => {},
setIcloudAliasPreservedState: async () => {},
diff --git a/tests/background-signup-method-settings.test.js b/tests/background-signup-method-settings.test.js
index b92dbb1..2970344 100644
--- a/tests/background-signup-method-settings.test.js
+++ b/tests/background-signup-method-settings.test.js
@@ -58,7 +58,7 @@ let state = {
signupMethod: 'phone',
phoneVerificationEnabled: true,
plusModeEnabled: false,
- contributionMode: false,
+ accountContributionEnabled: false,
resolvedSignupMethod: null,
};
async function getState() { return { ...state }; }
diff --git a/tests/background-step-completion.test.js b/tests/background-step-completion.test.js
index 84efa02..7c7d38a 100644
--- a/tests/background-step-completion.test.js
+++ b/tests/background-step-completion.test.js
@@ -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;
diff --git a/tests/contribution-content-update-service.test.js b/tests/contribution-content-update-service.test.js
index 7ee6357..cf76e19 100644
--- a/tests/contribution-content-update-service.test.js
+++ b/tests/contribution-content-update-service.test.js
@@ -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);
+});
diff --git a/tests/contribution-registry.test.js b/tests/contribution-registry.test.js
new file mode 100644
index 0000000..af07481
--- /dev/null
+++ b/tests/contribution-registry.test.js
@@ -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/
+ );
+});
diff --git a/tests/flow-capabilities-module.test.js b/tests/flow-capabilities-module.test.js
index db05a61..0f9befe 100644
--- a/tests/flow-capabilities-module.test.js
+++ b/tests/flow-capabilities-module.test.js
@@ -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),
diff --git a/tests/flow-registry-settings-schema.test.js b/tests/flow-registry-settings-schema.test.js
index f91be78..265a05f 100644
--- a/tests/flow-registry-settings-schema.test.js
+++ b/tests/flow-registry-settings-schema.test.js
@@ -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', () => {
diff --git a/tests/kiro-contribution-artifact.test.js b/tests/kiro-contribution-artifact.test.js
new file mode 100644
index 0000000..4325953
--- /dev/null
+++ b/tests/kiro-contribution-artifact.test.js
@@ -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'/);
+});
diff --git a/tests/sidepanel-auto-run-content-refresh.test.js b/tests/sidepanel-auto-run-content-refresh.test.js
index 7ffda27..25b3734 100644
--- a/tests/sidepanel-auto-run-content-refresh.test.js
+++ b/tests/sidepanel-auto-run-content-refresh.test.js
@@ -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 };
diff --git a/tests/sidepanel-contribution-mode-flow-scope.test.js b/tests/sidepanel-contribution-mode-flow-scope.test.js
index 0b39776..423932f 100644
--- a/tests/sidepanel-contribution-mode-flow-scope.test.js
+++ b/tests/sidepanel-contribution-mode-flow-scope.test.js
@@ -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');
+});
diff --git a/tests/sidepanel-contribution-mode.test.js b/tests/sidepanel-contribution-mode.test.js
index ef46feb..1e8995b 100644
--- a/tests/sidepanel-contribution-mode.test.js
+++ b/tests/sidepanel-contribution-mode.test.js
@@ -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();
});
diff --git a/tests/sidepanel-icloud-provider.test.js b/tests/sidepanel-icloud-provider.test.js
index a5a648b..1d61c3c 100644
--- a/tests/sidepanel-icloud-provider.test.js
+++ b/tests/sidepanel-icloud-provider.test.js
@@ -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';
diff --git a/tests/sidepanel-mail2925-base-email.test.js b/tests/sidepanel-mail2925-base-email.test.js
index 91c4326..99e39a3 100644
--- a/tests/sidepanel-mail2925-base-email.test.js
+++ b/tests/sidepanel-mail2925-base-email.test.js
@@ -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',
};
diff --git a/tests/sidepanel-new-user-guide.test.js b/tests/sidepanel-new-user-guide.test.js
index 31a294f..8b1db74 100644
--- a/tests/sidepanel-new-user-guide.test.js
+++ b/tests/sidepanel-new-user-guide.test.js
@@ -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;
diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js
index 16cc543..fa31ae8 100644
--- a/tests/sidepanel-phone-verification-settings.test.js
+++ b/tests/sidepanel-phone-verification-settings.test.js
@@ -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'],