feat: add flow-aware kiro device auth workflow
This commit is contained in:
+404
-71
@@ -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 = {}) {
|
||||
|
||||
+69
-15
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+107
-35
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
+32
-123
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -113,13 +113,12 @@
|
||||
<div class="data-row">
|
||||
<span class="data-label">注册</span>
|
||||
<select id="select-flow" class="data-select" aria-label="Flow 选择">
|
||||
<option value="codex" selected>codex</option>
|
||||
<option value="pending-1" disabled>kiro</option>
|
||||
<option value="pending-2" disabled>待添加</option>
|
||||
<option value="openai" selected>Codex / OpenAI</option>
|
||||
<option value="kiro">Kiro</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">来源</span>
|
||||
<span id="label-source-selector" class="data-label">来源</span>
|
||||
<select id="select-panel-mode" class="data-select">
|
||||
<option value="cpa">CPA 面板</option>
|
||||
<option value="sub2api">SUB2API</option>
|
||||
@@ -242,6 +241,38 @@
|
||||
title="显示 Codex2API 管理密钥"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-kiro-rs-url" style="display:none;">
|
||||
<span class="data-label">kiro.rs</span>
|
||||
<input type="text" id="input-kiro-rs-url" class="data-input"
|
||||
placeholder="https://kiro.leftcode.xyz/admin" />
|
||||
</div>
|
||||
<div class="data-row" id="row-kiro-rs-key" style="display:none;">
|
||||
<span class="data-label">API Key</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-kiro-rs-key" class="data-input data-input-with-icon"
|
||||
placeholder="请输入 kiro.rs API Key" />
|
||||
<button id="btn-toggle-kiro-rs-key" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-kiro-rs-key" data-show-label="显示 kiro.rs API Key"
|
||||
data-hide-label="隐藏 kiro.rs API Key" aria-label="显示 kiro.rs API Key"
|
||||
title="显示 kiro.rs API Key"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-kiro-region" style="display:none;">
|
||||
<span class="data-label">区域</span>
|
||||
<input type="text" id="input-kiro-region" class="data-input" placeholder="us-east-1" />
|
||||
</div>
|
||||
<div class="data-row" id="row-kiro-device-code" style="display:none;">
|
||||
<span class="data-label">设备码</span>
|
||||
<span id="display-kiro-device-code" class="data-value mono">未生成</span>
|
||||
</div>
|
||||
<div class="data-row" id="row-kiro-login-url" style="display:none;">
|
||||
<span class="data-label">登录地址</span>
|
||||
<span id="display-kiro-login-url" class="data-value mono">未生成</span>
|
||||
</div>
|
||||
<div class="data-row" id="row-kiro-upload-status" style="display:none;">
|
||||
<span class="data-label">上传状态</span>
|
||||
<span id="display-kiro-upload-status" class="data-value mono">未开始</span>
|
||||
</div>
|
||||
<div class="data-row module-divider-start" id="row-custom-password">
|
||||
<span class="data-label">账户密码</span>
|
||||
<div class="input-with-icon">
|
||||
@@ -1809,6 +1840,8 @@
|
||||
<script src="../hotmail-utils.js"></script>
|
||||
<script src="../luckmail-utils.js"></script>
|
||||
<script src="../yyds-mail-utils.js"></script>
|
||||
<script src="../shared/flow-registry.js"></script>
|
||||
<script src="../shared/settings-schema.js"></script>
|
||||
<script src="../shared/flow-capabilities.js"></script>
|
||||
<script src="../data/step-definitions.js"></script>
|
||||
<script src="update-service.js"></script>
|
||||
|
||||
+606
-76
@@ -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 (
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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, '\\$&')));
|
||||
});
|
||||
|
||||
@@ -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' } }]);
|
||||
});
|
||||
@@ -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]*\}\)/);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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] });
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 登录与授权');
|
||||
|
||||
|
||||
+68
-6
@@ -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. 测试是否要补
|
||||
|
||||
+21
-5
@@ -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 在资料页点击提交后立即完成当前步骤。
|
||||
|
||||
Reference in New Issue
Block a user