diff --git a/background.js b/background.js index c73a72b..277d4b8 100644 --- a/background.js +++ b/background.js @@ -1,6 +1,8 @@ // background.js — Service Worker: orchestration, state, tab management, message routing importScripts( + 'shared/flow-registry.js', + 'shared/settings-schema.js', 'shared/source-registry.js', 'shared/flow-capabilities.js', 'managed-alias-utils.js', @@ -51,6 +53,7 @@ importScripts( 'background/steps/fetch-login-code.js', 'background/steps/confirm-oauth.js', 'background/steps/platform-verify.js', + 'background/steps/kiro-device-auth.js', 'data/names.js', 'hotmail-utils.js', 'microsoft-email.js', @@ -136,22 +139,37 @@ const PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = self.MultiPageStepDe phoneSignupReloginAfterBindEmailEnabled: true, }) || PLUS_GPC_PHONE_STEP_DEFINITIONS; const PLUS_STEP_DEFINITIONS = PLUS_PAYPAL_STEP_DEFINITIONS; -const ALL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getAllSteps?.({ - activeFlowId: DEFAULT_ACTIVE_FLOW_ID, -}) || [ - ...NORMAL_STEP_DEFINITIONS, - ...NORMAL_PHONE_STEP_DEFINITIONS, - ...NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, - ...PLUS_PAYPAL_STEP_DEFINITIONS, - ...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS, - ...PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, - ...PLUS_GOPAY_STEP_DEFINITIONS, - ...PLUS_GOPAY_PHONE_STEP_DEFINITIONS, - ...PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, - ...PLUS_GPC_STEP_DEFINITIONS, - ...PLUS_GPC_PHONE_STEP_DEFINITIONS, - ...PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, -]; +const REGISTERED_STEP_FLOW_IDS = self.MultiPageStepDefinitions?.getRegisteredFlowIds?.() || [DEFAULT_ACTIVE_FLOW_ID]; +const ALL_STEP_DEFINITIONS = (() => { + if (self.MultiPageStepDefinitions?.getAllSteps) { + const keyedDefinitions = new Map(); + for (const flowId of REGISTERED_STEP_FLOW_IDS) { + const definitions = self.MultiPageStepDefinitions.getAllSteps({ activeFlowId: flowId }); + for (const definition of Array.isArray(definitions) ? definitions : []) { + const key = `${flowId}:${Number(definition?.id) || 0}:${String(definition?.key || '').trim()}`; + keyedDefinitions.set(key, definition); + } + } + const allDefinitions = Array.from(keyedDefinitions.values()); + if (allDefinitions.length) { + return allDefinitions; + } + } + return [ + ...NORMAL_STEP_DEFINITIONS, + ...NORMAL_PHONE_STEP_DEFINITIONS, + ...NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, + ...PLUS_PAYPAL_STEP_DEFINITIONS, + ...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS, + ...PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, + ...PLUS_GOPAY_STEP_DEFINITIONS, + ...PLUS_GOPAY_PHONE_STEP_DEFINITIONS, + ...PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, + ...PLUS_GPC_STEP_DEFINITIONS, + ...PLUS_GPC_PHONE_STEP_DEFINITIONS, + ...PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, + ]; +})(); const STEP_IDS = Array.from(new Set(ALL_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite))) @@ -870,6 +888,11 @@ function setupDeclarativeNetRequestRules() { const PERSISTED_SETTING_DEFAULTS = { panelMode: 'cpa', + activeFlowId: DEFAULT_ACTIVE_FLOW_ID, + kiroSourceId: 'kiro-rs', + kiroRsUrl: self.MultiPageFlowRegistry?.DEFAULT_KIRO_RS_URL || 'https://kiro.leftcode.xyz/admin', + kiroRsKey: '', + kiroRegion: self.MultiPageFlowRegistry?.DEFAULT_KIRO_REGION || 'us-east-1', vpsUrl: '', vpsPassword: '', localCpaStep9Mode: DEFAULT_LOCAL_CPA_STEP9_MODE, @@ -1051,6 +1074,7 @@ const PERSISTED_SETTING_DEFAULTS = { }; const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS); +const PERSISTED_SETTINGS_SCHEMA_KEYS = ['settingsSchemaVersion', 'settingsState']; const SETTINGS_EXPORT_SCHEMA_VERSION = 1; const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings'; const STEP6_REGISTRATION_SUCCESS_WAIT_MS = 20000; @@ -2773,6 +2797,30 @@ function normalizePersistentSettingValue(key, value) { switch (key) { case 'panelMode': return normalizePanelMode(value); + case 'activeFlowId': + if (typeof self.MultiPageFlowRegistry?.normalizeFlowId === 'function') { + return self.MultiPageFlowRegistry.normalizeFlowId(value, DEFAULT_ACTIVE_FLOW_ID); + } + return String(value || '').trim().toLowerCase() === 'kiro' ? 'kiro' : DEFAULT_ACTIVE_FLOW_ID; + case 'kiroSourceId': + if (typeof self.MultiPageFlowRegistry?.normalizeSourceId === 'function') { + return self.MultiPageFlowRegistry.normalizeSourceId('kiro', value, 'kiro-rs'); + } + return String(value || '').trim().toLowerCase() === 'kiro-rs' ? 'kiro-rs' : 'kiro-rs'; + case 'kiroRsUrl': + return String( + value + || self.MultiPageFlowRegistry?.DEFAULT_KIRO_RS_URL + || 'https://kiro.leftcode.xyz/admin' + ).trim() || 'https://kiro.leftcode.xyz/admin'; + case 'kiroRsKey': + return String(value || ''); + case 'kiroRegion': + return String( + value + || self.MultiPageFlowRegistry?.DEFAULT_KIRO_REGION + || 'us-east-1' + ).trim() || 'us-east-1'; case 'vpsUrl': return String(value || '').trim(); case 'vpsPassword': @@ -3198,6 +3246,11 @@ function buildPersistentSettingsPayload(input = {}, options = {}) { } } + const isPlainObjectForSettingsSchema = typeof isPlainObjectValue === 'function' + ? isPlainObjectValue + : ((value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value)); + const hasExplicitSettingsState = isPlainObjectForSettingsSchema(normalizedInput.settingsState); + const payload = {}; let matchedKeyCount = 0; for (const key of persistedSettingKeys) { @@ -3220,10 +3273,10 @@ function buildPersistentSettingsPayload(input = {}, options = {}) { : normalizedInput.fiveSimReuseEnabled); const normalizedReuseEnabled = normalizePersistentSettingValue('phoneSmsReuseEnabled', reuseSource); payload.phoneSmsReuseEnabled = normalizedReuseEnabled; - payload.heroSmsReuseEnabled = normalizedReuseEnabled; + payload.heroSmsReuseEnabled = normalizedReuseEnabled; } - if (requireKnownKeys && matchedKeyCount === 0) { + if (requireKnownKeys && matchedKeyCount === 0 && !hasExplicitSettingsState) { throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e2d\u6ca1\u6709\u53ef\u8bc6\u522b\u7684\u914d\u7f6e\u5185\u5bb9\u3002'); } @@ -3301,12 +3354,54 @@ function buildPersistentSettingsPayload(input = {}, options = {}) { payload.ipProxyRegion = String(activeProfile?.region || payload.ipProxyRegion || '').trim(); } + const hasExplicitSettingsSchema = hasExplicitSettingsState + || Object.prototype.hasOwnProperty.call(normalizedInput, 'settingsSchemaVersion'); + if (fillDefaults || hasExplicitSettingsSchema) { + const settingsSchemaApi = typeof getSettingsSchemaApi === 'function' + ? getSettingsSchemaApi() + : null; + if (settingsSchemaApi?.normalizeSettingsState && settingsSchemaApi?.buildLegacySettingsPayload) { + const settingsSchemaInput = {}; + for (const key of persistedSettingKeys) { + if (normalizedInput[key] !== undefined) { + settingsSchemaInput[key] = payload[key]; + } + } + const normalizedSettingsState = settingsSchemaApi.normalizeSettingsState({ + ...settingsSchemaInput, + ...(isPlainObjectForSettingsSchema(normalizedInput.settingsState) + ? { settingsState: normalizedInput.settingsState } + : {}), + ...(Object.prototype.hasOwnProperty.call(normalizedInput, 'settingsSchemaVersion') + ? { settingsSchemaVersion: normalizedInput.settingsSchemaVersion } + : {}), + }, { + activeFlowId: settingsSchemaInput.activeFlowId || normalizedInput.activeFlowId || DEFAULT_ACTIVE_FLOW_ID, + }); + Object.assign( + payload, + settingsSchemaApi.buildLegacySettingsPayload(normalizedSettingsState, payload) + ); + } + } + return payload; } +function getSettingsSchemaApi() { + if (typeof self.MultiPageSettingsSchema?.createSettingsSchema !== 'function') { + return null; + } + return self.MultiPageSettingsSchema.createSettingsSchema({ + flowRegistry: self.MultiPageFlowRegistry, + defaultFlowId: DEFAULT_ACTIVE_FLOW_ID, + }); +} + async function getPersistedSettings() { const stored = await chrome.storage.local.get([ ...PERSISTED_SETTING_KEYS, + ...PERSISTED_SETTINGS_SCHEMA_KEYS, ...LEGACY_AUTO_STEP_DELAY_KEYS, ...LEGACY_VERIFICATION_RESEND_COUNT_KEYS, ]); @@ -3396,11 +3491,87 @@ async function setState(updates) { } async function setPersistentSettings(updates) { - const persistedUpdates = buildPersistentSettingsPayload(updates); + const currentSettings = await getPersistedSettings(); + const nextUpdates = updates && typeof updates === 'object' && !Array.isArray(updates) + ? updates + : {}; + const settingsSchemaApi = typeof getSettingsSchemaApi === 'function' + ? getSettingsSchemaApi() + : null; + + let persistedUpdates; + if (settingsSchemaApi?.normalizeSettingsState && settingsSchemaApi?.buildLegacySettingsPayload) { + const isPlainObjectForSettingsSchema = typeof isPlainObjectValue === 'function' + ? isPlainObjectValue + : ((value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value)); + const cloneSettingsValue = (value) => { + if (Array.isArray(value)) { + return value.map((entry) => cloneSettingsValue(entry)); + } + if (isPlainObjectForSettingsSchema(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, entryValue]) => [key, cloneSettingsValue(entryValue)]) + ); + } + return value; + }; + const mergeSettingsState = (baseValue, patchValue) => { + if (Array.isArray(patchValue)) { + return patchValue.map((entry) => cloneSettingsValue(entry)); + } + if (!isPlainObjectForSettingsSchema(patchValue)) { + return patchValue === undefined ? cloneSettingsValue(baseValue) : patchValue; + } + + const baseObject = isPlainObjectForSettingsSchema(baseValue) ? baseValue : {}; + const nextObject = { + ...cloneSettingsValue(baseObject), + }; + for (const [key, entryValue] of Object.entries(patchValue)) { + nextObject[key] = mergeSettingsState(baseObject[key], entryValue); + } + return nextObject; + }; + + const currentSettingsState = settingsSchemaApi.normalizeSettingsState( + isPlainObjectForSettingsSchema(currentSettings?.settingsState) + ? { settingsState: currentSettings.settingsState } + : currentSettings, + { + activeFlowId: currentSettings?.activeFlowId || DEFAULT_ACTIVE_FLOW_ID, + } + ); + const mergedSettingsState = isPlainObjectForSettingsSchema(nextUpdates.settingsState) + ? mergeSettingsState(currentSettingsState, nextUpdates.settingsState) + : currentSettingsState; + const explicitFlatUpdates = { + ...nextUpdates, + }; + delete explicitFlatUpdates.settingsSchemaVersion; + delete explicitFlatUpdates.settingsState; + + persistedUpdates = buildPersistentSettingsPayload({ + ...explicitFlatUpdates, + settingsSchemaVersion: nextUpdates.settingsSchemaVersion ?? currentSettings.settingsSchemaVersion, + settingsState: mergedSettingsState, + }, { + fillDefaults: true, + }); + } else { + persistedUpdates = buildPersistentSettingsPayload({ + ...currentSettings, + ...nextUpdates, + settingsSchemaVersion: nextUpdates.settingsSchemaVersion ?? currentSettings.settingsSchemaVersion, + settingsState: nextUpdates.settingsState ?? currentSettings.settingsState, + }, { + fillDefaults: true, + }); + } if (Object.keys(persistedUpdates).length > 0) { await chrome.storage.local.set(persistedUpdates); } + return persistedUpdates; } function buildSettingsExportFilename(date = new Date()) { @@ -3469,10 +3640,10 @@ async function importSettingsBundle(configBundle) { }); } - await setPersistentSettings(importedSettings); + const persistedSettings = await setPersistentSettings(importedSettings) || importedSettings; const sessionUpdates = { - ...importedSettings, + ...persistedSettings, currentHotmailAccountId: null, email: null, registrationEmailState: { ...DEFAULT_REGISTRATION_EMAIL_STATE }, @@ -3480,7 +3651,7 @@ async function importSettingsBundle(configBundle) { await setState(sessionUpdates); broadcastDataUpdate({ - ...importedSettings, + ...persistedSettings, currentHotmailAccountId: null, ...(sessionUpdates.email !== undefined ? { email: sessionUpdates.email } : {}), registrationEmailState: sessionUpdates.registrationEmailState, @@ -8727,6 +8898,57 @@ function hasSavedProgress(statuses = {}, stateOverride = null) { function getDownstreamStateResets(step, state = {}) { const stepKey = getStepExecutionKeyForState(step, state); + if (stepKey === 'kiro-start-device-login') { + return { + flowStartTime: null, + kiroAccessToken: '', + kiroAuthError: '', + kiroAuthExpiresAt: 0, + kiroAuthIntervalSeconds: 0, + kiroAuthRegion: '', + kiroAuthStatus: '', + kiroAuthTabId: null, + kiroAuthorizedEmail: '', + kiroClientId: '', + kiroClientSecret: '', + kiroCredentialId: null, + kiroDeviceAuthorizationCode: '', + kiroDeviceCode: '', + kiroLastConnectionMessage: '', + kiroLastUploadAt: 0, + kiroLoginUrl: '', + kiroRefreshToken: '', + kiroUploadError: '', + kiroUploadStatus: '', + kiroUserCode: '', + kiroVerificationUri: '', + kiroVerificationUriComplete: '', + }; + } + if (stepKey === 'kiro-await-device-login') { + return { + kiroAccessToken: '', + kiroAuthError: '', + kiroAuthStatus: 'waiting_user', + kiroAuthorizedEmail: '', + kiroCredentialId: null, + kiroLastConnectionMessage: '', + kiroLastUploadAt: 0, + kiroRefreshToken: '', + kiroUploadError: '', + kiroUploadStatus: 'waiting_login', + }; + } + if (stepKey === 'kiro-upload-credential') { + return { + kiroAuthorizedEmail: '', + kiroCredentialId: null, + kiroLastConnectionMessage: '', + kiroLastUploadAt: 0, + kiroUploadError: '', + kiroUploadStatus: 'ready_to_upload', + }; + } const plusRuntimeResets = { plusCheckoutTabId: null, plusCheckoutUrl: null, @@ -9892,6 +10114,44 @@ async function handleStepData(step, payload) { async function handleNodeData(nodeId, payload) { const state = await getState(); + const nodeDefinition = getNodeDefinitionForState(nodeId, state); + if (String(nodeDefinition?.flowId || '').trim().toLowerCase() === 'kiro') { + const kiroFieldKeys = [ + 'kiroAccessToken', + 'kiroAuthError', + 'kiroAuthExpiresAt', + 'kiroAuthIntervalSeconds', + 'kiroAuthRegion', + 'kiroAuthStatus', + 'kiroAuthTabId', + 'kiroAuthorizedEmail', + 'kiroClientId', + 'kiroClientSecret', + 'kiroCredentialId', + 'kiroDeviceAuthorizationCode', + 'kiroDeviceCode', + 'kiroLastConnectionMessage', + 'kiroLastUploadAt', + 'kiroLoginUrl', + 'kiroRefreshToken', + 'kiroUploadError', + 'kiroUploadStatus', + 'kiroUserCode', + 'kiroVerificationUri', + 'kiroVerificationUriComplete', + ]; + const updates = {}; + for (const field of kiroFieldKeys) { + if (Object.prototype.hasOwnProperty.call(payload || {}, field)) { + updates[field] = payload[field]; + } + } + if (Object.keys(updates).length > 0) { + await setState(updates); + broadcastDataUpdate(updates); + } + return; + } const step = getStepIdByNodeIdForState(nodeId, state); if (!Number.isInteger(step) || step <= 0) { return; @@ -9933,6 +10193,9 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ 'fetch-bound-email-login-code', 'post-bound-email-phone-verification', 'confirm-oauth', + 'kiro-start-device-login', + 'kiro-await-device-login', + 'kiro-upload-credential', ]); const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([ 'fill-password', @@ -10611,7 +10874,8 @@ async function executeNode(nodeId, options = {}) { state = await getState(); // Set flow start time on first step - if (normalizedNodeId === 'open-chatgpt' && !state.flowStartTime) { + const firstNodeIdForFlow = String(getNodeIdsForState(state)?.[0] || '').trim(); + if (normalizedNodeId === firstNodeIdForFlow && !state.flowStartTime) { await setState({ flowStartTime: Date.now() }); } @@ -11977,6 +12241,70 @@ async function runAutoSequenceFromNodeGraph(startNodeId, context = {}) { setRestartNode(nodeId); return true; }; + const initialFlowState = await getState(); + const activeFlowId = String(initialFlowState?.activeFlowId || initialFlowState?.flowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID; + + if (activeFlowId !== DEFAULT_ACTIVE_FLOW_ID) { + await broadcastAutoRunStatus('running', { + currentRun: targetRun, + totalRuns, + attemptRun: attemptRuns, + }); + + while (true) { + if (continueCurrentAttempt) { + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从节点 ${currentStartNodeId} 开始(第 ${attemptRuns} 次尝试)===`, 'info'); + } else { + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,开始执行 ${activeFlowId} flow ===`, 'info'); + } + + let latestState = await getState(); + let nodeIds = getAutoRunWorkflowNodeIds(latestState); + let nodeIndex = Math.max(0, getNodeIndex(latestState, currentStartNodeId)); + + while (nodeIndex < nodeIds.length) { + latestState = await getState(); + nodeIds = getAutoRunWorkflowNodeIds(latestState); + const nodeId = nodeIds[nodeIndex]; + if (!nodeId) { + nodeIndex += 1; + continue; + } + if (typeof isNodeExecutionAllowedForState === 'function' && !isNodeExecutionAllowedForState(nodeId, latestState)) { + nodeIndex += 1; + continue; + } + + const currentStatus = getNodeStatusForNode(latestState, nodeId); + if (isStepDoneStatus(currentStatus)) { + await addLog(`自动运行:节点 ${nodeId} 当前状态为 ${currentStatus},将直接继续后续流程。`, 'info'); + nodeIndex += 1; + continue; + } + + try { + await executeNodeAndWaitWithAutoRunIdleLogWatchdog(nodeId, getAutoRunNodeDelayMs(nodeId)); + nodeIndex += 1; + } catch (err) { + attachFailedNode(err, nodeId, latestState); + if (isStopError(err)) { + throw err; + } + if (await restartCurrentNodeAfterIdle(nodeId, err)) { + latestState = await getState(); + nodeIds = getAutoRunWorkflowNodeIds(latestState); + nodeIndex = Math.max(0, getNodeIndex(latestState, currentStartNodeId)); + continue; + } + throw err; + } + } + + break; + } + + return; + } while (true) { @@ -12740,6 +13068,17 @@ const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.cre sleepWithStop, waitForTabUrlMatchUntilStopped, }); +const kiroDeviceAuthExecutor = self.MultiPageBackgroundKiroDeviceAuth?.createKiroDeviceAuthExecutor({ + addLog, + completeNodeFromBackground, + fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getState, + registerTab, + reuseOrCreateTab, + setState, + sleepWithStop, + throwIfStopped, +}); const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({ addLog, chrome, @@ -12817,6 +13156,9 @@ const stepExecutorsByKey = { 'post-bound-email-phone-verification': (state) => step8Executor.executeBoundEmailPostLoginPhoneVerification(state), 'confirm-oauth': (state) => step9Executor.executeStep9(state), 'platform-verify': (state) => executeStep10(state), + 'kiro-start-device-login': (state) => kiroDeviceAuthExecutor.executeKiroStartDeviceLogin(state), + 'kiro-await-device-login': (state) => kiroDeviceAuthExecutor.executeKiroAwaitDeviceLogin(state), + 'kiro-upload-credential': (state) => kiroDeviceAuthExecutor.executeKiroUploadCredential(state), }; const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({ addLog, @@ -12960,6 +13302,7 @@ function buildNodeRegistry(definitions = []) { return self.MultiPageBackgroundStepRegistry?.createNodeRegistry( definitions.map((definition) => ({ ...definition, + legacyStepId: definition.legacyStepId || definition.id, nodeId: definition.nodeId || definition.key, displayOrder: definition.displayOrder || definition.order, executeKey: definition.executeKey || definition.key, @@ -12968,75 +13311,65 @@ function buildNodeRegistry(definitions = []) { ); } +async function acquireTopLevelAuthChainExecution(step, state = {}) { + return acquireTopLevelAuthChainExecutionForNode(getNodeIdByStepForState(step, state), state); +} + function buildStepRegistry(definitions = []) { - const nodeRegistry = buildNodeRegistry(definitions); + const normalizedDefinitions = (Array.isArray(definitions) ? definitions : []) + .map((definition) => ({ + ...definition, + legacyStepId: Number(definition?.legacyStepId ?? definition?.id) || 0, + nodeId: String(definition?.nodeId || definition?.key || '').trim(), + })) + .filter((definition) => definition.nodeId); + const nodeRegistry = buildNodeRegistry(normalizedDefinitions); + const stepToNodeDefinition = new Map( + normalizedDefinitions + .filter((definition) => Number.isInteger(definition.legacyStepId) && definition.legacyStepId > 0) + .map((definition) => [definition.legacyStepId, definition]) + ); + return { executeNode: (nodeId, state) => nodeRegistry.executeNode(nodeId, state), getNodeDefinition: (nodeId) => nodeRegistry.getNodeDefinition(nodeId), getOrderedNodes: () => nodeRegistry.getOrderedNodes(), executeStep: (step, state) => { - const nodeId = String(getStepDefinitionForState(step, state)?.key || '').trim(); + const nodeId = String(stepToNodeDefinition.get(Number(step))?.nodeId || '').trim(); if (!nodeId) { - throw new Error(`未知节点:${step}`); + throw new Error(`Unknown step: ${step}`); } return nodeRegistry.executeNode(nodeId, state); }, getStepDefinition: (step) => { - const nodeId = String(getStepDefinitionForState(step, {})?.key || '').trim(); + const nodeId = String(stepToNodeDefinition.get(Number(step))?.nodeId || '').trim(); return nodeId ? nodeRegistry.getNodeDefinition(nodeId) : null; }, getOrderedSteps: () => nodeRegistry.getOrderedNodes(), }; } -async function acquireTopLevelAuthChainExecution(step, state = {}) { - return acquireTopLevelAuthChainExecutionForNode(getNodeIdByStepForState(step, state), state); -} - -const normalStepRegistry = buildStepRegistry(NORMAL_STEP_DEFINITIONS); -const normalPhoneStepRegistry = buildStepRegistry(NORMAL_PHONE_STEP_DEFINITIONS); -const normalPhoneBoundEmailReloginStepRegistry = buildStepRegistry(NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS); -const plusPayPalStepRegistry = buildStepRegistry(PLUS_PAYPAL_STEP_DEFINITIONS); -const plusPayPalPhoneStepRegistry = buildStepRegistry(PLUS_PAYPAL_PHONE_STEP_DEFINITIONS); -const plusPayPalPhoneBoundEmailReloginStepRegistry = buildStepRegistry(PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS); -const plusGoPayStepRegistry = buildStepRegistry(PLUS_GOPAY_STEP_DEFINITIONS); -const plusGoPayPhoneStepRegistry = buildStepRegistry(PLUS_GOPAY_PHONE_STEP_DEFINITIONS); -const plusGoPayPhoneBoundEmailReloginStepRegistry = buildStepRegistry(PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS); -const plusGpcStepRegistry = buildStepRegistry(PLUS_GPC_STEP_DEFINITIONS); -const plusGpcPhoneStepRegistry = buildStepRegistry(PLUS_GPC_PHONE_STEP_DEFINITIONS); -const plusGpcPhoneBoundEmailReloginStepRegistry = buildStepRegistry(PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS); +const stepRegistryCache = new Map(); function getStepRegistryForState(state = {}) { - const activeFlowId = String(state?.activeFlowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID; - if (activeFlowId !== DEFAULT_ACTIVE_FLOW_ID) { - throw new Error(`当前尚未注册 flow=${activeFlowId} 的步骤执行器。`); + const definitions = getNodeDefinitionsForState(state); + const activeFlowId = String(state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID; + const cacheKey = `${activeFlowId}:${(Array.isArray(definitions) ? definitions : []) + .map((definition) => [ + Number(definition?.legacyStepId ?? definition?.id) || 0, + String(definition?.nodeId || definition?.key || '').trim(), + String(definition?.executeKey || definition?.key || '').trim(), + Number(definition?.displayOrder ?? definition?.order) || 0, + ].join(':')) + .join('|')}`; + + if (!cacheKey || cacheKey === `${activeFlowId}:`) { + return buildStepRegistry([]); } - const signupMethod = getSignupMethodForStepDefinitions(state); - const useBoundEmailRelogin = signupMethod === SIGNUP_METHOD_PHONE - && Boolean(state?.phoneSignupReloginAfterBindEmailEnabled); - if (!isPlusModeState(state)) { - if (signupMethod === SIGNUP_METHOD_PHONE) { - return useBoundEmailRelogin ? normalPhoneBoundEmailReloginStepRegistry : normalPhoneStepRegistry; - } - return normalStepRegistry; + if (!stepRegistryCache.has(cacheKey)) { + stepRegistryCache.set(cacheKey, buildStepRegistry(definitions)); } - const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod); - if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { - if (signupMethod === SIGNUP_METHOD_PHONE) { - return useBoundEmailRelogin ? plusGpcPhoneBoundEmailReloginStepRegistry : plusGpcPhoneStepRegistry; - } - return plusGpcStepRegistry; - } - if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY) { - if (signupMethod === SIGNUP_METHOD_PHONE) { - return useBoundEmailRelogin ? plusGoPayPhoneBoundEmailReloginStepRegistry : plusGoPayPhoneStepRegistry; - } - return plusGoPayStepRegistry; - } - if (signupMethod === SIGNUP_METHOD_PHONE) { - return useBoundEmailRelogin ? plusPayPalPhoneBoundEmailReloginStepRegistry : plusPayPalPhoneStepRegistry; - } - return plusPayPalStepRegistry; + return stepRegistryCache.get(cacheKey); } async function requestOAuthUrlFromPanel(state, options = {}) { diff --git a/background/runtime-state.js b/background/runtime-state.js index 431f239..59d7154 100644 --- a/background/runtime-state.js +++ b/background/runtime-state.js @@ -120,6 +120,40 @@ 'step8VerificationTargetEmail', ]), }); + const KIRO_FLOW_FIELD_GROUPS = Object.freeze({ + auth: Object.freeze([ + 'kiroDeviceCode', + 'kiroUserCode', + 'kiroDeviceAuthorizationCode', + 'kiroLoginUrl', + 'kiroVerificationUri', + 'kiroVerificationUriComplete', + 'kiroClientId', + 'kiroClientSecret', + 'kiroAuthRegion', + 'kiroAuthExpiresAt', + 'kiroAuthIntervalSeconds', + 'kiroAuthTabId', + 'kiroAuthStatus', + 'kiroAuthError', + 'kiroAccessToken', + 'kiroRefreshToken', + ]), + upload: Object.freeze([ + 'kiroUploadStatus', + 'kiroUploadError', + 'kiroCredentialId', + 'kiroLastUploadAt', + 'kiroLastConnectionMessage', + ]), + identity: Object.freeze([ + 'kiroAuthorizedEmail', + ]), + }); + const FLOW_FIELD_GROUPS = Object.freeze({ + openai: OPENAI_FLOW_FIELD_GROUPS, + kiro: KIRO_FLOW_FIELD_GROUPS, + }); function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); @@ -210,11 +244,12 @@ }; } - function flattenOpenAiFlowState(flowState = {}) { - const openaiState = normalizePlainObject(flowState.openai); + function flattenFlowStateById(flowState = {}, flowId = 'openai') { + const fieldGroups = FLOW_FIELD_GROUPS[flowId] || {}; + const scopedState = normalizePlainObject(flowState[flowId]); const next = {}; - for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) { - const group = normalizePlainObject(openaiState[groupKey]); + for (const [groupKey, fields] of Object.entries(fieldGroups)) { + const group = normalizePlainObject(scopedState[groupKey]); for (const field of fields) { if (Object.prototype.hasOwnProperty.call(group, field)) { next[field] = cloneValue(group[field]); @@ -224,23 +259,29 @@ return next; } - function buildOpenAiFlowState(baseValue = {}, state = {}) { - const baseFlowState = cloneValue(normalizePlainObject(baseValue)); - const baseOpenAi = cloneValue(normalizePlainObject(baseFlowState.openai)); - const openaiState = { - ...baseOpenAi, + function buildScopedFlowState(baseFlowState = {}, state = {}, flowId = 'openai') { + const fieldGroups = FLOW_FIELD_GROUPS[flowId] || {}; + const baseScopedState = cloneValue(normalizePlainObject(baseFlowState[flowId])); + const scopedState = { + ...baseScopedState, }; - for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) { - openaiState[groupKey] = { - ...cloneValue(normalizePlainObject(baseOpenAi[groupKey])), + for (const [groupKey, fields] of Object.entries(fieldGroups)) { + scopedState[groupKey] = { + ...cloneValue(normalizePlainObject(baseScopedState[groupKey])), ...pickDefinedFields(state, fields), }; } + return scopedState; + } + + function buildOpenAiFlowState(baseValue = {}, state = {}) { + const baseFlowState = cloneValue(normalizePlainObject(baseValue)); return { ...baseFlowState, - openai: openaiState, + openai: buildScopedFlowState(baseFlowState, state, 'openai'), + kiro: buildScopedFlowState(baseFlowState, state, 'kiro'), }; } @@ -265,6 +306,11 @@ luckmail: {}, identity: {}, }, + kiro: { + auth: {}, + upload: {}, + identity: {}, + }, }, }; } @@ -375,9 +421,12 @@ ); } - Object.assign(next, flattenOpenAiFlowState(flowState)); + Object.assign(next, flattenFlowStateById(flowState, 'openai')); + Object.assign(next, flattenFlowStateById(flowState, 'kiro')); if (Object.prototype.hasOwnProperty.call(runtimeState, 'flowState')) { - Object.assign(next, flattenOpenAiFlowState(normalizePlainObject(runtimeState.flowState))); + const runtimeFlowState = normalizePlainObject(runtimeState.flowState); + Object.assign(next, flattenFlowStateById(runtimeFlowState, 'openai')); + Object.assign(next, flattenFlowStateById(runtimeFlowState, 'kiro')); } return next; @@ -396,6 +445,9 @@ flowState: cloneValue(runtimeState.flowState), sharedState: cloneValue(runtimeState.sharedState), serviceState: cloneValue(runtimeState.serviceState), + flows: cloneValue(runtimeState.flowState), + shared: cloneValue(runtimeState.sharedState), + services: cloneValue(runtimeState.serviceState), runtimeState, }; } @@ -422,6 +474,8 @@ return { DEFAULT_ACTIVE_FLOW_ID, + FLOW_FIELD_GROUPS, + KIRO_FLOW_FIELD_GROUPS, OPENAI_FLOW_FIELD_GROUPS, RUNTIME_PROXY_FIELDS, RUNTIME_SHARED_FIELDS, diff --git a/background/steps/kiro-device-auth.js b/background/steps/kiro-device-auth.js new file mode 100644 index 0000000..a8efa96 --- /dev/null +++ b/background/steps/kiro-device-auth.js @@ -0,0 +1,527 @@ +(function attachBackgroundKiroDeviceAuth(root, factory) { + root.MultiPageBackgroundKiroDeviceAuth = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroDeviceAuthModule() { + const DEFAULT_REGION = 'us-east-1'; + const DEVICE_LOGIN_START_URL = 'https://view.awsapps.com/start'; + const DEFAULT_SCOPES = Object.freeze([ + 'codewhisperer:completions', + 'codewhisperer:analysis', + 'codewhisperer:conversations', + 'codewhisperer:transformations', + 'codewhisperer:taskassist', + ]); + + function cleanString(value = '') { + return String(value ?? '').trim(); + } + + function normalizeRegion(value = '', fallback = DEFAULT_REGION) { + return cleanString(value) || fallback; + } + + function buildOidcBaseUrl(region = DEFAULT_REGION) { + return `https://oidc.${normalizeRegion(region)}.amazonaws.com`; + } + + function normalizeKiroRsBaseUrl(value = '') { + const normalized = cleanString(value).replace(/\/+$/, ''); + if (!normalized) { + throw new Error('Missing kiro.rs admin URL.'); + } + return normalized.endsWith('/admin') + ? normalized.slice(0, -'/admin'.length) + : normalized; + } + + async function readResponse(response) { + const text = await response.text(); + let json = null; + try { + json = text ? JSON.parse(text) : null; + } catch (_error) { + json = null; + } + return { text, json }; + } + + function getErrorMessage(error) { + return error instanceof Error ? error.message : String(error ?? 'Unknown error'); + } + + function normalizePositiveInteger(value, fallback) { + const numeric = Math.floor(Number(value)); + if (Number.isInteger(numeric) && numeric > 0) { + return numeric; + } + return fallback; + } + + function buildCredentialUploadOptions(state = {}) { + const next = { + priority: Math.max(0, Math.floor(Number(state?.kiroRsPriority) || 0)), + authMethod: 'IdC', + provider: 'BuilderId', + }; + + const endpoint = cleanString(state?.kiroRsEndpoint); + const authRegion = cleanString(state?.kiroRsAuthRegion); + const apiRegion = cleanString(state?.kiroRsApiRegion); + if (endpoint) { + next.endpoint = endpoint; + } + if (authRegion) { + next.authRegion = authRegion; + } + if (apiRegion) { + next.apiRegion = apiRegion; + } + + if (state?.ipProxyEnabled) { + const proxyUrl = cleanString(state?.ipProxyApiUrl) + || (() => { + const host = cleanString(state?.ipProxyHost); + const port = cleanString(state?.ipProxyPort); + if (!host || !port) { + return ''; + } + const protocol = cleanString(state?.ipProxyProtocol) || 'http'; + return `${protocol}://${host}:${port}`; + })(); + if (proxyUrl) { + next.proxyUrl = proxyUrl; + } + const proxyUsername = cleanString(state?.ipProxyUsername); + const proxyPassword = String(state?.ipProxyPassword || ''); + if (proxyUsername) { + next.proxyUsername = proxyUsername; + } + if (proxyPassword) { + next.proxyPassword = proxyPassword; + } + } + + return next; + } + + async function startBuilderIdDeviceLogin(region, fetchImpl) { + const normalizedRegion = normalizeRegion(region); + const oidcBaseUrl = buildOidcBaseUrl(normalizedRegion); + const registerResponse = await fetchImpl(`${oidcBaseUrl}/client/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + clientName: 'Codex Registration Extension', + clientType: 'public', + scopes: DEFAULT_SCOPES, + grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'], + issuerUrl: DEVICE_LOGIN_START_URL, + }), + }); + const registerBody = await readResponse(registerResponse); + if (!registerResponse.ok) { + throw new Error(`Builder ID client registration failed: ${cleanString(registerBody.text || registerResponse.statusText) || registerResponse.status}`); + } + + const clientId = cleanString(registerBody.json?.clientId); + const clientSecret = String(registerBody.json?.clientSecret || ''); + if (!clientId || !clientSecret) { + throw new Error('Builder ID client registration response is missing client credentials.'); + } + + const authorizationResponse = await fetchImpl(`${oidcBaseUrl}/device_authorization`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + clientId, + clientSecret, + startUrl: DEVICE_LOGIN_START_URL, + }), + }); + const authorizationBody = await readResponse(authorizationResponse); + if (!authorizationResponse.ok) { + throw new Error(`Builder ID device authorization failed: ${cleanString(authorizationBody.text || authorizationResponse.statusText) || authorizationResponse.status}`); + } + + const deviceCode = String(authorizationBody.json?.deviceCode || ''); + const userCode = cleanString(authorizationBody.json?.userCode); + const verificationUri = cleanString(authorizationBody.json?.verificationUri); + const verificationUriComplete = cleanString( + authorizationBody.json?.verificationUriComplete || verificationUri + ); + const interval = normalizePositiveInteger(authorizationBody.json?.interval, 5); + const expiresIn = normalizePositiveInteger(authorizationBody.json?.expiresIn, 600); + if (!deviceCode || !userCode || !verificationUriComplete) { + throw new Error('Builder ID device authorization response is missing required fields.'); + } + + return { + clientId, + clientSecret, + deviceCode, + expiresAt: Date.now() + expiresIn * 1000, + expiresIn, + interval, + region: normalizedRegion, + userCode, + verificationUri, + verificationUriComplete, + }; + } + + async function pollBuilderIdDeviceAuth(params = {}, fetchImpl) { + const oidcBaseUrl = buildOidcBaseUrl(params.region); + const response = await fetchImpl(`${oidcBaseUrl}/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + clientId: params.clientId, + clientSecret: params.clientSecret, + grantType: 'urn:ietf:params:oauth:grant-type:device_code', + deviceCode: params.deviceCode, + }), + }); + const body = await readResponse(response); + if (response.status === 200) { + return { + completed: true, + accessToken: String(body.json?.accessToken || ''), + refreshToken: String(body.json?.refreshToken || ''), + expiresIn: normalizePositiveInteger(body.json?.expiresIn, 3600), + region: normalizeRegion(params.region), + }; + } + if (response.status === 400) { + const errorCode = cleanString(body.json?.error); + if (errorCode === 'authorization_pending') { + return { completed: false, status: 'pending' }; + } + if (errorCode === 'slow_down') { + return { completed: false, status: 'slow_down' }; + } + if (errorCode === 'expired_token') { + throw new Error('Kiro device login expired.'); + } + if (errorCode === 'access_denied') { + throw new Error('User denied the Builder ID device login request.'); + } + throw new Error(`Builder ID authorization failed: ${errorCode || cleanString(body.text || response.statusText) || response.status}`); + } + throw new Error(`Builder ID token request failed: HTTP ${response.status}`); + } + + async function checkKiroRsConnection(baseUrl, apiKey, fetchImpl) { + const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl); + const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'x-api-key': String(apiKey || ''), + }, + }); + const body = await readResponse(response); + if (response.ok) { + return { + ok: true, + message: `kiro.rs connection ok (HTTP ${response.status})`, + }; + } + if (response.status === 405) { + return { + ok: true, + message: 'kiro.rs upload endpoint is reachable.', + }; + } + if (response.status === 401 || response.status === 403) { + return { + ok: false, + message: `kiro.rs API key rejected (HTTP ${response.status})`, + }; + } + if (response.status === 404) { + return { + ok: false, + message: 'kiro.rs admin endpoint not found.', + }; + } + return { + ok: false, + message: cleanString(body.json?.error?.message || body.json?.message || body.text || response.statusText) + || `kiro.rs connection failed (HTTP ${response.status})`, + }; + } + + async function uploadBuilderIdCredential(baseUrl, apiKey, payload, fetchImpl) { + const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl); + const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'x-api-key': String(apiKey || ''), + }, + body: JSON.stringify(payload), + }); + const body = await readResponse(response); + if (!response.ok) { + const message = cleanString(body.json?.error?.message || body.json?.message || body.text || response.statusText) + || `HTTP ${response.status}`; + throw new Error(`kiro.rs credential upload failed: ${message}`); + } + + return { + credentialId: Number(body.json?.credentialId || body.json?.credential_id || 0) || null, + email: cleanString(body.json?.email), + message: cleanString(body.json?.message) || 'Credential uploaded.', + raw: body.json, + }; + } + + function createKiroDeviceAuthExecutor(deps = {}) { + const { + addLog = async () => {}, + completeNodeFromBackground, + fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getState = async () => ({}), + registerTab = async () => {}, + reuseOrCreateTab = async () => null, + setState = async () => {}, + sleepWithStop = async (ms) => { + await new Promise((resolve) => setTimeout(resolve, ms)); + }, + throwIfStopped = () => {}, + } = deps; + + if (typeof completeNodeFromBackground !== 'function') { + throw new Error('Kiro device auth executor requires completeNodeFromBackground.'); + } + if (typeof fetchImpl !== 'function') { + throw new Error('Kiro device auth executor requires fetch support.'); + } + + async function log(message, level, nodeId) { + await addLog(message, level, { nodeId }); + } + + async function getExecutionState(state = {}) { + if (state && typeof state === 'object' && !Array.isArray(state) && Object.keys(state).length) { + return state; + } + return getState(); + } + + async function persistFailure(updates = {}) { + if (updates && Object.keys(updates).length) { + await setState(updates); + } + } + + async function executeKiroStartDeviceLogin(state = {}) { + const nodeId = String(state?.nodeId || 'kiro-start-device-login').trim(); + try { + const latestState = await getExecutionState(state); + const auth = await startBuilderIdDeviceLogin( + latestState.kiroRegion || DEFAULT_REGION, + fetchImpl + ); + const loginUrl = cleanString(auth.verificationUriComplete || auth.verificationUri); + const tabId = loginUrl ? await reuseOrCreateTab('kiro-device-auth', loginUrl) : null; + if (Number.isInteger(tabId)) { + await registerTab('kiro-device-auth', tabId); + } + + const updates = { + kiroAccessToken: '', + kiroAuthError: '', + kiroAuthExpiresAt: auth.expiresAt, + kiroAuthIntervalSeconds: auth.interval, + kiroAuthRegion: auth.region, + kiroAuthStatus: 'waiting_user', + kiroAuthTabId: Number.isInteger(tabId) ? tabId : null, + kiroClientId: auth.clientId, + kiroClientSecret: auth.clientSecret, + kiroCredentialId: null, + kiroDeviceAuthorizationCode: auth.deviceCode, + kiroDeviceCode: auth.userCode, + kiroLastConnectionMessage: '', + kiroLastUploadAt: 0, + kiroLoginUrl: loginUrl, + kiroRefreshToken: '', + kiroUploadError: '', + kiroUploadStatus: 'waiting_login', + kiroUserCode: auth.userCode, + kiroVerificationUri: auth.verificationUri, + kiroVerificationUriComplete: loginUrl, + }; + + await setState(updates); + await log(`Kiro device login started. Open ${loginUrl} and approve with code ${auth.userCode}.`, 'info', nodeId); + await completeNodeFromBackground(nodeId, updates); + } catch (error) { + const message = getErrorMessage(error); + await persistFailure({ + kiroAuthError: message, + kiroAuthStatus: 'error', + }); + throw error; + } + } + + async function executeKiroAwaitDeviceLogin(state = {}) { + const nodeId = String(state?.nodeId || 'kiro-await-device-login').trim(); + try { + const latestState = await getExecutionState(state); + const clientId = cleanString(latestState.kiroClientId); + const clientSecret = String(latestState.kiroClientSecret || ''); + const deviceCode = String(latestState.kiroDeviceAuthorizationCode || ''); + const region = normalizeRegion(latestState.kiroAuthRegion || latestState.kiroRegion || DEFAULT_REGION); + const expiresAt = Math.max(0, Number(latestState.kiroAuthExpiresAt) || 0); + if (!clientId || !clientSecret || !deviceCode) { + throw new Error('Kiro device login has not been started yet.'); + } + if (!expiresAt || expiresAt <= Date.now()) { + throw new Error('Kiro device login expired. Restart step 1.'); + } + + await setState({ + kiroAuthError: '', + kiroAuthStatus: 'waiting_user', + kiroUploadStatus: 'waiting_login', + }); + await log('Waiting for Kiro device login approval...', 'info', nodeId); + + let intervalSeconds = normalizePositiveInteger(latestState.kiroAuthIntervalSeconds, 5); + while (Date.now() < expiresAt) { + throwIfStopped(); + const result = await pollBuilderIdDeviceAuth({ + clientId, + clientSecret, + deviceCode, + region, + }, fetchImpl); + if (result.completed) { + const updates = { + kiroAccessToken: result.accessToken, + kiroAuthError: '', + kiroAuthStatus: 'authorized', + kiroRefreshToken: result.refreshToken, + kiroUploadError: '', + kiroUploadStatus: 'ready_to_upload', + }; + await setState(updates); + await log('Kiro device login approved. Refresh token captured.', 'ok', nodeId); + await completeNodeFromBackground(nodeId, updates); + return; + } + + if (result.status === 'slow_down') { + intervalSeconds = Math.max(intervalSeconds + 5, 10); + } + await sleepWithStop(intervalSeconds * 1000); + } + + throw new Error('Kiro device login expired. Restart step 1.'); + } catch (error) { + const message = getErrorMessage(error); + await persistFailure({ + kiroAuthError: message, + kiroAuthStatus: /expired/i.test(message) ? 'expired' : 'error', + }); + throw error; + } + } + + async function executeKiroUploadCredential(state = {}) { + const nodeId = String(state?.nodeId || 'kiro-upload-credential').trim(); + try { + const latestState = await getExecutionState(state); + const refreshToken = String(latestState.kiroRefreshToken || ''); + const clientId = cleanString(latestState.kiroClientId); + const clientSecret = String(latestState.kiroClientSecret || ''); + const region = normalizeRegion(latestState.kiroAuthRegion || latestState.kiroRegion || DEFAULT_REGION); + const kiroRsUrl = String(latestState.kiroRsUrl || ''); + const kiroRsKey = String(latestState.kiroRsKey || ''); + if (!refreshToken || !clientId || !clientSecret) { + throw new Error('Kiro refresh token is missing. Complete step 2 first.'); + } + if (!cleanString(kiroRsUrl)) { + throw new Error('Missing kiro.rs admin URL.'); + } + if (!cleanString(kiroRsKey)) { + throw new Error('Missing kiro.rs API key.'); + } + + await setState({ + kiroUploadError: '', + kiroUploadStatus: 'uploading', + }); + await log('Uploading Builder ID credential to kiro.rs...', 'info', nodeId); + + const connection = await checkKiroRsConnection(kiroRsUrl, kiroRsKey, fetchImpl); + await setState({ + kiroLastConnectionMessage: connection.message, + }); + if (!connection.ok) { + throw new Error(connection.message); + } + + const uploadOptions = buildCredentialUploadOptions(latestState); + const uploadPayload = { + refreshToken, + clientId, + clientSecret, + region, + ...(cleanString(latestState.kiroAuthorizedEmail) + ? { email: cleanString(latestState.kiroAuthorizedEmail) } + : {}), + ...uploadOptions, + }; + const uploadResult = await uploadBuilderIdCredential( + kiroRsUrl, + kiroRsKey, + uploadPayload, + fetchImpl + ); + const updates = { + kiroAuthorizedEmail: uploadResult.email || cleanString(latestState.kiroAuthorizedEmail), + kiroCredentialId: uploadResult.credentialId, + kiroLastUploadAt: Date.now(), + kiroUploadError: '', + kiroUploadStatus: uploadResult.message || 'uploaded', + }; + + await setState(updates); + await log(`kiro.rs upload completed: ${updates.kiroUploadStatus}`, 'ok', nodeId); + await completeNodeFromBackground(nodeId, updates); + } catch (error) { + const message = getErrorMessage(error); + await persistFailure({ + kiroUploadError: message, + kiroUploadStatus: 'error', + }); + throw error; + } + } + + return { + executeKiroAwaitDeviceLogin, + executeKiroStartDeviceLogin, + executeKiroUploadCredential, + }; + } + + return { + buildCredentialUploadOptions, + checkKiroRsConnection, + createKiroDeviceAuthExecutor, + normalizeKiroRsBaseUrl, + pollBuilderIdDeviceAuth, + startBuilderIdDeviceLogin, + uploadBuilderIdCredential, + }; +}); diff --git a/background/steps/registry.js b/background/steps/registry.js index 197c08a..072c513 100644 --- a/background/steps/registry.js +++ b/background/steps/registry.js @@ -4,13 +4,15 @@ function createNodeRegistry(definitions = []) { const ordered = (Array.isArray(definitions) ? definitions : []) .map((definition) => ({ + legacyStepId: Number(definition?.legacyStepId ?? definition?.id) || 0, + flowId: String(definition?.flowId || '').trim(), nodeId: String(definition?.nodeId || definition?.key || '').trim(), displayOrder: Number(definition?.displayOrder ?? definition?.order), executeKey: String(definition?.executeKey || definition?.key || definition?.nodeId || '').trim(), title: String(definition?.title || '').trim(), execute: definition?.execute, })) - .filter((definition) => definition.nodeId && typeof definition.execute === 'function') + .filter((definition) => definition.nodeId) .sort((left, right) => { const leftOrder = Number.isFinite(left.displayOrder) ? left.displayOrder : 0; const rightOrder = Number.isFinite(right.displayOrder) ? right.displayOrder : 0; @@ -31,7 +33,10 @@ function executeNode(nodeId, state) { const definition = getNodeDefinition(nodeId); if (!definition) { - throw new Error(`未知节点:${nodeId}`); + throw new Error(`Unknown node: ${nodeId}`); + } + if (typeof definition.execute !== 'function') { + throw new Error(`Missing node executor: ${definition.executeKey || definition.nodeId}`); } return definition.execute(state); } diff --git a/data/step-definitions.js b/data/step-definitions.js index dbcfbf5..c58047c 100644 --- a/data/step-definitions.js +++ b/data/step-definitions.js @@ -111,6 +111,35 @@ const PLUS_GPC_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GPC_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL); const PLUS_GPC_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GPC_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE); const PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GPC_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); + const KIRO_STEP_DEFINITIONS = [ + { + id: 1, + order: 10, + key: 'kiro-start-device-login', + title: 'Start device login', + sourceId: 'kiro-device-auth', + driverId: 'background/kiro-device-auth', + command: 'kiro-start-device-login', + }, + { + id: 2, + order: 20, + key: 'kiro-await-device-login', + title: 'Wait for device login', + sourceId: 'kiro-device-auth', + driverId: 'background/kiro-device-auth', + command: 'kiro-await-device-login', + }, + { + id: 3, + order: 30, + key: 'kiro-upload-credential', + title: 'Upload credential to kiro.rs', + sourceId: 'kiro-rs-admin', + driverId: 'background/kiro-device-auth', + command: 'kiro-upload-credential', + }, + ]; const PHONE_SIGNUP_TITLE_OVERRIDES = Object.freeze({ 'submit-signup-email': '注册并输入手机号', @@ -238,6 +267,20 @@ getPlusPaymentStepTitle: getOpenAiPlusPaymentStepTitle, resolveStepTitle: getOpenAiResolvedStepTitle, }, + kiro: { + getAllSteps() { + return KIRO_STEP_DEFINITIONS; + }, + getModeStepDefinitions() { + return KIRO_STEP_DEFINITIONS; + }, + getPlusPaymentStepTitle() { + return ''; + }, + resolveStepTitle(step) { + return step?.title || ''; + }, + }, }); function hasFlow(flowId) { diff --git a/shared/flow-capabilities.js b/shared/flow-capabilities.js index 89274e1..4ea7b1c 100644 --- a/shared/flow-capabilities.js +++ b/shared/flow-capabilities.js @@ -1,11 +1,21 @@ (function attachMultiPageFlowCapabilities(root, factory) { root.MultiPageFlowCapabilities = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createFlowCapabilitiesModule() { - const DEFAULT_FLOW_ID = 'openai'; - const DEFAULT_PANEL_MODE = 'cpa'; + const rootScope = typeof self !== 'undefined' ? self : globalThis; + const flowRegistryApi = rootScope.MultiPageFlowRegistry || {}; + const settingsSchemaApi = rootScope.MultiPageSettingsSchema || {}; + const DEFAULT_FLOW_ID = flowRegistryApi.DEFAULT_FLOW_ID || 'openai'; + const DEFAULT_PANEL_MODE = flowRegistryApi.DEFAULT_OPENAI_SOURCE_ID || 'cpa'; + const LEGACY_OPENAI_FLOW_ALIAS = String(flowRegistryApi.LEGACY_OPENAI_FLOW_ALIAS || 'codex').trim().toLowerCase(); const SIGNUP_METHOD_EMAIL = 'email'; const SIGNUP_METHOD_PHONE = 'phone'; - const VALID_PANEL_MODES = Object.freeze(['cpa', 'sub2api', 'codex2api']); + const VALID_PANEL_MODES = Array.isArray(flowRegistryApi.OPENAI_SOURCE_IDS) + ? flowRegistryApi.OPENAI_SOURCE_IDS.slice() + : ['cpa', 'sub2api', 'codex2api']; + const REGISTERED_FLOW_IDS = Array.isArray(flowRegistryApi.getRegisteredFlowIds?.()) + ? flowRegistryApi.getRegisteredFlowIds().map((flowId) => String(flowId || '').trim().toLowerCase()).filter(Boolean) + : [DEFAULT_FLOW_ID]; + const REGISTERED_FLOW_ID_SET = new Set(REGISTERED_FLOW_IDS); const DEFAULT_FLOW_CAPABILITIES = Object.freeze({ supportsEmailSignup: true, @@ -16,28 +26,33 @@ supportsPlatformBinding: [], supportsLuckmail: false, supportsOauthTimeoutBudget: false, - canSwitchFlow: false, + canSwitchFlow: true, stepDefinitionMode: 'default', + sourceSelectorLabel: '来源', }); - const FLOW_CAPABILITIES = Object.freeze({ - openai: Object.freeze({ - ...DEFAULT_FLOW_CAPABILITIES, - supportsPhoneSignup: true, - supportsPhoneVerificationSettings: true, - supportsPlusMode: true, - supportsContributionMode: true, - supportsPlatformBinding: ['cpa', 'sub2api', 'codex2api'], - supportsLuckmail: true, - supportsOauthTimeoutBudget: true, - stepDefinitionMode: 'openai-dynamic', - }), - }); + const FLOW_CAPABILITIES = Object.freeze( + Object.fromEntries( + (typeof flowRegistryApi.getRegisteredFlowIds === 'function' + ? flowRegistryApi.getRegisteredFlowIds() + : [DEFAULT_FLOW_ID] + ).map((flowId) => [ + flowId, + Object.freeze({ + ...DEFAULT_FLOW_CAPABILITIES, + ...(typeof flowRegistryApi.getFlowCapabilities === 'function' + ? flowRegistryApi.getFlowCapabilities(flowId) + : {}), + }), + ]) + ) + ); const DEFAULT_PANEL_CAPABILITIES = Object.freeze({ supportsPhoneSignup: true, requiresPhoneSignupWarning: false, }); + const MODE_SWITCH_RELEVANT_KEYS = Object.freeze([ 'activeFlowId', 'contributionMode', @@ -45,6 +60,7 @@ 'phoneVerificationEnabled', 'plusModeEnabled', 'signupMethod', + 'kiroSourceId', ]); const PANEL_CAPABILITIES = Object.freeze({ @@ -63,12 +79,30 @@ }); function normalizeFlowId(value = '', fallback = DEFAULT_FLOW_ID) { + if (typeof flowRegistryApi.normalizeFlowId === 'function') { + return flowRegistryApi.normalizeFlowId(value, fallback); + } const normalized = String(value || '').trim().toLowerCase(); + return normalized || String(fallback || '').trim().toLowerCase() || DEFAULT_FLOW_ID; + } + + function normalizeCapabilityFlowId(value = '', fallback = DEFAULT_FLOW_ID) { + const normalized = String(value || '').trim().toLowerCase(); + if (normalized === LEGACY_OPENAI_FLOW_ALIAS) { + return DEFAULT_FLOW_ID; + } if (normalized) { return normalized; } - const fallbackValue = String(fallback || '').trim().toLowerCase(); - return fallbackValue || DEFAULT_FLOW_ID; + const normalizedFallback = String(fallback || '').trim().toLowerCase(); + if (normalizedFallback === LEGACY_OPENAI_FLOW_ALIAS) { + return DEFAULT_FLOW_ID; + } + return normalizedFallback || DEFAULT_FLOW_ID; + } + + function isRegisteredFlowId(flowId = '') { + return REGISTERED_FLOW_ID_SET.has(normalizeCapabilityFlowId(flowId, '')); } function normalizePanelMode(value = '', fallback = DEFAULT_PANEL_MODE) { @@ -122,9 +156,14 @@ flowCapabilities = FLOW_CAPABILITIES, panelCapabilities = PANEL_CAPABILITIES, } = deps; + const settingsSchema = settingsSchemaApi.createSettingsSchema + ? settingsSchemaApi.createSettingsSchema({ + defaultFlowId, + }) + : null; function getFlowCapabilities(flowId) { - const normalizedFlowId = normalizeFlowId(flowId, defaultFlowId); + const normalizedFlowId = normalizeCapabilityFlowId(flowId, defaultFlowId); const entry = flowCapabilities[normalizedFlowId] || null; return { ...defaultFlowCapabilities, @@ -156,9 +195,33 @@ return normalized; } + function resolveEffectiveSourceId(activeFlowId, state = {}, requestedPanelMode = DEFAULT_PANEL_MODE) { + if (!isRegisteredFlowId(activeFlowId)) { + return normalizePanelMode(state?.panelMode || requestedPanelMode, requestedPanelMode || DEFAULT_PANEL_MODE); + } + if (settingsSchema?.getSelectedSourceId) { + const sourceFromSchema = settingsSchema.getSelectedSourceId({ + ...state, + activeFlowId, + }, activeFlowId); + if (sourceFromSchema) { + return sourceFromSchema; + } + } + if (typeof flowRegistryApi.normalizeSourceId === 'function') { + if (activeFlowId === 'openai') { + return flowRegistryApi.normalizeSourceId('openai', state?.panelMode || requestedPanelMode, DEFAULT_PANEL_MODE); + } + return flowRegistryApi.normalizeSourceId(activeFlowId, state?.kiroSourceId || '', flowRegistryApi.getDefaultSourceId?.(activeFlowId)); + } + return activeFlowId === 'openai' + ? normalizePanelMode(state?.panelMode || requestedPanelMode) + : ''; + } + function resolveSidepanelCapabilities(options = {}) { const state = options?.state || {}; - const activeFlowId = normalizeFlowId( + const activeFlowId = normalizeCapabilityFlowId( options?.activeFlowId ?? state?.activeFlowId, defaultFlowId ); @@ -173,20 +236,22 @@ : supportedPanelModes.includes(requestedPanelMode); const effectivePanelMode = panelModeSupported ? requestedPanelMode - : supportedPanelModes[0]; + : (supportedPanelModes[0] || requestedPanelMode); const panelState = getPanelCapabilities(effectivePanelMode); + const effectiveSourceId = resolveEffectiveSourceId(activeFlowId, state, effectivePanelMode); const runtimeLocks = { autoRunLocked: Boolean(options?.autoRunLocked ?? state?.autoRunLocked), - contributionMode: flowState.supportsContributionMode && Boolean(state?.contributionMode), - phoneVerificationEnabled: flowState.supportsPhoneVerificationSettings && Boolean(state?.phoneVerificationEnabled), - plusModeEnabled: flowState.supportsPlusMode && Boolean(state?.plusModeEnabled), + contributionMode: activeFlowId === 'openai' && flowState.supportsContributionMode && Boolean(state?.contributionMode), + phoneVerificationEnabled: activeFlowId === 'openai' && flowState.supportsPhoneVerificationSettings && Boolean(state?.phoneVerificationEnabled), + plusModeEnabled: activeFlowId === 'openai' && flowState.supportsPlusMode && Boolean(state?.plusModeEnabled), settingsMenuLocked: Boolean(options?.settingsMenuLocked ?? state?.settingsMenuLocked), }; const effectiveSignupMethods = []; if (flowState.supportsEmailSignup !== false) { effectiveSignupMethods.push(SIGNUP_METHOD_EMAIL); } - const canSelectPhoneSignup = Boolean(flowState.supportsPhoneSignup) + const canSelectPhoneSignup = activeFlowId === 'openai' + && Boolean(flowState.supportsPhoneSignup) && Boolean(panelState.supportsPhoneSignup) && runtimeLocks.phoneVerificationEnabled && !runtimeLocks.plusModeEnabled @@ -205,19 +270,24 @@ : (effectiveSignupMethods.includes(SIGNUP_METHOD_EMAIL) ? SIGNUP_METHOD_EMAIL : effectiveSignupMethods[0]); + const visibleGroupIds = typeof flowRegistryApi.getVisibleGroupIds === 'function' + && isRegisteredFlowId(activeFlowId) + ? flowRegistryApi.getVisibleGroupIds(activeFlowId, effectiveSourceId) + : []; return { activeFlowId, - canShowContributionMode: Boolean(flowState.supportsContributionMode), - canShowLuckmail: Boolean(flowState.supportsLuckmail), - canShowPhoneSettings: Boolean(flowState.supportsPhoneVerificationSettings), - canShowPlusSettings: Boolean(flowState.supportsPlusMode), + canShowContributionMode: activeFlowId === 'openai' && Boolean(flowState.supportsContributionMode), + canShowLuckmail: activeFlowId === 'openai' && Boolean(flowState.supportsLuckmail), + canShowPhoneSettings: activeFlowId === 'openai' && Boolean(flowState.supportsPhoneVerificationSettings), + canShowPlusSettings: activeFlowId === 'openai' && Boolean(flowState.supportsPlusMode), canSwitchFlow: Boolean(flowState.canSwitchFlow), canUsePhoneSignup: canSelectPhoneSignup, canUseSelectedPanelMode: panelModeSupported, effectivePanelMode, effectiveSignupMethod, effectiveSignupMethods, + effectiveSourceId, flowCapabilities: flowState, panelCapabilities: panelState, panelMode: effectivePanelMode, @@ -233,6 +303,7 @@ signupMethod: effectiveSignupMethod, }, supportedPanelModes, + visibleGroupIds, }; } @@ -250,13 +321,13 @@ if (!panelState.supportsPhoneSignup) { return { code: 'phone_signup_panel_unsupported', - message: `当前面板模式 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 不支持手机号注册。`, + message: `当前来源 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 不支持手机号注册。`, }; } if (!runtimeLocks.phoneVerificationEnabled) { return { code: 'phone_signup_phone_verification_disabled', - message: '请先开启接码功能后再使用手机号注册。', + message: '请先开启接码设置后再使用手机号注册。', }; } if (runtimeLocks.plusModeEnabled) { @@ -289,7 +360,7 @@ ) { errors.push({ code: 'panel_mode_unsupported', - message: `当前 flow 不支持 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 面板模式。`, + message: `当前 flow 不支持 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 来源。`, }); } @@ -345,7 +416,7 @@ normalizedUpdates.panelMode = capabilityState.effectivePanelMode; errors.push({ code: 'panel_mode_unsupported', - message: `当前 flow 不支持 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 面板模式。`, + message: `当前 flow 不支持 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 来源。`, }); } @@ -373,7 +444,7 @@ normalizedUpdates.phoneVerificationEnabled = false; errors.push({ code: 'phone_verification_unsupported', - message: '当前 flow 不支持接码配置。', + message: '当前 flow 不支持接码设置。', }); } @@ -430,6 +501,7 @@ PANEL_CAPABILITIES, SIGNUP_METHOD_EMAIL, SIGNUP_METHOD_PHONE, + VALID_PANEL_MODES, normalizeFlowId, normalizePanelMode, normalizeSignupMethod, diff --git a/shared/flow-registry.js b/shared/flow-registry.js new file mode 100644 index 0000000..8f5bb9b --- /dev/null +++ b/shared/flow-registry.js @@ -0,0 +1,509 @@ +(function attachMultiPageFlowRegistry(root, factory) { + root.MultiPageFlowRegistry = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createFlowRegistryModule() { + const DEFAULT_FLOW_ID = 'openai'; + const LEGACY_OPENAI_FLOW_ALIAS = 'codex'; + const DEFAULT_OPENAI_SOURCE_ID = 'cpa'; + const DEFAULT_KIRO_SOURCE_ID = 'kiro-rs'; + const DEFAULT_KIRO_REGION = 'us-east-1'; + const DEFAULT_KIRO_RS_URL = 'https://kiro.leftcode.xyz/admin'; + const OPENAI_SOURCE_IDS = Object.freeze(['cpa', 'sub2api', 'codex2api']); + const SHARED_SERVICE_IDS = Object.freeze(['email', 'proxy']); + + const DEFAULT_FLOW_CAPABILITIES = Object.freeze({ + supportsEmailSignup: true, + supportsPhoneSignup: false, + supportsPhoneVerificationSettings: false, + supportsPlusMode: false, + supportsContributionMode: false, + supportsPlatformBinding: [], + supportsLuckmail: false, + supportsOauthTimeoutBudget: false, + canSwitchFlow: true, + stepDefinitionMode: 'default', + sourceSelectorLabel: '来源', + }); + + function freezeDeep(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) { + return value; + } + Object.getOwnPropertyNames(value).forEach((key) => { + freezeDeep(value[key]); + }); + return Object.freeze(value); + } + + const FLOW_DEFINITIONS = freezeDeep({ + openai: { + id: 'openai', + label: 'Codex / OpenAI', + services: ['email', 'proxy'], + capabilities: { + ...DEFAULT_FLOW_CAPABILITIES, + supportsPhoneSignup: true, + supportsPhoneVerificationSettings: true, + supportsPlusMode: true, + supportsContributionMode: true, + supportsPlatformBinding: [...OPENAI_SOURCE_IDS], + supportsLuckmail: true, + supportsOauthTimeoutBudget: true, + stepDefinitionMode: 'openai-dynamic', + }, + baseGroups: [ + 'openai-account', + 'openai-plus', + 'openai-phone', + 'openai-oauth', + ], + sources: { + cpa: { + id: 'cpa', + label: 'CPA 面板', + legacyPanelMode: 'cpa', + groups: ['openai-source-cpa'], + }, + sub2api: { + id: 'sub2api', + label: 'SUB2API', + legacyPanelMode: 'sub2api', + groups: ['openai-source-sub2api'], + }, + codex2api: { + id: 'codex2api', + label: 'Codex2API', + legacyPanelMode: 'codex2api', + groups: ['openai-source-codex2api'], + }, + }, + runtimeSources: { + 'openai-auth': { + flowId: 'openai', + kind: 'flow-page', + label: '认证页', + readyPolicy: 'allow-child-frame', + family: 'openai-auth-family', + driverId: 'content/signup-page', + cleanupScopes: ['oauth-localhost-callback'], + }, + chatgpt: { + flowId: 'openai', + kind: 'flow-entry', + label: 'ChatGPT 首页', + readyPolicy: 'allow-child-frame', + family: 'chatgpt-entry-family', + driverId: null, + cleanupScopes: [], + }, + 'vps-panel': { + flowId: 'openai', + kind: 'panel-page', + label: 'CPA 面板', + readyPolicy: 'allow-child-frame', + family: 'vps-panel-family', + driverId: 'content/vps-panel', + cleanupScopes: [], + }, + 'platform-panel': { + flowId: 'openai', + kind: 'virtual-page', + label: '平台回调面板', + readyPolicy: 'disabled', + family: 'platform-panel-family', + driverId: 'content/platform-panel', + cleanupScopes: [], + }, + 'sub2api-panel': { + flowId: 'openai', + kind: 'panel-page', + label: 'SUB2API 后台', + readyPolicy: 'allow-child-frame', + family: 'sub2api-panel-family', + driverId: 'content/sub2api-panel', + cleanupScopes: [], + }, + 'codex2api-panel': { + flowId: 'openai', + kind: 'panel-page', + label: 'Codex2API 后台', + readyPolicy: 'allow-child-frame', + family: 'codex2api-panel-family', + driverId: 'content/sub2api-panel', + cleanupScopes: [], + }, + 'plus-checkout': { + flowId: 'openai', + kind: 'flow-page', + label: 'Plus Checkout', + readyPolicy: 'top-frame-only', + family: 'plus-checkout-family', + driverId: 'content/plus-checkout', + cleanupScopes: [], + }, + 'paypal-flow': { + flowId: 'openai', + kind: 'flow-page', + label: 'PayPal 授权页', + readyPolicy: 'allow-child-frame', + family: 'paypal-flow-family', + driverId: 'content/paypal-flow', + cleanupScopes: [], + }, + 'gopay-flow': { + flowId: 'openai', + kind: 'flow-page', + label: 'GoPay 授权页', + readyPolicy: 'allow-child-frame', + family: 'gopay-flow-family', + driverId: 'content/gopay-flow', + cleanupScopes: [], + }, + }, + driverDefinitions: { + 'content/signup-page': { + sourceId: 'openai-auth', + commands: [ + 'submit-signup-email', + 'fill-password', + 'fill-profile', + 'oauth-login', + 'submit-verification-code', + 'post-login-phone-verification', + 'bind-email', + 'fetch-bind-email-code', + 'confirm-oauth', + 'detect-auth-state', + ], + }, + 'content/sub2api-panel': { + sourceId: 'sub2api-panel', + commands: ['open-panel', 'fetch-oauth-url', 'platform-verify'], + }, + 'content/vps-panel': { + sourceId: 'vps-panel', + commands: ['open-panel', 'fetch-oauth-url', 'platform-verify'], + }, + 'content/platform-panel': { + sourceId: 'platform-panel', + commands: ['platform-verify', 'fetch-oauth-url'], + }, + 'content/plus-checkout': { + sourceId: 'plus-checkout', + commands: ['plus-checkout-create', 'plus-checkout-billing', 'plus-checkout-return'], + }, + 'content/paypal-flow': { + sourceId: 'paypal-flow', + commands: ['paypal-approve'], + }, + 'content/gopay-flow': { + sourceId: 'gopay-flow', + commands: ['gopay-subscription-confirm'], + }, + }, + }, + kiro: { + id: 'kiro', + label: 'Kiro', + services: ['email', 'proxy'], + capabilities: { + ...DEFAULT_FLOW_CAPABILITIES, + stepDefinitionMode: 'kiro-device-auth', + }, + baseGroups: [ + 'kiro-runtime-status', + ], + sources: { + 'kiro-rs': { + id: 'kiro-rs', + label: 'kiro.rs', + groups: ['kiro-source-kiro-rs'], + }, + }, + runtimeSources: { + 'kiro-device-auth': { + flowId: 'kiro', + kind: 'flow-page', + label: 'Kiro 授权页', + readyPolicy: 'top-frame-only', + family: 'kiro-device-auth-family', + driverId: null, + cleanupScopes: [], + }, + 'kiro-rs-admin': { + flowId: 'kiro', + kind: 'virtual-page', + label: 'kiro.rs Admin', + readyPolicy: 'disabled', + family: 'kiro-rs-admin-family', + driverId: null, + cleanupScopes: [], + }, + }, + driverDefinitions: { + 'background/kiro-device-auth': { + sourceId: 'kiro-device-auth', + commands: [ + 'kiro-start-device-login', + 'kiro-await-device-login', + 'kiro-upload-credential', + ], + }, + }, + }, + }); + + const SETTINGS_GROUP_DEFINITIONS = freezeDeep({ + 'service-email': { + id: 'service-email', + label: '邮箱服务', + }, + 'service-proxy': { + id: 'service-proxy', + label: 'IP 代理', + sectionIds: ['ip-proxy-section'], + }, + 'openai-source-cpa': { + id: 'openai-source-cpa', + label: 'CPA 来源', + rowIds: ['row-vps-url', 'row-vps-password', 'row-local-cpa-step9-mode'], + }, + 'openai-source-sub2api': { + id: 'openai-source-sub2api', + label: 'SUB2API 来源', + rowIds: [ + 'row-sub2api-url', + 'row-sub2api-email', + 'row-sub2api-password', + 'row-sub2api-group', + 'row-sub2api-account-priority', + 'row-sub2api-default-proxy', + ], + }, + 'openai-source-codex2api': { + id: 'openai-source-codex2api', + label: 'Codex2API 来源', + rowIds: ['row-codex2api-url', 'row-codex2api-admin-key'], + }, + 'openai-account': { + id: 'openai-account', + label: '账户', + rowIds: ['row-custom-password'], + }, + 'openai-plus': { + id: 'openai-plus', + label: 'Plus', + rowIds: [ + 'row-plus-mode', + 'row-plus-payment-method', + 'row-paypal-account', + 'row-gpc-helper-api', + 'row-gpc-helper-card-key', + 'row-gpc-helper-phone-mode', + 'row-gpc-helper-country-code', + 'row-gpc-helper-phone', + 'row-gpc-helper-otp-channel', + 'row-gpc-helper-local-sms-enabled', + 'row-gpc-helper-local-sms-url', + 'row-gpc-helper-pin', + 'row-gopay-country-code', + 'row-gopay-phone', + 'row-gopay-otp', + 'row-gopay-pin', + ], + }, + 'openai-phone': { + id: 'openai-phone', + label: '接码设置', + sectionIds: ['phone-verification-section'], + rowIds: ['row-signup-phone'], + }, + 'openai-oauth': { + id: 'openai-oauth', + label: 'OAuth', + rowIds: ['row-oauth-flow-timeout', 'row-oauth-display'], + }, + 'kiro-source-kiro-rs': { + id: 'kiro-source-kiro-rs', + label: 'kiro.rs 配置', + rowIds: ['row-kiro-rs-url', 'row-kiro-rs-key', 'row-kiro-region'], + }, + 'kiro-runtime-status': { + id: 'kiro-runtime-status', + label: 'Kiro 运行态', + rowIds: ['row-kiro-device-code', 'row-kiro-login-url', 'row-kiro-upload-status'], + }, + }); + + function normalizeFlowId(value = '', fallback = DEFAULT_FLOW_ID) { + const normalized = String(value || '').trim().toLowerCase(); + if (normalized === LEGACY_OPENAI_FLOW_ALIAS) { + return DEFAULT_FLOW_ID; + } + if (normalized && Object.prototype.hasOwnProperty.call(FLOW_DEFINITIONS, normalized)) { + return normalized; + } + const fallbackValue = String(fallback || '').trim().toLowerCase(); + if (fallbackValue === LEGACY_OPENAI_FLOW_ALIAS) { + return DEFAULT_FLOW_ID; + } + return Object.prototype.hasOwnProperty.call(FLOW_DEFINITIONS, fallbackValue) + ? fallbackValue + : DEFAULT_FLOW_ID; + } + + function getRegisteredFlowIds() { + return Object.keys(FLOW_DEFINITIONS); + } + + function getFlowDefinition(flowId) { + const normalizedFlowId = normalizeFlowId(flowId); + return FLOW_DEFINITIONS[normalizedFlowId] || FLOW_DEFINITIONS[DEFAULT_FLOW_ID]; + } + + function getFlowLabel(flowId) { + return getFlowDefinition(flowId)?.label || normalizeFlowId(flowId); + } + + function getDefaultSourceId(flowId) { + return normalizeFlowId(flowId) === 'kiro' + ? DEFAULT_KIRO_SOURCE_ID + : DEFAULT_OPENAI_SOURCE_ID; + } + + function normalizeOpenAiSourceId(value = '', fallback = DEFAULT_OPENAI_SOURCE_ID) { + const normalized = String(value || '').trim().toLowerCase(); + if (OPENAI_SOURCE_IDS.includes(normalized)) { + return normalized; + } + const fallbackValue = String(fallback || '').trim().toLowerCase(); + return OPENAI_SOURCE_IDS.includes(fallbackValue) ? fallbackValue : DEFAULT_OPENAI_SOURCE_ID; + } + + function normalizeKiroSourceId(value = '', fallback = DEFAULT_KIRO_SOURCE_ID) { + const normalized = String(value || '').trim().toLowerCase(); + if (normalized === DEFAULT_KIRO_SOURCE_ID) { + return normalized; + } + const fallbackValue = String(fallback || '').trim().toLowerCase(); + return fallbackValue === DEFAULT_KIRO_SOURCE_ID ? fallbackValue : DEFAULT_KIRO_SOURCE_ID; + } + + function normalizeSourceId(flowId, sourceId = '', fallback = undefined) { + const normalizedFlowId = normalizeFlowId(flowId); + if (normalizedFlowId === 'kiro') { + return normalizeKiroSourceId(sourceId, fallback || DEFAULT_KIRO_SOURCE_ID); + } + return normalizeOpenAiSourceId(sourceId, fallback || DEFAULT_OPENAI_SOURCE_ID); + } + + function getSourceDefinitions(flowId) { + return getFlowDefinition(flowId)?.sources || {}; + } + + function getSourceDefinition(flowId, sourceId) { + const normalizedFlowId = normalizeFlowId(flowId); + const normalizedSourceId = normalizeSourceId(normalizedFlowId, sourceId, getDefaultSourceId(normalizedFlowId)); + return getSourceDefinitions(normalizedFlowId)[normalizedSourceId] || null; + } + + function getSourceOptions(flowId) { + return Object.values(getSourceDefinitions(flowId)); + } + + function getSourceLabel(flowId, sourceId) { + return getSourceDefinition(flowId, sourceId)?.label || normalizeSourceId(flowId, sourceId); + } + + function mapPanelModeToSourceId(panelMode = '', fallback = DEFAULT_OPENAI_SOURCE_ID) { + return normalizeOpenAiSourceId(panelMode, fallback); + } + + function mapSourceIdToPanelMode(flowId, sourceId = '', fallback = DEFAULT_OPENAI_SOURCE_ID) { + if (normalizeFlowId(flowId) !== DEFAULT_FLOW_ID) { + return normalizeOpenAiSourceId(fallback, DEFAULT_OPENAI_SOURCE_ID); + } + return normalizeOpenAiSourceId(sourceId, fallback || DEFAULT_OPENAI_SOURCE_ID); + } + + function getFlowCapabilities(flowId) { + return { + ...DEFAULT_FLOW_CAPABILITIES, + ...(getFlowDefinition(flowId)?.capabilities || {}), + }; + } + + function getVisibleGroupIds(flowId, sourceId, options = {}) { + const normalizedFlowId = normalizeFlowId(flowId); + const flowDefinition = getFlowDefinition(normalizedFlowId); + const normalizedSourceId = normalizeSourceId(normalizedFlowId, sourceId, getDefaultSourceId(normalizedFlowId)); + const sourceDefinition = getSourceDefinition(normalizedFlowId, normalizedSourceId); + const includeSharedServices = options?.includeSharedServices !== false; + const serviceGroups = includeSharedServices + ? (Array.isArray(flowDefinition?.services) ? flowDefinition.services.map((serviceId) => `service-${serviceId}`) : []) + : []; + return Array.from(new Set([ + ...(Array.isArray(flowDefinition?.baseGroups) ? flowDefinition.baseGroups : []), + ...(Array.isArray(sourceDefinition?.groups) ? sourceDefinition.groups : []), + ...serviceGroups, + ])); + } + + function getSettingsGroupDefinition(groupId) { + const normalizedGroupId = String(groupId || '').trim(); + return SETTINGS_GROUP_DEFINITIONS[normalizedGroupId] || null; + } + + function getSettingsGroupDefinitions() { + return SETTINGS_GROUP_DEFINITIONS; + } + + function getRuntimeSourceDefinitions() { + const next = {}; + Object.values(FLOW_DEFINITIONS).forEach((flowDefinition) => { + Object.assign(next, flowDefinition.runtimeSources || {}); + }); + return next; + } + + function getDriverDefinitions() { + const next = {}; + Object.values(FLOW_DEFINITIONS).forEach((flowDefinition) => { + Object.assign(next, flowDefinition.driverDefinitions || {}); + }); + return next; + } + + return { + DEFAULT_FLOW_CAPABILITIES, + DEFAULT_FLOW_ID, + DEFAULT_KIRO_REGION, + DEFAULT_KIRO_RS_URL, + DEFAULT_KIRO_SOURCE_ID, + DEFAULT_OPENAI_SOURCE_ID, + FLOW_DEFINITIONS, + LEGACY_OPENAI_FLOW_ALIAS, + OPENAI_SOURCE_IDS, + SETTINGS_GROUP_DEFINITIONS, + SHARED_SERVICE_IDS, + getDefaultSourceId, + getDriverDefinitions, + getFlowCapabilities, + getFlowDefinition, + getFlowLabel, + getRegisteredFlowIds, + getRuntimeSourceDefinitions, + getSettingsGroupDefinition, + getSettingsGroupDefinitions, + getSourceDefinition, + getSourceDefinitions, + getSourceLabel, + getSourceOptions, + getVisibleGroupIds, + mapPanelModeToSourceId, + mapSourceIdToPanelMode, + normalizeFlowId, + normalizeKiroSourceId, + normalizeOpenAiSourceId, + normalizeSourceId, + }; +}); diff --git a/shared/settings-schema.js b/shared/settings-schema.js new file mode 100644 index 0000000..3564750 --- /dev/null +++ b/shared/settings-schema.js @@ -0,0 +1,442 @@ +(function attachMultiPageSettingsSchema(root, factory) { + root.MultiPageSettingsSchema = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createSettingsSchemaModule() { + function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); + } + + function cloneValue(value) { + if (Array.isArray(value)) { + return value.map((entry) => cloneValue(entry)); + } + if (isPlainObject(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)]) + ); + } + return value; + } + + function normalizeStepExecutionRangeEntry(value = {}, fallback = {}) { + const source = isPlainObject(value) ? value : {}; + const fallbackSource = isPlainObject(fallback) ? fallback : {}; + const fromStep = Math.max(1, Number(source.fromStep ?? fallbackSource.fromStep ?? 1) || 1); + const toStep = Math.max(fromStep, Number(source.toStep ?? fallbackSource.toStep ?? fromStep) || fromStep); + return { + enabled: Boolean(source.enabled ?? fallbackSource.enabled), + fromStep, + toStep, + }; + } + + function createSettingsSchema(deps = {}) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + const flowRegistry = deps.flowRegistry || rootScope.MultiPageFlowRegistry || {}; + const defaultFlowId = String(deps.defaultFlowId || flowRegistry.DEFAULT_FLOW_ID || 'openai').trim().toLowerCase() || 'openai'; + const defaultOpenAiSourceId = flowRegistry.DEFAULT_OPENAI_SOURCE_ID || 'cpa'; + const defaultKiroSourceId = flowRegistry.DEFAULT_KIRO_SOURCE_ID || 'kiro-rs'; + const defaultKiroRegion = flowRegistry.DEFAULT_KIRO_REGION || 'us-east-1'; + const defaultKiroRsUrl = flowRegistry.DEFAULT_KIRO_RS_URL || 'https://kiro.leftcode.xyz/admin'; + const normalizeFlowId = typeof flowRegistry.normalizeFlowId === 'function' + ? flowRegistry.normalizeFlowId + : ((value = '', fallback = defaultFlowId) => { + const normalized = String(value || '').trim().toLowerCase(); + return normalized || String(fallback || '').trim().toLowerCase() || defaultFlowId; + }); + const normalizeSourceId = typeof flowRegistry.normalizeSourceId === 'function' + ? flowRegistry.normalizeSourceId + : ((flowId, value = '', fallback = '') => String(value || fallback || '').trim().toLowerCase()); + const mapSourceIdToPanelMode = typeof flowRegistry.mapSourceIdToPanelMode === 'function' + ? flowRegistry.mapSourceIdToPanelMode + : ((_flowId, sourceId = '', fallback = defaultOpenAiSourceId) => String(sourceId || fallback || defaultOpenAiSourceId).trim().toLowerCase()); + const mapPanelModeToSourceId = typeof flowRegistry.mapPanelModeToSourceId === 'function' + ? flowRegistry.mapPanelModeToSourceId + : ((panelMode = '', fallback = defaultOpenAiSourceId) => String(panelMode || fallback || defaultOpenAiSourceId).trim().toLowerCase()); + + function buildDefaultSettingsState() { + return { + schemaVersion: 3, + activeFlowId: defaultFlowId, + services: { + email: { + provider: '163', + }, + proxy: { + enabled: false, + provider: '711proxy', + mode: 'account', + }, + }, + flows: { + openai: { + source: { + selected: defaultOpenAiSourceId, + entries: { + cpa: { + vpsUrl: '', + vpsPassword: '', + localCpaStep9Mode: 'submit', + }, + sub2api: { + sub2apiUrl: '', + sub2apiEmail: '', + sub2apiPassword: '', + sub2apiGroupName: 'codex', + sub2apiGroupNames: ['codex', 'openai-plus'], + sub2apiAccountPriority: 1, + sub2apiDefaultProxyName: '', + }, + codex2api: { + codex2apiUrl: '', + codex2apiAdminKey: '', + }, + }, + }, + account: { + customPassword: '', + }, + signup: { + signupMethod: 'email', + phoneVerificationEnabled: false, + phoneSignupReloginAfterBindEmailEnabled: false, + }, + plus: { + plusModeEnabled: false, + plusPaymentMethod: 'paypal', + }, + autoRun: { + stepExecutionRange: { + enabled: false, + fromStep: 1, + toStep: 11, + }, + }, + }, + kiro: { + source: { + selected: defaultKiroSourceId, + entries: { + 'kiro-rs': { + kiroRsUrl: defaultKiroRsUrl, + kiroRsKey: '', + }, + }, + }, + options: { + kiroRegion: defaultKiroRegion, + kiroRsPriority: 0, + kiroRsEndpoint: '', + kiroRsAuthRegion: '', + kiroRsApiRegion: '', + }, + autoRun: { + stepExecutionRange: { + enabled: false, + fromStep: 1, + toStep: 3, + }, + }, + }, + }, + }; + } + + function getSourceValue(settingsState, pathGetter, fallback = {}) { + return cloneValue(pathGetter(isPlainObject(settingsState) ? settingsState : {}) || fallback); + } + + function normalizeSettingsState(input = {}, options = {}) { + const defaults = buildDefaultSettingsState(); + const nested = isPlainObject(input?.settingsState) + ? input.settingsState + : (isPlainObject(input) && isPlainObject(input.flows) && isPlainObject(input.services) ? input : {}); + const activeFlowId = normalizeFlowId( + input?.activeFlowId + ?? nested?.activeFlowId + ?? options?.activeFlowId + ?? defaults.activeFlowId, + defaults.activeFlowId + ); + const openaiSelectedSource = normalizeSourceId( + 'openai', + nested?.flows?.openai?.source?.selected + ?? input?.panelMode + ?? input?.openaiSourceId + ?? defaults.flows.openai.source.selected, + defaults.flows.openai.source.selected + ); + const kiroSelectedSource = normalizeSourceId( + 'kiro', + nested?.flows?.kiro?.source?.selected + ?? input?.kiroSourceId + ?? defaults.flows.kiro.source.selected, + defaults.flows.kiro.source.selected + ); + const stepExecutionRangeByFlow = isPlainObject(input?.stepExecutionRangeByFlow) + ? input.stepExecutionRangeByFlow + : {}; + + return { + schemaVersion: Number(input?.settingsSchemaVersion || nested?.schemaVersion || defaults.schemaVersion) || defaults.schemaVersion, + activeFlowId, + services: { + email: { + provider: String( + nested?.services?.email?.provider + ?? input?.mailProvider + ?? defaults.services.email.provider + ).trim() || defaults.services.email.provider, + }, + proxy: { + enabled: Boolean( + nested?.services?.proxy?.enabled + ?? input?.ipProxyEnabled + ?? defaults.services.proxy.enabled + ), + provider: String( + nested?.services?.proxy?.provider + ?? input?.ipProxyService + ?? defaults.services.proxy.provider + ).trim() || defaults.services.proxy.provider, + mode: String( + nested?.services?.proxy?.mode + ?? input?.ipProxyMode + ?? defaults.services.proxy.mode + ).trim() || defaults.services.proxy.mode, + }, + }, + flows: { + openai: { + source: { + selected: openaiSelectedSource, + entries: { + cpa: { + ...defaults.flows.openai.source.entries.cpa, + ...getSourceValue(nested, (state) => state.flows?.openai?.source?.entries?.cpa), + vpsUrl: String(input?.vpsUrl ?? nested?.flows?.openai?.source?.entries?.cpa?.vpsUrl ?? '').trim(), + vpsPassword: String(input?.vpsPassword ?? nested?.flows?.openai?.source?.entries?.cpa?.vpsPassword ?? ''), + localCpaStep9Mode: String( + input?.localCpaStep9Mode + ?? nested?.flows?.openai?.source?.entries?.cpa?.localCpaStep9Mode + ?? defaults.flows.openai.source.entries.cpa.localCpaStep9Mode + ).trim() || defaults.flows.openai.source.entries.cpa.localCpaStep9Mode, + }, + sub2api: { + ...defaults.flows.openai.source.entries.sub2api, + ...getSourceValue(nested, (state) => state.flows?.openai?.source?.entries?.sub2api), + sub2apiUrl: String(input?.sub2apiUrl ?? nested?.flows?.openai?.source?.entries?.sub2api?.sub2apiUrl ?? '').trim(), + sub2apiEmail: String(input?.sub2apiEmail ?? nested?.flows?.openai?.source?.entries?.sub2api?.sub2apiEmail ?? '').trim(), + sub2apiPassword: String(input?.sub2apiPassword ?? nested?.flows?.openai?.source?.entries?.sub2api?.sub2apiPassword ?? ''), + sub2apiGroupName: String(input?.sub2apiGroupName ?? nested?.flows?.openai?.source?.entries?.sub2api?.sub2apiGroupName ?? defaults.flows.openai.source.entries.sub2api.sub2apiGroupName).trim() || defaults.flows.openai.source.entries.sub2api.sub2apiGroupName, + sub2apiGroupNames: Array.isArray(input?.sub2apiGroupNames) + ? input.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean) + : (Array.isArray(nested?.flows?.openai?.source?.entries?.sub2api?.sub2apiGroupNames) + ? nested.flows.openai.source.entries.sub2api.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean) + : [...defaults.flows.openai.source.entries.sub2api.sub2apiGroupNames]), + sub2apiAccountPriority: Math.max(1, Number(input?.sub2apiAccountPriority ?? nested?.flows?.openai?.source?.entries?.sub2api?.sub2apiAccountPriority ?? defaults.flows.openai.source.entries.sub2api.sub2apiAccountPriority) || defaults.flows.openai.source.entries.sub2api.sub2apiAccountPriority), + sub2apiDefaultProxyName: String(input?.sub2apiDefaultProxyName ?? nested?.flows?.openai?.source?.entries?.sub2api?.sub2apiDefaultProxyName ?? '').trim(), + }, + codex2api: { + ...defaults.flows.openai.source.entries.codex2api, + ...getSourceValue(nested, (state) => state.flows?.openai?.source?.entries?.codex2api), + codex2apiUrl: String(input?.codex2apiUrl ?? nested?.flows?.openai?.source?.entries?.codex2api?.codex2apiUrl ?? '').trim(), + codex2apiAdminKey: String(input?.codex2apiAdminKey ?? nested?.flows?.openai?.source?.entries?.codex2api?.codex2apiAdminKey ?? '').trim(), + }, + }, + }, + account: { + customPassword: String(input?.customPassword ?? nested?.flows?.openai?.account?.customPassword ?? '').trim(), + }, + signup: { + signupMethod: String(input?.signupMethod ?? nested?.flows?.openai?.signup?.signupMethod ?? defaults.flows.openai.signup.signupMethod).trim().toLowerCase() === 'phone' ? 'phone' : 'email', + phoneVerificationEnabled: Boolean(input?.phoneVerificationEnabled ?? nested?.flows?.openai?.signup?.phoneVerificationEnabled ?? defaults.flows.openai.signup.phoneVerificationEnabled), + phoneSignupReloginAfterBindEmailEnabled: Boolean(input?.phoneSignupReloginAfterBindEmailEnabled ?? nested?.flows?.openai?.signup?.phoneSignupReloginAfterBindEmailEnabled ?? defaults.flows.openai.signup.phoneSignupReloginAfterBindEmailEnabled), + }, + plus: { + plusModeEnabled: Boolean(input?.plusModeEnabled ?? nested?.flows?.openai?.plus?.plusModeEnabled ?? defaults.flows.openai.plus.plusModeEnabled), + plusPaymentMethod: String(input?.plusPaymentMethod ?? nested?.flows?.openai?.plus?.plusPaymentMethod ?? defaults.flows.openai.plus.plusPaymentMethod).trim() || defaults.flows.openai.plus.plusPaymentMethod, + }, + autoRun: { + stepExecutionRange: normalizeStepExecutionRangeEntry( + nested?.flows?.openai?.autoRun?.stepExecutionRange + ?? stepExecutionRangeByFlow.openai + ?? {}, + defaults.flows.openai.autoRun.stepExecutionRange + ), + }, + }, + kiro: { + source: { + selected: kiroSelectedSource, + entries: { + 'kiro-rs': { + ...defaults.flows.kiro.source.entries['kiro-rs'], + ...getSourceValue(nested, (state) => state.flows?.kiro?.source?.entries?.['kiro-rs']), + kiroRsUrl: String( + input?.kiroRsUrl + ?? nested?.flows?.kiro?.source?.entries?.['kiro-rs']?.kiroRsUrl + ?? defaults.flows.kiro.source.entries['kiro-rs'].kiroRsUrl + ).trim() || defaults.flows.kiro.source.entries['kiro-rs'].kiroRsUrl, + kiroRsKey: String( + input?.kiroRsKey + ?? nested?.flows?.kiro?.source?.entries?.['kiro-rs']?.kiroRsKey + ?? defaults.flows.kiro.source.entries['kiro-rs'].kiroRsKey + ), + }, + }, + }, + options: { + kiroRegion: String( + input?.kiroRegion + ?? nested?.flows?.kiro?.options?.kiroRegion + ?? defaults.flows.kiro.options.kiroRegion + ).trim() || defaults.flows.kiro.options.kiroRegion, + kiroRsPriority: Number( + input?.kiroRsPriority + ?? nested?.flows?.kiro?.options?.kiroRsPriority + ?? defaults.flows.kiro.options.kiroRsPriority + ) || 0, + kiroRsEndpoint: String( + input?.kiroRsEndpoint + ?? nested?.flows?.kiro?.options?.kiroRsEndpoint + ?? defaults.flows.kiro.options.kiroRsEndpoint + ).trim(), + kiroRsAuthRegion: String( + input?.kiroRsAuthRegion + ?? nested?.flows?.kiro?.options?.kiroRsAuthRegion + ?? defaults.flows.kiro.options.kiroRsAuthRegion + ).trim(), + kiroRsApiRegion: String( + input?.kiroRsApiRegion + ?? nested?.flows?.kiro?.options?.kiroRsApiRegion + ?? defaults.flows.kiro.options.kiroRsApiRegion + ).trim(), + }, + autoRun: { + stepExecutionRange: normalizeStepExecutionRangeEntry( + nested?.flows?.kiro?.autoRun?.stepExecutionRange + ?? stepExecutionRangeByFlow.kiro + ?? {}, + defaults.flows.kiro.autoRun.stepExecutionRange + ), + }, + }, + }, + }; + } + + function buildStepExecutionRangeByFlow(settingsState = {}) { + const normalizedState = normalizeSettingsState(settingsState); + return { + openai: normalizeStepExecutionRangeEntry( + normalizedState?.flows?.openai?.autoRun?.stepExecutionRange, + buildDefaultSettingsState().flows.openai.autoRun.stepExecutionRange + ), + kiro: normalizeStepExecutionRangeEntry( + normalizedState?.flows?.kiro?.autoRun?.stepExecutionRange, + buildDefaultSettingsState().flows.kiro.autoRun.stepExecutionRange + ), + }; + } + + function getFlowSettings(settingsState = {}, flowId) { + const normalizedState = normalizeSettingsState(settingsState); + const normalizedFlowId = normalizeFlowId(flowId, normalizedState.activeFlowId); + return cloneValue(normalizedState?.flows?.[normalizedFlowId] || {}); + } + + function getSelectedSourceId(settingsState = {}, flowId) { + const flowSettings = getFlowSettings(settingsState, flowId); + const normalizedFlowId = normalizeFlowId(flowId, normalizeSettingsState(settingsState).activeFlowId); + return normalizeSourceId( + normalizedFlowId, + flowSettings?.source?.selected, + normalizedFlowId === 'kiro' ? defaultKiroSourceId : defaultOpenAiSourceId + ); + } + + function buildLegacySettingsPayload(settingsState = {}, baseInput = {}) { + const normalizedState = normalizeSettingsState(settingsState); + const next = { + ...(isPlainObject(baseInput) ? cloneValue(baseInput) : {}), + }; + const openaiState = normalizedState.flows.openai; + const kiroState = normalizedState.flows.kiro; + next.activeFlowId = normalizedState.activeFlowId; + next.panelMode = mapSourceIdToPanelMode('openai', openaiState.source.selected, defaultOpenAiSourceId); + next.kiroSourceId = getSelectedSourceId(normalizedState, 'kiro'); + next.vpsUrl = openaiState.source.entries.cpa.vpsUrl; + next.vpsPassword = openaiState.source.entries.cpa.vpsPassword; + next.localCpaStep9Mode = openaiState.source.entries.cpa.localCpaStep9Mode; + next.sub2apiUrl = openaiState.source.entries.sub2api.sub2apiUrl; + next.sub2apiEmail = openaiState.source.entries.sub2api.sub2apiEmail; + next.sub2apiPassword = openaiState.source.entries.sub2api.sub2apiPassword; + next.sub2apiGroupName = openaiState.source.entries.sub2api.sub2apiGroupName; + next.sub2apiGroupNames = cloneValue(openaiState.source.entries.sub2api.sub2apiGroupNames); + next.sub2apiAccountPriority = openaiState.source.entries.sub2api.sub2apiAccountPriority; + next.sub2apiDefaultProxyName = openaiState.source.entries.sub2api.sub2apiDefaultProxyName; + next.codex2apiUrl = openaiState.source.entries.codex2api.codex2apiUrl; + next.codex2apiAdminKey = openaiState.source.entries.codex2api.codex2apiAdminKey; + next.customPassword = openaiState.account.customPassword; + next.signupMethod = openaiState.signup.signupMethod; + next.phoneVerificationEnabled = openaiState.signup.phoneVerificationEnabled; + next.phoneSignupReloginAfterBindEmailEnabled = openaiState.signup.phoneSignupReloginAfterBindEmailEnabled; + next.plusModeEnabled = openaiState.plus.plusModeEnabled; + next.plusPaymentMethod = openaiState.plus.plusPaymentMethod; + next.mailProvider = normalizedState.services.email.provider; + next.ipProxyEnabled = normalizedState.services.proxy.enabled; + next.ipProxyService = normalizedState.services.proxy.provider; + next.ipProxyMode = normalizedState.services.proxy.mode; + next.kiroRsUrl = kiroState.source.entries['kiro-rs'].kiroRsUrl; + next.kiroRsKey = kiroState.source.entries['kiro-rs'].kiroRsKey; + next.kiroRegion = kiroState.options.kiroRegion; + next.kiroRsPriority = kiroState.options.kiroRsPriority; + next.kiroRsEndpoint = kiroState.options.kiroRsEndpoint; + next.kiroRsAuthRegion = kiroState.options.kiroRsAuthRegion; + next.kiroRsApiRegion = kiroState.options.kiroRsApiRegion; + next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState); + next.settingsSchemaVersion = normalizedState.schemaVersion; + next.settingsState = cloneValue(normalizedState); + return next; + } + + function getFlowInputState(settingsState = {}, flowId) { + const normalizedState = normalizeSettingsState(settingsState); + const normalizedFlowId = normalizeFlowId(flowId, normalizedState.activeFlowId); + if (normalizedFlowId === 'kiro') { + return { + activeFlowId: normalizedFlowId, + sourceId: getSelectedSourceId(normalizedState, 'kiro'), + kiroRsUrl: normalizedState.flows.kiro.source.entries['kiro-rs'].kiroRsUrl, + kiroRsKey: normalizedState.flows.kiro.source.entries['kiro-rs'].kiroRsKey, + kiroRegion: normalizedState.flows.kiro.options.kiroRegion, + }; + } + return { + activeFlowId: normalizedFlowId, + sourceId: getSelectedSourceId(normalizedState, 'openai'), + panelMode: mapSourceIdToPanelMode('openai', normalizedState.flows.openai.source.selected, defaultOpenAiSourceId), + }; + } + + function normalizeFlatInput(input = {}) { + const state = normalizeSettingsState(input); + return buildLegacySettingsPayload(state, input); + } + + return { + buildDefaultSettingsState, + buildLegacySettingsPayload, + buildStepExecutionRangeByFlow, + getFlowInputState, + getFlowSettings, + getSelectedSourceId, + normalizeFlatInput, + normalizeSettingsState, + }; + } + + return { + createSettingsSchema, + }; +}); diff --git a/shared/source-registry.js b/shared/source-registry.js index 1083e50..a3ea104 100644 --- a/shared/source-registry.js +++ b/shared/source-registry.js @@ -1,29 +1,14 @@ (function attachMultiPageSourceRegistry(root, factory) { root.MultiPageSourceRegistry = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createSourceRegistryModule() { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + const flowRegistryApi = rootScope.MultiPageFlowRegistry || {}; + const SOURCE_ALIASES = Object.freeze({ 'signup-page': 'openai-auth', }); - const SOURCE_DEFINITIONS = Object.freeze({ - 'openai-auth': { - flowId: 'openai', - kind: 'flow-page', - label: '认证页', - readyPolicy: 'allow-child-frame', - family: 'openai-auth-family', - driverId: 'content/signup-page', - cleanupScopes: ['oauth-localhost-callback'], - }, - chatgpt: { - flowId: 'openai', - kind: 'flow-entry', - label: 'ChatGPT 首页', - readyPolicy: 'allow-child-frame', - family: 'chatgpt-entry-family', - driverId: null, - cleanupScopes: [], - }, + const SHARED_SOURCE_DEFINITIONS = Object.freeze({ 'qq-mail': { flowId: null, kind: 'mail-provider', @@ -87,69 +72,6 @@ driverId: 'content/duck-mail', cleanupScopes: [], }, - 'vps-panel': { - flowId: 'openai', - kind: 'panel-page', - label: 'CPA 面板', - readyPolicy: 'allow-child-frame', - family: 'vps-panel-family', - driverId: 'content/vps-panel', - cleanupScopes: [], - }, - 'platform-panel': { - flowId: 'openai', - kind: 'virtual-page', - label: '平台回调面板', - readyPolicy: 'disabled', - family: 'platform-panel-family', - driverId: 'content/platform-panel', - cleanupScopes: [], - }, - 'sub2api-panel': { - flowId: 'openai', - kind: 'panel-page', - label: 'SUB2API 后台', - readyPolicy: 'allow-child-frame', - family: 'sub2api-panel-family', - driverId: 'content/sub2api-panel', - cleanupScopes: [], - }, - 'codex2api-panel': { - flowId: 'openai', - kind: 'panel-page', - label: 'Codex2API 后台', - readyPolicy: 'allow-child-frame', - family: 'codex2api-panel-family', - driverId: 'content/sub2api-panel', - cleanupScopes: [], - }, - 'plus-checkout': { - flowId: 'openai', - kind: 'flow-page', - label: 'Plus Checkout', - readyPolicy: 'top-frame-only', - family: 'plus-checkout-family', - driverId: 'content/plus-checkout', - cleanupScopes: [], - }, - 'paypal-flow': { - flowId: 'openai', - kind: 'flow-page', - label: 'PayPal 授权页', - readyPolicy: 'allow-child-frame', - family: 'paypal-flow-family', - driverId: 'content/paypal-flow', - cleanupScopes: [], - }, - 'gopay-flow': { - flowId: 'openai', - kind: 'flow-page', - label: 'GoPay 授权页', - readyPolicy: 'allow-child-frame', - family: 'gopay-flow-family', - driverId: 'content/gopay-flow', - cleanupScopes: [], - }, 'unknown-source': { flowId: null, kind: 'unknown', @@ -161,22 +83,7 @@ }, }); - const DRIVER_DEFINITIONS = Object.freeze({ - 'content/signup-page': { - sourceId: 'openai-auth', - commands: [ - 'submit-signup-email', - 'fill-password', - 'fill-profile', - 'oauth-login', - 'submit-verification-code', - 'post-login-phone-verification', - 'bind-email', - 'fetch-bind-email-code', - 'confirm-oauth', - 'detect-auth-state', - ], - }, + const SHARED_DRIVER_DEFINITIONS = Object.freeze({ 'content/qq-mail': { sourceId: 'qq-mail', commands: ['POLL_EMAIL'], @@ -201,30 +108,6 @@ sourceId: 'duck-mail', commands: ['FETCH_ALIAS_EMAIL'], }, - 'content/sub2api-panel': { - sourceId: 'sub2api-panel', - commands: ['open-panel', 'fetch-oauth-url', 'platform-verify'], - }, - 'content/vps-panel': { - sourceId: 'vps-panel', - commands: ['open-panel', 'fetch-oauth-url', 'platform-verify'], - }, - 'content/platform-panel': { - sourceId: 'platform-panel', - commands: ['platform-verify', 'fetch-oauth-url'], - }, - 'content/plus-checkout': { - sourceId: 'plus-checkout', - commands: ['plus-checkout-create', 'plus-checkout-billing', 'plus-checkout-return'], - }, - 'content/paypal-flow': { - sourceId: 'paypal-flow', - commands: ['paypal-approve'], - }, - 'content/gopay-flow': { - sourceId: 'gopay-flow', - commands: ['gopay-subscription-confirm'], - }, }); const CLEANUP_SCOPE_OWNERS = Object.freeze({ @@ -240,9 +123,31 @@ 'mail-2925', 'inbucket-mail', 'plus-checkout', + 'kiro-device-auth', ]); + function getRuntimeSourceDefinitions() { + return { + ...(typeof flowRegistryApi.getRuntimeSourceDefinitions === 'function' + ? flowRegistryApi.getRuntimeSourceDefinitions() + : {}), + ...SHARED_SOURCE_DEFINITIONS, + }; + } + + function getDriverDefinitions() { + return { + ...(typeof flowRegistryApi.getDriverDefinitions === 'function' + ? flowRegistryApi.getDriverDefinitions() + : {}), + ...SHARED_DRIVER_DEFINITIONS, + }; + } + function createSourceRegistry() { + const SOURCE_DEFINITIONS = getRuntimeSourceDefinitions(); + const DRIVER_DEFINITIONS = getDriverDefinitions(); + function parseUrlSafely(rawUrl) { if (!rawUrl) return null; try { @@ -394,6 +299,9 @@ return candidate.hostname.endsWith('paypal.com'); case 'gopay-flow': return /gopay|gojek/i.test(candidate.hostname); + case 'kiro-device-auth': + return candidate.hostname === 'view.awsapps.com' + || candidate.hostname.endsWith('.amazonaws.com'); default: return false; } @@ -416,6 +324,7 @@ if (normalizedHostname === 'www.icloud.com' || normalizedHostname === 'www.icloud.com.cn') return 'icloud-mail'; if (normalizedUrl.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail'; if (normalizedUrl.includes('2925.com')) return 'mail-2925'; + if (normalizedHostname === 'view.awsapps.com' || normalizedHostname.endsWith('.amazonaws.com')) return 'kiro-device-auth'; if (isSignupEntryHost(normalizedHostname)) return 'chatgpt'; return 'unknown-source'; } @@ -436,10 +345,10 @@ return { detectSourceFromLocation, + driverAcceptsCommand, getCleanupOwnerSource, getDriverIdForSource, getDriverMeta, - driverAcceptsCommand, getSourceKeys, getSourceLabel, getSourceMeta, diff --git a/sidepanel/contribution-mode.js b/sidepanel/contribution-mode.js index e511d08..4ff1029 100644 --- a/sidepanel/contribution-mode.js +++ b/sidepanel/contribution-mode.js @@ -84,8 +84,16 @@ return getContributionSource(currentState) === CONTRIBUTION_SOURCE_SUB2API ? 'SUB2API' : 'CPA'; } + function getActiveFlowId(currentState = getLatestState()) { + return normalizeString(currentState.activeFlowId || currentState.flowId).toLowerCase() || 'openai'; + } + + function isContributionModeAvailable(currentState = getLatestState()) { + return getActiveFlowId(currentState) === 'openai'; + } + function isContributionModeEnabled(currentState = getLatestState()) { - return Boolean(currentState.contributionMode); + return isContributionModeAvailable(currentState) && Boolean(currentState.contributionMode); } function hasActiveContributionSession(currentState = getLatestState()) { @@ -107,13 +115,18 @@ }); } - function syncContributionButton(enabled, blocked) { + function syncContributionButton(enabled, blocked, available = true) { if (!dom.btnContributionMode) { return; } dom.btnContributionMode.classList.toggle('is-active', enabled); dom.btnContributionMode.setAttribute('aria-pressed', String(enabled)); + if (!available) { + dom.btnContributionMode.disabled = true; + dom.btnContributionMode.title = '当前 flow 不支持贡献模式'; + return; + } dom.btnContributionMode.disabled = enabled || blocked; dom.btnContributionMode.title = blocked ? '当前流程运行中,暂时不能切换贡献模式' @@ -333,10 +346,11 @@ function render() { const currentState = getLatestState(); + const available = isContributionModeAvailable(currentState); const enabled = isContributionModeEnabled(currentState); - const blocked = isModeSwitchBlocked(); + const blocked = available ? isModeSwitchBlocked() : false; const activeElement = typeof document !== 'undefined' ? document.activeElement : null; - const sourceLabel = getContributionSourceLabel(currentState); + const sourceLabel = available ? getContributionSourceLabel(currentState) : ''; if (enabled && dom.selectPanelMode) { dom.selectPanelMode.value = getContributionSource(currentState); @@ -346,7 +360,7 @@ helpers.updateAccountRunHistorySettingsUI?.(); if (dom.contributionModePanel) { - dom.contributionModePanel.hidden = !enabled; + dom.contributionModePanel.hidden = !available || !enabled; } if (dom.contributionModeText) { dom.contributionModeText.textContent = getSummaryText({ @@ -380,22 +394,22 @@ } syncContributionRows(enabled); - syncContributionButton(enabled, blocked); + syncContributionButton(enabled, blocked, available); if (dom.selectPanelMode) { - dom.selectPanelMode.disabled = enabled; + dom.selectPanelMode.disabled = available && enabled; } if (dom.btnStartContribution) { - dom.btnStartContribution.disabled = actionInFlight || blocked; + dom.btnStartContribution.disabled = !available || actionInFlight || blocked; } if (dom.btnOpenContributionUpload) { - dom.btnOpenContributionUpload.disabled = false; + dom.btnOpenContributionUpload.disabled = !available; } if (dom.btnExitContributionMode) { - dom.btnExitContributionMode.disabled = actionInFlight || blocked; + dom.btnExitContributionMode.disabled = !available || actionInFlight || blocked; dom.btnExitContributionMode.title = blocked ? '当前流程运行中,暂时不能退出贡献模式' : '退出贡献模式'; } @@ -403,7 +417,7 @@ dom.btnOpenAccountRecords.disabled = enabled; } - if (enabled) { + if (available && enabled) { helpers.closeConfigMenu?.(); helpers.closeAccountRecordsPanel?.(); ensurePolling(); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index af4680b..64e4909 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -113,13 +113,12 @@
注册
- 来源 + 来源 +
+ + + + +
账户密码
@@ -1809,6 +1840,8 @@ + + diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 28a1870..58f83db 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -82,6 +82,7 @@ const configMenu = document.getElementById('config-menu'); const btnExportSettings = document.getElementById('btn-export-settings'); const btnImportSettings = document.getElementById('btn-import-settings'); const inputImportSettingsFile = document.getElementById('input-import-settings-file'); +const labelSourceSelector = document.getElementById('label-source-selector'); const selectPanelMode = document.getElementById('select-panel-mode'); const rowVpsUrl = document.getElementById('row-vps-url'); const inputVpsUrl = document.getElementById('input-vps-url'); @@ -170,6 +171,18 @@ const rowCodex2ApiUrl = document.getElementById('row-codex2api-url'); const inputCodex2ApiUrl = document.getElementById('input-codex2api-url'); const rowCodex2ApiAdminKey = document.getElementById('row-codex2api-admin-key'); const inputCodex2ApiAdminKey = document.getElementById('input-codex2api-admin-key'); +const rowKiroRsUrl = document.getElementById('row-kiro-rs-url'); +const inputKiroRsUrl = document.getElementById('input-kiro-rs-url'); +const rowKiroRsKey = document.getElementById('row-kiro-rs-key'); +const inputKiroRsKey = document.getElementById('input-kiro-rs-key'); +const rowKiroRegion = document.getElementById('row-kiro-region'); +const inputKiroRegion = document.getElementById('input-kiro-region'); +const rowKiroDeviceCode = document.getElementById('row-kiro-device-code'); +const displayKiroDeviceCode = document.getElementById('display-kiro-device-code'); +const rowKiroLoginUrl = document.getElementById('row-kiro-login-url'); +const displayKiroLoginUrl = document.getElementById('display-kiro-login-url'); +const rowKiroUploadStatus = document.getElementById('row-kiro-upload-status'); +const displayKiroUploadStatus = document.getElementById('display-kiro-upload-status'); const rowCustomPassword = document.getElementById('row-custom-password'); const rowPlusMode = document.getElementById('row-plus-mode'); const inputPlusModeEnabled = document.getElementById('input-plus-mode-enabled'); @@ -539,6 +552,7 @@ let currentPlusModeEnabled = false; let currentPlusPaymentMethod = DEFAULT_PLUS_PAYMENT_METHOD; let currentSignupMethod = DEFAULT_SIGNUP_METHOD; let currentPhoneSignupReloginAfterBindEmailEnabled = DEFAULT_PHONE_SIGNUP_RELOGIN_AFTER_BIND_EMAIL_ENABLED; +let currentStepDefinitionFlowId = DEFAULT_ACTIVE_FLOW_ID; let phoneSignupReuseUiWasLocked = false; let lastConfirmedOperationDelayEnabled = true; let heroSmsCountrySelectionOrder = []; @@ -924,6 +938,7 @@ function getStepIdByNodeIdForCurrentMode(nodeId = '') { function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) { currentPlusModeEnabled = Boolean(plusModeEnabled); + const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' ? DEFAULT_ACTIVE_FLOW_ID : 'openai'; const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal'; const rawPaymentMethod = typeof options === 'string' ? options @@ -937,15 +952,23 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) { currentPlusPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod); currentSignupMethod = normalizeSignupMethod(rawSignupMethod); currentPhoneSignupReloginAfterBindEmailEnabled = phoneSignupReloginAfterBindEmailEnabled; + const nextActiveFlowId = String( + options?.activeFlowId + || (typeof latestState !== 'undefined' ? latestState?.activeFlowId : '') + || defaultFlowId + ).trim().toLowerCase() || defaultFlowId; + if (typeof currentStepDefinitionFlowId !== 'undefined') { + currentStepDefinitionFlowId = nextActiveFlowId; + } stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, { - activeFlowId: options?.activeFlowId, + activeFlowId: nextActiveFlowId, plusPaymentMethod: currentPlusPaymentMethod, signupMethod: currentSignupMethod, phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled, }); const nextWorkflowNodes = typeof getWorkflowNodesForMode === 'function' ? getWorkflowNodesForMode(currentPlusModeEnabled, { - activeFlowId: options?.activeFlowId, + activeFlowId: nextActiveFlowId, plusPaymentMethod: currentPlusPaymentMethod, signupMethod: currentSignupMethod, phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled, @@ -1991,7 +2014,9 @@ function shouldPromptNewUserGuide() { if (!btnContributionMode || btnContributionMode.disabled) { return false; } - if (latestState?.contributionMode) { + if (typeof isContributionModeActiveForFlow === 'function' + ? isContributionModeActiveForFlow(latestState) + : Boolean(latestState?.contributionMode)) { return false; } return true; @@ -2218,7 +2243,9 @@ async function openAutoRunFallbackRiskConfirmModal(totalRuns) { function updateConfigMenuControls() { const disabled = configActionInFlight || settingsSaveInFlight; - const contributionModeEnabled = Boolean(latestState?.contributionMode); + const contributionModeEnabled = typeof isContributionModeActiveForFlow === 'function' + ? isContributionModeActiveForFlow(latestState) + : Boolean(latestState?.contributionMode); if (contributionModeEnabled && configMenuOpen) { configMenuOpen = false; } @@ -2579,6 +2606,16 @@ function syncLatestState(nextState) { renderAccountRecords(latestState); } +function isContributionModeActiveForFlow(state = latestState, flowId = undefined) { + const rawFlowId = flowId !== undefined + ? flowId + : (state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID); + 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); +} + let accountRunHistoryRefreshTimer = null; function scheduleAccountRunHistoryRefresh(delayMs = 150) { @@ -3478,7 +3515,23 @@ function collectSettingsPayload() { const normalizeCloudMailDomainInput = typeof normalizeCloudMailDomainValue === 'function' ? normalizeCloudMailDomainValue : normalizeCloudflareTempEmailDomainValue; - const contributionModeEnabled = Boolean(latestState?.contributionMode); + const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' + ? DEFAULT_ACTIVE_FLOW_ID + : 'openai'; + const activeFlowId = typeof getSelectedFlowId === 'function' + ? getSelectedFlowId(latestState) + : (() => { + const normalized = String( + latestState?.activeFlowId || latestState?.flowId || defaultFlowId + ).trim().toLowerCase(); + if (normalized === 'codex') { + return defaultFlowId; + } + return normalized || defaultFlowId; + })(); + const contributionModeEnabled = typeof isContributionModeActiveForFlow === 'function' + ? isContributionModeActiveForFlow(latestState, activeFlowId) + : (activeFlowId === defaultFlowId && Boolean(latestState?.contributionMode)); const icloudFetchModeRawValue = typeof selectIcloudFetchMode !== 'undefined' ? String(selectIcloudFetchMode?.value || '') : ''; @@ -3985,17 +4038,27 @@ function collectSettingsPayload() { return normalized === 'sub2api' || normalized === 'codex2api' ? normalized : 'cpa'; }); const rawPanelMode = normalizePanelModeSafe(selectPanelMode?.value || latestState?.panelMode || 'cpa'); + const selectedSourceId = typeof getSelectedSourceId === 'function' + ? getSelectedSourceId(activeFlowId) + : (activeFlowId === defaultFlowId + ? rawPanelMode + : String(selectPanelMode?.value || latestState?.kiroSourceId || 'kiro-rs').trim().toLowerCase() || 'kiro-rs'); const rawPlusModeEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled ? Boolean(inputPlusModeEnabled.checked) : Boolean(latestState?.plusModeEnabled); const rawPhoneVerificationEnabled = Boolean(inputPhoneVerificationEnabled?.checked); const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function' ? resolveCurrentSidepanelCapabilities({ + activeFlowId, + sourceId: selectedSourceId, panelMode: rawPanelMode, signupMethod: selectedSignupMethod, state: { ...(latestState || {}), - panelMode: rawPanelMode, + activeFlowId, + ...(activeFlowId === defaultFlowId + ? { panelMode: rawPanelMode } + : { kiroSourceId: selectedSourceId }), plusModeEnabled: rawPlusModeEnabled, phoneVerificationEnabled: rawPhoneVerificationEnabled, signupMethod: selectedSignupMethod, @@ -4008,12 +4071,15 @@ function collectSettingsPayload() { }) || null; return registry?.resolveSidepanelCapabilities ? registry.resolveSidepanelCapabilities({ - activeFlowId: latestState?.activeFlowId, + activeFlowId, panelMode: rawPanelMode, signupMethod: selectedSignupMethod, state: { ...(latestState || {}), - panelMode: rawPanelMode, + activeFlowId, + ...(activeFlowId === defaultFlowId + ? { panelMode: rawPanelMode } + : { kiroSourceId: selectedSourceId }), plusModeEnabled: rawPlusModeEnabled, phoneVerificationEnabled: rawPhoneVerificationEnabled, signupMethod: selectedSignupMethod, @@ -4022,6 +4088,7 @@ function collectSettingsPayload() { : null; })(); const effectivePanelMode = capabilityState?.effectivePanelMode || capabilityState?.panelMode || rawPanelMode; + const effectiveSourceId = capabilityState?.effectiveSourceId || selectedSourceId; const effectivePlusModeEnabled = capabilityState ? Boolean(capabilityState.runtimeLocks?.plusModeEnabled) : rawPlusModeEnabled; @@ -4084,10 +4151,46 @@ function collectSettingsPayload() { const numeric = Number(String(value ?? '').trim()); return Number.isSafeInteger(numeric) && numeric >= 1 ? numeric : 1; }); + const flowRegistryApi = typeof getFlowRegistry === 'function' ? getFlowRegistry() : null; + const defaultKiroRsUrl = String( + flowRegistryApi?.DEFAULT_KIRO_RS_URL || 'https://kiro.leftcode.xyz/admin' + ).trim() || 'https://kiro.leftcode.xyz/admin'; + const defaultKiroRegion = String( + flowRegistryApi?.DEFAULT_KIRO_REGION || 'us-east-1' + ).trim() || 'us-east-1'; + const normalizeKiroSourceIdSafe = typeof normalizeSourceIdForFlow === 'function' + ? normalizeSourceIdForFlow + : ((_flowId, sourceId = '', fallback = 'kiro-rs') => { + const normalized = String(sourceId || '').trim().toLowerCase(); + return normalized || String(fallback || '').trim().toLowerCase() || 'kiro-rs'; + }); return { + activeFlowId, ...(contributionModeEnabled ? {} : { - panelMode: effectivePanelMode, + ...(activeFlowId === defaultFlowId ? { panelMode: effectivePanelMode } : {}), }), + kiroSourceId: normalizeKiroSourceIdSafe( + 'kiro', + activeFlowId === 'kiro' + ? effectiveSourceId + : (latestState?.kiroSourceId || 'kiro-rs'), + 'kiro-rs' + ), + kiroRsUrl: String( + (typeof inputKiroRsUrl !== 'undefined' && inputKiroRsUrl ? inputKiroRsUrl.value : '') + || latestState?.kiroRsUrl + || defaultKiroRsUrl + ).trim() || defaultKiroRsUrl, + kiroRsKey: String( + (typeof inputKiroRsKey !== 'undefined' && inputKiroRsKey ? inputKiroRsKey.value : '') + || latestState?.kiroRsKey + || '' + ), + kiroRegion: String( + (typeof inputKiroRegion !== 'undefined' && inputKiroRegion ? inputKiroRegion.value : '') + || latestState?.kiroRegion + || defaultKiroRegion + ).trim() || defaultKiroRegion, vpsUrl: inputVpsUrl.value.trim(), vpsPassword: inputVpsPassword.value, localCpaStep9Mode: getSelectedLocalCpaStep9Mode(), @@ -7999,8 +8102,227 @@ function normalizePanelMode(value = '') { return 'cpa'; } +let flowRegistry = null; +let settingsSchema = null; let flowCapabilityRegistry = null; +function getFlowRegistry() { + if (flowRegistry) { + return flowRegistry; + } + const rootScope = typeof window !== 'undefined' ? window : globalThis; + flowRegistry = rootScope.MultiPageFlowRegistry || null; + return flowRegistry; +} + +function getSettingsSchema() { + if (settingsSchema) { + return settingsSchema; + } + const rootScope = typeof window !== 'undefined' ? window : globalThis; + const registry = getFlowRegistry(); + settingsSchema = rootScope.MultiPageSettingsSchema?.createSettingsSchema?.({ + defaultFlowId: DEFAULT_ACTIVE_FLOW_ID, + flowRegistry: registry || undefined, + }) || null; + return settingsSchema; +} + +function normalizeFlowId(value = '', fallback = DEFAULT_ACTIVE_FLOW_ID) { + const registry = getFlowRegistry(); + if (registry?.normalizeFlowId) { + return registry.normalizeFlowId(value, fallback); + } + const normalized = String(value || '').trim().toLowerCase(); + if (normalized === 'codex') { + return DEFAULT_ACTIVE_FLOW_ID; + } + const fallbackValue = String(fallback || '').trim().toLowerCase(); + return normalized || fallbackValue || DEFAULT_ACTIVE_FLOW_ID; +} + +function getDefaultSourceIdForFlow(flowId = DEFAULT_ACTIVE_FLOW_ID) { + const registry = getFlowRegistry(); + if (registry?.getDefaultSourceId) { + return registry.getDefaultSourceId(normalizeFlowId(flowId)); + } + return normalizeFlowId(flowId) === 'kiro' ? 'kiro-rs' : 'cpa'; +} + +function normalizeSourceIdForFlow(flowId = DEFAULT_ACTIVE_FLOW_ID, sourceId = '', fallback = '') { + const normalizedFlowId = normalizeFlowId(flowId); + const registry = getFlowRegistry(); + const fallbackSourceId = fallback || getDefaultSourceIdForFlow(normalizedFlowId); + if (registry?.normalizeSourceId) { + return registry.normalizeSourceId(normalizedFlowId, sourceId, fallbackSourceId); + } + if (normalizedFlowId === DEFAULT_ACTIVE_FLOW_ID) { + return normalizePanelMode(sourceId || fallbackSourceId); + } + const normalized = String(sourceId || '').trim().toLowerCase(); + return normalized || String(fallbackSourceId || '').trim().toLowerCase() || 'kiro-rs'; +} + +function getSelectedFlowId(state = latestState) { + const selectedValue = typeof selectFlow !== 'undefined' && selectFlow + ? selectFlow.value + : ''; + return normalizeFlowId( + selectedValue || state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID, + state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID + ); +} + +function getSelectedSourceIdForState(state = latestState, flowId = getSelectedFlowId(state)) { + const normalizedFlowId = normalizeFlowId(flowId); + const schema = getSettingsSchema(); + if (schema?.getSelectedSourceId) { + return schema.getSelectedSourceId(state || {}, normalizedFlowId); + } + if (normalizedFlowId === DEFAULT_ACTIVE_FLOW_ID) { + return normalizePanelMode(state?.panelMode || getDefaultSourceIdForFlow(normalizedFlowId)); + } + return normalizeSourceIdForFlow( + normalizedFlowId, + state?.kiroSourceId || '', + getDefaultSourceIdForFlow(normalizedFlowId) + ); +} + +function getSelectedSourceId(flowId = getSelectedFlowId()) { + const normalizedFlowId = normalizeFlowId(flowId); + const selectedValue = typeof selectPanelMode !== 'undefined' && selectPanelMode + ? selectPanelMode.value + : ''; + if (normalizedFlowId === DEFAULT_ACTIVE_FLOW_ID) { + return normalizePanelMode( + selectedValue || latestState?.panelMode || getDefaultSourceIdForFlow(normalizedFlowId) + ); + } + return normalizeSourceIdForFlow( + normalizedFlowId, + selectedValue || latestState?.kiroSourceId || '', + getDefaultSourceIdForFlow(normalizedFlowId) + ); +} + +function renderFlowSelectorOptions(selectedFlowId = getSelectedFlowId()) { + if (!selectFlow) { + return []; + } + const registry = getFlowRegistry(); + const flowIds = Array.isArray(registry?.getRegisteredFlowIds?.()) + ? registry.getRegisteredFlowIds() + : [DEFAULT_ACTIVE_FLOW_ID]; + const normalizedSelectedFlowId = normalizeFlowId(selectedFlowId); + selectFlow.innerHTML = ''; + flowIds.forEach((flowId) => { + const option = document.createElement('option'); + option.value = flowId; + option.textContent = registry?.getFlowLabel?.(flowId) || flowId; + selectFlow.appendChild(option); + }); + selectFlow.value = normalizedSelectedFlowId; + return flowIds; +} + +function renderSourceSelectorOptions(flowId = getSelectedFlowId(), selectedSourceId = '') { + if (!selectPanelMode) { + return []; + } + const registry = getFlowRegistry(); + const normalizedFlowId = normalizeFlowId(flowId); + const sourceOptions = Array.isArray(registry?.getSourceOptions?.(normalizedFlowId)) + ? registry.getSourceOptions(normalizedFlowId) + : []; + const normalizedSourceId = normalizeSourceIdForFlow( + normalizedFlowId, + selectedSourceId, + getDefaultSourceIdForFlow(normalizedFlowId) + ); + selectPanelMode.innerHTML = ''; + sourceOptions.forEach((sourceOption) => { + const option = document.createElement('option'); + option.value = sourceOption.id; + option.textContent = sourceOption.label || sourceOption.id; + selectPanelMode.appendChild(option); + }); + if (labelSourceSelector) { + labelSourceSelector.textContent = '来源'; + } + selectPanelMode.disabled = sourceOptions.length <= 1; + if (sourceOptions.length > 0) { + selectPanelMode.value = normalizedSourceId; + } + return sourceOptions; +} + +function collectVisibleSettingsTargets(visibleGroupIds = []) { + const registry = getFlowRegistry(); + const visibleGroupIdSet = new Set( + Array.isArray(visibleGroupIds) + ? visibleGroupIds.map((groupId) => String(groupId || '').trim()).filter(Boolean) + : [] + ); + const groupDefinitions = registry?.getSettingsGroupDefinitions?.() || {}; + const rowIds = new Set(); + const sectionIds = new Set(); + Object.entries(groupDefinitions).forEach(([groupId, definition]) => { + if (!visibleGroupIdSet.has(groupId)) { + return; + } + (definition?.rowIds || []).forEach((rowId) => rowIds.add(rowId)); + (definition?.sectionIds || []).forEach((sectionId) => sectionIds.add(sectionId)); + }); + return { + rowIds: Array.from(rowIds), + sectionIds: Array.from(sectionIds), + }; +} + +function applyFlowSettingsGroupVisibility(visibleGroupIds = []) { + const registry = getFlowRegistry(); + const groupDefinitions = registry?.getSettingsGroupDefinitions?.() || {}; + const { rowIds: visibleRowIds, sectionIds: visibleSectionIds } = collectVisibleSettingsTargets(visibleGroupIds); + const visibleRowIdSet = new Set(visibleRowIds); + const visibleSectionIdSet = new Set(visibleSectionIds); + const allRowIds = new Set(); + const allSectionIds = new Set(); + Object.values(groupDefinitions).forEach((definition) => { + (definition?.rowIds || []).forEach((rowId) => allRowIds.add(rowId)); + (definition?.sectionIds || []).forEach((sectionId) => allSectionIds.add(sectionId)); + }); + allRowIds.forEach((rowId) => { + const element = document.getElementById(rowId); + if (!element) { + return; + } + element.style.display = visibleRowIdSet.has(rowId) ? '' : 'none'; + }); + allSectionIds.forEach((sectionId) => { + const element = document.getElementById(sectionId); + if (!element) { + return; + } + element.style.display = visibleSectionIdSet.has(sectionId) ? '' : 'none'; + }); + return { + rowIds: visibleRowIds, + sectionIds: visibleSectionIds, + }; +} + +function syncFlowSelectorsFromState(state = latestState) { + const activeFlowId = normalizeFlowId(state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID); + renderFlowSelectorOptions(activeFlowId); + const sourceId = getSelectedSourceIdForState(state, activeFlowId); + renderSourceSelectorOptions(activeFlowId, sourceId); + return { + activeFlowId, + sourceId, + }; +} + function getFlowCapabilityRegistry() { if (flowCapabilityRegistry) { return flowCapabilityRegistry; @@ -8017,13 +8339,37 @@ function resolveCurrentSidepanelCapabilities(options = {}) { if (!registry?.resolveSidepanelCapabilities) { return null; } + const activeFlowId = normalizeFlowId( + options?.activeFlowId + ?? options?.state?.activeFlowId + ?? latestState?.activeFlowId + ?? latestState?.flowId + ?? DEFAULT_ACTIVE_FLOW_ID + ); const state = { ...(latestState || {}), ...(options?.state || {}), + activeFlowId, }; + const sourceId = options?.sourceId !== undefined + ? options.sourceId + : (activeFlowId === DEFAULT_ACTIVE_FLOW_ID + ? (options?.panelMode ?? state?.panelMode) + : (options?.kiroSourceId ?? state?.kiroSourceId)); + if (activeFlowId === DEFAULT_ACTIVE_FLOW_ID) { + state.panelMode = normalizePanelMode( + sourceId || state?.panelMode || getDefaultSourceIdForFlow(activeFlowId) + ); + } else { + state.kiroSourceId = normalizeSourceIdForFlow( + activeFlowId, + sourceId || state?.kiroSourceId || '', + getDefaultSourceIdForFlow(activeFlowId) + ); + } return registry.resolveSidepanelCapabilities({ - activeFlowId: options?.activeFlowId ?? state?.activeFlowId, - panelMode: options?.panelMode ?? state?.panelMode, + activeFlowId, + panelMode: state?.panelMode, signupMethod: options?.signupMethod ?? state?.signupMethod, state, }); @@ -8051,10 +8397,11 @@ function resolveStepDefinitionCapabilityState(state = latestState, options = {}) } function getSelectedPanelMode() { - const selectedValue = typeof selectPanelMode !== 'undefined' && selectPanelMode - ? selectPanelMode.value - : (typeof latestState !== 'undefined' ? latestState?.panelMode : ''); - const resolvedPanelMode = normalizePanelMode(selectedValue || 'cpa'); + const resolvedPanelMode = normalizePanelMode( + typeof selectPanelMode !== 'undefined' && selectPanelMode + ? selectPanelMode.value + : (typeof latestState !== 'undefined' ? latestState?.panelMode : '') + ); const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function' ? resolveCurrentSidepanelCapabilities({ panelMode: resolvedPanelMode }) : null; @@ -8082,7 +8429,9 @@ function canSelectPhoneSignupMethod() { const plusModeEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled ? Boolean(inputPlusModeEnabled.checked) : Boolean(latestState?.plusModeEnabled); - const contributionModeEnabled = Boolean(latestState?.contributionMode); + const contributionModeEnabled = typeof isContributionModeActiveForFlow === 'function' + ? isContributionModeActiveForFlow(latestState) + : Boolean(latestState?.contributionMode); const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function' ? resolveCurrentSidepanelCapabilities({ panelMode: typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : latestState?.panelMode, @@ -8136,6 +8485,9 @@ function updateSignupMethodUI(options = {}) { let selectedMethod = normalizeSignupMethod(getSelectedSignupMethod()); const phoneSelectable = canSelectPhoneSignupMethod(); + const contributionModeEnabled = typeof isContributionModeActiveForFlow === 'function' + ? isContributionModeActiveForFlow(latestState) + : Boolean(latestState?.contributionMode); if (!phoneSelectable && selectedMethod === SIGNUP_METHOD_PHONE) { selectedMethod = setSignupMethod(SIGNUP_METHOD_EMAIL); if (options.notify && typeof showToast === 'function') { @@ -8156,7 +8508,7 @@ function updateSignupMethodUI(options = {}) { button.title = '开启接码后可选择手机号注册'; } else if (typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled?.checked) { button.title = 'Plus 模式第一版暂不支持手机号注册'; - } else if (latestState?.contributionMode) { + } else if (contributionModeEnabled) { button.title = '贡献模式第一版暂不支持手机号注册'; } else if (locked) { button.title = '自动流程运行中不能切换注册方式'; @@ -9330,6 +9682,9 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr || (typeof latestState !== 'undefined' ? latestState?.activeFlowId : '') || defaultFlowId ).trim().toLowerCase() || defaultFlowId; + const currentFlowId = typeof currentStepDefinitionFlowId !== 'undefined' + ? currentStepDefinitionFlowId + : defaultFlowId; const rootScope = typeof window !== 'undefined' ? window : globalThis; const currentPaymentStep = stepDefinitions.find((step) => step.key === 'paypal-approve'); const nextPaymentTitle = rootScope.MultiPageStepDefinitions?.getPlusPaymentStepTitle?.({ @@ -9345,6 +9700,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr || nextPaymentMethod !== currentPlusPaymentMethod || nextSignupMethod !== currentSignupMethod || nextPhoneSignupReloginAfterBindEmailEnabled !== currentPhoneSignupReloginAfterBindEmailEnabled + || nextActiveFlowId !== currentFlowId || paymentTitleChanged; if (!shouldRender) { return; @@ -9359,6 +9715,31 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr renderStepsList(); } +function syncStepDefinitionsFromUiState(stateOverrides = {}) { + const nextState = { + ...(latestState || {}), + ...(stateOverrides || {}), + }; + const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function' + ? resolveStepDefinitionCapabilityState(nextState, { + activeFlowId: nextState?.activeFlowId, + panelMode: nextState?.panelMode, + signupMethod: nextState?.signupMethod, + state: nextState, + }) + : { + plusModeEnabled: Boolean(nextState?.plusModeEnabled), + signupMethod: normalizeSignupMethod(nextState?.signupMethod || DEFAULT_SIGNUP_METHOD), + }; + syncStepDefinitionsForMode(stepDefinitionState.plusModeEnabled, { + activeFlowId: nextState?.activeFlowId || nextState?.flowId || DEFAULT_ACTIVE_FLOW_ID, + plusPaymentMethod: getSelectedPlusPaymentMethod(nextState), + signupMethod: stepDefinitionState.signupMethod, + phoneSignupReloginAfterBindEmailEnabled: Boolean(nextState?.phoneSignupReloginAfterBindEmailEnabled), + }); + return stepDefinitionState; +} + // ============================================================ // State Restore on load // ============================================================ @@ -9427,6 +9808,12 @@ function applySettingsState(state) { return Math.max(1, Math.min(1440, numeric)); }; syncLatestState(state); + const appliedFlowSelection = typeof syncFlowSelectorsFromState === 'function' + ? syncFlowSelectorsFromState(state) + : { + activeFlowId: String(state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID, + sourceId: String(state?.panelMode || 'cpa').trim().toLowerCase() || 'cpa', + }; if (typeof applyOperationDelayState === 'function') { applyOperationDelayState(state); } @@ -9522,7 +9909,6 @@ function applySettingsState(state) { inputVpsUrl.value = state?.vpsUrl || ''; inputVpsPassword.value = state?.vpsPassword || ''; setLocalCpaStep9Mode(state?.localCpaStep9Mode); - selectPanelMode.value = normalizePanelMode(state?.panelMode || 'cpa'); inputSub2ApiUrl.value = state?.sub2apiUrl || ''; inputSub2ApiEmail.value = state?.sub2apiEmail || ''; inputSub2ApiPassword.value = state?.sub2apiPassword || ''; @@ -9531,6 +9917,47 @@ function applySettingsState(state) { inputSub2ApiAccountPriority.value = String(normalizeSub2ApiAccountPriorityValue(state?.sub2apiAccountPriority)); } inputSub2ApiDefaultProxy.value = state?.sub2apiDefaultProxyName || ''; + if (typeof inputKiroRsUrl !== 'undefined' && inputKiroRsUrl) { + inputKiroRsUrl.value = String( + state?.kiroRsUrl + || getFlowRegistry()?.DEFAULT_KIRO_RS_URL + || 'https://kiro.leftcode.xyz/admin' + ).trim(); + } + if (typeof inputKiroRsKey !== 'undefined' && inputKiroRsKey) { + inputKiroRsKey.value = String(state?.kiroRsKey || ''); + } + if (typeof inputKiroRegion !== 'undefined' && inputKiroRegion) { + inputKiroRegion.value = String( + state?.kiroRegion + || getFlowRegistry()?.DEFAULT_KIRO_REGION + || 'us-east-1' + ).trim(); + } + if (typeof displayKiroDeviceCode !== 'undefined' && displayKiroDeviceCode) { + const kiroDeviceCode = String( + state?.flows?.kiro?.auth?.deviceCode + || state?.kiroDeviceCode + || '' + ).trim(); + displayKiroDeviceCode.textContent = kiroDeviceCode || '未生成'; + } + if (typeof displayKiroLoginUrl !== 'undefined' && displayKiroLoginUrl) { + const kiroLoginUrl = String( + state?.flows?.kiro?.auth?.loginUrl + || state?.kiroLoginUrl + || '' + ).trim(); + displayKiroLoginUrl.textContent = kiroLoginUrl || '未生成'; + } + if (typeof displayKiroUploadStatus !== 'undefined' && displayKiroUploadStatus) { + const kiroUploadStatus = String( + state?.flows?.kiro?.upload?.status + || state?.kiroUploadStatus + || '' + ).trim(); + displayKiroUploadStatus.textContent = kiroUploadStatus || '未开始'; + } const normalizedIpProxyService = resolveIpProxyService(state?.ipProxyService); const normalizedIpProxyServiceProfiles = typeof normalizeIpProxyServiceProfiles === 'function' ? normalizeIpProxyServiceProfiles(state?.ipProxyServiceProfiles || {}, state || {}) @@ -9616,6 +10043,12 @@ function applySettingsState(state) { if (typeof updateIpProxyUI === 'function') { updateIpProxyUI(latestState); } + if (selectFlow) { + selectFlow.value = appliedFlowSelection.activeFlowId; + } + if (selectPanelMode && appliedFlowSelection.sourceId) { + selectPanelMode.value = appliedFlowSelection.sourceId; + } inputCodex2ApiUrl.value = state?.codex2apiUrl || ''; inputCodex2ApiAdminKey.value = state?.codex2apiAdminKey || ''; const yydsMailProvider = typeof YYDS_MAIL_PROVIDER === 'string' @@ -10348,7 +10781,9 @@ function shouldShowContributionUpdateHint(snapshot = currentContributionContentS if (promptVersion === getDismissedContributionContentPromptVersion()) { return false; } - if (latestState?.contributionMode) { + if (typeof isContributionModeActiveForFlow === 'function' + ? isContributionModeActiveForFlow(latestState) + : Boolean(latestState?.contributionMode)) { return false; } return !btnContributionMode.disabled; @@ -10411,7 +10846,10 @@ async function refreshContributionContentHint() { } function syncPasswordField(state) { - inputPassword.value = state?.contributionMode ? '' : (state.customPassword || state.password || ''); + const contributionModeEnabled = typeof isContributionModeActiveForFlow === 'function' + ? isContributionModeActiveForFlow(state) + : Boolean(state?.contributionMode); + inputPassword.value = contributionModeEnabled ? '' : (state.customPassword || state.password || ''); } function isCustomMailProvider(provider = selectMailProvider.value) { @@ -11559,60 +11997,52 @@ async function handleDeleteSub2ApiGroup(groupName) { } function updatePanelModeUI() { - const rawPanelMode = normalizePanelMode(selectPanelMode?.value || latestState?.panelMode || 'cpa'); + const activeFlowId = typeof getSelectedFlowId === 'function' + ? getSelectedFlowId(latestState) + : normalizeFlowId(latestState?.activeFlowId || latestState?.flowId || DEFAULT_ACTIVE_FLOW_ID); + const sourceId = typeof getSelectedSourceId === 'function' + ? getSelectedSourceId(activeFlowId) + : (activeFlowId === DEFAULT_ACTIVE_FLOW_ID + ? normalizePanelMode(selectPanelMode?.value || latestState?.panelMode || 'cpa') + : String(selectPanelMode?.value || latestState?.kiroSourceId || 'kiro-rs').trim().toLowerCase() || 'kiro-rs'); + const rawPanelMode = activeFlowId === DEFAULT_ACTIVE_FLOW_ID + ? normalizePanelMode(sourceId || latestState?.panelMode || 'cpa') + : normalizePanelMode(latestState?.panelMode || 'cpa'); const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function' ? resolveCurrentSidepanelCapabilities({ + activeFlowId, + sourceId, panelMode: rawPanelMode, state: { ...(latestState || {}), - panelMode: rawPanelMode, + activeFlowId, + ...(activeFlowId === DEFAULT_ACTIVE_FLOW_ID + ? { panelMode: rawPanelMode } + : { kiroSourceId: sourceId }), }, }) : null; - const supportedPanelModes = Array.isArray(capabilityState?.supportedPanelModes) - ? capabilityState.supportedPanelModes + const effectiveSourceId = capabilityState?.effectiveSourceId || sourceId; + renderFlowSelectorOptions(activeFlowId); + renderSourceSelectorOptions(activeFlowId, effectiveSourceId); + const visibleGroupIds = Array.isArray(capabilityState?.visibleGroupIds) + ? capabilityState.visibleGroupIds : []; - if (selectPanelMode?.options && supportedPanelModes.length) { - Array.from(selectPanelMode.options).forEach((option) => { - if (!option) { - return; - } - const optionMode = normalizePanelMode(option.value || ''); - const enabled = supportedPanelModes.includes(optionMode); - option.disabled = !enabled; - option.hidden = !enabled; - }); - } else if (selectPanelMode?.options) { - Array.from(selectPanelMode.options).forEach((option) => { - if (!option) { - return; - } - option.disabled = false; - option.hidden = false; - }); + if (typeof applyFlowSettingsGroupVisibility === 'function') { + applyFlowSettingsGroupVisibility(visibleGroupIds); + } + const panelMode = capabilityState?.effectivePanelMode || capabilityState?.panelMode || rawPanelMode; + if (selectFlow) { + selectFlow.value = activeFlowId; } - const panelMode = capabilityState?.effectivePanelMode || capabilityState?.panelMode || getSelectedPanelMode(); if (selectPanelMode) { - selectPanelMode.value = panelMode; + selectPanelMode.value = effectiveSourceId; } - const useSub2Api = panelMode === 'sub2api'; - const useCodex2Api = panelMode === 'codex2api'; - const useCpa = !useSub2Api && !useCodex2Api; - rowVpsUrl.style.display = useCpa ? '' : 'none'; - rowVpsPassword.style.display = useCpa ? '' : 'none'; - rowLocalCpaStep9Mode.style.display = useCpa ? '' : 'none'; - rowSub2ApiUrl.style.display = useSub2Api ? '' : 'none'; - rowSub2ApiEmail.style.display = useSub2Api ? '' : 'none'; - rowSub2ApiPassword.style.display = useSub2Api ? '' : 'none'; - rowSub2ApiGroup.style.display = useSub2Api ? '' : 'none'; - rowSub2ApiAccountPriority.style.display = useSub2Api ? '' : 'none'; - rowSub2ApiDefaultProxy.style.display = useSub2Api ? '' : 'none'; - rowCodex2ApiUrl.style.display = useCodex2Api ? '' : 'none'; - rowCodex2ApiAdminKey.style.display = useCodex2Api ? '' : 'none'; + const useCodex2Api = panelMode === 'codex2api'; const step9Btn = document.querySelector('.step-btn[data-step-key="platform-verify"]'); - if (step9Btn) { - step9Btn.textContent = useSub2Api + if (step9Btn && activeFlowId === DEFAULT_ACTIVE_FLOW_ID) { + step9Btn.textContent = panelMode === 'sub2api' ? 'SUB2API 回调验证' : (useCodex2Api ? 'Codex2API 回调验证' : 'CPA 回调验证'); } @@ -13563,26 +13993,76 @@ checkboxAutoDeleteIcloud?.addEventListener('change', () => { }); selectPanelMode.addEventListener('change', async () => { - const previousPanelMode = normalizePanelMode(latestState?.panelMode || 'cpa'); - const rawNextPanelMode = normalizePanelMode(selectPanelMode.value); - selectPanelMode.value = rawNextPanelMode; - const nextPanelMode = getSelectedPanelMode(); - selectPanelMode.value = nextPanelMode; - const confirmed = await confirmCpaPhoneSignupIfNeeded({ - signupMethod: getSelectedSignupMethod(), - panelMode: nextPanelMode, - }); - if (!confirmed) { - selectPanelMode.value = previousPanelMode; - updatePanelModeUI(); - return; + const activeFlowId = typeof getSelectedFlowId === 'function' + ? getSelectedFlowId(latestState) + : normalizeFlowId(latestState?.activeFlowId || latestState?.flowId || DEFAULT_ACTIVE_FLOW_ID); + const defaultSourceId = typeof getDefaultSourceIdForFlow === 'function' + ? getDefaultSourceIdForFlow(activeFlowId) + : (activeFlowId === DEFAULT_ACTIVE_FLOW_ID ? 'cpa' : 'kiro-rs'); + const previousSourceId = typeof getSelectedSourceIdForState === 'function' + ? getSelectedSourceIdForState(latestState, activeFlowId) + : (activeFlowId === DEFAULT_ACTIVE_FLOW_ID + ? normalizePanelMode(latestState?.panelMode || defaultSourceId) + : String(latestState?.kiroSourceId || defaultSourceId).trim().toLowerCase() || defaultSourceId); + let nextSourceId = typeof normalizeSourceIdForFlow === 'function' + ? normalizeSourceIdForFlow(activeFlowId, selectPanelMode.value, defaultSourceId) + : (activeFlowId === DEFAULT_ACTIVE_FLOW_ID + ? normalizePanelMode(selectPanelMode.value) + : String(selectPanelMode.value || defaultSourceId).trim().toLowerCase() || defaultSourceId); + if (activeFlowId === DEFAULT_ACTIVE_FLOW_ID) { + const nextPanelMode = normalizePanelMode(nextSourceId); + selectPanelMode.value = nextPanelMode; + const confirmed = await confirmCpaPhoneSignupIfNeeded({ + signupMethod: getSelectedSignupMethod(), + panelMode: nextPanelMode, + }); + if (!confirmed) { + selectPanelMode.value = previousSourceId; + updatePanelModeUI(); + return; + } + nextSourceId = nextPanelMode; + syncLatestState({ + activeFlowId, + flowId: activeFlowId, + panelMode: nextPanelMode, + }); + } else { + syncLatestState({ + activeFlowId, + flowId: activeFlowId, + kiroSourceId: nextSourceId, + }); } - syncLatestState({ panelMode: nextPanelMode }); updatePanelModeUI(); + if (typeof syncStepDefinitionsFromUiState === 'function') { + syncStepDefinitionsFromUiState({ + plusModeEnabled: typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled + ? Boolean(inputPlusModeEnabled.checked) + : Boolean(latestState?.plusModeEnabled), + signupMethod: getSelectedSignupMethod(), + phoneSignupReloginAfterBindEmailEnabled: typeof inputPhoneSignupReloginAfterBindEmail !== 'undefined' && inputPhoneSignupReloginAfterBindEmail + ? Boolean(inputPhoneSignupReloginAfterBindEmail.checked) + : Boolean(latestState?.phoneSignupReloginAfterBindEmailEnabled), + }); + } + applyStepExecutionRangeState(latestState); + renderStepStatuses(latestState); + updateButtonStates(); markSettingsDirty(true); saveSettings({ silent: true }).catch(() => { }); }); +[inputKiroRsUrl, inputKiroRsKey, inputKiroRegion].forEach((input) => { + input?.addEventListener('input', () => { + markSettingsDirty(true); + scheduleSettingsAutoSave(); + }); + input?.addEventListener('blur', () => { + saveSettings({ silent: true }).catch(() => { }); + }); +}); + function syncCurrentIpProxyServiceProfileToLatestState() { const selectedService = normalizeIpProxyService( selectIpProxyService?.value || latestState?.ipProxyService || DEFAULT_IP_PROXY_SERVICE @@ -14347,9 +14827,50 @@ inputStepExecutionRangeEnabled?.addEventListener('change', () => { }); selectFlow?.addEventListener('change', () => { + const nextActiveFlowId = typeof normalizeFlowId === 'function' + ? normalizeFlowId(selectFlow.value, latestState?.activeFlowId || latestState?.flowId || DEFAULT_ACTIVE_FLOW_ID) + : (String(selectFlow.value || latestState?.activeFlowId || latestState?.flowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID); + const nextStateBase = { + ...(latestState || {}), + activeFlowId: nextActiveFlowId, + flowId: nextActiveFlowId, + }; + const defaultSourceId = typeof getDefaultSourceIdForFlow === 'function' + ? getDefaultSourceIdForFlow(nextActiveFlowId) + : (nextActiveFlowId === DEFAULT_ACTIVE_FLOW_ID ? 'cpa' : 'kiro-rs'); + const nextSourceId = typeof getSelectedSourceIdForState === 'function' + ? getSelectedSourceIdForState(nextStateBase, nextActiveFlowId) + : (nextActiveFlowId === DEFAULT_ACTIVE_FLOW_ID + ? normalizePanelMode(nextStateBase?.panelMode || defaultSourceId) + : String(nextStateBase?.kiroSourceId || defaultSourceId).trim().toLowerCase() || defaultSourceId); + syncLatestState({ + activeFlowId: nextActiveFlowId, + flowId: nextActiveFlowId, + ...(nextActiveFlowId === DEFAULT_ACTIVE_FLOW_ID + ? { panelMode: normalizePanelMode(nextSourceId || defaultSourceId) } + : { + kiroSourceId: typeof normalizeSourceIdForFlow === 'function' + ? normalizeSourceIdForFlow(nextActiveFlowId, nextSourceId, defaultSourceId) + : (String(nextSourceId || defaultSourceId).trim().toLowerCase() || defaultSourceId), + }), + }); + updatePanelModeUI(); + if (typeof syncStepDefinitionsFromUiState === 'function') { + syncStepDefinitionsFromUiState({ + plusModeEnabled: typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled + ? Boolean(inputPlusModeEnabled.checked) + : Boolean(latestState?.plusModeEnabled), + signupMethod: getSelectedSignupMethod(), + phoneSignupReloginAfterBindEmailEnabled: typeof inputPhoneSignupReloginAfterBindEmail !== 'undefined' && inputPhoneSignupReloginAfterBindEmail + ? Boolean(inputPhoneSignupReloginAfterBindEmail.checked) + : Boolean(latestState?.phoneSignupReloginAfterBindEmailEnabled), + }); + } applyStepExecutionRangeState(latestState); renderStepStatuses(latestState); updateButtonStates(); + markSettingsDirty(true); + saveSettings({ silent: true }).catch(() => { }); }); [inputStepExecutionRangeFrom, inputStepExecutionRangeTo].forEach((input) => { @@ -15249,8 +15770,17 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message.payload.localCpaStep9Mode !== undefined) { setLocalCpaStep9Mode(message.payload.localCpaStep9Mode); } - if (message.payload.panelMode !== undefined) { - selectPanelMode.value = normalizePanelMode(message.payload.panelMode || 'cpa'); + if ( + message.payload.panelMode !== undefined + || message.payload.activeFlowId !== undefined + || message.payload.flowId !== undefined + || message.payload.kiroSourceId !== undefined + ) { + if (typeof syncFlowSelectorsFromState === 'function') { + syncFlowSelectorsFromState(latestState); + } else if (message.payload.panelMode !== undefined) { + selectPanelMode.value = normalizePanelMode(message.payload.panelMode || 'cpa'); + } updatePanelModeUI(); } if ( diff --git a/tests/background-account-history-settings.test.js b/tests/background-account-history-settings.test.js index c08de09..ffdd0e4 100644 --- a/tests/background-account-history-settings.test.js +++ b/tests/background-account-history-settings.test.js @@ -121,6 +121,7 @@ const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS; const SIGNUP_METHOD_EMAIL = 'email'; const SIGNUP_METHOD_PHONE = 'phone'; const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL; +const DEFAULT_ACTIVE_FLOW_ID = 'openai'; const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; @@ -132,6 +133,28 @@ const FIVE_SIM_OPERATOR = 'any'; const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']); const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']); const self = { + MultiPageFlowRegistry: { + DEFAULT_KIRO_REGION: 'us-east-1', + DEFAULT_KIRO_RS_URL: 'https://kiro.leftcode.xyz/admin', + normalizeFlowId(value, fallback = 'openai') { + const normalized = String(value || '').trim().toLowerCase(); + if (normalized === 'kiro') { + return 'kiro'; + } + if (normalized === 'codex' || normalized === 'openai') { + return 'openai'; + } + return String(fallback || 'openai').trim().toLowerCase() === 'kiro' ? 'kiro' : 'openai'; + }, + normalizeSourceId(flowId, sourceId, fallback = 'kiro-rs') { + const normalizedFlowId = this.normalizeFlowId(flowId); + if (normalizedFlowId !== 'kiro') { + return 'cpa'; + } + const normalizedSourceId = String(sourceId || '').trim().toLowerCase(); + return normalizedSourceId === 'kiro-rs' ? normalizedSourceId : fallback; + }, + }, GoPayUtils: { normalizeGoPayCountryCode(value) { const digits = String(value || '').replace(/\\D/g, ''); @@ -252,6 +275,12 @@ return { assert.equal(api.normalizePersistentSettingValue('heroSmsPreferredPrice', '0.051234'), '0.0512'); assert.equal(api.normalizePersistentSettingValue('signupMethod', 'phone'), 'phone'); assert.equal(api.normalizePersistentSettingValue('signupMethod', 'unknown'), 'email'); + assert.equal(api.normalizePersistentSettingValue('activeFlowId', 'codex'), 'openai'); + assert.equal(api.normalizePersistentSettingValue('activeFlowId', 'kiro'), 'kiro'); + assert.equal(api.normalizePersistentSettingValue('kiroSourceId', 'unknown'), 'kiro-rs'); + assert.equal(api.normalizePersistentSettingValue('kiroRsUrl', ''), 'https://kiro.leftcode.xyz/admin'); + assert.equal(api.normalizePersistentSettingValue('kiroRegion', ''), 'us-east-1'); + assert.equal(api.normalizePersistentSettingValue('kiroRsKey', ' key-1 '), ' key-1 '); assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim'); assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms'); assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms'); diff --git a/tests/background-kiro-device-auth-module.test.js b/tests/background-kiro-device-auth-module.test.js new file mode 100644 index 0000000..b722a8a --- /dev/null +++ b/tests/background-kiro-device-auth-module.test.js @@ -0,0 +1,328 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +function loadKiroDeviceAuthApi() { + const source = fs.readFileSync('background/steps/kiro-device-auth.js', 'utf8'); + return new Function('self', `${source}; return self.MultiPageBackgroundKiroDeviceAuth;`)({}); +} + +function createResponse({ ok = true, status = 200, json = null, text = '' } = {}) { + const bodyText = text || (json ? JSON.stringify(json) : ''); + return { + ok, + status, + statusText: bodyText || `HTTP ${status}`, + async text() { + return bodyText; + }, + }; +} + +function mergeUpdates(updatesList = []) { + return updatesList.reduce((acc, item) => Object.assign(acc, item), {}); +} + +test('kiro device auth module exposes a factory', () => { + const api = loadKiroDeviceAuthApi(); + assert.equal(typeof api?.createKiroDeviceAuthExecutor, 'function'); + assert.equal(typeof api?.startBuilderIdDeviceLogin, 'function'); + assert.equal(typeof api?.uploadBuilderIdCredential, 'function'); +}); + +test('kiro start device login registers client, opens auth tab, and completes with runtime payload', async () => { + const api = loadKiroDeviceAuthApi(); + const fetchCalls = []; + const stateUpdates = []; + const registerCalls = []; + const reuseCalls = []; + const completeCalls = []; + + const executor = api.createKiroDeviceAuthExecutor({ + addLog: async () => {}, + completeNodeFromBackground: async (nodeId, payload) => { + completeCalls.push({ nodeId, payload }); + }, + fetchImpl: async (url, options = {}) => { + fetchCalls.push({ + url, + method: options.method || 'GET', + body: options.body ? JSON.parse(options.body) : null, + }); + if (url.endsWith('/client/register')) { + return createResponse({ + ok: true, + status: 200, + json: { + clientId: 'client-001', + clientSecret: 'secret-001', + }, + }); + } + if (url.endsWith('/device_authorization')) { + return createResponse({ + ok: true, + status: 200, + json: { + deviceCode: 'device-code-001', + userCode: 'ABCD-1234', + verificationUri: 'https://device.example.com/start', + verificationUriComplete: 'https://device.example.com/complete', + interval: 7, + expiresIn: 900, + }, + }); + } + throw new Error(`Unexpected fetch URL: ${url}`); + }, + getState: async () => ({ + kiroRegion: 'eu-west-1', + }), + registerTab: async (source, tabId) => { + registerCalls.push({ source, tabId }); + }, + reuseOrCreateTab: async (source, url) => { + reuseCalls.push({ source, url }); + return 88; + }, + setState: async (updates) => { + stateUpdates.push(updates); + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + await executor.executeKiroStartDeviceLogin({ + nodeId: 'kiro-start-device-login', + kiroRegion: 'eu-west-1', + }); + + assert.equal(fetchCalls.length, 2); + assert.equal(fetchCalls[0].url, 'https://oidc.eu-west-1.amazonaws.com/client/register'); + assert.equal(fetchCalls[1].url, 'https://oidc.eu-west-1.amazonaws.com/device_authorization'); + assert.deepEqual(fetchCalls[1].body, { + clientId: 'client-001', + clientSecret: 'secret-001', + startUrl: 'https://view.awsapps.com/start', + }); + assert.deepEqual(reuseCalls, [{ + source: 'kiro-device-auth', + url: 'https://device.example.com/complete', + }]); + assert.deepEqual(registerCalls, [{ + source: 'kiro-device-auth', + tabId: 88, + }]); + + const finalState = mergeUpdates(stateUpdates); + assert.equal(finalState.kiroClientId, 'client-001'); + assert.equal(finalState.kiroClientSecret, 'secret-001'); + assert.equal(finalState.kiroDeviceAuthorizationCode, 'device-code-001'); + assert.equal(finalState.kiroDeviceCode, 'ABCD-1234'); + assert.equal(finalState.kiroLoginUrl, 'https://device.example.com/complete'); + assert.equal(finalState.kiroAuthRegion, 'eu-west-1'); + assert.equal(finalState.kiroAuthIntervalSeconds, 7); + assert.equal(finalState.kiroAuthStatus, 'waiting_user'); + assert.equal(finalState.kiroUploadStatus, 'waiting_login'); + + assert.equal(completeCalls.length, 1); + assert.equal(completeCalls[0].nodeId, 'kiro-start-device-login'); + assert.equal(completeCalls[0].payload.kiroDeviceCode, 'ABCD-1234'); + assert.equal(completeCalls[0].payload.kiroLoginUrl, 'https://device.example.com/complete'); +}); + +test('kiro await device login polls until refresh token is captured', async () => { + const api = loadKiroDeviceAuthApi(); + const fetchCalls = []; + const stateUpdates = []; + const sleepCalls = []; + const completeCalls = []; + + let pollCount = 0; + const executor = api.createKiroDeviceAuthExecutor({ + addLog: async () => {}, + completeNodeFromBackground: async (nodeId, payload) => { + completeCalls.push({ nodeId, payload }); + }, + fetchImpl: async (url, options = {}) => { + fetchCalls.push({ + url, + method: options.method || 'GET', + body: options.body ? JSON.parse(options.body) : null, + }); + pollCount += 1; + if (pollCount === 1) { + return createResponse({ + ok: false, + status: 400, + json: { error: 'authorization_pending' }, + }); + } + return createResponse({ + ok: true, + status: 200, + json: { + accessToken: 'access-001', + refreshToken: 'refresh-001', + expiresIn: 3600, + }, + }); + }, + getState: async () => ({ + kiroClientId: 'client-001', + kiroClientSecret: 'secret-001', + kiroDeviceAuthorizationCode: 'device-code-001', + kiroAuthRegion: 'us-east-1', + kiroAuthExpiresAt: Date.now() + 60000, + kiroAuthIntervalSeconds: 5, + }), + setState: async (updates) => { + stateUpdates.push(updates); + }, + sleepWithStop: async (ms) => { + sleepCalls.push(ms); + }, + throwIfStopped: () => {}, + }); + + await executor.executeKiroAwaitDeviceLogin({ + nodeId: 'kiro-await-device-login', + kiroClientId: 'client-001', + kiroClientSecret: 'secret-001', + kiroDeviceAuthorizationCode: 'device-code-001', + kiroAuthRegion: 'us-east-1', + kiroAuthExpiresAt: Date.now() + 60000, + kiroAuthIntervalSeconds: 5, + }); + + assert.equal(fetchCalls.length, 2); + assert.equal(fetchCalls[0].url, 'https://oidc.us-east-1.amazonaws.com/token'); + assert.deepEqual(fetchCalls[0].body, { + clientId: 'client-001', + clientSecret: 'secret-001', + grantType: 'urn:ietf:params:oauth:grant-type:device_code', + deviceCode: 'device-code-001', + }); + assert.deepEqual(sleepCalls, [5000]); + + const finalState = mergeUpdates(stateUpdates); + assert.equal(finalState.kiroAuthStatus, 'authorized'); + assert.equal(finalState.kiroRefreshToken, 'refresh-001'); + assert.equal(finalState.kiroAccessToken, 'access-001'); + assert.equal(finalState.kiroUploadStatus, 'ready_to_upload'); + + assert.equal(completeCalls.length, 1); + assert.equal(completeCalls[0].nodeId, 'kiro-await-device-login'); + assert.equal(completeCalls[0].payload.kiroRefreshToken, 'refresh-001'); +}); + +test('kiro upload credential checks connection and uploads builder id credential to kiro.rs', async () => { + const api = loadKiroDeviceAuthApi(); + const fetchCalls = []; + const stateUpdates = []; + const completeCalls = []; + + const executor = api.createKiroDeviceAuthExecutor({ + addLog: async () => {}, + completeNodeFromBackground: async (nodeId, payload) => { + completeCalls.push({ nodeId, payload }); + }, + fetchImpl: async (url, options = {}) => { + fetchCalls.push({ + url, + method: options.method || 'GET', + headers: options.headers || {}, + body: options.body ? JSON.parse(options.body) : null, + }); + if (options.method === 'GET') { + return createResponse({ + ok: true, + status: 200, + json: { success: true }, + }); + } + return createResponse({ + ok: true, + status: 200, + json: { + success: true, + message: 'uploaded', + credentialId: 321, + email: 'aws-user@example.com', + }, + }); + }, + getState: async () => ({ + kiroRefreshToken: 'refresh-001', + kiroClientId: 'client-001', + kiroClientSecret: 'secret-001', + kiroAuthRegion: 'ap-southeast-1', + kiroAuthorizedEmail: 'cached@example.com', + kiroRsUrl: 'https://kiro.example.com/admin', + kiroRsKey: 'admin-key-001', + ipProxyEnabled: true, + ipProxyProtocol: 'socks5', + ipProxyHost: '127.0.0.1', + ipProxyPort: '1080', + ipProxyUsername: 'proxy-user', + ipProxyPassword: 'proxy-pass', + }), + setState: async (updates) => { + stateUpdates.push(updates); + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + await executor.executeKiroUploadCredential({ + nodeId: 'kiro-upload-credential', + kiroRefreshToken: 'refresh-001', + kiroClientId: 'client-001', + kiroClientSecret: 'secret-001', + kiroAuthRegion: 'ap-southeast-1', + kiroAuthorizedEmail: 'cached@example.com', + kiroRsUrl: 'https://kiro.example.com/admin', + kiroRsKey: 'admin-key-001', + ipProxyEnabled: true, + ipProxyProtocol: 'socks5', + ipProxyHost: '127.0.0.1', + ipProxyPort: '1080', + ipProxyUsername: 'proxy-user', + ipProxyPassword: 'proxy-pass', + }); + + assert.equal(fetchCalls.length, 2); + assert.equal(fetchCalls[0].url, 'https://kiro.example.com/api/admin/credentials'); + assert.equal(fetchCalls[0].method, 'GET'); + assert.equal(fetchCalls[0].headers['x-api-key'], 'admin-key-001'); + + assert.equal(fetchCalls[1].url, 'https://kiro.example.com/api/admin/credentials'); + assert.equal(fetchCalls[1].method, 'POST'); + assert.equal(fetchCalls[1].headers['x-api-key'], 'admin-key-001'); + assert.deepEqual(fetchCalls[1].body, { + refreshToken: 'refresh-001', + clientId: 'client-001', + clientSecret: 'secret-001', + region: 'ap-southeast-1', + email: 'cached@example.com', + priority: 0, + authMethod: 'IdC', + provider: 'BuilderId', + proxyUrl: 'socks5://127.0.0.1:1080', + proxyUsername: 'proxy-user', + proxyPassword: 'proxy-pass', + }); + + const finalState = mergeUpdates(stateUpdates); + assert.equal(finalState.kiroLastConnectionMessage, 'kiro.rs connection ok (HTTP 200)'); + assert.equal(finalState.kiroAuthorizedEmail, 'aws-user@example.com'); + assert.equal(finalState.kiroCredentialId, 321); + assert.equal(finalState.kiroUploadStatus, 'uploaded'); + assert.equal(typeof finalState.kiroLastUploadAt, 'number'); + assert.equal(finalState.kiroLastUploadAt > 0, true); + + assert.equal(completeCalls.length, 1); + assert.equal(completeCalls[0].nodeId, 'kiro-upload-credential'); + assert.equal(completeCalls[0].payload.kiroCredentialId, 321); + assert.equal(completeCalls[0].payload.kiroUploadStatus, 'uploaded'); +}); diff --git a/tests/background-settings-schema-persistence.test.js b/tests/background-settings-schema-persistence.test.js new file mode 100644 index 0000000..c519257 --- /dev/null +++ b/tests/background-settings-schema-persistence.test.js @@ -0,0 +1,407 @@ +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 settingsSchemaSource = fs.readFileSync('shared/settings-schema.js', 'utf8'); +const backgroundSource = fs.readFileSync('background.js', 'utf8'); + +function extractFunction(name) { + const markers = [`async function ${name}(`, `function ${name}(`]; + const start = markers + .map((marker) => backgroundSource.indexOf(marker)) + .find((index) => index >= 0); + if (start < 0) { + throw new Error(`missing function ${name}`); + } + + let parenDepth = 0; + let signatureEnded = false; + let braceStart = -1; + for (let i = start; i < backgroundSource.length; i += 1) { + const ch = backgroundSource[i]; + if (ch === '(') parenDepth += 1; + if (ch === ')') { + parenDepth -= 1; + if (parenDepth === 0) signatureEnded = true; + } + if (ch === '{' && signatureEnded) { + braceStart = i; + break; + } + } + if (braceStart < 0) { + throw new Error(`missing body for function ${name}`); + } + + let depth = 0; + let end = braceStart; + for (; end < backgroundSource.length; end += 1) { + const ch = backgroundSource[end]; + if (ch === '{') depth += 1; + if (ch === '}') { + depth -= 1; + if (depth === 0) { + end += 1; + break; + } + } + } + return backgroundSource.slice(start, end); +} + +function buildHarness(extra = '') { + return new Function(` +const self = {}; +${flowRegistrySource} +${settingsSchemaSource} +const DEFAULT_ACTIVE_FLOW_ID = 'openai'; +const DEFAULT_SUB2API_GROUP_NAMES = ['codex', 'openai-plus']; +const PERSISTED_SETTING_DEFAULTS = { + activeFlowId: DEFAULT_ACTIVE_FLOW_ID, + panelMode: 'cpa', + signupMethod: 'email', + plusModeEnabled: false, + plusPaymentMethod: 'paypal', + phoneVerificationEnabled: false, + mailProvider: '163', + ipProxyEnabled: false, + ipProxyService: '711proxy', + ipProxyMode: 'account', + kiroSourceId: 'kiro-rs', + kiroRsUrl: 'https://kiro.leftcode.xyz/admin', + kiroRsKey: '', + kiroRegion: 'us-east-1', + stepExecutionRangeByFlow: {}, +}; +const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS); +const PERSISTED_SETTINGS_SCHEMA_KEYS = ['settingsSchemaVersion', 'settingsState']; +const LEGACY_AUTO_STEP_DELAY_KEYS = []; +const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = []; +function normalizePanelMode(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'sub2api' || normalized === 'codex2api' ? normalized : 'cpa'; +} +function normalizeSignupMethod(value = '') { + return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'; +} +function normalizePlusPaymentMethod(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal'; +} +function normalizeSub2ApiGroupNames(value) { + return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : []; +} +function normalizeCloudflareDomains(value) { return Array.isArray(value) ? value : []; } +function normalizeCloudflareTempEmailDomains(value) { return Array.isArray(value) ? value : []; } +function normalizeCloudMailDomains(value) { return Array.isArray(value) ? value : []; } +function normalizeMailProvider(value = '') { return String(value || '163').trim().toLowerCase() || '163'; } +function normalizeStepExecutionRangeByFlow(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; } +function normalizeIpProxyProviderValue(value) { return String(value || '711proxy').trim() || '711proxy'; } +function normalizeIpProxyMode(value) { return String(value || 'account').trim() || 'account'; } +function normalizeIpProxyServiceProfiles(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; } +function buildIpProxyServiceProfileFromState() { + return { + mode: 'account', + apiUrl: '', + accountList: '', + accountSessionPrefix: '', + accountLifeMinutes: '', + poolTargetCount: '20', + host: '', + port: '', + protocol: 'http', + username: '', + password: '', + region: '', + }; +} +function normalizeIpProxyAccountList(value) { return String(value || ''); } +function normalizeIpProxyAccountSessionPrefix(value) { return String(value || ''); } +function normalizeIpProxyAccountLifeMinutes(value) { return String(value || ''); } +function normalizeIpProxyPoolTargetCount(value) { return String(value || '20'); } +function normalizeIpProxyPort(value) { return String(value || '').trim(); } +function normalizeIpProxyProtocol(value) { return String(value || 'http').trim() || 'http'; } +function resolveSignupMethod(state = {}) { + const activeFlowId = String(state?.activeFlowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID; + if (activeFlowId === 'kiro') { + return 'email'; + } + return String(state?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'; +} +function resolveLegacyAutoStepDelaySeconds() { return undefined; } +${extractFunction('normalizePersistentSettingValue')} +${extractFunction('getSettingsSchemaApi')} +${extractFunction('buildPersistentSettingsPayload')} +${extractFunction('getPersistedSettings')} +${extractFunction('setPersistentSettings')} +${extra} +return { + buildPersistentSettingsPayload, + getPersistedSettings, + setPersistentSettings, + getRequestedKeys: typeof getRequestedKeys === 'function' ? getRequestedKeys : () => [], + getPersistedWrites: typeof getPersistedWrites === 'function' ? getPersistedWrites : () => [], +}; +`)(); +} + +test('buildPersistentSettingsPayload writes canonical settings schema into persisted payloads when defaults are materialized', () => { + const api = buildHarness(); + + const payload = api.buildPersistentSettingsPayload({ + activeFlowId: 'kiro', + kiroRsUrl: 'https://kiro.example.com/admin', + kiroRsKey: 'secret-key', + kiroRegion: 'eu-west-1', + }, { fillDefaults: true }); + + assert.equal(payload.activeFlowId, 'kiro'); + assert.equal(payload.kiroSourceId, 'kiro-rs'); + assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin'); + assert.equal(payload.kiroRsKey, 'secret-key'); + assert.equal(payload.kiroRegion, 'eu-west-1'); + assert.equal(payload.settingsSchemaVersion, 3); + assert.equal(payload.settingsState.activeFlowId, 'kiro'); + assert.equal(payload.settingsState.flows.kiro.source.selected, 'kiro-rs'); +}); + +test('buildPersistentSettingsPayload accepts schema-only input when requireKnownKeys is enabled', () => { + const api = buildHarness(); + + const payload = api.buildPersistentSettingsPayload({ + settingsSchemaVersion: 3, + settingsState: { + activeFlowId: 'kiro', + services: { + email: { provider: '163' }, + proxy: { enabled: false, provider: '711proxy', mode: 'account' }, + }, + flows: { + openai: { + source: { selected: 'cpa', entries: {} }, + account: { customPassword: '' }, + signup: { + signupMethod: 'email', + phoneVerificationEnabled: false, + phoneSignupReloginAfterBindEmailEnabled: false, + }, + plus: { + plusModeEnabled: false, + plusPaymentMethod: 'paypal', + }, + autoRun: { + stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 }, + }, + }, + kiro: { + source: { + selected: 'kiro-rs', + entries: { + 'kiro-rs': { + kiroRsUrl: 'https://kiro.example.com/admin', + kiroRsKey: 'schema-only-key', + }, + }, + }, + options: { + kiroRegion: 'eu-west-1', + }, + autoRun: { + stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 }, + }, + }, + }, + }, + }, { requireKnownKeys: true }); + + assert.equal(payload.activeFlowId, 'kiro'); + assert.equal(payload.kiroSourceId, 'kiro-rs'); + assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin'); + assert.equal(payload.kiroRsKey, 'schema-only-key'); + assert.equal(payload.kiroRegion, 'eu-west-1'); + assert.equal(payload.settingsSchemaVersion, 3); +}); + +test('getPersistedSettings reads schema keys alongside legacy flat settings keys', async () => { + const api = buildHarness(` +let requestedKeys = []; +const chrome = { + storage: { + local: { + async get(keys) { + requestedKeys = Array.isArray(keys) ? [...keys] : []; + return {}; + }, + }, + }, +}; +function getRequestedKeys() { + return requestedKeys; +} +`); + + const state = await api.getPersistedSettings(); + + assert.ok(api.getRequestedKeys().includes('settingsSchemaVersion')); + assert.ok(api.getRequestedKeys().includes('settingsState')); + assert.equal(state.settingsSchemaVersion, 3); + assert.equal(state.settingsState.activeFlowId, 'openai'); +}); + +test('getPersistedSettings can project schema-only storage back into legacy flat settings', async () => { + const api = buildHarness(` +const chrome = { + storage: { + local: { + async get() { + return { + settingsSchemaVersion: 3, + settingsState: { + activeFlowId: 'kiro', + services: { + email: { provider: 'hotmail' }, + proxy: { enabled: true, provider: '711proxy', mode: 'account' }, + }, + flows: { + openai: { + source: { + selected: 'sub2api', + entries: {}, + }, + account: { customPassword: '' }, + signup: { + signupMethod: 'email', + phoneVerificationEnabled: false, + phoneSignupReloginAfterBindEmailEnabled: false, + }, + plus: { + plusModeEnabled: false, + plusPaymentMethod: 'paypal', + }, + autoRun: { + stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 }, + }, + }, + kiro: { + source: { + selected: 'kiro-rs', + entries: { + 'kiro-rs': { + kiroRsUrl: 'https://kiro.example.com/admin', + kiroRsKey: 'stored-key', + }, + }, + }, + options: { + kiroRegion: 'ap-southeast-1', + }, + autoRun: { + stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 }, + }, + }, + }, + }, + }; + }, + }, + }, +}; +`); + + const state = await api.getPersistedSettings(); + + assert.equal(state.activeFlowId, 'kiro'); + assert.equal(state.panelMode, 'sub2api'); + assert.equal(state.mailProvider, 'hotmail'); + assert.equal(state.ipProxyEnabled, true); + assert.equal(state.kiroRsUrl, 'https://kiro.example.com/admin'); + assert.equal(state.kiroRsKey, 'stored-key'); + assert.equal(state.kiroRegion, 'ap-southeast-1'); + assert.deepEqual(state.stepExecutionRangeByFlow.kiro, { + enabled: true, + fromStep: 1, + toStep: 3, + }); +}); + +test('setPersistentSettings materializes canonical schema keys for schema-only updates', async () => { + const api = buildHarness(` +const persistedWrites = []; +const chrome = { + storage: { + local: { + async get() { + return {}; + }, + async set(payload) { + persistedWrites.push(JSON.parse(JSON.stringify(payload))); + }, + }, + }, +}; +function getPersistedWrites() { + return persistedWrites; +} +`); + + const persisted = await api.setPersistentSettings({ + settingsSchemaVersion: 3, + settingsState: { + activeFlowId: 'kiro', + services: { + email: { provider: '163' }, + proxy: { enabled: false, provider: '711proxy', mode: 'account' }, + }, + flows: { + openai: { + source: { selected: 'cpa', entries: {} }, + account: { customPassword: '' }, + signup: { + signupMethod: 'email', + phoneVerificationEnabled: false, + phoneSignupReloginAfterBindEmailEnabled: false, + }, + plus: { + plusModeEnabled: false, + plusPaymentMethod: 'paypal', + }, + autoRun: { + stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 }, + }, + }, + kiro: { + source: { + selected: 'kiro-rs', + entries: { + 'kiro-rs': { + kiroRsUrl: 'https://kiro.example.com/admin', + kiroRsKey: 'nested-only-key', + }, + }, + }, + options: { + kiroRegion: 'us-west-2', + }, + autoRun: { + stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 }, + }, + }, + }, + }, + }); + + const write = api.getPersistedWrites().at(-1); + + assert.equal(persisted.activeFlowId, 'kiro'); + assert.equal(persisted.kiroRsUrl, 'https://kiro.example.com/admin'); + assert.equal(persisted.kiroRsKey, 'nested-only-key'); + assert.equal(persisted.kiroRegion, 'us-west-2'); + assert.equal(persisted.settingsSchemaVersion, 3); + assert.equal(write.activeFlowId, 'kiro'); + assert.equal(write.kiroRsUrl, 'https://kiro.example.com/admin'); + assert.equal(write.kiroRsKey, 'nested-only-key'); + assert.equal(write.kiroRegion, 'us-west-2'); + assert.equal(write.settingsSchemaVersion, 3); + assert.equal(write.settingsState.activeFlowId, 'kiro'); +}); diff --git a/tests/background-step-modules.test.js b/tests/background-step-modules.test.js index 7b661cf..e4d40e6 100644 --- a/tests/background-step-modules.test.js +++ b/tests/background-step-modules.test.js @@ -2,7 +2,7 @@ const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); -test('background imports step 1~10 modules', () => { +test('background imports workflow step modules including Kiro device auth', () => { const source = fs.readFileSync('background.js', 'utf8'); [ @@ -16,6 +16,7 @@ test('background imports step 1~10 modules', () => { 'background/steps/fetch-login-code.js', 'background/steps/confirm-oauth.js', 'background/steps/platform-verify.js', + 'background/steps/kiro-device-auth.js', ].forEach((path) => { assert.match(source, new RegExp(path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); }); diff --git a/tests/background-step-node-registry-module.test.js b/tests/background-step-node-registry-module.test.js new file mode 100644 index 0000000..e749719 --- /dev/null +++ b/tests/background-step-node-registry-module.test.js @@ -0,0 +1,64 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +test('background node registry preserves node metadata even before an executor is registered', () => { + const source = fs.readFileSync('background/steps/registry.js', 'utf8'); + const api = new Function('self', `${source}; return self.MultiPageBackgroundStepRegistry;`)({}); + const registry = api.createNodeRegistry([ + { + flowId: 'kiro', + legacyStepId: 1, + nodeId: 'kiro-start-device-login', + displayOrder: 10, + executeKey: 'kiro-start-device-login', + title: 'Start device login', + }, + ]); + + const node = registry.getNodeDefinition('kiro-start-device-login'); + + assert.equal(node.flowId, 'kiro'); + assert.equal(node.legacyStepId, 1); + assert.equal(node.title, 'Start device login'); + assert.throws( + () => registry.executeNode('kiro-start-device-login', {}), + /Missing node executor: kiro-start-device-login/ + ); +}); + +test('background node registry executes registered nodes in display order', async () => { + const source = fs.readFileSync('background/steps/registry.js', 'utf8'); + const api = new Function('self', `${source}; return self.MultiPageBackgroundStepRegistry;`)({}); + const events = []; + const registry = api.createNodeRegistry([ + { + flowId: 'openai', + legacyStepId: 2, + nodeId: 'submit-signup-email', + displayOrder: 20, + executeKey: 'submit-signup-email', + title: 'Submit signup email', + execute: async (state) => { + events.push({ type: 'execute', state }); + }, + }, + { + flowId: 'openai', + legacyStepId: 1, + nodeId: 'open-chatgpt', + displayOrder: 10, + executeKey: 'open-chatgpt', + title: 'Open ChatGPT', + }, + ]); + + assert.deepStrictEqual( + registry.getOrderedNodes().map((node) => node.nodeId), + ['open-chatgpt', 'submit-signup-email'] + ); + + await registry.executeNode('submit-signup-email', { activeFlowId: 'openai' }); + + assert.deepStrictEqual(events, [{ type: 'execute', state: { activeFlowId: 'openai' } }]); +}); diff --git a/tests/background-step-registry.test.js b/tests/background-step-registry.test.js index 5b67266..47fa503 100644 --- a/tests/background-step-registry.test.js +++ b/tests/background-step-registry.test.js @@ -8,37 +8,10 @@ test('background imports node registry and shared workflow definitions', () => { assert.match(source, /data\/step-definitions\.js/); assert.match(source, /background\/workflow-engine\.js/); assert.match(source, /MultiPageStepDefinitions\?\.getNodes/); - assert.match(source, /getStepRegistryForState\(state\)/); assert.match(source, /buildNodeRegistry\(definitions/); - assert.match(source, /PLUS_PAYPAL_STEP_DEFINITIONS/); - assert.match(source, /PLUS_GOPAY_STEP_DEFINITIONS/); - assert.match(source, /NORMAL_PHONE_STEP_DEFINITIONS/); - assert.match(source, /NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/); - assert.match(source, /PLUS_PAYPAL_PHONE_STEP_DEFINITIONS/); - assert.match(source, /PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/); - assert.match(source, /PLUS_GOPAY_PHONE_STEP_DEFINITIONS/); - assert.match(source, /PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/); - assert.match(source, /PLUS_GPC_PHONE_STEP_DEFINITIONS/); - assert.match(source, /PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/); - assert.match(source, /plusPayPalStepRegistry/); - assert.match(source, /plusGoPayStepRegistry/); - assert.match(source, /normalPhoneStepRegistry/); - assert.match(source, /normalPhoneBoundEmailReloginStepRegistry/); - assert.match(source, /plusPayPalPhoneStepRegistry/); - assert.match(source, /plusPayPalPhoneBoundEmailReloginStepRegistry/); - assert.match(source, /plusGoPayPhoneStepRegistry/); - assert.match(source, /plusGoPayPhoneBoundEmailReloginStepRegistry/); - assert.match(source, /plusGpcPhoneStepRegistry/); - assert.match(source, /plusGpcPhoneBoundEmailReloginStepRegistry/); - assert.match(source, /const signupMethod = getSignupMethodForStepDefinitions\(state\);/); - assert.match(source, /const useBoundEmailRelogin = signupMethod === SIGNUP_METHOD_PHONE/); - assert.match(source, /useBoundEmailRelogin \? normalPhoneBoundEmailReloginStepRegistry : normalPhoneStepRegistry/); - assert.match(source, /const paymentMethod = normalizePlusPaymentMethod\(state\?\.plusPaymentMethod\);/); - assert.match(source, /paymentMethod === PLUS_PAYMENT_METHOD_GOPAY/); - assert.match(source, /useBoundEmailRelogin \? plusGoPayPhoneBoundEmailReloginStepRegistry : plusGoPayPhoneStepRegistry/); - assert.match(source, /useBoundEmailRelogin \? plusPayPalPhoneBoundEmailReloginStepRegistry : plusPayPalPhoneStepRegistry/); - assert.match(source, /useBoundEmailRelogin \? plusGpcPhoneBoundEmailReloginStepRegistry : plusGpcPhoneStepRegistry/); - assert.match(source, /activeStepRegistry\.executeNode\(normalizedNodeId,\s*\{/); + assert.match(source, /const stepRegistryCache = new Map\(\);/); + assert.match(source, /const definitions = getNodeDefinitionsForState\(state\);/); + assert.match(source, /stepRegistryCache\.set\(cacheKey, buildStepRegistry\(definitions\)\)/); assert.match(source, /'bind-email': \(state\) => step8Executor\.executeBindEmail\(state\)/); assert.match(source, /'fetch-bind-email-code': \(state\) => step8Executor\.executeFetchBindEmailCode\(state\)/); assert.match(source, /'relogin-bound-email': \(state\) => executeReloginBoundEmail\(state\)/); @@ -51,9 +24,14 @@ test('background imports node registry and shared workflow definitions', () => { assert.match(source, /background\/steps\/paypal-approve\.js/); assert.match(source, /background\/steps\/gopay-approve\.js/); assert.match(source, /background\/steps\/plus-return-confirm\.js/); + assert.match(source, /background\/steps\/kiro-device-auth\.js/); + assert.match(source, /const kiroDeviceAuthExecutor = self\.MultiPageBackgroundKiroDeviceAuth\?\.createKiroDeviceAuthExecutor\(/); + assert.match(source, /'kiro-start-device-login': \(state\) => kiroDeviceAuthExecutor\.executeKiroStartDeviceLogin\(state\)/); + assert.match(source, /'kiro-await-device-login': \(state\) => kiroDeviceAuthExecutor\.executeKiroAwaitDeviceLogin\(state\)/); + assert.match(source, /'kiro-upload-credential': \(state\) => kiroDeviceAuthExecutor\.executeKiroUploadCredential\(state\)/); + assert.match(source, /'kiro-start-device-login',[\s\S]*'kiro-await-device-login',[\s\S]*'kiro-upload-credential'/); }); - test('GoPay approve executor receives debugger click and manual OTP helpers', () => { const source = fs.readFileSync('background.js', 'utf8'); assert.match(source, /createGoPayApproveExecutor\(\{[\s\S]*clickWithDebugger[\s\S]*requestGoPayOtpInput[\s\S]*\}\)/); diff --git a/tests/flow-capabilities-module.test.js b/tests/flow-capabilities-module.test.js index d974976..14ac97a 100644 --- a/tests/flow-capabilities-module.test.js +++ b/tests/flow-capabilities-module.test.js @@ -2,11 +2,16 @@ 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 settingsSchemaSource = fs.readFileSync('shared/settings-schema.js', 'utf8'); const source = fs.readFileSync('shared/flow-capabilities.js', 'utf8'); function loadApi() { const scope = {}; - return new Function('self', `${source}; return self.MultiPageFlowCapabilities;`)(scope); + return new Function( + 'self', + `${flowRegistrySource}; ${settingsSchemaSource}; ${source}; return self.MultiPageFlowCapabilities;` + )(scope); } test('flow capability registry keeps OpenAI phone signup available only when runtime locks allow it', () => { @@ -71,6 +76,32 @@ test('flow capability registry defaults unknown flows to minimal non-phone capab assert.deepEqual(capabilityState.supportedPanelModes, []); }); +test('flow capability registry exposes Kiro as an independent flow with its own visible groups', () => { + const api = loadApi(); + const registry = api.createFlowCapabilityRegistry(); + + const capabilityState = registry.resolveSidepanelCapabilities({ + state: { + activeFlowId: 'kiro', + kiroSourceId: 'kiro-rs', + panelMode: 'sub2api', + signupMethod: 'phone', + plusModeEnabled: true, + phoneVerificationEnabled: true, + }, + }); + + assert.equal(capabilityState.activeFlowId, 'kiro'); + assert.equal(capabilityState.canShowPhoneSettings, false); + assert.equal(capabilityState.canShowPlusSettings, false); + assert.equal(capabilityState.effectiveSignupMethod, 'email'); + assert.equal(capabilityState.effectiveSourceId, 'kiro-rs'); + assert.deepEqual( + capabilityState.visibleGroupIds, + ['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-email', 'service-proxy'] + ); +}); + test('flow capability registry exposes shared auto-run validation for phone locks and panel support', () => { const api = loadApi(); const registry = api.createFlowCapabilityRegistry({ diff --git a/tests/flow-registry-settings-schema.test.js b/tests/flow-registry-settings-schema.test.js new file mode 100644 index 0000000..20db86b --- /dev/null +++ b/tests/flow-registry-settings-schema.test.js @@ -0,0 +1,83 @@ +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 settingsSchemaSource = fs.readFileSync('shared/settings-schema.js', 'utf8'); + +function loadApis() { + const scope = {}; + return new Function('self', `${flowRegistrySource}; ${settingsSchemaSource}; return { + flowRegistry: self.MultiPageFlowRegistry, + settingsSchema: self.MultiPageSettingsSchema, + };`)(scope); +} + +test('flow registry exposes openai and kiro with canonical source metadata', () => { + const { flowRegistry } = loadApis(); + + assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro']); + assert.equal(flowRegistry.getFlowLabel('codex'), 'Codex / OpenAI'); + assert.equal(flowRegistry.normalizeFlowId('codex'), 'openai'); + assert.equal(flowRegistry.normalizeSourceId('openai', 'sub2api'), 'sub2api'); + assert.equal(flowRegistry.normalizeSourceId('kiro', 'anything-else'), 'kiro-rs'); + assert.deepEqual( + flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'), + ['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-email', 'service-proxy'] + ); +}); + +test('settings schema normalizes flat input into canonical flow and service namespaces', () => { + const { settingsSchema } = loadApis(); + const schema = settingsSchema.createSettingsSchema(); + + const normalized = schema.normalizeSettingsState({ + activeFlowId: 'kiro', + panelMode: 'sub2api', + mailProvider: 'hotmail', + ipProxyEnabled: true, + ipProxyService: '711proxy', + kiroRsUrl: 'https://kiro.example.com/admin', + kiroRsKey: 'secret-key', + kiroRegion: 'eu-west-1', + stepExecutionRangeByFlow: { + openai: { enabled: true, fromStep: 2, toStep: 9 }, + kiro: { enabled: true, fromStep: 1, toStep: 3 }, + }, + }); + + assert.equal(normalized.activeFlowId, 'kiro'); + assert.equal(normalized.services.email.provider, 'hotmail'); + assert.equal(normalized.services.proxy.enabled, true); + assert.equal(normalized.flows.openai.source.selected, 'sub2api'); + assert.equal(normalized.flows.kiro.source.selected, 'kiro-rs'); + assert.equal(normalized.flows.kiro.source.entries['kiro-rs'].kiroRsUrl, 'https://kiro.example.com/admin'); + assert.equal(normalized.flows.kiro.source.entries['kiro-rs'].kiroRsKey, 'secret-key'); + assert.equal(normalized.flows.kiro.options.kiroRegion, 'eu-west-1'); + assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, { + enabled: true, + fromStep: 1, + toStep: 3, + }); +}); + +test('settings schema can project canonical state back to legacy payload without losing flow selection', () => { + const { settingsSchema } = loadApis(); + const schema = settingsSchema.createSettingsSchema(); + const payload = schema.buildLegacySettingsPayload(schema.normalizeSettingsState({ + activeFlowId: 'kiro', + kiroSourceId: 'kiro-rs', + kiroRsUrl: 'https://kiro.example.com/admin', + kiroRsKey: 'key-123', + kiroRegion: 'ap-southeast-1', + })); + + assert.equal(payload.activeFlowId, 'kiro'); + assert.equal(payload.panelMode, 'cpa'); + assert.equal(payload.kiroSourceId, 'kiro-rs'); + assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin'); + assert.equal(payload.kiroRsKey, 'key-123'); + assert.equal(payload.kiroRegion, 'ap-southeast-1'); + assert.equal(payload.settingsSchemaVersion, 3); + assert.equal(payload.settingsState.activeFlowId, 'kiro'); +}); diff --git a/tests/sidepanel-contribution-mode-flow-scope.test.js b/tests/sidepanel-contribution-mode-flow-scope.test.js new file mode 100644 index 0000000..0b39776 --- /dev/null +++ b/tests/sidepanel-contribution-mode-flow-scope.test.js @@ -0,0 +1,90 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const vm = require('node:vm'); + +const source = fs.readFileSync('sidepanel/contribution-mode.js', 'utf8'); + +function createElement() { + return { + hidden: false, + disabled: false, + title: '', + textContent: '', + value: '', + classList: { + hiddenState: false, + toggle(_className, hidden) { + this.hiddenState = Boolean(hidden); + }, + }, + setAttribute() {}, + addEventListener() {}, + }; +} + +test('contribution mode manager does not project openai-only ui state into kiro flow', () => { + const context = { + window: {}, + document: { activeElement: null }, + console, + setTimeout, + clearTimeout, + }; + vm.runInNewContext(source, context); + + const createContributionModeManager = context.window.SidepanelContributionMode.createContributionModeManager; + const rowVpsUrl = createElement(); + const dom = { + btnContributionMode: createElement(), + contributionModePanel: createElement(), + contributionModeText: createElement(), + contributionModeBadge: createElement(), + contributionOauthStatus: createElement(), + contributionCallbackStatus: createElement(), + contributionModeSummary: createElement(), + inputContributionNickname: createElement(), + inputContributionQq: createElement(), + btnStartContribution: createElement(), + btnOpenContributionUpload: createElement(), + btnExitContributionMode: createElement(), + btnOpenAccountRecords: createElement(), + selectPanelMode: createElement(), + rowVpsUrl, + }; + const manager = createContributionModeManager({ + state: { + getLatestState: () => ({ + activeFlowId: 'kiro', + flowId: 'kiro', + contributionMode: true, + contributionSource: 'cpa', + }), + }, + dom, + helpers: { + updatePanelModeUI() {}, + updateAccountRunHistorySettingsUI() {}, + updateConfigMenuControls() {}, + closeConfigMenu() {}, + closeAccountRecordsPanel() {}, + isModeSwitchBlocked() { + return false; + }, + }, + runtime: { + sendMessage: async () => ({}), + }, + constants: {}, + }); + + manager.render(); + + assert.equal(dom.contributionModePanel.hidden, true); + 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(rowVpsUrl.classList.hiddenState, false); +}); diff --git a/tests/sidepanel-flow-source-registry.test.js b/tests/sidepanel-flow-source-registry.test.js new file mode 100644 index 0000000..6a538bc --- /dev/null +++ b/tests/sidepanel-flow-source-registry.test.js @@ -0,0 +1,115 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8'); +const sidepanelHtml = fs.readFileSync('sidepanel/sidepanel.html', 'utf8'); + +function extractFunction(source, name) { + const asyncStart = source.indexOf(`async function ${name}`); + const normalStart = source.indexOf(`function ${name}`); + const start = asyncStart !== -1 + ? asyncStart + : normalStart; + if (start === -1) { + throw new Error(`Function ${name} not found`); + } + const signatureEnd = source.indexOf(')', start); + const bodyStart = source.indexOf('{', signatureEnd); + let depth = 0; + let end = bodyStart; + for (; end < source.length; end += 1) { + const char = source[end]; + if (char === '{') { + depth += 1; + } else if (char === '}') { + depth -= 1; + if (depth === 0) { + end += 1; + break; + } + } + } + return source.slice(start, end); +} + +test('sidepanel html exposes flow selector and kiro source fields', () => { + [ + 'id="select-flow"', + 'id="label-source-selector"', + 'id="row-kiro-rs-url"', + 'id="row-kiro-rs-key"', + 'id="row-kiro-region"', + 'id="row-kiro-device-code"', + 'id="row-kiro-login-url"', + 'id="row-kiro-upload-status"', + ].forEach((snippet) => { + assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + }); +}); + +test('sidepanel step definitions rerender when active flow changes even if plus/signup settings stay the same', () => { + const bundle = [ + extractFunction(sidepanelSource, 'normalizeSignupMethod'), + extractFunction(sidepanelSource, 'normalizePlusPaymentMethod'), + extractFunction(sidepanelSource, 'getStepDefinitionsForMode'), + extractFunction(sidepanelSource, 'rebuildStepDefinitionState'), + extractFunction(sidepanelSource, 'syncStepDefinitionsForMode'), + ].join('\n'); + + const api = new Function(` +const calls = []; +const window = { + MultiPageStepDefinitions: { + getSteps(options) { + calls.push({ type: 'getSteps', options }); + return [{ id: options.activeFlowId === 'kiro' ? 88 : 6, order: 1, key: options.activeFlowId }]; + }, + }, +}; +let latestState = { activeFlowId: 'openai' }; +let currentPlusModeEnabled = false; +let currentPlusPaymentMethod = 'paypal'; +let currentSignupMethod = 'email'; +let currentPhoneSignupReloginAfterBindEmailEnabled = false; +let currentStepDefinitionFlowId = 'openai'; +const DEFAULT_ACTIVE_FLOW_ID = 'openai'; +const DEFAULT_SIGNUP_METHOD = 'email'; +const DEFAULT_PLUS_PAYMENT_METHOD = 'paypal'; +let stepDefinitions = [{ id: 6, key: 'openai' }]; +let STEP_IDS = [6]; +let STEP_DEFAULT_STATUSES = { 6: 'pending' }; +let SKIPPABLE_STEPS = new Set([6]); +function renderStepsList() { + calls.push({ type: 'render', stepIds: [...STEP_IDS] }); +} +${bundle} +return { + calls, + syncStepDefinitionsForMode, + getStepIds: () => [...STEP_IDS], + getCurrentFlowId: () => currentStepDefinitionFlowId, +}; +`)(); + + api.syncStepDefinitionsForMode(false, { + activeFlowId: 'kiro', + plusPaymentMethod: 'paypal', + signupMethod: 'email', + phoneSignupReloginAfterBindEmailEnabled: false, + }); + + assert.equal(api.getCurrentFlowId(), 'kiro'); + assert.deepEqual(api.getStepIds(), [88]); + assert.deepEqual(api.calls[0], { + type: 'getSteps', + options: { + activeFlowId: 'kiro', + plusModeEnabled: false, + plusPaymentMethod: 'paypal', + signupMethod: 'email', + phoneSignupReloginAfterBindEmailEnabled: false, + }, + }); + assert.deepEqual(api.calls[1], { type: 'render', stepIds: [88] }); +}); diff --git a/tests/source-registry-module.test.js b/tests/source-registry-module.test.js index 7ebe678..6e8048a 100644 --- a/tests/source-registry-module.test.js +++ b/tests/source-registry-module.test.js @@ -4,6 +4,8 @@ const fs = require('node:fs'); test('background imports shared source registry module', () => { const source = fs.readFileSync('background.js', 'utf8'); + assert.match(source, /shared\/flow-registry\.js/); + assert.match(source, /shared\/settings-schema\.js/); assert.match(source, /shared\/source-registry\.js/); }); @@ -21,8 +23,9 @@ test('manifest loads shared source registry before content utils in static bundl }); test('shared source registry exposes canonical source, alias, detection, and ready policies', () => { + const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8'); const source = fs.readFileSync('shared/source-registry.js', 'utf8'); - const api = new Function('self', `${source}; return self.MultiPageSourceRegistry;`)({}); + const api = new Function('self', `${flowRegistrySource}; ${source}; return self.MultiPageSourceRegistry;`)({}); const registry = api.createSourceRegistry(); assert.equal(registry.resolveCanonicalSource('signup-page'), 'openai-auth'); @@ -50,6 +53,10 @@ test('shared source registry exposes canonical source, alias, detection, and rea }), 'unknown-source' ); + assert.equal(registry.detectSourceFromLocation({ + url: 'https://view.awsapps.com/start', + hostname: 'view.awsapps.com', + }), 'kiro-device-auth'); assert.equal(registry.shouldReportReadyForFrame('mail-163', true), false); assert.equal(registry.shouldReportReadyForFrame('unknown-source', false), false); assert.equal(registry.getCleanupOwnerSource('oauth-localhost-callback'), 'openai-auth'); @@ -59,4 +66,5 @@ test('shared source registry exposes canonical source, alias, detection, and rea assert.equal(registry.driverAcceptsCommand('openai-auth', 'fetch-bind-email-code'), true); assert.equal(registry.driverAcceptsCommand('content/platform-panel', 'platform-verify'), true); assert.equal(registry.driverAcceptsCommand('openai-auth', 'platform-verify'), false); + assert.equal(registry.driverAcceptsCommand('background/kiro-device-auth', 'kiro-start-device-login'), true); }); diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js index 835161d..351e36d 100644 --- a/tests/step-definitions-module.test.js +++ b/tests/step-definitions-module.test.js @@ -22,6 +22,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () }); const goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }); const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }); + const kiroSteps = api.getSteps({ activeFlowId: 'kiro' }); assert.equal(Array.isArray(steps), true); assert.equal(steps.length, 11); @@ -158,10 +159,28 @@ test('step definitions module exposes ordered normal and Plus step metadata', () assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]); assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 18); assert.equal(api.hasFlow('openai'), true); + assert.equal(api.hasFlow('kiro'), true); assert.equal(api.hasFlow('site-a'), false); - assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai']); + assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro']); assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []); assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null); + assert.deepStrictEqual( + kiroSteps.map((step) => step.key), + [ + 'kiro-start-device-login', + 'kiro-await-device-login', + 'kiro-upload-credential', + ] + ); + assert.equal(kiroSteps.every((step) => step.flowId === 'kiro'), true); + assert.equal(kiroSteps[0].driverId, 'background/kiro-device-auth'); + assert.equal(kiroSteps[2].sourceId, 'kiro-rs-admin'); + assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'kiro' }), [1, 2, 3]); + assert.equal(api.getLastStepId({ activeFlowId: 'kiro' }), 3); + assert.deepStrictEqual( + api.getNodes({ activeFlowId: 'kiro' }).map((node) => node.next), + [['kiro-await-device-login'], ['kiro-upload-credential'], []] + ); assert.equal(plusSteps[5].title, '创建 Plus Checkout'); assert.equal(plusSteps[7].title, 'PayPal 登录与授权'); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index ec13eac..bd6daf2 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -10,7 +10,10 @@ ## 1. 项目目标 -这是一个 Chrome 扩展,用于自动执行一整套 OpenAI / ChatGPT OAuth 注册与登录流程。 +这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册两条主 flow: + +- `openai`:OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路 +- `kiro`:Builder ID device auth + `kiro.rs` 凭据上传链路 它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路: @@ -25,6 +28,8 @@ - 自动确认 OAuth 同意页 - 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API +同时,这套扩展现在不再把“来源、步骤、侧边栏显隐、运行态字段”硬编码在单一 OpenAI 链路里,而是统一挂到 `flow registry + source registry + settings schema + step definitions` 这一层。Kiro 这类新 flow 可以复用共享邮箱服务与 IP 代理服务,但不会复用 OpenAI 的接码、Plus、平台绑定和贡献模式逻辑。 + ## 2. 核心运行参与者 ### 2.1 Sidepanel @@ -38,6 +43,8 @@ - 向后台发送命令 - 接收后台广播并更新 UI - 动态渲染步骤列表 +- 维护 `activeFlowId + sourceId` 的双层选择;`openai` flow 继续把 `panelMode` 作为 legacy 来源映射,`kiro` flow 则改用 `kiroSourceId` +- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key / region`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态,不显示 OpenAI 接码 / Plus / 平台绑定配置 - 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源 - 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态 - 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现 @@ -110,18 +117,32 @@ - 极少量保留函数 - Chrome 事件挂接入口 -### 3.3 步骤注册 +### 3.3 Flow / Source 注册 -[data/step-definitions.js](c:/Users/projectf/Downloads/codex注册扩展/data/step-definitions.js) 提供共享步骤元数据,并会按 `plusPaymentMethod / signupMethod` 解析当前运行态应显示的步骤标题。 -[background/steps/registry.js](c:/Users/projectf/Downloads/codex注册扩展/background/steps/registry.js) 负责把“步骤元数据”映射到“步骤执行器”。 +[shared/flow-registry.js](c:/Users/projectf/Downloads/codex注册扩展/shared/flow-registry.js) 定义 flow、来源、可见设置分组、runtime source 与 driver。 +[shared/source-registry.js](c:/Users/projectf/Downloads/codex注册扩展/shared/source-registry.js) 负责把 flow runtime source 与共享 mail source 合并成统一来源视图。 +[shared/flow-capabilities.js](c:/Users/projectf/Downloads/codex注册扩展/shared/flow-capabilities.js) 负责把当前 flow/source 解析成 sidepanel 的能力矩阵。 +[shared/settings-schema.js](c:/Users/projectf/Downloads/codex注册扩展/shared/settings-schema.js) 负责把旧平铺字段与新的 `flows.*` 配置结构互转。 + +这意味着: + +- flow 是第一层边界,source 是第二层边界 +- “显示什么配置”“允许什么步骤”“哪些运行态字段属于哪个 flow” 都不再由 sidepanel 或 background 临时硬编码 +- 共享服务目前主要是 `email` 与 `proxy` + +### 3.4 步骤注册 + +[data/step-definitions.js](c:/Users/projectf/Downloads/codex注册扩展/data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod` 动态解析步骤标题与节点集合,`kiro` flow 则输出固定的 3 节点 workflow。 +[background/steps/registry.js](c:/Users/projectf/Downloads/codex注册扩展/background/steps/registry.js) 负责把“workflow node 元数据”映射到“步骤执行器”,同时保留 `flowId / nodeId / displayOrder / executeKey` 等稳定节点元数据。 这意味着: - 步骤顺序靠 `order` - 步骤文件名靠语义 - 新增步骤时不需要重命名后续文件 +- 非 OpenAI flow 不需要复用旧的数字 step registry;直接按 node workflow 注册即可 -### 3.4 日志步骤号链路 +### 3.5 日志步骤号链路 日志步骤号不再从日志正文里提取,也不再兼容旧的 `步骤 X` / `Step X` 文本解析。 @@ -137,6 +158,7 @@ 保存运行态: +- 当前 flow 选择:`activeFlowId / flowId` - 当前步骤状态 - 当前 sidepanel UI 模式 `contributionMode` - 贡献 OAuth 会话字段: @@ -148,6 +170,7 @@ - `contributionStatusMessage` - `contributionLastPollAt` - OAuth 链接 +- Kiro 运行态:`kiroDeviceCode / kiroLoginUrl / kiroAuthStatus / kiroRefreshToken / kiroUploadStatus / kiroAuthorizedEmail / kiroCredentialId` - 当前轮冻结后的注册方式 `resolvedSignupMethod` - 当前统一账号标识 `accountIdentifierType / accountIdentifier` - 当前邮箱 / 密码 @@ -184,6 +207,8 @@ 保存持久配置与账号运行历史: +- 当前 flow 与来源配置:`activeFlowId`、OpenAI 的 `panelMode`、Kiro 的 `kiroSourceId` +- flow-aware 配置树:`flows.openai.*`、`flows.kiro.*` - CPA / SUB2API 配置 - Codex2API 配置 - IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值 @@ -202,6 +227,7 @@ - 接码开关、接码平台 `phoneSmsProvider`,以及 HeroSMS / 5sim 各自的 API Key、国家/地区、价格上限和平台扩展设置 - iCloud 相关偏好 - LuckMail API 配置 +- Kiro 来源配置:`kiroRsUrl / kiroRsKey / kiroRegion / kiroRsPriority / kiroRsEndpoint / kiroRsAuthRegion / kiroRsApiRegion` - 自动运行默认配置 - OAuth 授权后链总超时开关 `oauthFlowTimeoutEnabled`:默认开启;关闭后会立即清空已存在的 OAuth 总预算 deadline,仅保留各步骤本地等待超时 - flow 级执行范围配置 `stepExecutionRangeByFlow`:按 flowId 保存,例如 `openai: { enabled: true, fromStep: 3, toStep: 6 }`;侧栏中的 `codex` 会归一到内部默认 flowId `openai`。这是通用配置结构,但当前只给 codex/openai flow 暴露 UI。 @@ -211,7 +237,7 @@ 注意: - `contributionMode` 不属于持久配置,也不参与导入/导出;它只存在于运行态 -- `panelMode` 当前表示 `cpa | sub2api | codex2api` 三个来源;贡献模式仍不是新的来源枚举 +- `panelMode` 现在只代表 `openai` flow 下的 `cpa | sub2api | codex2api` 三个 legacy 来源;`kiro` flow 使用独立的 `kiroSourceId` 账号运行历史会默认通过 [scripts/hotmail_helper.py](c:/Users/projectf/Downloads/codex注册扩展/scripts/hotmail_helper.py) 的本地 helper 地址尝试同步写入 `data/account-run-history.json` 快照文件;如果 helper 没有运行,则静默跳过,不再要求用户先手动打开一个“本地同步”开关。 这条配置链路独立于 `mailProvider` 和 Hotmail 的接码模式。 @@ -591,6 +617,40 @@ Plus 模式可见步骤: 等待模型: - Plus Checkout 和 PayPal 专用步骤使用“无限等待但可停止”的后台等待 helper。 + +## 6.2 Kiro Flow 链路 + +Kiro flow 的目标不是复用 OpenAI 注册链,而是单独完成“拿 Builder ID refresh token -> 上传到 `kiro.rs`”这条链路。 + +当前节点只有 3 个: + +1. `kiro-start-device-login` +2. `kiro-await-device-login` +3. `kiro-upload-credential` + +链路如下: + +1. sidepanel 把当前 flow 切到 `kiro`,来源固定为 `kiro-rs` +2. 用户填写 `kiro.rs URL / API Key / region` +3. 步骤 1 调用 AWS Builder ID OIDC: + - 注册 public client + - 请求 `device_authorization` + - 打开 `verificationUriComplete` + - 保存 `kiroDeviceCode / kiroLoginUrl / kiroClientId / kiroClientSecret` +4. 步骤 2 轮询 token 接口,直到用户完成 device login: + - 成功后保存 `kiroRefreshToken` + - 状态进入 `ready_to_upload` +5. 步骤 3 调用 `kiro.rs` Admin API: + - 先探活 `/api/admin/credentials` + - 再用 `x-api-key` 上传 Builder ID 凭据 + - payload 会带上 `refreshToken / clientId / clientSecret / region` + - 如果当前启用了共享 IP 代理,也会把 `proxyUrl / proxyUsername / proxyPassword` 一并上传 + +这条链路的关键边界是: + +- Kiro 只复用共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路 +- Kiro 自动运行不再走 OpenAI 专用步骤文案和重开逻辑,而是走通用 linear node runner +- Kiro 节点完成回写直接按 `flowId=kiro` 写运行态,不会再误入 OpenAI 的旧 `step` 分支 - 每次页面加载完成后固定等待 1 秒,再继续下一次输入、点击或状态判断。 - 第一版不实时抓取外部地址网站;地址自动化只依赖本地 seed query 和 checkout 页面内置 Google 地址推荐。 @@ -944,6 +1004,7 @@ Hide My Email 获取与管理链路: - 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态 - fresh-attempt reset 会保留 `stepExecutionRangeByFlow`,避免重置运行态时丢失用户设置的执行窗口 5. 执行 `runAutoSequenceFromStep` + - 如果当前 `activeFlowId !== openai`,后台会切到通用 linear node runner,按当前 flow 的 workflow node 顺序执行,并复用统一的完成状态与 idle watchdog - 如果 `stepExecutionRangeByFlow` 已启用,自动运行只会遍历允许范围内的 workflow node;范围外节点会被跳过,`getFirstUnfinishedNodeId` 与保存进度判断也只统计允许节点 - 手动执行、手动跳过、completion signal 执行和 `executeNodeAndWait` 都会在后台校验执行范围,范围外节点即使被外部消息或旧状态触发,也会直接报错,不会真正执行 - 步骤 7 内部仍保留登录态恢复的有限重试,但 `add-phone / 手机号页` 属于立即跳出的不可重试错误 @@ -971,6 +1032,7 @@ Hide My Email 获取与管理链路: 1. [data/step-definitions.js](c:/Users/projectf/Downloads/codex注册扩展/data/step-definitions.js) 2. [background/steps](c:/Users/projectf/Downloads/codex注册扩展/background/steps) 3. [background/steps/registry.js](c:/Users/projectf/Downloads/codex注册扩展/background/steps/registry.js) +4. 如果步骤属于新 flow,还必须同步检查 [shared/flow-registry.js](c:/Users/projectf/Downloads/codex注册扩展/shared/flow-registry.js) 与 [shared/settings-schema.js](c:/Users/projectf/Downloads/codex注册扩展/shared/settings-schema.js) 4. 自动运行链路是否需要纳入 5. Step 状态传播和侧边栏展示是否需要适配 6. 测试是否要补 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index c45443a..abb99d3 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -21,7 +21,7 @@ - `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/`、`.github/`、`_metadata/`、`.vscode/` 等目录。 - `LICENSE`:项目许可证文件。 - `README.md`:面向使用者的项目介绍、安装说明、能力清单与操作指引。 -- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前承接 `oauthFlowTimeoutEnabled` 持久化配置与 OAuth 授权后链总预算开关,同时新增通用 `stepExecutionRangeByFlow` 持久配置,按当前 flow 的 workflow node 解析执行范围,后台手动执行、自动执行、手动跳过与 completion signal 路径都会拦截范围外节点。 +- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,`openai` flow 继续承接 `oauthFlowTimeoutEnabled` 与 `stepExecutionRangeByFlow`,`kiro` flow 则走独立的 device auth 与 `kiro.rs` 上传链路。 - `cloudmail-utils.js`:Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。 - `cloudflare-temp-email-utils.js`:Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。 - `gopay-utils.js`:GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。 @@ -82,7 +82,7 @@ - `background/steps/paypal-approve.js`:Plus 模式第 8 步实现,负责驱动 PayPal 登录、关闭可见通行密钥提示、点击“同意并继续”,并等待授权流程离开 PayPal 页面。 - `background/steps/platform-verify.js`:平台回调验证实现,普通模式为步骤 10,Plus 模式为可见步骤 13;负责 CPA 页面回调验证、SUB2API 管理接口 callback code/state 交换与账号创建,以及 Codex2API 的协议式 callback code/state 交换,所有平台验证日志和完成信号都按当前可见步骤上报。 - `background/steps/plus-return-confirm.js`:Plus 模式第 9 步实现,负责等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒再完成。 -- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。 +- `background/steps/registry.js`:节点注册表工厂,负责保留 `flowId / nodeId / displayOrder / executeKey` 等稳定节点元数据,并把 flow-specific workflow node 映射到后台执行器。 - `background/steps/submit-signup-email.js`:步骤 2 实现,负责按 `signupMethod` 在邮箱注册与手机号注册之间分发;切回已有注册页标签时会先等待加载完成并额外稳定 3 秒,再检查注册入口状态;邮箱分支会先点击官网“免费注册”再提交邮箱并清理旧手机号注册运行态,手机号分支会先点击官网“免费注册”并切到手机号注册入口,确认手机号输入页就绪后优先复用已有 signup activation,其次使用手动运行态手机号,最后才申请接码平台号码并提交,最后根据落地页判断是否跳过后续密码步骤。 ## `content/` @@ -109,7 +109,7 @@ - `data/names.js`:随机姓名、生日等测试数据源。 - `data/address-sources.js`:Plus 模式本地地址 seed 表,负责按国家选择用于触发 checkout 内置 Google 地址推荐的查询词和结构化地址 fallback;第一版不实时抓取外部地址网站。 -- `data/step-definitions.js`:共享步骤元数据,前后台共同使用,用于动态渲染和步骤注册;当前同时提供普通 10 步定义与 Plus 模式 13 步定义,并会按 `plusPaymentMethod / signupMethod` 动态解析实际步骤标题。 +- `data/step-definitions.js`:共享步骤元数据,前后台共同使用,用于动态渲染、workflow 生成和步骤注册;当前 `openai` flow 会按 `plusPaymentMethod / signupMethod` 动态生成普通/Plus workflow,`kiro` flow 则提供独立的 3 节点 device auth workflow。 ## `docs/` @@ -139,6 +139,13 @@ - `scripts/gpc_sms_helper_macos.py`:GPC Plus helper 的 macOS 本地短信验证码 helper,读取本机 Messages 数据库中的 GoPay/OpenAI 短信 OTP,并通过 `http://127.0.0.1:18767/latest-otp?phone=...&consume=1` 提供给扩展按手机号自动轮询;`/otp` 仍保留兼容,`consume=1` 会消费本次返回的验证码记录,避免下次重复读取。 - `scripts/hotmail_helper.py`:本地 helper 服务,负责通过本地接口协助 Hotmail 获取邮件和验证码,并提供账号记录 JSON 快照同步接口;旧的文本追加接口仍保留作兼容。 +## `shared/` + +- `shared/flow-capabilities.js`:flow 能力矩阵归一化模块,负责根据 `activeFlowId` 与来源解析 sidepanel 的显隐能力、来源作用域、贡献模式边界与步骤范围 UI 作用域。 +- `shared/flow-registry.js`:flow/source/settings group 总注册表,定义 `openai / kiro` 两套 flow、各自来源、可见分组、runtime source、driver 定义,以及默认来源和默认 `kiro.rs` 配置。 +- `shared/settings-schema.js`:统一设置 schema 与归一化层,负责把旧平铺字段与新 `flows.*` 结构互转,并维护 `activeFlowId`、`kiroSourceId`、`kiroRs*`、`stepExecutionRangeByFlow` 等跨 flow 配置。 +- `shared/source-registry.js`:运行时来源注册表,负责合并 flow runtime source 与共享 mail source,统一 source family、driver、URL 归属和 cleanup owner 判定。 + ## `sidepanel/` - `sidepanel/account-pool-ui.js`:侧边栏号池表单共享 UI helper,负责 Hotmail / 2925 账号池新增表单的显隐、头部“添加账号/取消添加”按钮文案切换、清空表单与首字段聚焦。 @@ -178,6 +185,7 @@ Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;IP 代理配置保存、服务配置快照、开关应用和运行态回显由这里接入消息路由,具体解析/状态渲染下沉到 `sidepanel/ip-proxy-panel.js`;侧边栏初始化与点击“自动”前会刷新一次贡献站 公开内容摘要,并按本地关闭版本决定是否展示轻提示,同时在首次初始化后按现有规则决定是否弹出新手引导提示;当前会保存、回显并热更新 `oauthFlowTimeoutEnabled` 设置;日志渲染只读取结构化 `entry.step` 生成步骤标签,不再正则解析日志正文;注册手机号输入框只同步运行态身份,不写入持久配置。 - `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Ultra` / 历史 `Pro` / legacy `v` 版本族排序、缓存读取与版本展示。 +- 补充:`sidepanel/sidepanel.html` 与 `sidepanel/sidepanel.js` 当前已改为 flow-aware 侧边栏主界面;顶部提供 `flow/source` 双下拉,`openai` flow 继续把 `panelMode` 作为 legacy 来源映射,`kiro` flow 则改用 `kiroSourceId` 与 `kiro.rs URL / API Key / region` 专属字段,并只显示 Kiro 运行状态与共享邮箱/IP 代理配置。 ## `tests/` @@ -193,6 +201,7 @@ - `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。 - `tests/background-account-run-history-module.test.js`:测试账号记录模块已接入、导出工厂,能够保留并归一化停止记录、归一化失败标签、计算自动重试次数、归并同一轮邮箱/手机号身份,并支持清理后同步完整快照。 - `tests/background-account-history-settings.test.js`:测试账号运行历史的独立配置项归一化,不再复用 Hotmail 模式作为启停条件。 +- `tests/background-kiro-device-auth-module.test.js`:测试 Kiro device auth 模块已接入且导出工厂,并覆盖启动授权、等待授权完成、上传到 `kiro.rs` 的主链路。 - `tests/background-contribution-mode.test.js`:测试贡献模式的后台运行态与公开 OAuth 接入,覆盖 `contributionMode` 只存在于运行态、`SET_CONTRIBUTION_MODE / POLL_CONTRIBUTION_STATUS` 消息接入、reset 保留贡献运行态,以及 callback 自动提交在“无需手动提交”场景下的兼容处理。 - `tests/background-custom-email-pool.test.js`:测试后台对自定义邮箱池生成方式,以及自定义邮箱服务号池的归一化、标签文案和按轮次取邮箱逻辑。 - `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂,并覆盖 Duck 对比基线优先级、2925 receive 模式回退普通邮箱生成、自定义邮箱池按轮次取值、2925 provide 模式账号池预热,以及 Duck / iCloud 在保留手机号身份场景下的共享持久化传参。 @@ -214,13 +223,15 @@ - `tests/background-registration-email-state.test.js`:测试注册邮箱运行态共享模块对 `registrationEmailState` 的维护,覆盖清空当前邮箱时保留上一比较基线、Duck 生成前优先使用侧栏当前可见邮箱作为基线,以及流程邮箱写入时保留手机号身份的共享纯函数。 - `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。 - `tests/background-skip-step-linking.test.js`:测试手动跳过步骤 1 时,会级联跳过步骤 2~6,并仅跳过其中未完成、未运行且仍在当前执行范围内的步骤。 +- `tests/background-settings-schema-persistence.test.js`:测试后台设置持久化已接入统一 settings schema,并覆盖 flow-aware 配置与旧字段兼容回写。 - `tests/background-step-execution-range.test.js`:测试 `stepExecutionRangeByFlow` 对 codex/openai flow 的归一化、节点执行范围过滤、后台执行禁用拦截,以及保存进度只统计允许执行节点。 - `tests/background-signup-step2-branching.test.js`:测试在 Gmail / 2925 模式下,已有兼容别名邮箱时应直接复用,不应再次重生成,并覆盖生成阶段已落库邮箱不会被流程层重复覆盖,以及 Step 8 `add-email` 复用已有邮箱时会委托共享注册邮箱状态持久化保留手机号身份。 - `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。 +- `tests/background-step-node-registry-module.test.js`:测试节点注册表模块已接入且导出工厂,并覆盖节点元数据保留与 display order 执行顺序。 - `tests/background-step5-submit-short-circuit.test.js`:测试步骤 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。 - `tests/background-step6-retry-limit.test.js`:测试步骤 6 的注册成功等待、可选 cookies 清理开关,以及步骤 7 的有限重试上限与 `add-phone` 命中后的立即跳出行为。 - `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),覆盖邮箱验证码页、真实手机验证码页、`add-email` 绑定邮箱后收码、`email_in_use / max_check_attempts` 当前步骤恢复、2925 固定回看窗口与关闭重发间隔。 -- `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。 +- `tests/background-step-registry.test.js`:测试后台步骤注册表、Kiro 执行器映射与共享 workflow 定义已接入。 - `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成、等待标签稳定完成,以及等待过程中的 Stop 中断行为。 - `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。 - `tests/phone-auth-country-match.test.js`:测试认证页手机号国家选择在页面本地化时,仍能用 HeroSMS 的英文国家名匹配对应国家选项。 @@ -253,8 +264,12 @@ - `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。 - `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。 - `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。 +- `tests/flow-capabilities-module.test.js`:测试 flow 能力矩阵模块对来源作用域、显隐能力和贡献模式边界的归一化。 +- `tests/flow-registry-settings-schema.test.js`:测试 flow registry 与 settings schema 之间的默认值、来源映射和 flow-specific 配置结构保持一致。 - `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。 - `tests/sidepanel-contribution-mode.test.js`:测试侧边栏贡献模式的 HTML 接线、runtime-only 设置保护,以及贡献模式 manager 复用主自动流启动、状态轮询和退出清理逻辑。 +- `tests/sidepanel-contribution-mode-flow-scope.test.js`:测试贡献模式只在 `openai` flow 内生效,不会污染 Kiro flow 的来源与面板作用域。 +- `tests/sidepanel-flow-source-registry.test.js`:测试 sidepanel 的 flow/source 选择器、Kiro 来源切换与显隐分组接线。 - `tests/sidepanel-auto-run-content-refresh.test.js`:测试点击“自动”时会先冻结当前轮数并刷新贡献内容更新摘要,且刷新失败或启动前状态回灌不会阻塞或改写自动流程启动目标。 - `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。 - `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析、目标邮箱类型 / 转发邮箱 provider 保存、回显和显隐联动。 @@ -265,7 +280,8 @@ - `tests/signup-entry-diagnostics.test.js`:测试注册入口与密码页诊断快照输出,包括密码页错误文案字段。 - `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式、本地化邮箱输入框识别、官网注册入口点击等待与最多 5 次重试,以及手机号注册时国家下拉框可视区号同步。 - `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。 -- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。 +- `tests/source-registry-module.test.js`:测试共享来源注册表模块已接入且导出工厂,并覆盖 flow runtime source 与共享 mail source 的合并。 +- `tests/step-definitions-module.test.js`:测试共享步骤定义模块、flow-specific workflow 输出与侧边栏脚本加载顺序。 - `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。 - `tests/step5-age-consent.test.js`:测试步骤 5 在年龄页会先勾选顶部“我同意以下所有各项”复选框,再点击提交。 - `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。