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 @@
注册
- 来源
+ 来源
+
+ kiro.rs
+
+
+
+
+ 区域
+
+
+
+ 设备码
+ 未生成
+
+
+ 登录地址
+ 未生成
+
+
+ 上传状态
+ 未开始
+
账户密码
@@ -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 在资料页点击提交后立即完成当前步骤。