refactor: 重构 Kiro flow 配置与运行链路

This commit is contained in:
QLHazyCoder
2026-05-18 19:34:59 +08:00
parent ccd21206c1
commit d9795f8b3a
14 changed files with 1048 additions and 762 deletions
+123 -183
View File
@@ -798,9 +798,8 @@ function getNodeDefinitionsForState(state = {}) {
}
return getStepDefinitionsForState(state)
.map((definition) => ({
legacyStepId: Number(definition?.id),
nodeId: String(definition?.key || '').trim(),
displayOrder: Number.isFinite(Number(definition?.order)) ? Number(definition.order) : Number(definition?.id),
displayOrder: Number.isFinite(Number(definition?.id)) ? Number(definition.id) : Number(definition?.order),
title: String(definition?.title || '').trim(),
executeKey: String(definition?.key || '').trim(),
}))
@@ -829,17 +828,21 @@ function getLastNodeIdForState(state = {}) {
}
function getNodeIdByStepForState(step, state = {}) {
const definition = getStepDefinitionForState(step, state);
return String(definition?.key || '').trim();
const numericStep = Number(step);
if (!Number.isInteger(numericStep) || numericStep <= 0) {
return '';
}
const node = getNodeDefinitionsForState(state).find((definition) => Number(definition?.displayOrder) === numericStep);
return String(node?.nodeId || '').trim();
}
function getStepIdByNodeIdForState(nodeId, state = {}) {
const normalizedNodeId = String(nodeId || '').trim();
if (!normalizedNodeId) return null;
const node = getNodeDefinitionForState(normalizedNodeId, state);
const legacyStepId = Number(node?.legacyStepId);
if (Number.isInteger(legacyStepId) && legacyStepId > 0) {
return legacyStepId;
const displayOrder = Number(node?.displayOrder);
if (Number.isInteger(displayOrder) && displayOrder > 0) {
return displayOrder;
}
return getStepIdByKeyForState(normalizedNodeId, state);
}
@@ -1074,6 +1077,39 @@ const PERSISTED_SETTING_DEFAULTS = {
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
const PERSISTED_SETTINGS_SCHEMA_KEYS = ['settingsSchemaVersion', 'settingsState'];
const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'activeFlowId',
'openaiIntegrationTargetId',
'kiroIntegrationTargetId',
'panelMode',
'kiroSourceId',
'vpsUrl',
'vpsPassword',
'localCpaStep9Mode',
'sub2apiUrl',
'sub2apiEmail',
'sub2apiPassword',
'sub2apiGroupName',
'sub2apiGroupNames',
'sub2apiAccountPriority',
'sub2apiDefaultProxyName',
'codex2apiUrl',
'codex2apiAdminKey',
'customPassword',
'signupMethod',
'phoneVerificationEnabled',
'phoneSignupReloginAfterBindEmailEnabled',
'plusModeEnabled',
'plusPaymentMethod',
'mailProvider',
'ipProxyEnabled',
'ipProxyService',
'ipProxyMode',
'kiroRsUrl',
'kiroRsKey',
'stepExecutionRangeByFlow',
]);
const SETTINGS_SCHEMA_VIEW_KEY_SET = new Set(SETTINGS_SCHEMA_VIEW_KEYS);
const SETTINGS_EXPORT_SCHEMA_VERSION = 1;
const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings';
const STEP6_REGISTRATION_SUCCESS_WAIT_MS = 20000;
@@ -1087,91 +1123,14 @@ const DEFAULT_STATE = {
nodeStatuses: { ...DEFAULT_NODE_STATUSES },
runtimeState: runtimeStateHelpers?.buildDefaultRuntimeState?.() || null,
...CONTRIBUTION_RUNTIME_DEFAULTS,
oauthUrl: null, // 运行时抓取到的 OAuth 地址,不要手动预填。
resolvedSignupMethod: null, // 当前自动轮次冻结后的实际注册方式。
accountIdentifierType: null,
accountIdentifier: '',
registrationEmailState: { ...DEFAULT_REGISTRATION_EMAIL_STATE },
email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。
password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。
accounts: [], // 已生成账号记录:{ email, password, createdAt }。
accountRunHistory: [], // 账号运行历史快照,实际持久化在 chrome.storage.local。
manualAliasUsage: {},
preservedAliases: {},
icloudAliasCache: [],
icloudAliasCacheAt: 0,
lastEmailTimestamp: null, // 最近一次获取到邮箱数据的运行时时间戳。
lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。
lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。
localhostUrl: null, // 运行时捕获到的 localhost 回调地址,不要手动预填。
cpaOAuthState: null, // CPA OAuth state。
cpaManagementOrigin: null, // CPA 管理接口 origin。
sub2apiSessionId: null, // SUB2API OpenAI Auth 会话 ID。
sub2apiOAuthState: null, // SUB2API OpenAI Auth state。
sub2apiGroupId: null, // SUB2API 目标分组 ID。
sub2apiGroupIds: [], // SUB2API 多目标分组 ID。
sub2apiDraftName: null, // SUB2API 本轮预生成的账号名称。
sub2apiProxyId: null, // SUB2API 本轮使用的代理 ID。
codex2apiSessionId: null, // Codex2API OAuth 会话 ID。
codex2apiOAuthState: null, // Codex2API OAuth state。
plusCheckoutTabId: null, // Plus checkout / PayPal 标签页 ID。
automationWindowId: null, // 当前任务锁定的浏览器窗口 ID,避免新标签页跑到其它窗口。
plusCheckoutUrl: null, // Plus checkout 运行时短链,不写入持久配置。
plusCheckoutCountry: 'DE',
plusCheckoutCurrency: 'EUR',
plusCheckoutSource: '',
plusBillingCountryText: '',
plusBillingAddress: null,
plusPaypalApprovedAt: null,
plusGoPayApprovedAt: null,
plusReturnUrl: '',
plusManualConfirmationPending: false,
plusManualConfirmationRequestId: '',
plusManualConfirmationStep: 0,
plusManualConfirmationMethod: '',
plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '',
gopayHelperReferenceId: '',
gopayHelperGoPayGuid: '',
gopayHelperRedirectUrl: '',
gopayHelperNextAction: '',
gopayHelperFlowId: '',
gopayHelperChallengeId: '',
gopayHelperStartPayload: null,
gopayHelperTaskId: '',
gopayHelperTaskStatus: '',
gopayHelperStatusText: '',
gopayHelperRemoteStage: '',
gopayHelperApiWaitingFor: '',
gopayHelperApiInputDeadlineAt: '',
gopayHelperApiInputWaitSeconds: 0,
gopayHelperLastInputError: '',
gopayHelperOtpInvalidCount: 0,
gopayHelperFailureStage: '',
gopayHelperFailureDetail: '',
gopayHelperTaskPayload: null,
gopayHelperOrderCreatedAt: 0,
gopayHelperTaskProgressSignature: '',
gopayHelperTaskProgressAt: 0,
gopayHelperTaskProgressTaskId: '',
gopayHelperPinPayload: null,
gopayHelperResolvedOtp: '',
gopayHelperOtpRequestId: '',
gopayHelperOtpReferenceId: '',
flowStartTime: null, // 当前流程开始时间。
tabRegistry: {}, // 程序维护的标签页注册表。
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
logs: [], // 侧边栏展示的运行日志。
...PERSISTED_SETTING_DEFAULTS, // 合并 chrome.storage.local 中持久化保存的用户配置。
ipProxyApiPool: [],
ipProxyApiCurrentIndex: 0,
ipProxyApiCurrent: null,
ipProxyAccountPool: [],
ipProxyAccountCurrentIndex: 0,
ipProxyAccountCurrent: null,
ipProxyPool: [],
ipProxyCurrentIndex: 0,
ipProxyCurrent: null,
luckmailApiKey: '',
luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL,
luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE,
@@ -1179,23 +1138,6 @@ const DEFAULT_STATE = {
luckmailUsedPurchases: {},
luckmailPreserveTagId: 0,
luckmailPreserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
currentLuckmailPurchase: null,
currentLuckmailMailCursor: null,
currentYydsMailInbox: null,
currentPhoneActivation: null,
phoneNumber: '',
currentPhoneVerificationCode: '',
currentPhoneVerificationCountdownEndsAt: 0,
currentPhoneVerificationCountdownWindowIndex: 0,
currentPhoneVerificationCountdownWindowTotal: 0,
reusablePhoneActivation: null,
freeReusablePhoneActivation: null,
phoneReusableActivationPool: [],
signupPhoneNumber: '',
signupPhoneActivation: null,
signupPhoneCompletedActivation: null,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
heroSmsLastPriceTiers: [],
heroSmsLastPriceCountryId: 0,
heroSmsLastPriceCountryLabel: '',
@@ -3353,14 +3295,14 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
const settingsSchemaApi = typeof getSettingsSchemaApi === 'function'
? getSettingsSchemaApi()
: null;
if (settingsSchemaApi?.normalizeSettingsState && settingsSchemaApi?.buildLegacySettingsPayload) {
if (settingsSchemaApi?.normalizeSettingsState && settingsSchemaApi?.buildSettingsView) {
const settingsSchemaInput = {};
for (const key of persistedSettingKeys) {
if (normalizedInput[key] !== undefined) {
settingsSchemaInput[key] = payload[key];
}
}
const normalizedSettingsState = settingsSchemaApi.normalizeSettingsState({
Object.assign(payload, projectSettingsSchemaView(settingsSchemaApi, {
...settingsSchemaInput,
...(isPlainObjectForSettingsSchema(normalizedInput.settingsState)
? { settingsState: normalizedInput.settingsState }
@@ -3368,13 +3310,7 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
...(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)
);
}, payload));
}
}
@@ -3391,6 +3327,32 @@ function getSettingsSchemaApi() {
});
}
function projectSettingsSchemaView(settingsSchemaApi, normalizedInput = {}, payload = {}) {
if (
!settingsSchemaApi?.normalizeSettingsState
|| !settingsSchemaApi?.buildSettingsView
) {
return payload;
}
const normalizedSettingsState = settingsSchemaApi.normalizeSettingsState(normalizedInput, {
activeFlowId: normalizedInput?.activeFlowId || DEFAULT_ACTIVE_FLOW_ID,
});
return settingsSchemaApi.buildSettingsView(normalizedSettingsState, payload);
}
function buildPersistedSettingsStoragePayload(payload = {}) {
const storagePayload = {};
Object.entries(payload || {}).forEach(([key, value]) => {
if (SETTINGS_SCHEMA_VIEW_KEY_SET.has(key)) {
return;
}
storagePayload[key] = value;
});
storagePayload.settingsSchemaVersion = Number(payload?.settingsSchemaVersion) || 0;
storagePayload.settingsState = payload?.settingsState;
return storagePayload;
}
async function getPersistedSettings() {
const stored = await chrome.storage.local.get([
...PERSISTED_SETTING_KEYS,
@@ -3481,6 +3443,9 @@ function buildAutoRunFreshResetSettingsState(prevState = {}, activeFlowId = DEFA
const nextSettingsStatePatch = {
activeFlowId,
services: {
account: {
customPassword: prevState?.customPassword,
},
email: {
provider: prevState?.mailProvider,
},
@@ -3492,9 +3457,7 @@ function buildAutoRunFreshResetSettingsState(prevState = {}, activeFlowId = DEFA
},
flows: {
openai: {
source: {
selected: prevState?.panelMode,
},
integrationTargetId: prevState?.openaiIntegrationTargetId || prevState?.panelMode,
autoRun: normalizedStepExecutionRangeByFlow.openai
? {
stepExecutionRange: normalizedStepExecutionRangeByFlow.openai,
@@ -3502,9 +3465,7 @@ function buildAutoRunFreshResetSettingsState(prevState = {}, activeFlowId = DEFA
: undefined,
},
kiro: {
source: {
selected: prevState?.kiroSourceId,
},
integrationTargetId: prevState?.kiroIntegrationTargetId || prevState?.kiroSourceId,
autoRun: normalizedStepExecutionRangeByFlow.kiro
? {
stepExecutionRange: normalizedStepExecutionRangeByFlow.kiro,
@@ -3651,78 +3612,58 @@ async function setPersistentSettings(updates) {
const settingsSchemaApi = typeof getSettingsSchemaApi === 'function'
? getSettingsSchemaApi()
: null;
const hasSchemaApi = Boolean(
settingsSchemaApi?.normalizeSettingsState
&& settingsSchemaApi?.buildSettingsView
);
const explicitFlatUpdates = {
...nextUpdates,
};
delete explicitFlatUpdates.settingsSchemaVersion;
delete explicitFlatUpdates.settingsState;
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;
};
let mergedSettingsState = nextUpdates.settingsState ?? currentSettings.settingsState;
if (hasSchemaApi) {
const currentSettingsState = settingsSchemaApi.normalizeSettingsState(
isPlainObjectForSettingsSchema(currentSettings?.settingsState)
isPlainObjectValue(currentSettings?.settingsState)
? { settingsState: currentSettings.settingsState }
: currentSettings,
{
activeFlowId: currentSettings?.activeFlowId || DEFAULT_ACTIVE_FLOW_ID,
}
);
const mergedSettingsState = isPlainObjectForSettingsSchema(nextUpdates.settingsState)
? mergeSettingsState(currentSettingsState, nextUpdates.settingsState)
mergedSettingsState = isPlainObjectValue(nextUpdates.settingsState)
? (typeof settingsSchemaApi.mergeSettingsState === 'function'
? settingsSchemaApi.mergeSettingsState(currentSettingsState, nextUpdates.settingsState)
: 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,
});
}
const nextPayloadInput = {
...currentSettings,
...explicitFlatUpdates,
settingsSchemaVersion: nextUpdates.settingsSchemaVersion ?? currentSettings.settingsSchemaVersion,
settingsState: mergedSettingsState,
};
if (hasSchemaApi && isPlainObjectValue(nextUpdates.settingsState)) {
for (const key of SETTINGS_SCHEMA_VIEW_KEYS) {
delete nextPayloadInput[key];
}
Object.assign(nextPayloadInput, explicitFlatUpdates);
}
const persistedUpdates = buildPersistentSettingsPayload(nextPayloadInput, {
fillDefaults: true,
});
if (Object.keys(persistedUpdates).length > 0) {
await chrome.storage.local.set(persistedUpdates);
const storagePayload = hasSchemaApi
? buildPersistedSettingsStoragePayload(persistedUpdates)
: persistedUpdates;
if (hasSchemaApi && chrome.storage?.local?.remove) {
await chrome.storage.local.remove(SETTINGS_SCHEMA_VIEW_KEYS);
}
await chrome.storage.local.set(storagePayload);
}
return persistedUpdates;
}
@@ -13563,9 +13504,8 @@ 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,
displayOrder: definition.displayOrder || definition.id || definition.order,
executeKey: definition.executeKey || definition.key,
execute: stepExecutorsByKey[definition.executeKey || definition.key || definition.nodeId],
}))
@@ -13580,15 +13520,15 @@ function buildStepRegistry(definitions = []) {
const normalizedDefinitions = (Array.isArray(definitions) ? definitions : [])
.map((definition) => ({
...definition,
legacyStepId: Number(definition?.legacyStepId ?? definition?.id) || 0,
displayOrder: Number(definition?.displayOrder ?? definition?.id ?? definition?.order) || 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])
.filter((definition) => Number.isInteger(definition.displayOrder) && definition.displayOrder > 0)
.map((definition) => [definition.displayOrder, definition])
);
return {
@@ -13617,10 +13557,10 @@ function getStepRegistryForState(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,
Number(definition?.displayOrder ?? definition?.id ?? definition?.order) || 0,
String(definition?.nodeId || definition?.key || '').trim(),
String(definition?.executeKey || definition?.key || '').trim(),
Number(definition?.displayOrder ?? definition?.order) || 0,
Number(definition?.displayOrder ?? definition?.id ?? definition?.order) || 0,
].join(':'))
.join('|')}`;
+135 -80
View File
@@ -244,21 +244,6 @@
};
}
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(fieldGroups)) {
const group = normalizePlainObject(scopedState[groupKey]);
for (const field of fields) {
if (Object.prototype.hasOwnProperty.call(group, field)) {
next[field] = cloneValue(group[field]);
}
}
}
return next;
}
function buildScopedFlowState(baseFlowState = {}, state = {}, flowId = 'openai') {
const fieldGroups = FLOW_FIELD_GROUPS[flowId] || {};
const baseScopedState = cloneValue(normalizePlainObject(baseFlowState[flowId]));
@@ -276,7 +261,7 @@
return scopedState;
}
function buildOpenAiFlowState(baseValue = {}, state = {}) {
function buildFlowState(baseValue = {}, state = {}) {
const baseFlowState = cloneValue(normalizePlainObject(baseValue));
return {
...baseFlowState,
@@ -285,6 +270,126 @@
};
}
function listFlowFieldNames() {
const fields = [];
for (const flowGroups of Object.values(FLOW_FIELD_GROUPS)) {
for (const groupFields of Object.values(normalizePlainObject(flowGroups))) {
for (const field of Array.isArray(groupFields) ? groupFields : []) {
const normalizedField = String(field || '').trim();
if (normalizedField) {
fields.push(normalizedField);
}
}
}
}
return Array.from(new Set(fields));
}
const FLOW_RUNTIME_FIELDS = Object.freeze(listFlowFieldNames());
const RUNTIME_TOP_LEVEL_FIELDS = Object.freeze([
...RUNTIME_SHARED_FIELDS,
...RUNTIME_PROXY_FIELDS,
...FLOW_RUNTIME_FIELDS,
]);
const RUNTIME_TOP_LEVEL_FIELD_SET = new Set(RUNTIME_TOP_LEVEL_FIELDS);
const RUNTIME_PATCH_IGNORED_KEYS = new Set([
'runtimeState',
'flowState',
'sharedState',
'serviceState',
'flows',
'shared',
'services',
'currentStep',
'stepStatuses',
'legacyStepCompat',
'flowId',
'runId',
'activeFlowId',
'activeRunId',
'currentNodeId',
'nodeStatuses',
...RUNTIME_TOP_LEVEL_FIELDS,
]);
function projectScopedFlowFields(flowState = {}) {
const next = {};
for (const [flowId, fieldGroups] of Object.entries(FLOW_FIELD_GROUPS)) {
const scopedState = normalizePlainObject(normalizePlainObject(flowState)[flowId]);
for (const [groupKey, fields] of Object.entries(fieldGroups)) {
const group = normalizePlainObject(scopedState[groupKey]);
Object.assign(next, pickDefinedFields(group, fields));
}
}
return next;
}
function projectRuntimeViewFields(runtimeState = {}) {
const normalizedRuntimeState = normalizePlainObject(runtimeState);
return {
...pickDefinedFields(
normalizePlainObject(normalizedRuntimeState.sharedState),
RUNTIME_SHARED_FIELDS
),
...pickDefinedFields(
normalizePlainObject(normalizePlainObject(normalizedRuntimeState.serviceState).proxy),
RUNTIME_PROXY_FIELDS
),
...projectScopedFlowFields(normalizedRuntimeState.flowState),
};
}
function buildRuntimeInputFromPatch(updates = {}) {
const normalizedUpdates = normalizePlainObject(updates);
const runtimeStatePatch = normalizePlainObject(normalizedUpdates.runtimeState);
const next = {
...pickDefinedFields(normalizedUpdates, [
'flowId',
'runId',
'activeFlowId',
'activeRunId',
'currentNodeId',
'nodeStatuses',
]),
...pickDefinedFields(runtimeStatePatch, [
'flowId',
'runId',
'activeFlowId',
'activeRunId',
'currentNodeId',
'nodeStatuses',
]),
...projectRuntimeViewFields(runtimeStatePatch),
...pickDefinedFields(normalizedUpdates, RUNTIME_TOP_LEVEL_FIELDS),
};
if (Object.prototype.hasOwnProperty.call(normalizedUpdates, 'sharedState')) {
Object.assign(
next,
pickDefinedFields(normalizePlainObject(normalizedUpdates.sharedState), RUNTIME_SHARED_FIELDS)
);
}
if (Object.prototype.hasOwnProperty.call(normalizedUpdates, 'serviceState')) {
Object.assign(
next,
pickDefinedFields(
normalizePlainObject(normalizePlainObject(normalizedUpdates.serviceState).proxy),
RUNTIME_PROXY_FIELDS
)
);
}
if (Object.prototype.hasOwnProperty.call(normalizedUpdates, 'flowState')) {
Object.assign(next, projectScopedFlowFields(normalizedUpdates.flowState));
}
if (!Object.prototype.hasOwnProperty.call(next, 'flowId') && Object.prototype.hasOwnProperty.call(next, 'activeFlowId')) {
next.flowId = next.activeFlowId;
}
if (!Object.prototype.hasOwnProperty.call(next, 'runId') && Object.prototype.hasOwnProperty.call(next, 'activeRunId')) {
next.runId = next.activeRunId;
}
return next;
}
function buildRuntimeStateDefault() {
return {
flowId: DEFAULT_ACTIVE_FLOW_ID,
@@ -366,69 +471,18 @@
nodeStatuses,
sharedState: buildSharedState(baseRuntimeState.sharedState, state),
serviceState: buildServiceState(baseRuntimeState.serviceState, state),
flowState: buildOpenAiFlowState(baseRuntimeState.flowState, state),
flowState: buildFlowState(baseRuntimeState.flowState, state),
};
}
function buildFlattenedUpdates(updates = {}) {
const ignoredKeys = new Set(['current' + 'Step', 'step' + 'Statuses', 'legacy' + 'StepCompat']);
function buildPersistentPatchPayload(updates = {}) {
const next = {};
for (const [key, value] of Object.entries(updates || {})) {
if (!ignoredKeys.has(key)) {
next[key] = value;
for (const [key, value] of Object.entries(normalizePlainObject(updates))) {
if (RUNTIME_PATCH_IGNORED_KEYS.has(key)) {
continue;
}
next[key] = cloneValue(value);
}
const runtimeState = normalizePlainObject(updates.runtimeState);
const sharedState = normalizePlainObject(updates.sharedState);
const serviceState = normalizePlainObject(updates.serviceState);
const flowState = normalizePlainObject(updates.flowState);
if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeFlowId')) {
next.activeFlowId = runtimeState.activeFlowId;
}
if (Object.prototype.hasOwnProperty.call(runtimeState, 'flowId')) {
next.flowId = runtimeState.flowId;
next.activeFlowId = runtimeState.flowId;
}
if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeRunId')) {
next.activeRunId = runtimeState.activeRunId;
}
if (Object.prototype.hasOwnProperty.call(runtimeState, 'runId')) {
next.runId = runtimeState.runId;
next.activeRunId = runtimeState.runId;
}
if (Object.prototype.hasOwnProperty.call(runtimeState, 'currentNodeId')) {
next.currentNodeId = runtimeState.currentNodeId;
}
if (Object.prototype.hasOwnProperty.call(runtimeState, 'nodeStatuses')) {
next.nodeStatuses = cloneValue(runtimeState.nodeStatuses);
}
Object.assign(next, pickDefinedFields(sharedState, RUNTIME_SHARED_FIELDS));
if (Object.prototype.hasOwnProperty.call(runtimeState, 'sharedState')) {
Object.assign(
next,
pickDefinedFields(normalizePlainObject(runtimeState.sharedState), RUNTIME_SHARED_FIELDS)
);
}
const serviceProxy = normalizePlainObject(serviceState.proxy);
Object.assign(next, pickDefinedFields(serviceProxy, RUNTIME_PROXY_FIELDS));
if (Object.prototype.hasOwnProperty.call(runtimeState, 'serviceState')) {
const runtimeServiceState = normalizePlainObject(runtimeState.serviceState);
Object.assign(
next,
pickDefinedFields(normalizePlainObject(runtimeServiceState.proxy), RUNTIME_PROXY_FIELDS)
);
}
Object.assign(next, flattenFlowStateById(flowState, 'openai'));
Object.assign(next, flattenFlowStateById(flowState, 'kiro'));
if (Object.prototype.hasOwnProperty.call(runtimeState, 'flowState')) {
const runtimeFlowState = normalizePlainObject(runtimeState.flowState);
Object.assign(next, flattenFlowStateById(runtimeFlowState, 'openai'));
Object.assign(next, flattenFlowStateById(runtimeFlowState, 'kiro'));
}
return next;
}
@@ -436,6 +490,7 @@
const runtimeState = ensureRuntimeState(state);
return {
...state,
...projectRuntimeViewFields(runtimeState),
flowId: runtimeState.flowId,
runId: runtimeState.runId,
activeFlowId: runtimeState.activeFlowId,
@@ -453,15 +508,15 @@
}
function buildSessionStatePatch(currentState = {}, updates = {}) {
const flattenedUpdates = buildFlattenedUpdates(updates);
const nextState = {
...currentState,
...flattenedUpdates,
};
const runtimeState = ensureRuntimeState(nextState);
const currentRuntimeState = ensureRuntimeState(currentState);
const runtimeState = ensureRuntimeState({
runtimeState: currentRuntimeState,
...projectRuntimeViewFields(currentRuntimeState),
...buildRuntimeInputFromPatch(updates),
});
return {
...flattenedUpdates,
...buildPersistentPatchPayload(updates),
flowId: runtimeState.flowId,
runId: runtimeState.runId,
activeFlowId: runtimeState.activeFlowId,
+1 -2
View File
@@ -4,10 +4,9 @@
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),
displayOrder: Number(definition?.displayOrder ?? definition?.order ?? definition?.id),
executeKey: String(definition?.executeKey || definition?.key || definition?.nodeId || '').trim(),
title: String(definition?.title || '').trim(),
execute: definition?.execute,
+1 -2
View File
@@ -348,11 +348,10 @@
function cloneNodes(steps = [], options = {}, flowId = DEFAULT_ACTIVE_FLOW_ID) {
const { builder } = getFlowDefinitionBuilder({ activeFlowId: flowId });
return steps.map((step) => ({
legacyStepId: Number(step.id),
nodeId: String(step.key || '').trim(),
flowId,
title: builder?.resolveStepTitle ? builder.resolveStepTitle(step, options) : step.title,
displayOrder: Number.isFinite(Number(step.order)) ? Number(step.order) : Number(step.id),
displayOrder: Number.isFinite(Number(step.id)) ? Number(step.id) : Number(step.order),
nodeType: 'task',
sourceId: step.sourceId || '',
driverId: step.driverId || '',
+163 -87
View File
@@ -5,12 +5,11 @@
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 DEFAULT_OPENAI_INTEGRATION_TARGET_ID = flowRegistryApi.DEFAULT_OPENAI_INTEGRATION_TARGET_ID || 'cpa';
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const VALID_PANEL_MODES = Array.isArray(flowRegistryApi.OPENAI_SOURCE_IDS)
? flowRegistryApi.OPENAI_SOURCE_IDS.slice()
const VALID_OPENAI_INTEGRATION_TARGET_IDS = Array.isArray(flowRegistryApi.OPENAI_INTEGRATION_TARGET_IDS)
? flowRegistryApi.OPENAI_INTEGRATION_TARGET_IDS.slice()
: ['cpa', 'sub2api', 'codex2api'];
const REGISTERED_FLOW_IDS = Array.isArray(flowRegistryApi.getRegisteredFlowIds?.())
? flowRegistryApi.getRegisteredFlowIds().map((flowId) => String(flowId || '').trim().toLowerCase()).filter(Boolean)
@@ -23,7 +22,7 @@
supportsPhoneVerificationSettings: false,
supportsPlusMode: false,
supportsContributionMode: false,
supportsPlatformBinding: [],
supportedIntegrationTargets: [],
supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
canSwitchFlow: true,
@@ -48,7 +47,7 @@
)
);
const DEFAULT_PANEL_CAPABILITIES = Object.freeze({
const DEFAULT_INTEGRATION_TARGET_CAPABILITIES = Object.freeze({
supportsPhoneSignup: true,
requiresPhoneSignupWarning: false,
});
@@ -61,9 +60,11 @@
'plusModeEnabled',
'signupMethod',
'kiroSourceId',
'openaiIntegrationTargetId',
'kiroIntegrationTargetId',
]);
const PANEL_CAPABILITIES = Object.freeze({
const OPENAI_INTEGRATION_TARGET_CAPABILITIES = Object.freeze({
cpa: Object.freeze({
supportsPhoneSignup: true,
requiresPhoneSignupWarning: true,
@@ -88,30 +89,26 @@
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 normalizedFallback = String(fallback || '').trim().toLowerCase();
if (normalizedFallback === LEGACY_OPENAI_FLOW_ALIAS) {
return DEFAULT_FLOW_ID;
}
return normalizedFallback || DEFAULT_FLOW_ID;
return normalizeFlowId(fallback, DEFAULT_FLOW_ID);
}
function isRegisteredFlowId(flowId = '') {
return REGISTERED_FLOW_ID_SET.has(normalizeCapabilityFlowId(flowId, ''));
const normalized = String(flowId || '').trim().toLowerCase();
return Boolean(normalized) && REGISTERED_FLOW_ID_SET.has(normalized);
}
function normalizePanelMode(value = '', fallback = DEFAULT_PANEL_MODE) {
function normalizeOpenAiIntegrationTargetId(value = '', fallback = DEFAULT_OPENAI_INTEGRATION_TARGET_ID) {
const normalized = String(value || '').trim().toLowerCase();
if (VALID_PANEL_MODES.includes(normalized)) {
if (VALID_OPENAI_INTEGRATION_TARGET_IDS.includes(normalized)) {
return normalized;
}
const fallbackValue = String(fallback || '').trim().toLowerCase();
return VALID_PANEL_MODES.includes(fallbackValue) ? fallbackValue : DEFAULT_PANEL_MODE;
return VALID_OPENAI_INTEGRATION_TARGET_IDS.includes(fallbackValue)
? fallbackValue
: DEFAULT_OPENAI_INTEGRATION_TARGET_ID;
}
function normalizeSignupMethod(value = '') {
@@ -120,41 +117,50 @@
: SIGNUP_METHOD_EMAIL;
}
function normalizePanelModeList(values = []) {
function normalizeOpenAiIntegrationTargetList(values = []) {
if (!Array.isArray(values)) {
return [];
}
const seen = new Set();
const normalized = [];
values.forEach((value) => {
const mode = normalizePanelMode(value, '');
if (!mode || seen.has(mode)) {
const integrationTargetId = normalizeOpenAiIntegrationTargetId(value, '');
if (!integrationTargetId || seen.has(integrationTargetId)) {
return;
}
seen.add(mode);
normalized.push(mode);
seen.add(integrationTargetId);
normalized.push(integrationTargetId);
});
return normalized;
}
function getPanelModeLabel(panelMode = '') {
const normalized = normalizePanelMode(panelMode);
function getIntegrationTargetLabel(flowId = DEFAULT_FLOW_ID, integrationTargetId = '') {
if (
isRegisteredFlowId(flowId)
&& typeof flowRegistryApi.getIntegrationTargetLabel === 'function'
) {
return flowRegistryApi.getIntegrationTargetLabel(flowId, integrationTargetId);
}
const normalized = String(integrationTargetId || '').trim().toLowerCase();
if (normalized === 'sub2api') {
return 'SUB2API';
}
if (normalized === 'codex2api') {
return 'Codex2API';
}
return 'CPA';
if (normalized === 'cpa') {
return 'CPA';
}
return normalized || String(integrationTargetId || '').trim();
}
function createFlowCapabilityRegistry(deps = {}) {
const {
defaultFlowCapabilities = DEFAULT_FLOW_CAPABILITIES,
defaultFlowId = DEFAULT_FLOW_ID,
defaultPanelCapabilities = DEFAULT_PANEL_CAPABILITIES,
defaultIntegrationTargetCapabilities = DEFAULT_INTEGRATION_TARGET_CAPABILITIES,
flowCapabilities = FLOW_CAPABILITIES,
panelCapabilities = PANEL_CAPABILITIES,
integrationTargetCapabilities = OPENAI_INTEGRATION_TARGET_CAPABILITIES,
} = deps;
const settingsSchema = settingsSchemaApi.createSettingsSchema
? settingsSchemaApi.createSettingsSchema({
@@ -165,21 +171,72 @@
function getFlowCapabilities(flowId) {
const normalizedFlowId = normalizeCapabilityFlowId(flowId, defaultFlowId);
const entry = flowCapabilities[normalizedFlowId] || null;
const supportedIntegrationTargets = normalizedFlowId === 'openai'
? normalizeOpenAiIntegrationTargetList(
entry?.supportedIntegrationTargets || defaultFlowCapabilities.supportedIntegrationTargets
)
: (Array.isArray(entry?.supportedIntegrationTargets)
? entry.supportedIntegrationTargets.map((value) => String(value || '').trim().toLowerCase()).filter(Boolean)
: []);
return {
...defaultFlowCapabilities,
...(entry || {}),
supportsPlatformBinding: normalizePanelModeList(entry?.supportsPlatformBinding || defaultFlowCapabilities.supportsPlatformBinding),
supportedIntegrationTargets,
};
}
function getPanelCapabilities(panelMode) {
const normalizedPanelMode = normalizePanelMode(panelMode);
function getOpenAiIntegrationTargetCapabilities(integrationTargetId) {
const normalizedIntegrationTargetId = normalizeOpenAiIntegrationTargetId(integrationTargetId);
return {
...defaultPanelCapabilities,
...(panelCapabilities[normalizedPanelMode] || {}),
...defaultIntegrationTargetCapabilities,
...(integrationTargetCapabilities[normalizedIntegrationTargetId] || {}),
};
}
function normalizeRequestedIntegrationTargetId(activeFlowId, state = {}, options = {}) {
if (activeFlowId === 'openai') {
return normalizeOpenAiIntegrationTargetId(
options?.integrationTargetId
?? options?.panelMode
?? state?.openaiIntegrationTargetId
?? state?.panelMode,
DEFAULT_OPENAI_INTEGRATION_TARGET_ID
);
}
const rawIntegrationTargetId = activeFlowId === 'kiro'
? (
options?.integrationTargetId
?? state?.kiroIntegrationTargetId
?? state?.kiroSourceId
?? flowRegistryApi.getDefaultIntegrationTargetId?.(activeFlowId)
?? ''
)
: (
options?.integrationTargetId
?? state?.integrationTargetId
?? state?.openaiIntegrationTargetId
?? state?.panelMode
?? state?.kiroIntegrationTargetId
?? state?.kiroSourceId
?? flowRegistryApi.getDefaultIntegrationTargetId?.(activeFlowId)
?? ''
);
if (
isRegisteredFlowId(activeFlowId)
&& typeof flowRegistryApi.normalizeIntegrationTargetId === 'function'
) {
return flowRegistryApi.normalizeIntegrationTargetId(
activeFlowId,
rawIntegrationTargetId,
flowRegistryApi.getDefaultIntegrationTargetId?.(activeFlowId)
);
}
return String(rawIntegrationTargetId || '').trim().toLowerCase();
}
function normalizeChangedKeys(values = []) {
const list = Array.isArray(values) ? values : [];
const seen = new Set();
@@ -195,28 +252,33 @@
return normalized;
}
function resolveEffectiveSourceId(activeFlowId, state = {}, requestedPanelMode = DEFAULT_PANEL_MODE) {
function resolveEffectiveIntegrationTargetId(activeFlowId, state = {}, requestedIntegrationTargetId = DEFAULT_OPENAI_INTEGRATION_TARGET_ID) {
if (!isRegisteredFlowId(activeFlowId)) {
return normalizePanelMode(state?.panelMode || requestedPanelMode, requestedPanelMode || DEFAULT_PANEL_MODE);
return normalizeRequestedIntegrationTargetId(activeFlowId, state, {
integrationTargetId: requestedIntegrationTargetId,
});
}
if (settingsSchema?.getSelectedSourceId) {
const sourceFromSchema = settingsSchema.getSelectedSourceId({
if (settingsSchema?.getSelectedIntegrationTargetId) {
const integrationTargetId = settingsSchema.getSelectedIntegrationTargetId({
...state,
activeFlowId,
}, activeFlowId);
if (sourceFromSchema) {
return sourceFromSchema;
if (integrationTargetId) {
return integrationTargetId;
}
}
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));
if (typeof flowRegistryApi.normalizeIntegrationTargetId === 'function') {
return flowRegistryApi.normalizeIntegrationTargetId(
activeFlowId,
activeFlowId === 'openai'
? (state?.openaiIntegrationTargetId || state?.panelMode || requestedIntegrationTargetId)
: (state?.kiroIntegrationTargetId || state?.kiroSourceId || requestedIntegrationTargetId),
flowRegistryApi.getDefaultIntegrationTargetId?.(activeFlowId)
);
}
return activeFlowId === 'openai'
? normalizePanelMode(state?.panelMode || requestedPanelMode)
: '';
? normalizeOpenAiIntegrationTargetId(requestedIntegrationTargetId)
: String(requestedIntegrationTargetId || '').trim().toLowerCase();
}
function resolveSidepanelCapabilities(options = {}) {
@@ -226,19 +288,25 @@
defaultFlowId
);
const flowState = getFlowCapabilities(activeFlowId);
const requestedPanelMode = normalizePanelMode(
options?.panelMode ?? state?.panelMode,
DEFAULT_PANEL_MODE
const requestedIntegrationTargetId = normalizeRequestedIntegrationTargetId(
activeFlowId,
state,
options
);
const supportedPanelModes = normalizePanelModeList(flowState.supportsPlatformBinding);
const panelModeSupported = supportedPanelModes.length === 0
const supportedIntegrationTargets = activeFlowId === 'openai'
? normalizeOpenAiIntegrationTargetList(flowState.supportedIntegrationTargets)
: (Array.isArray(flowState.supportedIntegrationTargets)
? flowState.supportedIntegrationTargets.slice()
: []);
const integrationTargetSupported = supportedIntegrationTargets.length === 0
? true
: supportedPanelModes.includes(requestedPanelMode);
const effectivePanelMode = panelModeSupported
? requestedPanelMode
: (supportedPanelModes[0] || requestedPanelMode);
const panelState = getPanelCapabilities(effectivePanelMode);
const effectiveSourceId = resolveEffectiveSourceId(activeFlowId, state, effectivePanelMode);
: supportedIntegrationTargets.includes(requestedIntegrationTargetId);
const effectiveIntegrationTargetId = integrationTargetSupported
? requestedIntegrationTargetId
: (supportedIntegrationTargets[0] || requestedIntegrationTargetId);
const integrationTargetState = activeFlowId === 'openai'
? getOpenAiIntegrationTargetCapabilities(effectiveIntegrationTargetId)
: defaultIntegrationTargetCapabilities;
const runtimeLocks = {
autoRunLocked: Boolean(options?.autoRunLocked ?? state?.autoRunLocked),
contributionMode: activeFlowId === 'openai' && flowState.supportsContributionMode && Boolean(state?.contributionMode),
@@ -252,7 +320,7 @@
}
const canSelectPhoneSignup = activeFlowId === 'openai'
&& Boolean(flowState.supportsPhoneSignup)
&& Boolean(panelState.supportsPhoneSignup)
&& Boolean(integrationTargetState.supportsPhoneSignup)
&& runtimeLocks.phoneVerificationEnabled
&& !runtimeLocks.plusModeEnabled
&& !runtimeLocks.contributionMode;
@@ -272,7 +340,7 @@
: effectiveSignupMethods[0]);
const visibleGroupIds = typeof flowRegistryApi.getVisibleGroupIds === 'function'
&& isRegisteredFlowId(activeFlowId)
? flowRegistryApi.getVisibleGroupIds(activeFlowId, effectiveSourceId)
? flowRegistryApi.getVisibleGroupIds(activeFlowId, effectiveIntegrationTargetId)
: [];
return {
@@ -283,33 +351,38 @@
canShowPlusSettings: activeFlowId === 'openai' && Boolean(flowState.supportsPlusMode),
canSwitchFlow: Boolean(flowState.canSwitchFlow),
canUsePhoneSignup: canSelectPhoneSignup,
canUseSelectedPanelMode: panelModeSupported,
effectivePanelMode,
canUseSelectedPanelMode: integrationTargetSupported,
effectiveIntegrationTargetId,
effectivePanelMode: effectiveIntegrationTargetId,
effectiveSignupMethod,
effectiveSignupMethods,
effectiveSourceId,
effectiveSourceId: effectiveIntegrationTargetId,
flowCapabilities: flowState,
panelCapabilities: panelState,
panelMode: effectivePanelMode,
requestedPanelMode,
integrationTargetCapabilities: integrationTargetState,
panelCapabilities: integrationTargetState,
panelMode: effectiveIntegrationTargetId,
requestedIntegrationTargetId,
requestedPanelMode: requestedIntegrationTargetId,
requestedSignupMethod,
runtimeLocks,
shouldWarnCpaPhoneSignup: effectiveSignupMethod === SIGNUP_METHOD_PHONE
&& Boolean(panelState.requiresPhoneSignupWarning),
&& Boolean(integrationTargetState.requiresPhoneSignupWarning),
stepDefinitionOptions: {
activeFlowId,
panelMode: effectivePanelMode,
integrationTargetId: effectiveIntegrationTargetId,
panelMode: effectiveIntegrationTargetId,
plusModeEnabled: runtimeLocks.plusModeEnabled,
signupMethod: effectiveSignupMethod,
},
supportedPanelModes,
supportedIntegrationTargets,
supportedPanelModes: supportedIntegrationTargets,
visibleGroupIds,
};
}
function buildPhoneSignupValidationError(capabilityState = {}) {
const flowState = capabilityState.flowCapabilities || {};
const panelState = capabilityState.panelCapabilities || {};
const integrationTargetState = capabilityState.integrationTargetCapabilities || {};
const runtimeLocks = capabilityState.runtimeLocks || {};
if (!flowState.supportsPhoneSignup) {
@@ -318,10 +391,10 @@
message: '当前 flow 不支持手机号注册。',
};
}
if (!panelState.supportsPhoneSignup) {
if (!integrationTargetState.supportsPhoneSignup) {
return {
code: 'phone_signup_panel_unsupported',
message: `当前来源 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 不支持手机号注册。`,
message: `当前来源 ${getIntegrationTargetLabel(capabilityState.activeFlowId, capabilityState.requestedIntegrationTargetId)} 不支持手机号注册。`,
};
}
if (!runtimeLocks.phoneVerificationEnabled) {
@@ -354,13 +427,13 @@
const errors = [];
if (
Array.isArray(capabilityState.supportedPanelModes)
&& capabilityState.supportedPanelModes.length > 0
Array.isArray(capabilityState.supportedIntegrationTargets)
&& capabilityState.supportedIntegrationTargets.length > 0
&& capabilityState.canUseSelectedPanelMode === false
) {
errors.push({
code: 'panel_mode_unsupported',
message: `当前 flow 不支持 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 来源。`,
message: `当前 flow 不支持 ${getIntegrationTargetLabel(capabilityState.activeFlowId, capabilityState.requestedIntegrationTargetId)} 来源。`,
});
}
@@ -408,15 +481,18 @@
const shouldReconcileSignupMethod = MODE_SWITCH_RELEVANT_KEYS.some((key) => changedKeySet.has(key));
if (
changedKeySet.has('panelMode')
&& Array.isArray(capabilityState.supportedPanelModes)
&& capabilityState.supportedPanelModes.length > 0
(changedKeySet.has('panelMode') || changedKeySet.has('openaiIntegrationTargetId') || changedKeySet.has('kiroIntegrationTargetId'))
&& Array.isArray(capabilityState.supportedIntegrationTargets)
&& capabilityState.supportedIntegrationTargets.length > 0
&& capabilityState.canUseSelectedPanelMode === false
) {
normalizedUpdates.panelMode = capabilityState.effectivePanelMode;
normalizedUpdates.panelMode = capabilityState.effectiveIntegrationTargetId;
normalizedUpdates.openaiIntegrationTargetId = capabilityState.effectiveIntegrationTargetId;
normalizedUpdates.kiroIntegrationTargetId = capabilityState.effectiveIntegrationTargetId;
normalizedUpdates.kiroSourceId = capabilityState.effectiveIntegrationTargetId;
errors.push({
code: 'panel_mode_unsupported',
message: `当前 flow 不支持 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 来源。`,
message: `当前 flow 不支持 ${getIntegrationTargetLabel(capabilityState.activeFlowId, capabilityState.requestedIntegrationTargetId)} 来源。`,
});
}
@@ -480,9 +556,9 @@
return {
canUsePhoneSignup,
getFlowCapabilities,
getPanelCapabilities,
getOpenAiIntegrationTargetCapabilities,
normalizeFlowId,
normalizePanelMode,
normalizeOpenAiIntegrationTargetId,
normalizeSignupMethod,
resolveSidepanelCapabilities,
resolveSignupMethod,
@@ -495,15 +571,15 @@
createFlowCapabilityRegistry,
DEFAULT_FLOW_CAPABILITIES,
DEFAULT_FLOW_ID,
DEFAULT_PANEL_CAPABILITIES,
DEFAULT_PANEL_MODE,
DEFAULT_INTEGRATION_TARGET_CAPABILITIES,
DEFAULT_OPENAI_INTEGRATION_TARGET_ID,
FLOW_CAPABILITIES,
PANEL_CAPABILITIES,
OPENAI_INTEGRATION_TARGET_CAPABILITIES,
SIGNUP_METHOD_EMAIL,
SIGNUP_METHOD_PHONE,
VALID_PANEL_MODES,
VALID_OPENAI_INTEGRATION_TARGET_IDS,
normalizeFlowId,
normalizePanelMode,
normalizeOpenAiIntegrationTargetId,
normalizeSignupMethod,
};
});
+105 -81
View File
@@ -2,11 +2,11 @@
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_OPENAI_INTEGRATION_TARGET_ID = 'cpa';
const DEFAULT_KIRO_INTEGRATION_TARGET_ID = 'kiro-rs';
const DEFAULT_KIRO_PUBLICATION_TARGET_ID = 'kiro-rs';
const DEFAULT_KIRO_RS_URL = 'https://kiro.leftcode.xyz/admin';
const OPENAI_SOURCE_IDS = Object.freeze(['cpa', 'sub2api', 'codex2api']);
const OPENAI_INTEGRATION_TARGET_IDS = Object.freeze(['cpa', 'sub2api', 'codex2api']);
const SHARED_SERVICE_IDS = Object.freeze(['account', 'email', 'proxy']);
const DEFAULT_FLOW_CAPABILITIES = Object.freeze({
@@ -15,7 +15,7 @@
supportsPhoneVerificationSettings: false,
supportsPlusMode: false,
supportsContributionMode: false,
supportsPlatformBinding: [],
supportedIntegrationTargets: [],
supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
canSwitchFlow: true,
@@ -44,7 +44,7 @@
supportsPhoneVerificationSettings: true,
supportsPlusMode: true,
supportsContributionMode: true,
supportsPlatformBinding: [...OPENAI_SOURCE_IDS],
supportedIntegrationTargets: [...OPENAI_INTEGRATION_TARGET_IDS],
supportsLuckmail: true,
supportsOauthTimeoutBudget: true,
stepDefinitionMode: 'openai-dynamic',
@@ -55,24 +55,21 @@
'openai-oauth',
'openai-step6',
],
sources: {
integrationTargets: {
cpa: {
id: 'cpa',
label: 'CPA 面板',
legacyPanelMode: 'cpa',
groups: ['openai-source-cpa'],
groups: ['openai-target-cpa'],
},
sub2api: {
id: 'sub2api',
label: 'SUB2API',
legacyPanelMode: 'sub2api',
groups: ['openai-source-sub2api'],
groups: ['openai-target-sub2api'],
},
codex2api: {
id: 'codex2api',
label: 'Codex2API',
legacyPanelMode: 'codex2api',
groups: ['openai-source-codex2api'],
groups: ['openai-target-codex2api'],
},
},
runtimeSources: {
@@ -206,16 +203,23 @@
services: ['account', 'email', 'proxy'],
capabilities: {
...DEFAULT_FLOW_CAPABILITIES,
supportedIntegrationTargets: [DEFAULT_KIRO_INTEGRATION_TARGET_ID],
stepDefinitionMode: 'kiro-device-auth',
},
baseGroups: [
'kiro-runtime-status',
],
sources: {
integrationTargets: {
'kiro-rs': {
id: 'kiro-rs',
label: 'kiro.rs',
groups: ['kiro-target-kiro-rs'],
},
},
publicationTargets: {
'kiro-rs': {
id: 'kiro-rs',
label: 'kiro.rs',
groups: ['kiro-source-kiro-rs'],
},
},
runtimeSources: {
@@ -280,13 +284,13 @@
label: 'IP 代理',
sectionIds: ['ip-proxy-section'],
},
'openai-source-cpa': {
id: 'openai-source-cpa',
'openai-target-cpa': {
id: 'openai-target-cpa',
label: 'CPA 来源',
rowIds: ['row-vps-url', 'row-vps-password', 'row-local-cpa-step9-mode'],
},
'openai-source-sub2api': {
id: 'openai-source-sub2api',
'openai-target-sub2api': {
id: 'openai-target-sub2api',
label: 'SUB2API 来源',
rowIds: [
'row-sub2api-url',
@@ -297,17 +301,15 @@
'row-sub2api-default-proxy',
],
},
'openai-source-codex2api': {
id: 'openai-source-codex2api',
'openai-target-codex2api': {
id: 'openai-target-codex2api',
label: 'Codex2API 来源',
rowIds: ['row-codex2api-url', 'row-codex2api-admin-key'],
},
'openai-plus': {
id: 'openai-plus',
label: 'Plus',
rowIds: [
'row-plus-mode',
],
rowIds: ['row-plus-mode'],
},
'openai-phone': {
id: 'openai-phone',
@@ -325,8 +327,8 @@
label: '第六步',
rowIds: ['row-step6-cookie-settings'],
},
'kiro-source-kiro-rs': {
id: 'kiro-source-kiro-rs',
'kiro-target-kiro-rs': {
id: 'kiro-target-kiro-rs',
label: 'kiro.rs 配置',
rowIds: ['row-kiro-rs-url', 'row-kiro-rs-key'],
},
@@ -339,16 +341,10 @@
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;
@@ -359,73 +355,92 @@
}
function getFlowDefinition(flowId) {
const normalizedFlowId = normalizeFlowId(flowId);
return FLOW_DEFINITIONS[normalizedFlowId] || FLOW_DEFINITIONS[DEFAULT_FLOW_ID];
return FLOW_DEFINITIONS[normalizeFlowId(flowId)] || FLOW_DEFINITIONS[DEFAULT_FLOW_ID];
}
function getFlowLabel(flowId) {
return getFlowDefinition(flowId)?.label || normalizeFlowId(flowId);
}
function getDefaultSourceId(flowId) {
function getDefaultIntegrationTargetId(flowId) {
return normalizeFlowId(flowId) === 'kiro'
? DEFAULT_KIRO_SOURCE_ID
: DEFAULT_OPENAI_SOURCE_ID;
? DEFAULT_KIRO_INTEGRATION_TARGET_ID
: DEFAULT_OPENAI_INTEGRATION_TARGET_ID;
}
function normalizeOpenAiSourceId(value = '', fallback = DEFAULT_OPENAI_SOURCE_ID) {
function normalizeOpenAiIntegrationTargetId(value = '', fallback = DEFAULT_OPENAI_INTEGRATION_TARGET_ID) {
const normalized = String(value || '').trim().toLowerCase();
if (OPENAI_SOURCE_IDS.includes(normalized)) {
if (OPENAI_INTEGRATION_TARGET_IDS.includes(normalized)) {
return normalized;
}
const fallbackValue = String(fallback || '').trim().toLowerCase();
return OPENAI_SOURCE_IDS.includes(fallbackValue) ? fallbackValue : DEFAULT_OPENAI_SOURCE_ID;
return OPENAI_INTEGRATION_TARGET_IDS.includes(fallbackValue)
? fallbackValue
: DEFAULT_OPENAI_INTEGRATION_TARGET_ID;
}
function normalizeKiroSourceId(value = '', fallback = DEFAULT_KIRO_SOURCE_ID) {
function normalizeKiroIntegrationTargetId(value = '', fallback = DEFAULT_KIRO_INTEGRATION_TARGET_ID) {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === DEFAULT_KIRO_SOURCE_ID) {
if (normalized === DEFAULT_KIRO_INTEGRATION_TARGET_ID) {
return normalized;
}
const fallbackValue = String(fallback || '').trim().toLowerCase();
return fallbackValue === DEFAULT_KIRO_SOURCE_ID ? fallbackValue : DEFAULT_KIRO_SOURCE_ID;
return fallbackValue === DEFAULT_KIRO_INTEGRATION_TARGET_ID
? fallbackValue
: DEFAULT_KIRO_INTEGRATION_TARGET_ID;
}
function normalizeSourceId(flowId, sourceId = '', fallback = undefined) {
function normalizeIntegrationTargetId(flowId, integrationTargetId = '', fallback = undefined) {
const normalizedFlowId = normalizeFlowId(flowId);
if (normalizedFlowId === 'kiro') {
return normalizeKiroSourceId(sourceId, fallback || DEFAULT_KIRO_SOURCE_ID);
return normalizeKiroIntegrationTargetId(
integrationTargetId,
fallback || DEFAULT_KIRO_INTEGRATION_TARGET_ID
);
}
return normalizeOpenAiSourceId(sourceId, fallback || DEFAULT_OPENAI_SOURCE_ID);
return normalizeOpenAiIntegrationTargetId(
integrationTargetId,
fallback || DEFAULT_OPENAI_INTEGRATION_TARGET_ID
);
}
function getSourceDefinitions(flowId) {
return getFlowDefinition(flowId)?.sources || {};
function getIntegrationTargetDefinitions(flowId) {
return getFlowDefinition(flowId)?.integrationTargets || {};
}
function getSourceDefinition(flowId, sourceId) {
function getIntegrationTargetDefinition(flowId, integrationTargetId) {
const normalizedFlowId = normalizeFlowId(flowId);
const normalizedSourceId = normalizeSourceId(normalizedFlowId, sourceId, getDefaultSourceId(normalizedFlowId));
return getSourceDefinitions(normalizedFlowId)[normalizedSourceId] || null;
const normalizedIntegrationTargetId = normalizeIntegrationTargetId(
normalizedFlowId,
integrationTargetId,
getDefaultIntegrationTargetId(normalizedFlowId)
);
return getIntegrationTargetDefinitions(normalizedFlowId)[normalizedIntegrationTargetId] || null;
}
function getSourceOptions(flowId) {
return Object.values(getSourceDefinitions(flowId));
function getIntegrationTargetOptions(flowId) {
return Object.values(getIntegrationTargetDefinitions(flowId));
}
function getSourceLabel(flowId, sourceId) {
return getSourceDefinition(flowId, sourceId)?.label || normalizeSourceId(flowId, sourceId);
function getIntegrationTargetLabel(flowId, integrationTargetId) {
return getIntegrationTargetDefinition(flowId, integrationTargetId)?.label
|| normalizeIntegrationTargetId(flowId, integrationTargetId);
}
function mapPanelModeToSourceId(panelMode = '', fallback = DEFAULT_OPENAI_SOURCE_ID) {
return normalizeOpenAiSourceId(panelMode, fallback);
function getPublicationTargetDefinitions(flowId) {
return getFlowDefinition(flowId)?.publicationTargets || {};
}
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 getPublicationTargetDefinition(flowId, publicationTargetId) {
const normalizedFlowId = normalizeFlowId(flowId);
const normalizedPublicationTargetId = String(
publicationTargetId || (
normalizedFlowId === 'kiro'
? DEFAULT_KIRO_PUBLICATION_TARGET_ID
: ''
)
).trim().toLowerCase();
return getPublicationTargetDefinitions(normalizedFlowId)[normalizedPublicationTargetId] || null;
}
function getFlowCapabilities(flowId) {
@@ -435,18 +450,27 @@
};
}
function getVisibleGroupIds(flowId, sourceId, options = {}) {
function getVisibleGroupIds(flowId, integrationTargetId, options = {}) {
const normalizedFlowId = normalizeFlowId(flowId);
const flowDefinition = getFlowDefinition(normalizedFlowId);
const normalizedSourceId = normalizeSourceId(normalizedFlowId, sourceId, getDefaultSourceId(normalizedFlowId));
const sourceDefinition = getSourceDefinition(normalizedFlowId, normalizedSourceId);
const normalizedIntegrationTargetId = normalizeIntegrationTargetId(
normalizedFlowId,
integrationTargetId,
getDefaultIntegrationTargetId(normalizedFlowId)
);
const integrationTargetDefinition = getIntegrationTargetDefinition(
normalizedFlowId,
normalizedIntegrationTargetId
);
const includeSharedServices = options?.includeSharedServices !== false;
const serviceGroups = includeSharedServices
? (Array.isArray(flowDefinition?.services) ? flowDefinition.services.map((serviceId) => `service-${serviceId}`) : [])
? (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 : []),
...(Array.isArray(integrationTargetDefinition?.groups) ? integrationTargetDefinition.groups : []),
...serviceGroups,
]));
}
@@ -479,33 +503,33 @@
return {
DEFAULT_FLOW_CAPABILITIES,
DEFAULT_FLOW_ID,
DEFAULT_KIRO_INTEGRATION_TARGET_ID,
DEFAULT_KIRO_PUBLICATION_TARGET_ID,
DEFAULT_KIRO_RS_URL,
DEFAULT_KIRO_SOURCE_ID,
DEFAULT_OPENAI_SOURCE_ID,
DEFAULT_OPENAI_INTEGRATION_TARGET_ID,
FLOW_DEFINITIONS,
LEGACY_OPENAI_FLOW_ALIAS,
OPENAI_SOURCE_IDS,
OPENAI_INTEGRATION_TARGET_IDS,
SETTINGS_GROUP_DEFINITIONS,
SHARED_SERVICE_IDS,
getDefaultSourceId,
getDefaultIntegrationTargetId,
getDriverDefinitions,
getFlowCapabilities,
getFlowDefinition,
getFlowLabel,
getIntegrationTargetDefinition,
getIntegrationTargetDefinitions,
getIntegrationTargetLabel,
getIntegrationTargetOptions,
getPublicationTargetDefinition,
getPublicationTargetDefinitions,
getRegisteredFlowIds,
getRuntimeSourceDefinitions,
getSettingsGroupDefinition,
getSettingsGroupDefinitions,
getSourceDefinition,
getSourceDefinitions,
getSourceLabel,
getSourceOptions,
getVisibleGroupIds,
mapPanelModeToSourceId,
mapSourceIdToPanelMode,
normalizeFlowId,
normalizeKiroSourceId,
normalizeOpenAiSourceId,
normalizeSourceId,
normalizeIntegrationTargetId,
normalizeKiroIntegrationTargetId,
normalizeOpenAiIntegrationTargetId,
};
});
+233 -188
View File
@@ -32,9 +32,11 @@
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 defaultFlowId = String(
deps.defaultFlowId || flowRegistry.DEFAULT_FLOW_ID || 'openai'
).trim().toLowerCase() || 'openai';
const defaultOpenAiIntegrationTargetId = flowRegistry.DEFAULT_OPENAI_INTEGRATION_TARGET_ID || 'cpa';
const defaultKiroIntegrationTargetId = flowRegistry.DEFAULT_KIRO_INTEGRATION_TARGET_ID || 'kiro-rs';
const defaultKiroRsUrl = flowRegistry.DEFAULT_KIRO_RS_URL || 'https://kiro.leftcode.xyz/admin';
const normalizeFlowId = typeof flowRegistry.normalizeFlowId === 'function'
? flowRegistry.normalizeFlowId
@@ -42,19 +44,13 @@
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());
const normalizeIntegrationTargetId = typeof flowRegistry.normalizeIntegrationTargetId === 'function'
? flowRegistry.normalizeIntegrationTargetId
: ((_flowId, value = '', fallback = '') => String(value || fallback || '').trim().toLowerCase());
function buildDefaultSettingsState() {
return {
schemaVersion: 3,
schemaVersion: 4,
activeFlowId: defaultFlowId,
services: {
account: {
@@ -71,27 +67,25 @@
},
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: '',
},
integrationTargetId: defaultOpenAiIntegrationTargetId,
integrationTargets: {
cpa: {
vpsUrl: '',
vpsPassword: '',
localCpaStep9Mode: 'submit',
},
sub2api: {
sub2apiUrl: '',
sub2apiEmail: '',
sub2apiPassword: '',
sub2apiGroupName: 'codex',
sub2apiGroupNames: ['codex', 'openai-plus'],
sub2apiAccountPriority: 1,
sub2apiDefaultProxyName: '',
},
codex2api: {
codex2apiUrl: '',
codex2apiAdminKey: '',
},
},
signup: {
@@ -112,21 +106,13 @@
},
},
kiro: {
source: {
selected: defaultKiroSourceId,
entries: {
'kiro-rs': {
kiroRsUrl: defaultKiroRsUrl,
kiroRsKey: '',
},
integrationTargetId: defaultKiroIntegrationTargetId,
integrationTargets: {
'kiro-rs': {
baseUrl: defaultKiroRsUrl,
apiKey: '',
},
},
options: {
kiroRsPriority: 0,
kiroRsEndpoint: '',
kiroRsAuthRegion: '',
kiroRsApiRegion: '',
},
autoRun: {
stepExecutionRange: {
enabled: false,
@@ -139,7 +125,7 @@
};
}
function getSourceValue(settingsState, pathGetter, fallback = {}) {
function getIntegrationTargetValue(settingsState, pathGetter, fallback = {}) {
return cloneValue(pathGetter(isPlainObject(settingsState) ? settingsState : {}) || fallback);
}
@@ -155,20 +141,21 @@
?? defaults.activeFlowId,
defaults.activeFlowId
);
const openaiSelectedSource = normalizeSourceId(
const openaiIntegrationTargetId = normalizeIntegrationTargetId(
'openai',
nested?.flows?.openai?.source?.selected
nested?.flows?.openai?.integrationTargetId
?? input?.openaiIntegrationTargetId
?? input?.panelMode
?? input?.openaiSourceId
?? defaults.flows.openai.source.selected,
defaults.flows.openai.source.selected
?? defaults.flows.openai.integrationTargetId,
defaults.flows.openai.integrationTargetId
);
const kiroSelectedSource = normalizeSourceId(
const kiroIntegrationTargetId = normalizeIntegrationTargetId(
'kiro',
nested?.flows?.kiro?.source?.selected
nested?.flows?.kiro?.integrationTargetId
?? input?.kiroIntegrationTargetId
?? input?.kiroSourceId
?? defaults.flows.kiro.source.selected,
defaults.flows.kiro.source.selected
?? defaults.flows.kiro.integrationTargetId,
defaults.flows.kiro.integrationTargetId
);
const stepExecutionRangeByFlow = isPlainObject(input?.stepExecutionRangeByFlow)
? input.stepExecutionRangeByFlow
@@ -206,58 +193,115 @@
customPassword: String(
input?.customPassword
?? nested?.services?.account?.customPassword
?? nested?.flows?.openai?.account?.customPassword
?? defaults.services.account.customPassword
).trim(),
},
},
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(),
},
integrationTargetId: openaiIntegrationTargetId,
integrationTargets: {
cpa: {
...defaults.flows.openai.integrationTargets.cpa,
...getIntegrationTargetValue(nested, (state) => state.flows?.openai?.integrationTargets?.cpa),
vpsUrl: String(
input?.vpsUrl
?? nested?.flows?.openai?.integrationTargets?.cpa?.vpsUrl
?? ''
).trim(),
vpsPassword: String(
input?.vpsPassword
?? nested?.flows?.openai?.integrationTargets?.cpa?.vpsPassword
?? ''
),
localCpaStep9Mode: String(
input?.localCpaStep9Mode
?? nested?.flows?.openai?.integrationTargets?.cpa?.localCpaStep9Mode
?? defaults.flows.openai.integrationTargets.cpa.localCpaStep9Mode
).trim() || defaults.flows.openai.integrationTargets.cpa.localCpaStep9Mode,
},
sub2api: {
...defaults.flows.openai.integrationTargets.sub2api,
...getIntegrationTargetValue(nested, (state) => state.flows?.openai?.integrationTargets?.sub2api),
sub2apiUrl: String(
input?.sub2apiUrl
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiUrl
?? ''
).trim(),
sub2apiEmail: String(
input?.sub2apiEmail
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiEmail
?? ''
).trim(),
sub2apiPassword: String(
input?.sub2apiPassword
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiPassword
?? ''
),
sub2apiGroupName: String(
input?.sub2apiGroupName
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiGroupName
?? defaults.flows.openai.integrationTargets.sub2api.sub2apiGroupName
).trim() || defaults.flows.openai.integrationTargets.sub2api.sub2apiGroupName,
sub2apiGroupNames: Array.isArray(input?.sub2apiGroupNames)
? input.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: (Array.isArray(nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiGroupNames)
? nested.flows.openai.integrationTargets.sub2api.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: [...defaults.flows.openai.integrationTargets.sub2api.sub2apiGroupNames]),
sub2apiAccountPriority: Math.max(1, Number(
input?.sub2apiAccountPriority
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiAccountPriority
?? defaults.flows.openai.integrationTargets.sub2api.sub2apiAccountPriority
) || defaults.flows.openai.integrationTargets.sub2api.sub2apiAccountPriority),
sub2apiDefaultProxyName: String(
input?.sub2apiDefaultProxyName
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiDefaultProxyName
?? ''
).trim(),
},
codex2api: {
...defaults.flows.openai.integrationTargets.codex2api,
...getIntegrationTargetValue(nested, (state) => state.flows?.openai?.integrationTargets?.codex2api),
codex2apiUrl: String(
input?.codex2apiUrl
?? nested?.flows?.openai?.integrationTargets?.codex2api?.codex2apiUrl
?? ''
).trim(),
codex2apiAdminKey: String(
input?.codex2apiAdminKey
?? nested?.flows?.openai?.integrationTargets?.codex2api?.codex2apiAdminKey
?? ''
).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),
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,
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(
@@ -269,47 +313,25 @@
},
},
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
),
},
integrationTargetId: kiroIntegrationTargetId,
integrationTargets: {
'kiro-rs': {
...defaults.flows.kiro.integrationTargets['kiro-rs'],
...getIntegrationTargetValue(nested, (state) => state.flows?.kiro?.integrationTargets?.['kiro-rs']),
baseUrl: String(
input?.kiroRsUrl
?? input?.kiroRsBaseUrl
?? nested?.flows?.kiro?.integrationTargets?.['kiro-rs']?.baseUrl
?? defaults.flows.kiro.integrationTargets['kiro-rs'].baseUrl
).trim() || defaults.flows.kiro.integrationTargets['kiro-rs'].baseUrl,
apiKey: String(
input?.kiroRsKey
?? input?.kiroRsApiKey
?? nested?.flows?.kiro?.integrationTargets?.['kiro-rs']?.apiKey
?? defaults.flows.kiro.integrationTargets['kiro-rs'].apiKey
),
},
},
options: {
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
@@ -323,6 +345,53 @@
};
}
function mergeSettingsState(baseValue = {}, patchValue = {}) {
const baseSettingsState = normalizeSettingsState(baseValue);
const patchSettingsState = normalizeSettingsState({
settingsState: patchValue,
activeFlowId: patchValue?.activeFlowId ?? baseSettingsState.activeFlowId,
});
function mergeRecursive(baseNode, patchNode) {
if (Array.isArray(patchNode)) {
return patchNode.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchNode)) {
return patchNode === undefined ? cloneValue(baseNode) : patchNode;
}
const next = {
...cloneValue(isPlainObject(baseNode) ? baseNode : {}),
};
Object.entries(patchNode).forEach(([key, value]) => {
next[key] = mergeRecursive(baseNode?.[key], value);
});
return next;
}
return normalizeSettingsState({
settingsState: mergeRecursive(baseSettingsState, patchSettingsState),
});
}
function getFlowSettings(settingsState = {}, flowId) {
const normalizedState = normalizeSettingsState(settingsState);
const normalizedFlowId = normalizeFlowId(flowId, normalizedState.activeFlowId);
return cloneValue(normalizedState?.flows?.[normalizedFlowId] || {});
}
function getSelectedIntegrationTargetId(settingsState = {}, flowId) {
const normalizedState = normalizeSettingsState(settingsState);
const normalizedFlowId = normalizeFlowId(flowId, normalizedState.activeFlowId);
const flowSettings = normalizedState?.flows?.[normalizedFlowId] || {};
return normalizeIntegrationTargetId(
normalizedFlowId,
flowSettings?.integrationTargetId,
normalizedFlowId === 'kiro'
? defaultKiroIntegrationTargetId
: defaultOpenAiIntegrationTargetId
);
}
function buildStepExecutionRangeByFlow(settingsState = {}) {
const normalizedState = normalizeSettingsState(settingsState);
return {
@@ -337,23 +406,7 @@
};
}
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 = {}) {
function buildSettingsView(settingsState = {}, baseInput = {}) {
const normalizedState = normalizeSettingsState(settingsState);
const next = {
...(isPlainObject(baseInput) ? cloneValue(baseInput) : {}),
@@ -361,20 +414,22 @@
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.openaiIntegrationTargetId = getSelectedIntegrationTargetId(normalizedState, 'openai');
next.kiroIntegrationTargetId = getSelectedIntegrationTargetId(normalizedState, 'kiro');
next.panelMode = next.openaiIntegrationTargetId;
next.kiroSourceId = next.kiroIntegrationTargetId;
next.vpsUrl = openaiState.integrationTargets.cpa.vpsUrl;
next.vpsPassword = openaiState.integrationTargets.cpa.vpsPassword;
next.localCpaStep9Mode = openaiState.integrationTargets.cpa.localCpaStep9Mode;
next.sub2apiUrl = openaiState.integrationTargets.sub2api.sub2apiUrl;
next.sub2apiEmail = openaiState.integrationTargets.sub2api.sub2apiEmail;
next.sub2apiPassword = openaiState.integrationTargets.sub2api.sub2apiPassword;
next.sub2apiGroupName = openaiState.integrationTargets.sub2api.sub2apiGroupName;
next.sub2apiGroupNames = cloneValue(openaiState.integrationTargets.sub2api.sub2apiGroupNames);
next.sub2apiAccountPriority = openaiState.integrationTargets.sub2api.sub2apiAccountPriority;
next.sub2apiDefaultProxyName = openaiState.integrationTargets.sub2api.sub2apiDefaultProxyName;
next.codex2apiUrl = openaiState.integrationTargets.codex2api.codex2apiUrl;
next.codex2apiAdminKey = openaiState.integrationTargets.codex2api.codex2apiAdminKey;
next.customPassword = normalizedState.services.account.customPassword;
next.signupMethod = openaiState.signup.signupMethod;
next.phoneVerificationEnabled = openaiState.signup.phoneVerificationEnabled;
@@ -385,13 +440,8 @@
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.kiroRsPriority = kiroState.options.kiroRsPriority;
next.kiroRsEndpoint = kiroState.options.kiroRsEndpoint;
next.kiroRsAuthRegion = kiroState.options.kiroRsAuthRegion;
next.kiroRsApiRegion = kiroState.options.kiroRsApiRegion;
delete next.kiroRegion;
next.kiroRsUrl = kiroState.integrationTargets['kiro-rs'].baseUrl;
next.kiroRsKey = kiroState.integrationTargets['kiro-rs'].apiKey;
next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState);
next.settingsSchemaVersion = normalizedState.schemaVersion;
next.settingsState = cloneValue(normalizedState);
@@ -401,34 +451,29 @@
function getFlowInputState(settingsState = {}, flowId) {
const normalizedState = normalizeSettingsState(settingsState);
const normalizedFlowId = normalizeFlowId(flowId, normalizedState.activeFlowId);
const integrationTargetId = getSelectedIntegrationTargetId(normalizedState, normalizedFlowId);
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,
integrationTargetId,
kiroRsUrl: normalizedState.flows.kiro.integrationTargets['kiro-rs'].baseUrl,
kiroRsKey: normalizedState.flows.kiro.integrationTargets['kiro-rs'].apiKey,
};
}
return {
activeFlowId: normalizedFlowId,
sourceId: getSelectedSourceId(normalizedState, 'openai'),
panelMode: mapSourceIdToPanelMode('openai', normalizedState.flows.openai.source.selected, defaultOpenAiSourceId),
integrationTargetId,
};
}
function normalizeFlatInput(input = {}) {
const state = normalizeSettingsState(input);
return buildLegacySettingsPayload(state, input);
}
return {
buildDefaultSettingsState,
buildLegacySettingsPayload,
buildSettingsView,
buildStepExecutionRangeByFlow,
getFlowInputState,
getFlowSettings,
getSelectedSourceId,
normalizeFlatInput,
getSelectedIntegrationTargetId,
mergeSettingsState,
normalizeSettingsState,
};
}
+3 -3
View File
@@ -683,10 +683,10 @@
</label>
</div>
<div class="setting-group setting-group-secondary step-range-controls">
<span class="setting-caption">步骤</span>
<input type="number" id="input-step-execution-range-from" class="data-input auto-delay-input step-range-input" value="1" min="1" step="1" title="允许执行的起始步骤" />
<span class="setting-caption">节点</span>
<select id="input-step-execution-range-from" class="data-select step-range-input" title="允许执行的起始节点"></select>
<span class="data-unit"></span>
<input type="number" id="input-step-execution-range-to" class="data-input auto-delay-input step-range-input" value="1" min="1" step="1" title="允许执行的结束步骤" />
<select id="input-step-execution-range-to" class="data-select step-range-input" title="允许执行的结束节点"></select>
</div>
</div>
</div>
+81 -31
View File
@@ -884,18 +884,17 @@ function getWorkflowNodesForMode(plusModeEnabled = false, options = {}) {
});
if (Array.isArray(nodes) && nodes.length) {
return nodes.slice().sort((left, right) => {
const leftOrder = Number.isFinite(Number(left.displayOrder)) ? Number(left.displayOrder) : Number(left.legacyStepId);
const rightOrder = Number.isFinite(Number(right.displayOrder)) ? Number(right.displayOrder) : Number(right.legacyStepId);
const leftOrder = Number.isFinite(Number(left.displayOrder)) ? Number(left.displayOrder) : 0;
const rightOrder = Number.isFinite(Number(right.displayOrder)) ? Number(right.displayOrder) : 0;
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
return String(left.nodeId || '').localeCompare(String(right.nodeId || ''));
});
}
return getStepDefinitionsForMode(plusModeEnabled, options).map((step) => ({
legacyStepId: Number(step.id),
nodeId: String(step.key || '').trim(),
title: step.title,
displayOrder: Number.isFinite(Number(step.order)) ? Number(step.order) : Number(step.id),
displayOrder: Number.isFinite(Number(step.id)) ? Number(step.id) : Number(step.order),
executeKey: String(step.key || '').trim(),
})).filter((node) => node.nodeId);
}
@@ -911,7 +910,7 @@ function getStepIdByKeyForCurrentMode(stepKey = '') {
function getNodeIdByStepForCurrentMode(step) {
const numericStep = Number(step);
const node = (workflowNodes || []).find((candidate) => Number(candidate?.legacyStepId) === numericStep);
const node = (workflowNodes || []).find((candidate) => Number(candidate?.displayOrder) === numericStep);
if (node?.nodeId) {
return String(node.nodeId).trim();
}
@@ -925,9 +924,9 @@ function getStepIdByNodeIdForCurrentMode(nodeId = '') {
return 0;
}
const node = (workflowNodes || []).find((candidate) => String(candidate?.nodeId || '').trim() === normalizedNodeId);
const legacyStepId = Number(node?.legacyStepId);
if (Number.isInteger(legacyStepId) && legacyStepId > 0) {
return legacyStepId;
const displayOrder = Number(node?.displayOrder);
if (Number.isInteger(displayOrder) && displayOrder > 0) {
return displayOrder;
}
return getStepIdByKeyForCurrentMode(normalizedNodeId);
}
@@ -970,10 +969,9 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled,
})
: stepDefinitions.map((step) => ({
legacyStepId: Number(step.id),
nodeId: String(step.key || step.id || '').trim(),
title: step.title,
displayOrder: Number.isFinite(Number(step.order)) ? Number(step.order) : Number(step.id),
displayOrder: Number.isFinite(Number(step.id)) ? Number(step.id) : Number(step.order),
}));
if (typeof workflowNodes !== 'undefined') {
workflowNodes = nextWorkflowNodes;
@@ -2436,24 +2434,76 @@ function getLastCurrentStepId() {
return STEP_IDS.length ? Math.max(...STEP_IDS) : 1;
}
function getStepExecutionRangeNodes() {
return Array.isArray(workflowNodes)
? workflowNodes.filter((node) => String(node?.nodeId || '').trim())
: [];
}
function getStepExecutionRangeNodeLabel(node = {}) {
const nodeId = String(node?.nodeId || '').trim();
const displayOrder = Number(node?.displayOrder);
const title = String(node?.title || nodeId).trim();
const orderLabel = Number.isInteger(displayOrder) && displayOrder > 0
? `步骤 ${displayOrder}`
: nodeId;
return title ? `${orderLabel} · ${title}` : orderLabel;
}
function getStepExecutionRangeBoundaryNodeId(stepNumber, boundary = 'start') {
const nodes = getStepExecutionRangeNodes();
const fallbackNode = boundary === 'end' ? nodes[nodes.length - 1] : nodes[0];
const resolvedNodeId = getNodeIdByStepForCurrentMode(stepNumber);
return String(resolvedNodeId || fallbackNode?.nodeId || '').trim();
}
function syncStepExecutionRangeSelectOptions(selectedFromNodeId = '', selectedToNodeId = '') {
const nodes = getStepExecutionRangeNodes();
const fromSelect = inputStepExecutionRangeFrom;
const toSelect = inputStepExecutionRangeTo;
if (!fromSelect || !toSelect) {
return;
}
const buildOptions = (selectedValue) => nodes.map((node) => {
const nodeId = String(node?.nodeId || '').trim();
const label = getStepExecutionRangeNodeLabel(node);
return `<option value="${escapeHtml(nodeId)}"${nodeId === selectedValue ? ' selected' : ''}>${escapeHtml(label)}</option>`;
}).join('');
fromSelect.innerHTML = buildOptions(String(selectedFromNodeId || nodes[0]?.nodeId || '').trim());
toSelect.innerHTML = buildOptions(String(selectedToNodeId || nodes[nodes.length - 1]?.nodeId || '').trim());
}
function isStepExecutionRangeUiAvailable(state = latestState) {
return getCurrentStepExecutionRangeFlowId(state) === DEFAULT_ACTIVE_FLOW_ID;
}
function clampStepExecutionRangeInputs() {
const maxStep = getLastCurrentStepId();
const fromStep = Math.min(maxStep, Math.max(1, normalizePositiveStepNumber(inputStepExecutionRangeFrom?.value, 1)));
const toStep = Math.min(maxStep, Math.max(1, normalizePositiveStepNumber(inputStepExecutionRangeTo?.value, maxStep)));
const normalizedFrom = Math.min(fromStep, toStep);
const normalizedTo = Math.max(fromStep, toStep);
const nodes = getStepExecutionRangeNodes();
const firstNodeId = String(nodes[0]?.nodeId || '').trim();
const lastNodeId = String(nodes[nodes.length - 1]?.nodeId || '').trim();
const selectedFromNodeId = String(inputStepExecutionRangeFrom?.value || firstNodeId).trim() || firstNodeId;
const selectedToNodeId = String(inputStepExecutionRangeTo?.value || lastNodeId).trim() || lastNodeId;
const fromStep = Math.max(1, getStepIdByNodeIdForCurrentMode(selectedFromNodeId) || 1);
const toStep = Math.max(1, getStepIdByNodeIdForCurrentMode(selectedToNodeId) || fromStep);
const normalizedFromStep = Math.min(fromStep, toStep);
const normalizedToStep = Math.max(fromStep, toStep);
const normalizedFromNodeId = getStepExecutionRangeBoundaryNodeId(normalizedFromStep, 'start');
const normalizedToNodeId = getStepExecutionRangeBoundaryNodeId(normalizedToStep, 'end');
syncStepExecutionRangeSelectOptions(normalizedFromNodeId, normalizedToNodeId);
if (inputStepExecutionRangeFrom) {
inputStepExecutionRangeFrom.max = String(maxStep);
inputStepExecutionRangeFrom.value = String(normalizedFrom);
inputStepExecutionRangeFrom.value = normalizedFromNodeId;
}
if (inputStepExecutionRangeTo) {
inputStepExecutionRangeTo.max = String(maxStep);
inputStepExecutionRangeTo.value = String(normalizedTo);
inputStepExecutionRangeTo.value = normalizedToNodeId;
}
return {
fromNodeId: normalizedFromNodeId,
toNodeId: normalizedToNodeId,
fromStep: normalizedFromStep,
toStep: normalizedToStep,
};
}
function buildStepExecutionRangeByFlowPayload(existingConfig = latestState?.stepExecutionRangeByFlow || {}) {
@@ -2461,12 +2511,12 @@ function buildStepExecutionRangeByFlowPayload(existingConfig = latestState?.step
if (!isStepExecutionRangeUiAvailable(latestState)) {
return config;
}
clampStepExecutionRangeInputs();
const normalizedRange = clampStepExecutionRangeInputs();
const flowId = getCurrentStepExecutionRangeFlowId(latestState);
config[flowId] = normalizeStepExecutionRangeEntry({
enabled: Boolean(inputStepExecutionRangeEnabled?.checked),
fromStep: inputStepExecutionRangeFrom?.value,
toStep: inputStepExecutionRangeTo?.value,
fromStep: normalizedRange?.fromStep,
toStep: normalizedRange?.toStep,
});
return config;
}
@@ -2493,17 +2543,15 @@ function applyStepExecutionRangeState(state = latestState) {
}
const available = isStepExecutionRangeUiAvailable(state);
rowStepExecutionRange.style.display = available ? '' : 'none';
const maxStep = getLastCurrentStepId();
const range = getStepExecutionRangeForCurrentFlow(state);
const fromNodeId = getStepExecutionRangeBoundaryNodeId(range.fromStep, 'start');
const toNodeId = getStepExecutionRangeBoundaryNodeId(range.toStep, 'end');
syncStepExecutionRangeSelectOptions(fromNodeId, toNodeId);
if (inputStepExecutionRangeFrom) {
inputStepExecutionRangeFrom.min = '1';
inputStepExecutionRangeFrom.max = String(maxStep);
inputStepExecutionRangeFrom.value = String(Math.min(maxStep, Math.max(1, normalizePositiveStepNumber(range.fromStep, 1))));
inputStepExecutionRangeFrom.value = fromNodeId;
}
if (inputStepExecutionRangeTo) {
inputStepExecutionRangeTo.min = '1';
inputStepExecutionRangeTo.max = String(maxStep);
inputStepExecutionRangeTo.value = String(Math.min(maxStep, Math.max(1, normalizePositiveStepNumber(range.toStep, maxStep))));
inputStepExecutionRangeTo.value = toNodeId;
}
if (inputStepExecutionRangeEnabled) {
inputStepExecutionRangeEnabled.checked = Boolean(range.enabled);
@@ -14932,14 +14980,16 @@ selectFlow?.addEventListener('change', () => {
});
[inputStepExecutionRangeFrom, inputStepExecutionRangeTo].forEach((input) => {
input?.addEventListener('input', () => {
const handleRangeChange = () => {
const stepExecutionRangeByFlow = buildStepExecutionRangeByFlowPayload(latestState?.stepExecutionRangeByFlow);
syncLatestState({ stepExecutionRangeByFlow });
markSettingsDirty(true);
renderStepStatuses(latestState);
updateButtonStates();
scheduleSettingsAutoSave();
});
};
input?.addEventListener('input', handleRangeChange);
input?.addEventListener('change', handleRangeChange);
input?.addEventListener('blur', () => {
clampStepExecutionRangeInputs();
saveSettings({ silent: true }).catch(() => { });
@@ -124,8 +124,8 @@ test('runtime-state patch accepts nested flow and node updates without legacy st
assert.equal(patch.activeFlowId, 'openai');
assert.equal(patch.activeRunId, 'run-001');
assert.equal(patch.currentNodeId, 'oauth-login');
assert.equal(patch.oauthUrl, 'https://new.example.com/start');
assert.equal(patch.plusCheckoutTabId, 99);
assert.equal(Object.prototype.hasOwnProperty.call(patch, 'oauthUrl'), false);
assert.equal(Object.prototype.hasOwnProperty.call(patch, 'plusCheckoutTabId'), false);
assert.equal(Object.prototype.hasOwnProperty.call(patch, 'currentStep'), false);
assert.equal(Object.prototype.hasOwnProperty.call(patch, 'stepStatuses'), false);
assert.deepStrictEqual(patch.nodeStatuses, {
@@ -135,6 +135,10 @@ test('runtime-state patch accepts nested flow and node updates without legacy st
});
assert.equal(patch.runtimeState.flowState.openai.auth.oauthUrl, 'https://new.example.com/start');
assert.equal(patch.runtimeState.flowState.openai.plus.plusCheckoutTabId, 99);
const view = helpers.buildStateView(patch);
assert.equal(view.oauthUrl, 'https://new.example.com/start');
assert.equal(view.plusCheckoutTabId, 99);
});
test('runtime-state patch prefers explicit activeFlowId over stale legacy flowId', () => {
@@ -57,6 +57,39 @@ ${flowRegistrySource}
${settingsSchemaSource}
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const DEFAULT_SUB2API_GROUP_NAMES = ['codex', 'openai-plus'];
const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'activeFlowId',
'openaiIntegrationTargetId',
'kiroIntegrationTargetId',
'panelMode',
'kiroSourceId',
'vpsUrl',
'vpsPassword',
'localCpaStep9Mode',
'sub2apiUrl',
'sub2apiEmail',
'sub2apiPassword',
'sub2apiGroupName',
'sub2apiGroupNames',
'sub2apiAccountPriority',
'sub2apiDefaultProxyName',
'codex2apiUrl',
'codex2apiAdminKey',
'customPassword',
'signupMethod',
'phoneVerificationEnabled',
'phoneSignupReloginAfterBindEmailEnabled',
'plusModeEnabled',
'plusPaymentMethod',
'mailProvider',
'ipProxyEnabled',
'ipProxyService',
'ipProxyMode',
'kiroRsUrl',
'kiroRsKey',
'stepExecutionRangeByFlow',
]);
const SETTINGS_SCHEMA_VIEW_KEY_SET = new Set(SETTINGS_SCHEMA_VIEW_KEYS);
const PERSISTED_SETTING_DEFAULTS = {
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
panelMode: 'cpa',
@@ -77,6 +110,9 @@ 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 isPlainObjectValue(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function normalizePanelMode(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'sub2api' || normalized === 'codex2api' ? normalized : 'cpa';
@@ -131,6 +167,8 @@ function resolveSignupMethod(state = {}) {
function resolveLegacyAutoStepDelaySeconds() { return undefined; }
${extractFunction('normalizePersistentSettingValue')}
${extractFunction('getSettingsSchemaApi')}
${extractFunction('projectSettingsSchemaView')}
${extractFunction('buildPersistedSettingsStoragePayload')}
${extractFunction('buildPersistentSettingsPayload')}
${extractFunction('getPersistedSettings')}
${extractFunction('setPersistentSettings')}
@@ -141,6 +179,7 @@ return {
setPersistentSettings,
getRequestedKeys: typeof getRequestedKeys === 'function' ? getRequestedKeys : () => [],
getPersistedWrites: typeof getPersistedWrites === 'function' ? getPersistedWrites : () => [],
getRemovedKeys: typeof getRemovedKeys === 'function' ? getRemovedKeys : () => [],
};
`)();
}
@@ -159,26 +198,50 @@ test('buildPersistentSettingsPayload writes canonical settings schema into persi
assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(payload.kiroRsKey, 'secret-key');
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
assert.equal(payload.settingsSchemaVersion, 3);
assert.equal(payload.settingsSchemaVersion, 4);
assert.equal(payload.settingsState.activeFlowId, 'kiro');
assert.equal(payload.settingsState.flows.kiro.source.selected, 'kiro-rs');
assert.equal(payload.settingsState.flows.kiro.integrationTargetId, 'kiro-rs');
assert.equal(
payload.settingsState.flows.kiro.integrationTargets['kiro-rs'].baseUrl,
'https://kiro.example.com/admin'
);
});
test('buildPersistentSettingsPayload accepts schema-only input when requireKnownKeys is enabled', () => {
const api = buildHarness();
const payload = api.buildPersistentSettingsPayload({
settingsSchemaVersion: 3,
settingsSchemaVersion: 4,
settingsState: {
activeFlowId: 'kiro',
services: {
account: { customPassword: '' },
email: { provider: '163' },
proxy: { enabled: false, provider: '711proxy', mode: 'account' },
},
flows: {
openai: {
source: { selected: 'cpa', entries: {} },
account: { customPassword: '' },
integrationTargetId: 'cpa',
integrationTargets: {
cpa: {
vpsUrl: '',
vpsPassword: '',
localCpaStep9Mode: 'submit',
},
sub2api: {
sub2apiUrl: '',
sub2apiEmail: '',
sub2apiPassword: '',
sub2apiGroupName: 'codex',
sub2apiGroupNames: ['codex', 'openai-plus'],
sub2apiAccountPriority: 1,
sub2apiDefaultProxyName: '',
},
codex2api: {
codex2apiUrl: '',
codex2apiAdminKey: '',
},
},
signup: {
signupMethod: 'email',
phoneVerificationEnabled: false,
@@ -193,17 +256,13 @@ test('buildPersistentSettingsPayload accepts schema-only input when requireKnown
},
},
kiro: {
source: {
selected: 'kiro-rs',
entries: {
'kiro-rs': {
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'schema-only-key',
},
integrationTargetId: 'kiro-rs',
integrationTargets: {
'kiro-rs': {
baseUrl: 'https://kiro.example.com/admin',
apiKey: 'schema-only-key',
},
},
options: {
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 7 },
},
@@ -217,7 +276,7 @@ test('buildPersistentSettingsPayload accepts schema-only input when requireKnown
assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(payload.kiroRsKey, 'schema-only-key');
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
assert.equal(payload.settingsSchemaVersion, 3);
assert.equal(payload.settingsSchemaVersion, 4);
});
test('getPersistedSettings reads schema keys alongside legacy flat settings keys', async () => {
@@ -242,7 +301,7 @@ function getRequestedKeys() {
assert.ok(api.getRequestedKeys().includes('settingsSchemaVersion'));
assert.ok(api.getRequestedKeys().includes('settingsState'));
assert.equal(state.settingsSchemaVersion, 3);
assert.equal(state.settingsSchemaVersion, 4);
assert.equal(state.settingsState.activeFlowId, 'openai');
});
@@ -253,20 +312,37 @@ const chrome = {
local: {
async get() {
return {
settingsSchemaVersion: 3,
settingsSchemaVersion: 4,
settingsState: {
activeFlowId: 'kiro',
services: {
account: { customPassword: '' },
email: { provider: 'hotmail' },
proxy: { enabled: true, provider: '711proxy', mode: 'account' },
},
flows: {
openai: {
source: {
selected: 'sub2api',
entries: {},
integrationTargetId: 'sub2api',
integrationTargets: {
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,
@@ -281,17 +357,13 @@ const chrome = {
},
},
kiro: {
source: {
selected: 'kiro-rs',
entries: {
'kiro-rs': {
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'stored-key',
},
integrationTargetId: 'kiro-rs',
integrationTargets: {
'kiro-rs': {
baseUrl: 'https://kiro.example.com/admin',
apiKey: 'stored-key',
},
},
options: {
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 7 },
},
@@ -324,12 +396,16 @@ const chrome = {
test('setPersistentSettings materializes canonical schema keys for schema-only updates', async () => {
const api = buildHarness(`
const persistedWrites = [];
const removedKeys = [];
const chrome = {
storage: {
local: {
async get() {
return {};
},
async remove(keys) {
removedKeys.push(...(Array.isArray(keys) ? keys : [keys]));
},
async set(payload) {
persistedWrites.push(JSON.parse(JSON.stringify(payload)));
},
@@ -339,20 +415,43 @@ const chrome = {
function getPersistedWrites() {
return persistedWrites;
}
function getRemovedKeys() {
return removedKeys;
}
`);
const persisted = await api.setPersistentSettings({
settingsSchemaVersion: 3,
settingsSchemaVersion: 4,
settingsState: {
activeFlowId: 'kiro',
services: {
account: { customPassword: '' },
email: { provider: '163' },
proxy: { enabled: false, provider: '711proxy', mode: 'account' },
},
flows: {
openai: {
source: { selected: 'cpa', entries: {} },
account: { customPassword: '' },
integrationTargetId: 'cpa',
integrationTargets: {
cpa: {
vpsUrl: '',
vpsPassword: '',
localCpaStep9Mode: 'submit',
},
sub2api: {
sub2apiUrl: '',
sub2apiEmail: '',
sub2apiPassword: '',
sub2apiGroupName: 'codex',
sub2apiGroupNames: ['codex', 'openai-plus'],
sub2apiAccountPriority: 1,
sub2apiDefaultProxyName: '',
},
codex2api: {
codex2apiUrl: '',
codex2apiAdminKey: '',
},
},
signup: {
signupMethod: 'email',
phoneVerificationEnabled: false,
@@ -367,17 +466,13 @@ function getPersistedWrites() {
},
},
kiro: {
source: {
selected: 'kiro-rs',
entries: {
'kiro-rs': {
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'nested-only-key',
},
integrationTargetId: 'kiro-rs',
integrationTargets: {
'kiro-rs': {
baseUrl: 'https://kiro.example.com/admin',
apiKey: 'nested-only-key',
},
},
options: {
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 7 },
},
@@ -392,11 +487,14 @@ function getPersistedWrites() {
assert.equal(persisted.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(persisted.kiroRsKey, 'nested-only-key');
assert.equal(Object.prototype.hasOwnProperty.call(persisted, 'kiroRegion'), false);
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(persisted.settingsSchemaVersion, 4);
assert.equal(Object.prototype.hasOwnProperty.call(write, 'activeFlowId'), false);
assert.equal(Object.prototype.hasOwnProperty.call(write, 'kiroRsUrl'), false);
assert.equal(Object.prototype.hasOwnProperty.call(write, 'kiroRsKey'), false);
assert.equal(Object.prototype.hasOwnProperty.call(write, 'kiroRegion'), false);
assert.equal(write.settingsSchemaVersion, 3);
assert.equal(write.settingsSchemaVersion, 4);
assert.equal(write.settingsState.activeFlowId, 'kiro');
assert.equal(write.settingsState.flows.kiro.integrationTargetId, 'kiro-rs');
assert.ok(api.getRemovedKeys().includes('panelMode'));
assert.ok(api.getRemovedKeys().includes('kiroRsUrl'));
});
@@ -8,9 +8,8 @@ test('background node registry preserves node metadata even before an executor i
const registry = api.createNodeRegistry([
{
flowId: 'kiro',
legacyStepId: 1,
nodeId: 'kiro-start-device-login',
displayOrder: 10,
displayOrder: 1,
executeKey: 'kiro-start-device-login',
title: 'Start device login',
},
@@ -19,7 +18,7 @@ test('background node registry preserves node metadata even before an executor i
const node = registry.getNodeDefinition('kiro-start-device-login');
assert.equal(node.flowId, 'kiro');
assert.equal(node.legacyStepId, 1);
assert.equal(node.displayOrder, 1);
assert.equal(node.title, 'Start device login');
assert.throws(
() => registry.executeNode('kiro-start-device-login', {}),
@@ -34,9 +33,8 @@ test('background node registry executes registered nodes in display order', asyn
const registry = api.createNodeRegistry([
{
flowId: 'openai',
legacyStepId: 2,
displayOrder: 2,
nodeId: 'submit-signup-email',
displayOrder: 20,
executeKey: 'submit-signup-email',
title: 'Submit signup email',
execute: async (state) => {
@@ -45,9 +43,8 @@ test('background node registry executes registered nodes in display order', asyn
},
{
flowId: 'openai',
legacyStepId: 1,
displayOrder: 1,
nodeId: 'open-chatgpt',
displayOrder: 10,
executeKey: 'open-chatgpt',
title: 'Open ChatGPT',
},
+18 -15
View File
@@ -21,7 +21,7 @@ test('flow capability registry keeps OpenAI phone signup available only when run
const enabledState = registry.resolveSidepanelCapabilities({
state: {
activeFlowId: 'openai',
panelMode: 'cpa',
openaiIntegrationTargetId: 'cpa',
phoneVerificationEnabled: true,
plusModeEnabled: false,
contributionMode: false,
@@ -37,7 +37,7 @@ test('flow capability registry keeps OpenAI phone signup available only when run
const plusLockedState = registry.resolveSidepanelCapabilities({
state: {
activeFlowId: 'openai',
panelMode: 'sub2api',
openaiIntegrationTargetId: 'sub2api',
phoneVerificationEnabled: true,
plusModeEnabled: true,
contributionMode: false,
@@ -58,7 +58,7 @@ test('flow capability registry defaults unknown flows to minimal non-phone capab
const capabilityState = registry.resolveSidepanelCapabilities({
state: {
activeFlowId: 'site-a',
panelMode: 'codex2api',
openaiIntegrationTargetId: 'codex2api',
phoneVerificationEnabled: true,
plusModeEnabled: true,
contributionMode: true,
@@ -73,7 +73,7 @@ test('flow capability registry defaults unknown flows to minimal non-phone capab
assert.equal(capabilityState.canUsePhoneSignup, false);
assert.equal(capabilityState.effectiveSignupMethod, 'email');
assert.equal(capabilityState.panelMode, 'codex2api');
assert.deepEqual(capabilityState.supportedPanelModes, []);
assert.deepEqual(capabilityState.supportedIntegrationTargets, []);
});
test('flow capability registry exposes Kiro as an independent flow with its own visible groups', () => {
@@ -83,8 +83,8 @@ test('flow capability registry exposes Kiro as an independent flow with its own
const capabilityState = registry.resolveSidepanelCapabilities({
state: {
activeFlowId: 'kiro',
kiroSourceId: 'kiro-rs',
panelMode: 'sub2api',
kiroIntegrationTargetId: 'kiro-rs',
openaiIntegrationTargetId: 'sub2api',
signupMethod: 'phone',
plusModeEnabled: true,
phoneVerificationEnabled: true,
@@ -95,21 +95,21 @@ test('flow capability registry exposes Kiro as an independent flow with its own
assert.equal(capabilityState.canShowPhoneSettings, false);
assert.equal(capabilityState.canShowPlusSettings, false);
assert.equal(capabilityState.effectiveSignupMethod, 'email');
assert.equal(capabilityState.effectiveSourceId, 'kiro-rs');
assert.equal(capabilityState.effectiveIntegrationTargetId, 'kiro-rs');
assert.deepEqual(
capabilityState.visibleGroupIds,
['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-account', 'service-email', 'service-proxy']
['kiro-runtime-status', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
);
});
test('flow capability registry exposes shared auto-run validation for phone locks and panel support', () => {
test('flow capability registry exposes shared auto-run validation for phone locks and target support', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry({
flowCapabilities: {
openai: api.FLOW_CAPABILITIES.openai,
'site-a': {
...api.DEFAULT_FLOW_CAPABILITIES,
supportsPlatformBinding: ['cpa'],
supportedIntegrationTargets: ['cpa'],
},
},
});
@@ -117,7 +117,7 @@ test('flow capability registry exposes shared auto-run validation for phone lock
const plusLockedResult = registry.validateAutoRunStart({
state: {
activeFlowId: 'openai',
panelMode: 'cpa',
openaiIntegrationTargetId: 'cpa',
signupMethod: 'phone',
phoneVerificationEnabled: true,
plusModeEnabled: true,
@@ -131,7 +131,7 @@ test('flow capability registry exposes shared auto-run validation for phone lock
const unsupportedPanelResult = registry.validateAutoRunStart({
state: {
activeFlowId: 'site-a',
panelMode: 'sub2api',
openaiIntegrationTargetId: 'sub2api',
signupMethod: 'email',
},
});
@@ -147,7 +147,7 @@ test('flow capability registry normalizes unsupported mode switches back to the
openai: api.FLOW_CAPABILITIES.openai,
'site-a': {
...api.DEFAULT_FLOW_CAPABILITIES,
supportsPlatformBinding: ['cpa'],
supportedIntegrationTargets: ['cpa'],
},
},
});
@@ -155,14 +155,14 @@ test('flow capability registry normalizes unsupported mode switches back to the
const validation = registry.validateModeSwitch({
state: {
activeFlowId: 'site-a',
panelMode: 'sub2api',
openaiIntegrationTargetId: 'sub2api',
signupMethod: 'phone',
phoneVerificationEnabled: true,
plusModeEnabled: true,
contributionMode: true,
},
changedKeys: [
'panelMode',
'openaiIntegrationTargetId',
'signupMethod',
'phoneVerificationEnabled',
'plusModeEnabled',
@@ -173,6 +173,9 @@ test('flow capability registry normalizes unsupported mode switches back to the
assert.equal(validation.ok, false);
assert.deepEqual(validation.normalizedUpdates, {
panelMode: 'cpa',
openaiIntegrationTargetId: 'cpa',
kiroIntegrationTargetId: 'cpa',
kiroSourceId: 'cpa',
signupMethod: 'email',
phoneVerificationEnabled: false,
plusModeEnabled: false,
+30 -34
View File
@@ -13,37 +13,31 @@ function loadApis() {
};`)(scope);
}
test('flow registry exposes openai and kiro with canonical source metadata', () => {
test('flow registry exposes canonical flow and integration target 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.equal(flowRegistry.normalizeFlowId('kiro'), 'kiro');
assert.equal(flowRegistry.normalizeFlowId('unknown'), 'openai');
assert.equal(flowRegistry.getFlowLabel('openai'), 'Codex / OpenAI');
assert.equal(flowRegistry.normalizeIntegrationTargetId('openai', 'sub2api'), 'sub2api');
assert.equal(flowRegistry.normalizeIntegrationTargetId('kiro', 'anything-else'), 'kiro-rs');
assert.deepEqual(
flowRegistry.getVisibleGroupIds('openai', 'cpa'),
['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-source-cpa', 'service-account', 'service-email', 'service-proxy']
['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-target-cpa', 'service-account', 'service-email', 'service-proxy']
);
assert.deepEqual(
flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'),
['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-account', 'service-email', 'service-proxy']
['kiro-runtime-status', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
);
assert.deepEqual(
flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds || [],
['row-plus-mode']
);
assert.deepEqual(
flowRegistry.getSettingsGroupDefinition('openai-phone')?.rowIds || [],
[]
);
assert.deepEqual(
flowRegistry.getSettingsGroupDefinition('openai-step6')?.rowIds || [],
['row-step6-cookie-settings']
flowRegistry.getIntegrationTargetOptions('openai').map((entry) => entry.id),
['cpa', 'sub2api', 'codex2api']
);
assert.equal(flowRegistry.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs');
});
test('settings schema normalizes flat input into canonical flow and service namespaces', () => {
test('settings schema normalizes view input into canonical nested namespaces', () => {
const { settingsSchema } = loadApis();
const schema = settingsSchema.createSettingsSchema();
@@ -66,10 +60,10 @@ test('settings schema normalizes flat input into canonical flow and service name
assert.equal(normalized.services.email.provider, 'hotmail');
assert.equal(normalized.services.proxy.enabled, true);
assert.equal(normalized.services.account.customPassword, 'SharedSecret123!');
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.openai.integrationTargetId, 'sub2api');
assert.equal(normalized.flows.kiro.integrationTargetId, 'kiro-rs');
assert.equal(normalized.flows.kiro.integrationTargets['kiro-rs'].baseUrl, 'https://kiro.example.com/admin');
assert.equal(normalized.flows.kiro.integrationTargets['kiro-rs'].apiKey, 'secret-key');
assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
enabled: true,
fromStep: 1,
@@ -77,22 +71,24 @@ test('settings schema normalizes flat input into canonical flow and service name
});
});
test('settings schema can project canonical state back to legacy payload without losing flow selection', () => {
test('settings schema can project canonical state into a read view without legacy rebuild helpers', () => {
const { settingsSchema } = loadApis();
const schema = settingsSchema.createSettingsSchema();
const payload = schema.buildLegacySettingsPayload(schema.normalizeSettingsState({
const normalized = schema.normalizeSettingsState({
activeFlowId: 'kiro',
kiroSourceId: 'kiro-rs',
kiroIntegrationTargetId: 'kiro-rs',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'key-123',
}));
});
const view = schema.buildSettingsView(normalized);
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(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
assert.equal(payload.settingsSchemaVersion, 3);
assert.equal(payload.settingsState.activeFlowId, 'kiro');
assert.equal(view.activeFlowId, 'kiro');
assert.equal(view.openaiIntegrationTargetId, 'cpa');
assert.equal(view.kiroIntegrationTargetId, 'kiro-rs');
assert.equal(view.panelMode, 'cpa');
assert.equal(view.kiroSourceId, 'kiro-rs');
assert.equal(view.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(view.kiroRsKey, 'key-123');
assert.equal(view.settingsSchemaVersion, 4);
assert.equal(view.settingsState.activeFlowId, 'kiro');
});