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