修复设置自动保存与标签清理边界

This commit is contained in:
QLHazyCoder
2026-05-20 05:15:56 +08:00
119 changed files with 24116 additions and 3966 deletions
+3 -3
View File
@@ -431,7 +431,7 @@
source,
autoRunContext: source === 'auto' ? autoRunContext : null,
plusModeEnabled: Boolean(record.plusModeEnabled),
contributionMode: Boolean(record.contributionMode),
accountContributionEnabled: Boolean(record.accountContributionEnabled),
};
}
@@ -526,7 +526,7 @@
source,
autoRunContext,
plusModeEnabled: Boolean(state.plusModeEnabled),
contributionMode: Boolean(state.contributionMode),
accountContributionEnabled: Boolean(state.accountContributionEnabled),
};
}
@@ -637,7 +637,7 @@
}
function shouldSyncAccountRunHistorySnapshot(state = {}) {
if (Boolean(state.contributionMode)) {
if (Boolean(state.accountContributionEnabled)) {
return false;
}
+98 -26
View File
@@ -11,6 +11,7 @@
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
broadcastAutoRunStatus,
broadcastStopToContentScripts,
buildFreshAutoRunKeepState,
cancelPendingCommands,
clearStopRequest,
createAutoRunSessionId,
@@ -24,6 +25,7 @@
hasSavedNodeProgress,
isAddPhoneAuthFailure,
isGpcTaskEndedFailure,
isKiroProxyFailure,
isPhoneSmsPlatformRateLimitFailure,
isPlusCheckoutNonFreeTrialFailure,
isRestartCurrentAttemptError,
@@ -77,6 +79,67 @@
throw new Error('自动运行节点执行器未接入。');
}
function buildFreshStartStateSnapshot(state = {}) {
return {
...(state || {}),
currentNodeId: '',
nodeStatuses: {},
stepStatuses: {},
};
}
function resolveFreshStartNodeId(state = {}) {
const freshState = buildFreshStartStateSnapshot(state);
return String(getFirstUnfinishedWorkflowNode(freshState) || '').trim();
}
function buildFreshAttemptKeepState(state = {}, context = {}) {
if (typeof buildFreshAutoRunKeepState === 'function') {
const helperPatch = buildFreshAutoRunKeepState(state, context);
if (helperPatch && typeof helperPatch === 'object' && !Array.isArray(helperPatch)) {
return {
...helperPatch,
};
}
}
return {
activeFlowId: state.activeFlowId,
flowId: state.flowId || state.activeFlowId,
panelMode: state.panelMode,
kiroTargetId: state.kiroTargetId,
vpsUrl: state.vpsUrl,
vpsPassword: state.vpsPassword,
customPassword: state.customPassword,
plusModeEnabled: state.plusModeEnabled,
plusPaymentMethod: state.plusPaymentMethod,
phoneVerificationEnabled: state.phoneVerificationEnabled,
phoneSignupReloginAfterBindEmailEnabled: state.phoneSignupReloginAfterBindEmailEnabled,
paypalEmail: state.paypalEmail,
paypalPassword: state.paypalPassword,
kiroRsUrl: state.kiroRsUrl,
kiroRsKey: state.kiroRsKey,
autoRunSkipFailures: state.autoRunSkipFailures,
autoRunFallbackThreadIntervalMinutes: state.autoRunFallbackThreadIntervalMinutes,
autoRunDelayEnabled: state.autoRunDelayEnabled,
autoRunDelayMinutes: state.autoRunDelayMinutes,
autoStepDelaySeconds: state.autoStepDelaySeconds,
stepExecutionRangeByFlow: state.stepExecutionRangeByFlow,
signupMethod: state.signupMethod,
mailProvider: state.mailProvider,
emailGenerator: state.emailGenerator,
gmailBaseEmail: state.gmailBaseEmail,
mail2925BaseEmail: state.mail2925BaseEmail,
currentMail2925AccountId: state.currentMail2925AccountId,
emailPrefix: state.emailPrefix,
inbucketHost: state.inbucketHost,
inbucketMailbox: state.inbucketMailbox,
cloudflareDomain: state.cloudflareDomain,
cloudflareDomains: state.cloudflareDomains,
reusablePhoneActivation: state.reusablePhoneActivation,
};
}
function createAutoRunRoundSummary(round) {
return {
round,
@@ -502,12 +565,13 @@
autoRunAttemptRun: attemptRun,
});
roundSummary.attempts = attemptRun;
const defaultStartNodeId = typeof runAutoSequenceFromNode === 'function' ? 'open-chatgpt' : 1;
const attemptState = await getState();
const defaultStartNodeId = resolveFreshStartNodeId(attemptState);
let startNodeId = defaultStartNodeId;
let useExistingProgress = false;
if (reuseExistingProgress) {
let currentState = await getState();
let currentState = attemptState;
if (getRunningWorkflowNodes(currentState).length) {
currentState = await waitForRunningWorkflowNodesToFinish({
currentRun: targetRun,
@@ -525,31 +589,14 @@
}
if (!useExistingProgress) {
const prevState = await getState();
const prevState = attemptState;
const keepSettings = {
vpsUrl: prevState.vpsUrl,
vpsPassword: prevState.vpsPassword,
customPassword: prevState.customPassword,
plusModeEnabled: prevState.plusModeEnabled,
paypalEmail: prevState.paypalEmail,
paypalPassword: prevState.paypalPassword,
autoRunSkipFailures: prevState.autoRunSkipFailures,
autoRunFallbackThreadIntervalMinutes: prevState.autoRunFallbackThreadIntervalMinutes,
autoRunDelayEnabled: prevState.autoRunDelayEnabled,
autoRunDelayMinutes: prevState.autoRunDelayMinutes,
autoStepDelaySeconds: prevState.autoStepDelaySeconds,
signupMethod: prevState.signupMethod,
mailProvider: prevState.mailProvider,
emailGenerator: prevState.emailGenerator,
gmailBaseEmail: prevState.gmailBaseEmail,
mail2925BaseEmail: prevState.mail2925BaseEmail,
currentMail2925AccountId: prevState.currentMail2925AccountId,
emailPrefix: prevState.emailPrefix,
inbucketHost: prevState.inbucketHost,
inbucketMailbox: prevState.inbucketMailbox,
cloudflareDomain: prevState.cloudflareDomain,
cloudflareDomains: prevState.cloudflareDomains,
reusablePhoneActivation: prevState.reusablePhoneActivation,
...buildFreshAttemptKeepState(prevState, {
targetRun,
totalRuns,
attemptRun,
sessionId,
}),
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
autoRunSessionId: sessionId,
tabRegistry: {},
@@ -658,11 +705,15 @@
&& isSignupUserAlreadyExistsFailure(err);
const blockedByStep4Route405 = typeof isStep4Route405RecoveryLimitFailure === 'function'
&& isStep4Route405RecoveryLimitFailure(err);
const blockedByKiroProxy = typeof isKiroProxyFailure === 'function'
&& isKiroProxyFailure(err);
const canRetry = !blockedByAddPhone
&& !blockedByPhoneNoSupply
&& !blockedByPlusNonFreeTrial
&& !blockedByGpcTaskEnded
&& !blockedBySignupUserAlreadyExists
&& !blockedByStep4Route405
&& !blockedByKiroProxy
&& autoRunSkipFailures
&& attemptRun < maxAttemptsForRound;
@@ -880,6 +931,27 @@
break;
}
if (blockedByKiroProxy) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮检测到 Kiro 代理异常页,已停止自动运行,等待用户切换代理。');
await broadcastStopToContentScripts();
await addLog(`${targetRun}/${totalRuns} 轮检测到 Kiro 代理异常页:${reason}`, 'error');
await addLog('当前代理可能不可用,请先切换代理后再继续。自动运行已停止。', 'warn');
stoppedEarly = true;
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
if (canRetry) {
const retryIndex = attemptRun;
if (isRestartCurrentAttemptError(err)) {
+12 -10
View File
@@ -8,8 +8,10 @@
const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']);
const RUNTIME_DEFAULTS = {
contributionMode: false,
contributionModeExpected: false,
accountContributionEnabled: false,
accountContributionExpected: false,
contributionAdapterId: '',
flowContributionRuntime: {},
contributionSource: 'sub2api',
contributionTargetGroupName: 'codex号池',
contributionNickname: '',
@@ -265,14 +267,14 @@
return Boolean(state?.plusModeEnabled);
}
function normalizeContributionModeSource(value = '') {
function normalizeOpenAiContributionSource(value = '') {
const normalized = normalizeString(value).toLowerCase();
return normalized === 'sub2api' ? 'sub2api' : 'cpa';
}
function resolveContributionModeRoutingState(state = {}) {
function resolveOpenAiContributionRoutingState(state = {}) {
const currentStatus = normalizeString(state?.contributionStatus).toLowerCase();
const currentSource = normalizeContributionModeSource(state?.contributionSource);
const currentSource = normalizeOpenAiContributionSource(state?.contributionSource);
const hasActiveSession = Boolean(
normalizeString(state?.contributionSessionId)
&& currentStatus
@@ -533,7 +535,7 @@
async function handleCapturedCallback(rawUrl, metadata = {}) {
const currentState = await getState();
if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) {
if (!normalizeString(currentState.contributionSessionId) || !currentState.accountContributionEnabled) {
return currentState;
}
if (!isContributionCallbackUrl(rawUrl, currentState)) {
@@ -642,10 +644,10 @@
return nextState;
}
async function startContributionFlow(options = {}) {
async function startFlowContribution(options = {}) {
const currentState = options.stateOverride || await getState();
const shouldOpenAuthTab = options.openAuthTab !== false;
if (!currentState.contributionMode) {
if (!currentState.accountContributionEnabled) {
throw new Error('请先进入贡献模式。');
}
@@ -662,7 +664,7 @@
return pollContributionStatus({ reason: 'resume_existing' });
}
const routingState = resolveContributionModeRoutingState(currentState);
const routingState = resolveOpenAiContributionRoutingState(currentState);
const payload = await fetchContributionJson('/start', {
method: 'POST',
body: {
@@ -744,7 +746,7 @@
isContributionCallbackUrl,
isContributionFinalStatus,
pollContributionStatus,
startContributionFlow,
startFlowContribution,
submitContributionCallback,
};
}
@@ -0,0 +1,245 @@
(function attachBackgroundKiroBuilderIdContributionAdapter(root, factory) {
root.MultiPageBackgroundKiroBuilderIdContributionAdapter = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroBuilderIdContributionAdapterModule(root) {
const artifactApi = root?.MultiPageBackgroundKiroCredentialArtifact || {};
const FLOW_ID = artifactApi.FLOW_ID || 'kiro';
const ADAPTER_ID = artifactApi.ADAPTER_ID || 'kiro-builder-id';
const ARTIFACT_KIND = artifactApi.ARTIFACT_KIND || 'kiro-builder-id';
const DEFAULT_CONTRIBUTION_API_URL = 'https://flowpilot.qlhazycoder.top/api/contributions';
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function cleanString(value = '') {
return String(value ?? '').trim();
}
function getErrorMessage(error) {
return error instanceof Error ? error.message : String(error ?? '未知错误');
}
function normalizeFlowId(state = {}) {
return cleanString(state.activeFlowId || state.flowId).toLowerCase() || 'openai';
}
function normalizeAdapterId(state = {}) {
return cleanString(state.contributionAdapterId || ADAPTER_ID).toLowerCase() || ADAPTER_ID;
}
function shouldSubmitKiroBuilderIdContribution(state = {}) {
return Boolean(state?.accountContributionEnabled)
&& normalizeFlowId(state) === FLOW_ID
&& normalizeAdapterId(state) === ADAPTER_ID;
}
function normalizeContributionApiUrl(value = '') {
const normalized = cleanString(value).replace(/\/+$/, '');
if (!normalized) {
return DEFAULT_CONTRIBUTION_API_URL;
}
if (/\/api\/contributions$/i.test(normalized)) {
return normalized;
}
return `${normalized}/api/contributions`;
}
function mergeFlowContributionRuntime(currentRuntime = {}, flowId = FLOW_ID, patch = {}) {
const runtime = isPlainObject(currentRuntime) ? currentRuntime : {};
const currentFlowRuntime = isPlainObject(runtime[flowId]) ? runtime[flowId] : {};
return {
...runtime,
[flowId]: {
...currentFlowRuntime,
...patch,
},
};
}
function redactKnownArtifactSecrets(message = '', artifact = {}) {
let result = cleanString(message);
const secrets = [
artifact?.credentials?.refreshToken,
artifact?.credentials?.clientSecret,
]
.map((value) => cleanString(value))
.filter(Boolean);
secrets.forEach((secret) => {
result = result.split(secret).join(artifactApi.redactSecret?.(secret) || '[redacted]');
});
return result;
}
async function readJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (_error) {
return { message: text };
}
}
async function submitContributionArtifact(artifact = {}, options = {}) {
const fetchImpl = options.fetchImpl;
if (typeof fetchImpl !== 'function') {
throw new Error('账号贡献提交能力缺少 fetch。');
}
const endpoint = normalizeContributionApiUrl(options.apiUrl);
const payload = {
flow: FLOW_ID,
adapter_id: ADAPTER_ID,
artifact_kind: ARTIFACT_KIND,
source: 'flowpilot-extension',
contributor: {
nickname: cleanString(options.nickname),
qq: cleanString(options.qq),
},
artifact,
};
const response = await fetchImpl(endpoint, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const body = await readJsonResponse(response);
if (!response.ok || body?.ok === false) {
throw new Error(cleanString(body?.message || body?.detail || body?.error) || `贡献服务请求失败(HTTP ${response.status})。`);
}
return {
ok: true,
status: response.status,
contributionId: cleanString(body?.contribution_id || body?.contributionId || body?.id),
message: cleanString(body?.message) || '贡献提交成功',
raw: body,
};
}
function createKiroBuilderIdContributionAdapter(deps = {}) {
const {
addLog = async () => {},
contributionApiUrl = DEFAULT_CONTRIBUTION_API_URL,
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
getState = async () => ({}),
setState = async () => {},
} = deps;
async function updateFlowContributionRuntime(currentState = {}, patch = {}) {
const runtime = mergeFlowContributionRuntime(currentState.flowContributionRuntime, FLOW_ID, {
adapterId: ADAPTER_ID,
...patch,
});
await setState({
flowContributionRuntime: runtime,
});
return runtime;
}
async function log(message, level = 'info', nodeId = '') {
await addLog(message, level, nodeId ? { nodeId } : {});
}
async function maybeSubmitFlowContribution(state = {}, options = {}) {
const currentState = Object.keys(state || {}).length ? state : await getState();
const nodeId = cleanString(options.nodeId || 'kiro-complete-desktop-authorize');
if (!shouldSubmitKiroBuilderIdContribution(currentState)) {
return {
ok: true,
skipped: true,
reason: 'not_enabled_for_kiro_builder_id',
};
}
let artifact = null;
try {
artifact = artifactApi.buildKiroBuilderIdArtifact(currentState);
} catch (error) {
const message = redactKnownArtifactSecrets(getErrorMessage(error), artifact);
await updateFlowContributionRuntime(currentState, {
enabled: true,
status: 'skipped',
error: message,
lastMessage: message,
updatedAt: Date.now(),
});
await log(`Kiro 账号贡献已跳过:${message}`, 'warn', nodeId);
return {
ok: false,
skipped: true,
reason: error?.code || 'artifact_invalid',
message,
};
}
await updateFlowContributionRuntime(currentState, {
enabled: true,
status: 'submitting',
error: '',
lastMessage: '正在提交 Kiro Builder ID 贡献',
updatedAt: Date.now(),
});
const safeSummary = artifactApi.buildSafeArtifactSummary?.(artifact) || {};
await log(`Kiro 账号贡献:正在提交 Builder ID,邮箱 ${safeSummary.email || '未知'}`, 'info', nodeId);
try {
const result = await submitContributionArtifact(artifact, {
apiUrl: options.contributionApiUrl || contributionApiUrl,
fetchImpl,
nickname: currentState.contributionNickname,
qq: currentState.contributionQq,
});
await updateFlowContributionRuntime(currentState, {
enabled: true,
status: 'submitted',
error: '',
contributionId: result.contributionId,
lastMessage: result.message,
submittedAt: Date.now(),
updatedAt: Date.now(),
});
await log(`Kiro 账号贡献:提交完成,${result.message}`, 'ok', nodeId);
return result;
} catch (error) {
const message = redactKnownArtifactSecrets(getErrorMessage(error), artifact);
await updateFlowContributionRuntime(currentState, {
enabled: true,
status: 'error',
error: message,
lastMessage: message,
updatedAt: Date.now(),
});
await log(`Kiro 账号贡献提交失败:${message}`, 'warn', nodeId);
return {
ok: false,
skipped: false,
reason: 'submit_failed',
message,
};
}
}
return {
maybeSubmitFlowContribution,
shouldSubmitKiroBuilderIdContribution,
submitContributionArtifact: (artifact, options = {}) => submitContributionArtifact(artifact, {
...options,
apiUrl: options.apiUrl || contributionApiUrl,
fetchImpl: options.fetchImpl || fetchImpl,
}),
};
}
return {
ADAPTER_ID,
ARTIFACT_KIND,
DEFAULT_CONTRIBUTION_API_URL,
FLOW_ID,
createKiroBuilderIdContributionAdapter,
normalizeContributionApiUrl,
shouldSubmitKiroBuilderIdContribution,
submitContributionArtifact,
};
});
+507
View File
@@ -0,0 +1,507 @@
(function attachBackgroundCpaApi(root, factory) {
root.MultiPageBackgroundCpaApi = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundCpaApiModule() {
function createCpaApi(deps = {}) {
const {
addLog = async () => {},
fetchImpl = (...args) => fetch(...args),
} = deps;
function normalizeString(value = '') {
return String(value || '').trim();
}
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function firstNonEmpty(...values) {
for (const value of values) {
const normalized = normalizeString(value);
if (normalized) {
return normalized;
}
}
return '';
}
function normalizeEmailValue(value = '') {
const email = normalizeString(value);
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : '';
}
function extractStateFromAuthUrl(authUrl = '') {
try {
return new URL(authUrl).searchParams.get('state') || '';
} catch {
return '';
}
}
function deriveCpaManagementOrigin(vpsUrl) {
const normalizedUrl = normalizeString(vpsUrl);
if (!normalizedUrl) {
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
}
let parsed;
try {
parsed = new URL(normalizedUrl);
} catch {
throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
}
return parsed.origin;
}
function getCpaApiErrorMessage(payload, responseStatus = 500) {
const candidates = [
payload?.error,
payload?.message,
payload?.detail,
payload?.reason,
];
const message = candidates.map(normalizeString).find(Boolean);
return message || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
}
async function fetchCpaManagementJson(origin, path, options = {}) {
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const managementKey = normalizeString(options.managementKey);
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
if (managementKey) {
headers.Authorization = `Bearer ${managementKey}`;
headers['X-Management-Key'] = managementKey;
}
const response = await fetchImpl(`${origin}${path}`, {
method: options.method || 'POST',
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
signal: controller.signal,
});
let payload = {};
try {
payload = await response.json();
} catch {
payload = {};
}
if (!response.ok) {
throw new Error(getCpaApiErrorMessage(payload, response.status));
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error('CPA 管理接口请求超时,请稍后重试。');
}
throw error;
} finally {
clearTimeout(timer);
}
}
function decodeBase64UrlSegment(segment = '') {
const normalized = normalizeString(segment)
.replace(/-/g, '+')
.replace(/_/g, '/');
if (!normalized) {
return '';
}
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
try {
if (typeof Buffer !== 'undefined') {
return Buffer.from(padded, 'base64').toString('utf8');
}
if (typeof atob === 'function') {
const binary = atob(padded);
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder().decode(bytes);
}
return binary;
}
} catch {
return '';
}
return '';
}
function encodeBase64UrlJson(value) {
const json = JSON.stringify(value);
if (typeof Buffer !== 'undefined') {
return Buffer.from(json, 'utf8')
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
}
const bytes = new TextEncoder().encode(json);
let binary = '';
bytes.forEach((byte) => {
binary += String.fromCharCode(byte);
});
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
}
function parseJwtPayload(token = '') {
const normalized = normalizeString(token);
if (!normalized) {
return null;
}
const parts = normalized.split('.');
if (parts.length < 2) {
return null;
}
try {
return JSON.parse(decodeBase64UrlSegment(parts[1]));
} catch {
return null;
}
}
function getOpenAiAuthSection(payload) {
if (!isPlainObject(payload)) {
return {};
}
const auth = payload['https://api.openai.com/auth'];
return isPlainObject(auth) ? auth : {};
}
function getOpenAiProfileSection(payload) {
if (!isPlainObject(payload)) {
return {};
}
const profile = payload['https://api.openai.com/profile'];
return isPlainObject(profile) ? profile : {};
}
function normalizeTimestamp(value) {
if (value instanceof Date && !Number.isNaN(value.getTime())) {
return value.toISOString();
}
if (typeof value === 'number' && Number.isFinite(value)) {
const milliseconds = value > 1e11 ? value : value * 1000;
const date = new Date(milliseconds);
return Number.isNaN(date.getTime()) ? '' : date.toISOString();
}
if (typeof value !== 'string' || !value.trim()) {
return '';
}
const date = new Date(value);
return Number.isNaN(date.getTime()) ? '' : date.toISOString();
}
function timestampFromUnixSeconds(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return '';
}
const date = new Date(numeric * 1000);
return Number.isNaN(date.getTime()) ? '' : date.toISOString();
}
function epochSecondsFromValue(value) {
if (value === undefined || value === null || value === '') {
return 0;
}
const numeric = Number(value);
if (Number.isFinite(numeric)) {
return Math.trunc(numeric > 1e11 ? numeric / 1000 : numeric);
}
const parsed = Date.parse(String(value));
return Number.isFinite(parsed) ? Math.trunc(parsed / 1000) : 0;
}
function buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt) {
const normalizedAccountId = normalizeString(accountId);
if (!normalizedAccountId) {
return '';
}
const now = Math.trunc(Date.now() / 1000);
const expires = epochSecondsFromValue(expiresAt) || now + 90 * 24 * 60 * 60;
const authInfo = { chatgpt_account_id: normalizedAccountId };
if (planType) {
authInfo.chatgpt_plan_type = normalizeString(planType);
}
if (userId) {
authInfo.chatgpt_user_id = normalizeString(userId);
authInfo.user_id = normalizeString(userId);
}
const payload = {
iat: now,
exp: expires,
'https://api.openai.com/auth': authInfo,
};
if (email) {
payload.email = normalizeString(email);
}
return `${encodeBase64UrlJson({ alg: 'none', typ: 'JWT', cpa_synthetic: true })}.${encodeBase64UrlJson(payload)}.synthetic`;
}
function normalizePlanTypeForFileName(planType = '') {
return normalizeString(planType)
.split(/[^a-zA-Z0-9]+/)
.map((part) => part.trim().toLowerCase())
.filter(Boolean)
.join('-');
}
function sanitizeFileSegment(value = '', fallback = 'chatgpt-session') {
const normalized = normalizeString(value)
.replace(/[\\/:*?"<>|]+/g, '-')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
return normalized || fallback;
}
function buildCpaAuthFileName(metadata = {}) {
const email = sanitizeFileSegment(metadata.email || '');
const planType = normalizePlanTypeForFileName(metadata.planType || '');
const accountId = sanitizeFileSegment(metadata.accountId || '');
if (email && planType) {
return `codex-${email}-${planType}.json`;
}
if (email) {
return `codex-${email}.json`;
}
if (accountId && planType) {
return `codex-${accountId}-${planType}.json`;
}
if (accountId) {
return `codex-${accountId}.json`;
}
return `codex-${Date.now()}.json`;
}
function buildCpaSessionAuthJson(state = {}, options = {}) {
const session = isPlainObject(state?.session) ? state.session : {};
const accessToken = normalizeString(state?.accessToken || session?.accessToken);
if (!accessToken) {
throw new Error('未读取到可导入的 ChatGPT accessToken。');
}
const inputIdToken = firstNonEmpty(
state?.idToken,
state?.id_token,
session?.idToken,
session?.id_token
);
const refreshToken = firstNonEmpty(
state?.refreshToken,
state?.refresh_token,
session?.refreshToken,
session?.refresh_token
);
const sessionToken = firstNonEmpty(
state?.sessionToken,
state?.session_token,
session?.sessionToken,
session?.session_token
);
const accessPayload = parseJwtPayload(accessToken);
const idPayload = parseJwtPayload(inputIdToken);
const accessAuth = getOpenAiAuthSection(accessPayload);
const idAuth = getOpenAiAuthSection(idPayload);
const profile = getOpenAiProfileSection(accessPayload);
const expiresAt = firstNonEmpty(
timestampFromUnixSeconds(accessPayload?.exp),
normalizeTimestamp(session?.expires),
normalizeTimestamp(session?.expiresAt),
normalizeTimestamp(session?.expired),
normalizeTimestamp(session?.expires_at)
);
const accountIdentifierEmail = normalizeString(state?.accountIdentifierType).toLowerCase() === 'email'
? normalizeEmailValue(state?.accountIdentifier)
: '';
const email = firstNonEmpty(
normalizeEmailValue(session?.user?.email),
normalizeEmailValue(session?.email),
normalizeEmailValue(state?.email),
accountIdentifierEmail,
normalizeEmailValue(profile?.email),
normalizeEmailValue(idPayload?.email),
normalizeEmailValue(accessPayload?.email)
);
const accountId = firstNonEmpty(
session?.account?.id,
session?.account_id,
accessAuth?.chatgpt_account_id,
idAuth?.chatgpt_account_id
);
const userId = firstNonEmpty(
session?.user?.id,
session?.user_id,
accessAuth?.chatgpt_user_id,
accessAuth?.user_id,
idAuth?.chatgpt_user_id,
idAuth?.user_id
);
const planType = firstNonEmpty(
session?.account?.planType,
session?.account?.plan_type,
session?.planType,
session?.plan_type,
accessAuth?.chatgpt_plan_type,
idAuth?.chatgpt_plan_type
);
const exportedAt = normalizeTimestamp(options.now || new Date()) || new Date().toISOString();
const syntheticIdToken = inputIdToken
? ''
: buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt);
const idToken = inputIdToken || syntheticIdToken;
const authJson = Object.fromEntries(
Object.entries({
type: 'codex',
account_id: accountId,
chatgpt_account_id: accountId,
email,
name: firstNonEmpty(email, state?.email, 'ChatGPT Account'),
plan_type: planType,
chatgpt_plan_type: planType,
id_token: idToken,
id_token_synthetic: syntheticIdToken ? true : undefined,
access_token: accessToken,
refresh_token: refreshToken || '',
session_token: sessionToken,
last_refresh: exportedAt,
expired: expiresAt,
disabled: session?.disabled === true ? true : undefined,
}).filter(([, value]) => value !== undefined && value !== null && value !== '')
);
return {
authJson,
accountId,
email,
expiresAt,
fileName: buildCpaAuthFileName({ email, planType, accountId }),
hasRefreshToken: Boolean(refreshToken),
};
}
async function logWithOptions(message, level = 'info', options = {}) {
await addLog(message, level, options.logOptions || {});
}
async function requestOAuthUrl(state, options = {}) {
const managementKey = normalizeString(state?.vpsPassword);
if (!managementKey) {
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
}
const origin = deriveCpaManagementOrigin(state?.vpsUrl);
const result = await fetchCpaManagementJson(origin, '/v0/management/codex-auth-url', {
method: 'GET',
managementKey,
timeoutMs: options.timeoutMs,
});
const oauthUrl = firstNonEmpty(
result?.url,
result?.auth_url,
result?.authUrl,
result?.data?.url,
result?.data?.auth_url,
result?.data?.authUrl
);
const oauthState = firstNonEmpty(
result?.state,
result?.auth_state,
result?.authState,
result?.data?.state,
result?.data?.auth_state,
result?.data?.authState,
extractStateFromAuthUrl(oauthUrl)
);
if (!oauthUrl || !oauthUrl.startsWith('http')) {
throw new Error('CPA 管理接口未返回有效的 auth_url。');
}
return {
oauthUrl,
cpaOAuthState: oauthState || null,
cpaManagementOrigin: origin,
};
}
async function submitOAuthCallback(state, callbackUrl, options = {}) {
const managementKey = normalizeString(state?.vpsPassword);
if (!managementKey) {
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
}
const origin = normalizeString(state?.cpaManagementOrigin) || deriveCpaManagementOrigin(state?.vpsUrl);
const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', {
method: 'POST',
managementKey,
timeoutMs: options.timeoutMs,
body: {
provider: 'codex',
redirect_url: normalizeString(callbackUrl),
},
});
return {
localhostUrl: normalizeString(callbackUrl),
verifiedStatus: firstNonEmpty(result?.message, result?.status_message, 'CPA 已通过接口提交回调'),
};
}
async function importCurrentChatGptSession(state = {}, options = {}) {
const logLabel = normalizeString(options.logLabel) || 'CPA 会话导入';
const managementKey = normalizeString(state?.vpsPassword);
if (!managementKey) {
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
}
const origin = deriveCpaManagementOrigin(state?.vpsUrl);
const sessionAuth = buildCpaSessionAuthJson(state, options);
await logWithOptions(`${logLabel}:正在通过 CPA 管理接口导入当前 ChatGPT 会话...`, 'info', options);
if (!sessionAuth.hasRefreshToken) {
await logWithOptions(`${logLabel}:未包含 refresh_tokenaccess_token 过期后无法自动续期。`, 'warn', options);
}
await fetchCpaManagementJson(origin, `/v0/management/auth-files?name=${encodeURIComponent(sessionAuth.fileName)}`, {
method: 'POST',
managementKey,
timeoutMs: options.importTimeoutMs || options.timeoutMs,
body: sessionAuth.authJson,
});
const verifiedStatus = sessionAuth.email
? `CPA 会话导入完成:${sessionAuth.email}`
: `CPA 会话导入完成:${sessionAuth.fileName}`;
await logWithOptions(verifiedStatus, 'ok', options);
return {
verifiedStatus,
cpaImportedFileName: sessionAuth.fileName,
cpaImportedEmail: sessionAuth.email || null,
};
}
return {
buildCpaSessionAuthJson,
deriveCpaManagementOrigin,
fetchCpaManagementJson,
importCurrentChatGptSession,
requestOAuthUrl,
submitOAuthCallback,
};
}
return {
createCpaApi,
};
});
+138
View File
@@ -0,0 +1,138 @@
(function attachBackgroundKiroCredentialArtifact(root, factory) {
root.MultiPageBackgroundKiroCredentialArtifact = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroCredentialArtifactModule(root) {
const kiroStateApi = root?.MultiPageBackgroundKiroState || null;
const FLOW_ID = 'kiro';
const ADAPTER_ID = 'kiro-builder-id';
const ARTIFACT_KIND = 'kiro-builder-id';
const DEFAULT_REGION = kiroStateApi?.DEFAULT_REGION || 'us-east-1';
const DEFAULT_TARGET_ID = kiroStateApi?.DEFAULT_TARGET_ID || 'kiro-rs';
const BUILDER_ID_PROFILE_ARN = 'arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX';
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function cleanString(value = '') {
return String(value ?? '').trim();
}
function readKiroRuntime(state = {}) {
return kiroStateApi?.ensureRuntimeState
? kiroStateApi.ensureRuntimeState(state)
: (isPlainObject(state?.kiroRuntime) ? state.kiroRuntime : {});
}
function resolveKiroTargetId(state = {}, runtimeState = readKiroRuntime(state)) {
return cleanString(
state?.settingsState?.flows?.kiro?.targetId
|| state?.flows?.kiro?.targetId
|| state?.kiroTargetId
|| runtimeState?.upload?.targetId
|| DEFAULT_TARGET_ID
) || DEFAULT_TARGET_ID;
}
function resolveEmail(state = {}, runtimeState = readKiroRuntime(state)) {
return cleanString(
runtimeState?.register?.email
|| state?.email
|| state?.registrationEmailState?.current
|| state?.accountIdentifier
);
}
function resolveRegion(state = {}, runtimeState = readKiroRuntime(state), targetId = DEFAULT_TARGET_ID) {
return cleanString(
runtimeState?.desktopAuth?.region
|| state?.settingsState?.flows?.kiro?.targets?.[targetId]?.region
|| state?.flows?.kiro?.targets?.[targetId]?.region
|| DEFAULT_REGION
) || DEFAULT_REGION;
}
function assertRequiredField(name, value, message) {
if (!cleanString(value)) {
const error = new Error(message);
error.code = `missing_${name}`;
throw error;
}
}
function redactSecret(value = '') {
const normalized = cleanString(value);
if (!normalized) {
return '';
}
if (normalized.length <= 10) {
return `${normalized.slice(0, 2)}***`;
}
return `${normalized.slice(0, 6)}...${normalized.slice(-4)}`;
}
function buildKiroBuilderIdArtifact(state = {}, options = {}) {
const runtimeState = readKiroRuntime(state);
const desktopAuth = runtimeState?.desktopAuth || {};
const targetId = resolveKiroTargetId(state, runtimeState);
const region = resolveRegion(state, runtimeState, targetId);
const email = resolveEmail(state, runtimeState);
const refreshToken = String(desktopAuth.refreshToken || '');
const clientId = cleanString(desktopAuth.clientId);
const clientSecret = String(desktopAuth.clientSecret || '');
assertRequiredField('refreshToken', refreshToken, '缺少桌面授权 refreshToken,无法提交 Kiro Builder ID 贡献。');
assertRequiredField('clientId', clientId, '缺少桌面授权 clientId,无法提交 Kiro Builder ID 贡献。');
assertRequiredField('clientSecret', clientSecret, '缺少桌面授权 clientSecret,无法提交 Kiro Builder ID 贡献。');
assertRequiredField('email', email, '缺少注册邮箱,无法提交 Kiro Builder ID 贡献。');
return {
schemaVersion: 1,
flow: FLOW_ID,
adapter: ADAPTER_ID,
artifact: ARTIFACT_KIND,
account: {
email,
},
credentials: {
refreshToken,
clientId,
clientSecret,
profileArn: cleanString(options.profileArn) || BUILDER_ID_PROFILE_ARN,
authMethod: 'idc',
region,
authRegion: region,
apiRegion: region,
tokenSource: cleanString(desktopAuth.tokenSource) || 'desktop_authorization_code_pkce',
},
metadata: {
targetId,
authorizedAt: Math.max(0, Number(desktopAuth.authorizedAt) || 0),
generatedAt: new Date().toISOString(),
source: 'flowpilot-extension',
},
};
}
function buildSafeArtifactSummary(artifact = {}) {
return {
flow: cleanString(artifact.flow),
adapter: cleanString(artifact.adapter),
artifact: cleanString(artifact.artifact),
email: cleanString(artifact.account?.email),
clientId: cleanString(artifact.credentials?.clientId),
refreshToken: redactSecret(artifact.credentials?.refreshToken),
clientSecret: redactSecret(artifact.credentials?.clientSecret),
region: cleanString(artifact.credentials?.region),
};
}
return {
ADAPTER_ID,
ARTIFACT_KIND,
BUILDER_ID_PROFILE_ARN,
FLOW_ID,
buildKiroBuilderIdArtifact,
buildSafeArtifactSummary,
redactSecret,
};
});
File diff suppressed because it is too large Load Diff
+196
View File
@@ -0,0 +1,196 @@
(function attachBackgroundKiroDesktopClient(root, factory) {
root.MultiPageBackgroundKiroDesktopClient = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroDesktopClientModule() {
const DEFAULT_REGION = 'us-east-1';
const DEFAULT_START_URL = 'https://view.awsapps.com/start';
const DEFAULT_SCOPES = Object.freeze([
'codewhisperer:completions',
'codewhisperer:analysis',
'codewhisperer:conversations',
'codewhisperer:transformations',
'codewhisperer:taskassist',
]);
function cleanString(value = '') {
return String(value ?? '').trim();
}
function normalizeRegion(value = '', fallback = DEFAULT_REGION) {
return cleanString(value) || fallback;
}
function buildOidcBaseUrl(region = DEFAULT_REGION) {
return `https://oidc.${normalizeRegion(region)}.amazonaws.com`;
}
async function readResponse(response) {
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch (_error) {
json = null;
}
return { text, json };
}
async function sha256Bytes(input) {
const encoder = new TextEncoder();
return new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(String(input || ''))));
}
function base64UrlEncode(bytes) {
let binary = '';
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
async function sha256Hex(input) {
const bytes = await sha256Bytes(input);
return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, '0')).join('');
}
function randomUrlSafeString(length = 64) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
const size = Math.max(32, Math.floor(Number(length) || 64));
const bytes = new Uint8Array(size);
crypto.getRandomValues(bytes);
let output = '';
for (let index = 0; index < size; index += 1) {
output += alphabet[bytes[index] % alphabet.length];
}
return output;
}
async function generatePkcePair() {
const codeVerifier = randomUrlSafeString(64);
const codeChallenge = base64UrlEncode(await sha256Bytes(codeVerifier));
return {
codeVerifier,
codeChallenge,
};
}
function chooseRedirectPort() {
return 49152 + Math.floor(Math.random() * 16384);
}
function buildRedirectUri(port) {
const normalizedPort = Math.max(1, Math.floor(Number(port) || 0));
if (!normalizedPort) {
throw new Error('缺少桌面授权回调端口。');
}
return `http://127.0.0.1:${normalizedPort}/oauth/callback`;
}
async function registerDesktopClient(params = {}, fetchImpl) {
if (typeof fetchImpl !== 'function') {
throw new Error('registerDesktopClient requires fetch support.');
}
const region = normalizeRegion(params.region);
const oidcBaseUrl = buildOidcBaseUrl(region);
const response = await fetchImpl(`${oidcBaseUrl}/client/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientName: cleanString(params.clientName) || 'Kiro IDE',
clientType: 'public',
scopes: Array.isArray(params.scopes) && params.scopes.length ? params.scopes : DEFAULT_SCOPES,
grantTypes: ['authorization_code', 'refresh_token'],
redirectUris: ['http://127.0.0.1/oauth/callback'],
issuerUrl: cleanString(params.issuerUrl) || DEFAULT_START_URL,
}),
});
const body = await readResponse(response);
if (!response.ok) {
throw new Error(`Kiro 桌面客户端注册失败:${cleanString(body.text || response.statusText) || response.status}`);
}
const clientId = cleanString(body.json?.clientId);
const clientSecret = String(body.json?.clientSecret || '');
if (!clientId || !clientSecret) {
throw new Error('Kiro 桌面客户端注册响应缺少 clientId 或 clientSecret。');
}
return {
region,
clientId,
clientSecret,
clientSecretExpiresAt: Number(body.json?.clientSecretExpiresAt || 0) || 0,
clientIdHash: await sha256Hex(JSON.stringify({
startUrl: cleanString(params.issuerUrl) || DEFAULT_START_URL,
clientId,
})),
};
}
function buildAuthorizeUrl(params = {}) {
const region = normalizeRegion(params.region);
const search = new URLSearchParams();
search.set('response_type', 'code');
search.set('client_id', cleanString(params.clientId));
search.set('redirect_uri', cleanString(params.redirectUri));
search.set('scopes', Array.isArray(params.scopes) && params.scopes.length
? params.scopes.join(',')
: DEFAULT_SCOPES.join(','));
search.set('state', cleanString(params.state));
search.set('code_challenge', cleanString(params.codeChallenge));
search.set('code_challenge_method', 'S256');
return `${buildOidcBaseUrl(region)}/authorize?${search.toString()}`;
}
async function exchangeDesktopAuthorizationCode(params = {}, fetchImpl) {
if (typeof fetchImpl !== 'function') {
throw new Error('exchangeDesktopAuthorizationCode requires fetch support.');
}
const region = normalizeRegion(params.region);
const response = await fetchImpl(`${buildOidcBaseUrl(region)}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientId: cleanString(params.clientId),
clientSecret: String(params.clientSecret || ''),
grantType: 'authorization_code',
code: cleanString(params.code),
redirectUri: cleanString(params.redirectUri),
codeVerifier: cleanString(params.codeVerifier),
}),
});
const body = await readResponse(response);
if (!response.ok) {
throw new Error(`Kiro 桌面授权换取 Token 失败:${cleanString(body.text || response.statusText) || response.status}`);
}
const accessToken = String(body.json?.accessToken || '');
const refreshToken = String(body.json?.refreshToken || '');
if (!accessToken || !refreshToken) {
throw new Error('Kiro 桌面授权换取 Token 响应缺少 accessToken 或 refreshToken。');
}
return {
accessToken,
refreshToken,
expiresIn: Number(body.json?.expiresIn || 0) || 0,
tokenType: cleanString(body.json?.tokenType),
region,
};
}
return {
DEFAULT_REGION,
DEFAULT_SCOPES,
DEFAULT_START_URL,
buildAuthorizeUrl,
buildRedirectUri,
chooseRedirectPort,
exchangeDesktopAuthorizationCode,
generatePkcePair,
registerDesktopClient,
};
});
+471
View File
@@ -0,0 +1,471 @@
(function attachBackgroundKiroPublisherKiroRs(root, factory) {
root.MultiPageBackgroundKiroPublisherKiroRs = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroPublisherKiroRsModule(root) {
const kiroStateApi = root?.MultiPageBackgroundKiroState || null;
const DEFAULT_REGION = kiroStateApi?.DEFAULT_REGION || 'us-east-1';
const DEFAULT_TARGET_ID = kiroStateApi?.DEFAULT_TARGET_ID || 'kiro-rs';
const BUILDER_ID_PROFILE_ARN = 'arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX';
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function cloneValue(value) {
if (Array.isArray(value)) {
return value.map((entry) => cloneValue(entry));
}
if (isPlainObject(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
);
}
return value;
}
function deepMerge(baseValue, patchValue) {
if (Array.isArray(patchValue)) {
return patchValue.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchValue)) {
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
}
const baseObject = isPlainObject(baseValue) ? baseValue : {};
const next = {
...cloneValue(baseObject),
};
Object.entries(patchValue).forEach(([key, value]) => {
next[key] = deepMerge(baseObject[key], value);
});
return next;
}
function cleanString(value = '') {
return String(value ?? '').trim();
}
function normalizeRegion(value = '', fallback = DEFAULT_REGION) {
return cleanString(value) || fallback;
}
function normalizeKiroRsApiKey(value = '') {
return cleanString(value);
}
function buildKiroRsAdminHeaders(apiKey = '', extraHeaders = {}) {
const normalizedApiKey = normalizeKiroRsApiKey(apiKey);
return {
...extraHeaders,
...(normalizedApiKey ? {
'x-api-key': normalizedApiKey,
Authorization: `Bearer ${normalizedApiKey}`,
} : {}),
};
}
function readKiroRsResponseMessage(body = {}, fallback = '') {
return cleanString(body?.json?.error?.message || body?.json?.message || body?.text || fallback);
}
function normalizeKiroRsBaseUrl(value = '') {
const normalized = cleanString(value).replace(/\/+$/, '');
if (!normalized) {
throw new Error('缺少 kiro.rs 管理后台地址。');
}
return normalized.endsWith('/admin')
? normalized.slice(0, -'/admin'.length)
: normalized;
}
function normalizeKiroUploadMessage(value = '') {
const rawValue = cleanString(value);
if (!rawValue) {
return '上传成功';
}
const normalizedValue = rawValue.toLowerCase();
if (normalizedValue === 'uploaded' || normalizedValue === 'credential uploaded.') {
return '上传成功';
}
return rawValue;
}
function getErrorMessage(error) {
return error instanceof Error ? error.message : String(error ?? '未知错误');
}
async function readResponse(response) {
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch (_error) {
json = null;
}
return { text, json };
}
function readKiroRuntime(state = {}) {
return kiroStateApi?.ensureRuntimeState
? kiroStateApi.ensureRuntimeState(state)
: (isPlainObject(state?.kiroRuntime) ? state.kiroRuntime : {});
}
function mergeRuntimePatch(currentState = {}, patch = {}) {
return {
kiroRuntime: deepMerge(readKiroRuntime(currentState), patch),
};
}
function resolveKiroTargetId(state = {}) {
return cleanString(
state?.settingsState?.flows?.kiro?.targetId
|| state?.flows?.kiro?.targetId
|| state?.kiroTargetId
|| readKiroRuntime(state).upload?.targetId
|| DEFAULT_TARGET_ID
) || DEFAULT_TARGET_ID;
}
function resolveKiroTargetConfig(state = {}, targetId = DEFAULT_TARGET_ID) {
if (targetId !== DEFAULT_TARGET_ID) {
throw new Error(`暂不支持 Kiro 发布目标:${targetId}`);
}
const nestedConfig = state?.settingsState?.flows?.kiro?.targets?.[targetId]
|| state?.flows?.kiro?.targets?.[targetId]
|| {};
return {
baseUrl: cleanString(nestedConfig.baseUrl || state?.kiroRsUrl),
apiKey: normalizeKiroRsApiKey(nestedConfig.apiKey ?? state?.kiroRsKey ?? ''),
};
}
function buildProxyPayload(state = {}) {
if (!state?.ipProxyEnabled) {
return {};
}
const apiProxyUrl = cleanString(state?.ipProxyApiUrl);
const host = cleanString(state?.ipProxyHost);
const port = cleanString(state?.ipProxyPort);
const protocol = cleanString(state?.ipProxyProtocol) || 'http';
const proxyUrl = apiProxyUrl || (host && port ? `${protocol}://${host}:${port}` : '');
const proxyUsername = cleanString(state?.ipProxyUsername);
const proxyPassword = String(state?.ipProxyPassword || '');
return {
...(proxyUrl ? { proxyUrl } : {}),
...(proxyUsername ? { proxyUsername } : {}),
...(proxyPassword ? { proxyPassword } : {}),
};
}
async function sha256Hex(input = '') {
const encoder = new TextEncoder();
const bytes = encoder.encode(String(input ?? ''));
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
return Array.from(new Uint8Array(digest))
.map((value) => value.toString(16).padStart(2, '0'))
.join('');
}
async function buildMachineId(refreshToken = '') {
const normalizedRefreshToken = cleanString(refreshToken);
if (!normalizedRefreshToken) {
throw new Error('缺少 refreshToken,无法生成 machineId。');
}
return sha256Hex(`KotlinNativeAPI/${normalizedRefreshToken}`);
}
function buildUploadPayload(state = {}) {
const runtimeState = readKiroRuntime(state);
const targetId = resolveKiroTargetId(state);
const desktopAuth = runtimeState.desktopAuth || {};
const register = runtimeState.register || {};
const refreshToken = String(desktopAuth.refreshToken || '');
const clientId = cleanString(desktopAuth.clientId);
const clientSecret = String(desktopAuth.clientSecret || '');
const region = normalizeRegion(
desktopAuth.region
|| state?.settingsState?.flows?.kiro?.targets?.[targetId]?.region
|| state?.flows?.kiro?.targets?.[targetId]?.region
|| DEFAULT_REGION
);
const email = cleanString(register.email || state?.email);
if (!refreshToken) {
throw new Error('缺少桌面授权 refreshToken,请先完成步骤 8。');
}
if (!clientId || !clientSecret) {
throw new Error('缺少桌面授权 clientId 或 clientSecret,请先完成步骤 7-8。');
}
if (!email) {
throw new Error('缺少注册邮箱,无法上传到 kiro.rs。');
}
return {
targetId,
region,
email,
refreshToken,
profileArn: BUILDER_ID_PROFILE_ARN,
clientId,
clientSecret,
authMethod: 'idc',
authRegion: region,
apiRegion: region,
...buildProxyPayload(state),
};
}
async function checkKiroRsConnection(baseUrl, apiKey, fetchImpl) {
const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl);
const normalizedApiKey = normalizeKiroRsApiKey(apiKey);
const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, {
method: 'GET',
headers: buildKiroRsAdminHeaders(normalizedApiKey, {
Accept: 'application/json',
}),
});
const body = await readResponse(response);
const detail = readKiroRsResponseMessage(body, response.statusText);
if (response.ok) {
return {
ok: true,
status: response.status,
message: `kiro.rs 连接正常(HTTP ${response.status}`,
};
}
if (response.status === 405) {
return {
ok: true,
status: response.status,
message: 'kiro.rs 上传接口可访问。',
};
}
if (response.status === 401 || response.status === 403) {
return {
ok: false,
status: response.status,
message: `kiro.rs API Key 被拒绝(HTTP ${response.status}${detail ? `${detail}` : ''}`,
};
}
if (response.status === 404) {
return {
ok: false,
status: response.status,
message: `未找到 kiro.rs 管理接口(HTTP 404${detail ? `${detail}` : ''}`,
};
}
return {
ok: false,
status: response.status,
message: detail || `kiro.rs 连接失败(HTTP ${response.status}`,
};
}
async function uploadBuilderIdCredential(baseUrl, apiKey, payload, fetchImpl) {
const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl);
const normalizedApiKey = normalizeKiroRsApiKey(apiKey);
const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, {
method: 'POST',
headers: buildKiroRsAdminHeaders(normalizedApiKey, {
'Content-Type': 'application/json',
Accept: 'application/json',
}),
body: JSON.stringify(payload),
});
const body = await readResponse(response);
if (!response.ok) {
const message = readKiroRsResponseMessage(body, response.statusText) || `HTTP ${response.status}`;
throw new Error(`kiro.rs 凭据上传失败:${message}`);
}
return {
credentialId: Number(body.json?.credentialId || body.json?.credential_id || 0) || null,
email: cleanString(body.json?.email),
message: normalizeKiroUploadMessage(body.json?.message),
raw: body.json,
};
}
function createKiroRsPublisher(deps = {}) {
const {
addLog = async () => {},
completeNodeFromBackground,
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
getState = async () => ({}),
maybeSubmitFlowContribution = async () => ({ ok: true, skipped: true, reason: 'not_configured' }),
setState = async () => {},
} = deps;
if (typeof completeNodeFromBackground !== 'function') {
throw new Error('Kiro kiro.rs publisher requires completeNodeFromBackground.');
}
if (typeof fetchImpl !== 'function') {
throw new Error('Kiro kiro.rs publisher requires fetch support.');
}
async function log(message, level = 'info', nodeId = '') {
await addLog(message, level, nodeId ? { nodeId } : {});
}
async function applyRuntimeState(currentState = {}, patch = {}) {
const nextPatch = mergeRuntimePatch(currentState, patch);
await setState(nextPatch);
return nextPatch;
}
async function persistFailure(currentState = {}, message = '') {
const nextPatch = mergeRuntimePatch(currentState, {
session: {
currentStage: 'upload',
lastError: message,
},
upload: {
status: 'error',
error: message,
},
});
await setState(nextPatch);
}
function shouldUseContributionUpload(state = {}) {
return Boolean(state?.accountContributionEnabled)
&& cleanString(state?.activeFlowId || state?.flowId).toLowerCase() === 'kiro'
&& cleanString(state?.contributionAdapterId).toLowerCase() === 'kiro-builder-id';
}
async function executeKiroUploadCredential(state = {}) {
const nodeId = String(state?.nodeId || 'kiro-upload-credential').trim();
const currentState = await getState();
try {
if (shouldUseContributionUpload(currentState)) {
await applyRuntimeState(currentState, {
session: {
currentStage: 'upload',
lastError: '',
lastWarning: '',
},
upload: {
targetId: 'contribution',
status: 'uploading',
error: '',
},
});
await log('步骤 9:正在上传 Builder ID 到贡献池...', 'info', nodeId);
const contributionResult = await maybeSubmitFlowContribution(currentState, {
nodeId,
trigger: 'kiro-step-9',
});
if (!contributionResult?.ok || contributionResult?.skipped) {
throw new Error(contributionResult?.message || 'Kiro 贡献上传失败。');
}
const uploadedAt = Date.now();
const payload = await applyRuntimeState(currentState, {
session: {
currentStage: 'upload',
lastError: '',
},
upload: {
targetId: 'contribution',
status: 'uploaded',
error: '',
credentialId: contributionResult.contributionId || '',
lastMessage: contributionResult.message || '贡献上传成功',
lastUploadedAt: uploadedAt,
},
});
await log(`步骤 9:贡献上传完成,状态:${contributionResult.message || '贡献上传成功'}`, 'ok', nodeId);
await completeNodeFromBackground(nodeId, payload);
return;
}
const targetId = resolveKiroTargetId(currentState);
const targetConfig = resolveKiroTargetConfig(currentState, targetId);
const baseUrl = normalizeKiroRsBaseUrl(targetConfig.baseUrl);
const apiKey = String(targetConfig.apiKey || '');
if (!apiKey) {
throw new Error('缺少 kiro.rs API Key。');
}
const uploadInput = buildUploadPayload(currentState);
const machineId = await buildMachineId(uploadInput.refreshToken);
await applyRuntimeState(currentState, {
session: {
currentStage: 'upload',
lastError: '',
lastWarning: '',
},
upload: {
targetId,
status: 'uploading',
error: '',
},
});
await log('步骤 9:正在上传 Builder ID 凭据到 kiro.rs...', 'info', nodeId);
const connection = await checkKiroRsConnection(baseUrl, apiKey, fetchImpl);
if (!connection.ok) {
throw new Error(connection.message);
}
const uploadResult = await uploadBuilderIdCredential(baseUrl, apiKey, {
refreshToken: uploadInput.refreshToken,
profileArn: uploadInput.profileArn,
authMethod: uploadInput.authMethod,
clientId: uploadInput.clientId,
clientSecret: uploadInput.clientSecret,
region: uploadInput.region,
authRegion: uploadInput.authRegion,
apiRegion: uploadInput.apiRegion,
machineId,
email: uploadInput.email,
...(uploadInput.proxyUrl ? { proxyUrl: uploadInput.proxyUrl } : {}),
...(uploadInput.proxyUsername ? { proxyUsername: uploadInput.proxyUsername } : {}),
...(uploadInput.proxyPassword ? { proxyPassword: uploadInput.proxyPassword } : {}),
}, fetchImpl);
const uploadedAt = Date.now();
const payload = await applyRuntimeState(currentState, {
session: {
currentStage: 'upload',
lastError: '',
},
upload: {
targetId,
status: 'uploaded',
error: '',
credentialId: uploadResult.credentialId,
lastMessage: uploadResult.message || '上传成功',
lastUploadedAt: uploadedAt,
},
});
await log(`步骤 9:kiro.rs 上传完成,状态:${uploadResult.message || '上传成功'}`, 'ok', nodeId);
await completeNodeFromBackground(nodeId, payload);
} catch (error) {
const message = getErrorMessage(error);
await persistFailure(currentState, message);
throw error;
}
}
return {
executeKiroUploadCredential,
};
}
return {
buildKiroRsPayload: buildUploadPayload,
buildMachineId,
checkKiroRsConnection,
createKiroRsPublisher,
normalizeKiroRsBaseUrl,
normalizeKiroUploadMessage,
uploadBuilderIdCredential,
};
});
File diff suppressed because it is too large Load Diff
+356
View File
@@ -0,0 +1,356 @@
(function attachBackgroundKiroState(root, factory) {
root.MultiPageBackgroundKiroState = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroStateModule() {
const DEFAULT_TARGET_ID = 'kiro-rs';
const DEFAULT_REGION = 'us-east-1';
const FLAT_FIELD_DEFINITIONS = Object.freeze([]);
const FLAT_FIELD_KEYS = Object.freeze([]);
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function cloneValue(value) {
if (Array.isArray(value)) {
return value.map((entry) => cloneValue(entry));
}
if (isPlainObject(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
);
}
return value;
}
function deepMerge(baseValue, patchValue) {
if (Array.isArray(patchValue)) {
return patchValue.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchValue)) {
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
}
const baseObject = isPlainObject(baseValue) ? baseValue : {};
const next = {
...cloneValue(baseObject),
};
Object.entries(patchValue).forEach(([key, value]) => {
next[key] = deepMerge(baseObject[key], value);
});
return next;
}
function normalizeString(value = '', fallback = '') {
const normalized = String(value ?? '').trim();
return normalized || fallback;
}
function normalizeInteger(value, fallback = 0) {
const numeric = Math.floor(Number(value));
return Number.isInteger(numeric) ? numeric : fallback;
}
function normalizeNullableInteger(value, fallback = null) {
if (value === null || value === undefined || value === '') {
return fallback;
}
const numeric = Math.floor(Number(value));
return Number.isInteger(numeric) ? numeric : fallback;
}
function normalizeBoolean(value, fallback = false) {
if (value === true || value === false) {
return value;
}
return fallback;
}
function buildDefaultRuntimeState() {
return {
session: {
currentStage: '',
registerTabId: null,
desktopTabId: null,
startedAt: 0,
pageState: '',
pageUrl: '',
lastError: '',
lastWarning: '',
},
register: {
email: '',
fullName: '',
verificationRequestedAt: 0,
loginUrl: '',
status: '',
completedAt: 0,
},
webAuth: {
status: '',
completedAt: 0,
hasAccessToken: false,
hasSessionToken: false,
},
desktopAuth: {
region: DEFAULT_REGION,
clientId: '',
clientSecret: '',
clientIdHash: '',
state: '',
codeVerifier: '',
codeChallenge: '',
redirectUri: '',
redirectPort: 0,
authorizeUrl: '',
authorizationCode: '',
accessToken: '',
refreshToken: '',
status: '',
authorizedAt: 0,
otpRequestedAt: 0,
tokenSource: 'desktop_authorization_code_pkce',
},
upload: {
targetId: DEFAULT_TARGET_ID,
status: '',
error: '',
credentialId: null,
lastMessage: '',
lastUploadedAt: 0,
},
};
}
function normalizeRuntimeState(runtimeState = {}) {
const merged = deepMerge(buildDefaultRuntimeState(), runtimeState);
return {
session: {
currentStage: normalizeString(merged.session?.currentStage),
registerTabId: normalizeNullableInteger(merged.session?.registerTabId),
desktopTabId: normalizeNullableInteger(merged.session?.desktopTabId),
startedAt: Math.max(0, normalizeInteger(merged.session?.startedAt)),
pageState: normalizeString(merged.session?.pageState),
pageUrl: normalizeString(merged.session?.pageUrl),
lastError: normalizeString(merged.session?.lastError),
lastWarning: normalizeString(merged.session?.lastWarning),
},
register: {
email: normalizeString(merged.register?.email),
fullName: normalizeString(merged.register?.fullName),
verificationRequestedAt: Math.max(0, normalizeInteger(merged.register?.verificationRequestedAt)),
loginUrl: normalizeString(merged.register?.loginUrl),
status: normalizeString(merged.register?.status),
completedAt: Math.max(0, normalizeInteger(merged.register?.completedAt)),
},
webAuth: {
status: normalizeString(merged.webAuth?.status),
completedAt: Math.max(0, normalizeInteger(merged.webAuth?.completedAt)),
hasAccessToken: normalizeBoolean(merged.webAuth?.hasAccessToken),
hasSessionToken: normalizeBoolean(merged.webAuth?.hasSessionToken),
},
desktopAuth: {
region: normalizeString(merged.desktopAuth?.region, DEFAULT_REGION),
clientId: normalizeString(merged.desktopAuth?.clientId),
clientSecret: normalizeString(merged.desktopAuth?.clientSecret),
clientIdHash: normalizeString(merged.desktopAuth?.clientIdHash),
state: normalizeString(merged.desktopAuth?.state),
codeVerifier: normalizeString(merged.desktopAuth?.codeVerifier),
codeChallenge: normalizeString(merged.desktopAuth?.codeChallenge),
redirectUri: normalizeString(merged.desktopAuth?.redirectUri),
redirectPort: Math.max(0, normalizeInteger(merged.desktopAuth?.redirectPort)),
authorizeUrl: normalizeString(merged.desktopAuth?.authorizeUrl),
authorizationCode: normalizeString(merged.desktopAuth?.authorizationCode),
accessToken: normalizeString(merged.desktopAuth?.accessToken),
refreshToken: normalizeString(merged.desktopAuth?.refreshToken),
status: normalizeString(merged.desktopAuth?.status),
authorizedAt: Math.max(0, normalizeInteger(merged.desktopAuth?.authorizedAt)),
otpRequestedAt: Math.max(0, normalizeInteger(merged.desktopAuth?.otpRequestedAt)),
tokenSource: normalizeString(
merged.desktopAuth?.tokenSource,
'desktop_authorization_code_pkce'
),
},
upload: {
targetId: normalizeString(merged.upload?.targetId, DEFAULT_TARGET_ID),
status: normalizeString(merged.upload?.status),
error: normalizeString(merged.upload?.error),
credentialId: normalizeNullableInteger(merged.upload?.credentialId),
lastMessage: normalizeString(merged.upload?.lastMessage),
lastUploadedAt: Math.max(0, normalizeInteger(merged.upload?.lastUploadedAt)),
},
};
}
function ensureRuntimeState(state = {}) {
return normalizeRuntimeState(isPlainObject(state?.kiroRuntime) ? state.kiroRuntime : {});
}
function projectRuntimeFields() {
return {};
}
function buildStateView(state = {}) {
return {
...state,
kiroRuntime: ensureRuntimeState(state),
};
}
function buildSessionStatePatch(currentState = {}, updates = {}) {
if (!isPlainObject(updates?.kiroRuntime)) {
return {};
}
const nextRuntimeState = normalizeRuntimeState(
deepMerge(ensureRuntimeState(currentState), updates.kiroRuntime)
);
return {
kiroRuntime: nextRuntimeState,
};
}
function buildRuntimeResetPatch(currentState = {}, patch = {}) {
return {
kiroRuntime: normalizeRuntimeState(
deepMerge(ensureRuntimeState(currentState), patch)
),
};
}
function buildStartRegisterResetPatch(currentState = {}) {
const currentRuntimeState = ensureRuntimeState(currentState);
const nextRuntimeState = buildDefaultRuntimeState();
nextRuntimeState.upload.targetId = currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID;
return {
kiroRuntime: nextRuntimeState,
};
}
function buildRegisterOnlyResetPatch(currentState = {}, registerPatch = {}) {
const currentRuntimeState = ensureRuntimeState(currentState);
const nextRuntimeState = normalizeRuntimeState({
...buildDefaultRuntimeState(),
session: {
...currentRuntimeState.session,
currentStage: 'register',
desktopTabId: null,
pageState: '',
pageUrl: '',
lastError: '',
lastWarning: '',
},
register: {
...currentRuntimeState.register,
completedAt: 0,
status: '',
...registerPatch,
},
upload: {
...buildDefaultRuntimeState().upload,
targetId: currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID,
},
});
return {
kiroRuntime: nextRuntimeState,
};
}
function buildDesktopResetPatch(currentState = {}) {
const currentRuntimeState = ensureRuntimeState(currentState);
return {
kiroRuntime: normalizeRuntimeState({
...currentRuntimeState,
session: {
...currentRuntimeState.session,
currentStage: 'desktop-authorize',
desktopTabId: null,
pageState: '',
pageUrl: '',
lastError: '',
lastWarning: '',
},
desktopAuth: buildDefaultRuntimeState().desktopAuth,
upload: {
...buildDefaultRuntimeState().upload,
targetId: currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID,
},
}),
};
}
function buildUploadResetPatch(currentState = {}) {
const currentRuntimeState = ensureRuntimeState(currentState);
return {
kiroRuntime: normalizeRuntimeState({
...currentRuntimeState,
upload: {
...buildDefaultRuntimeState().upload,
targetId: currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID,
},
}),
};
}
function buildDownstreamResetPatch(stepKey = '', currentState = {}) {
switch (normalizeString(stepKey)) {
case 'kiro-open-register-page':
return buildStartRegisterResetPatch(currentState);
case 'kiro-submit-email':
return buildRegisterOnlyResetPatch(currentState, {
email: '',
fullName: '',
verificationRequestedAt: 0,
});
case 'kiro-submit-name':
return buildRegisterOnlyResetPatch(currentState, {
fullName: '',
verificationRequestedAt: 0,
});
case 'kiro-submit-verification-code':
return buildRegisterOnlyResetPatch(currentState, {});
case 'kiro-submit-password':
return buildRegisterOnlyResetPatch(currentState, {});
case 'kiro-complete-register-consent':
return buildDesktopResetPatch(currentState);
case 'kiro-start-desktop-authorize':
return buildDesktopResetPatch(currentState);
case 'kiro-complete-desktop-authorize':
return buildUploadResetPatch(currentState);
case 'kiro-upload-credential':
return buildUploadResetPatch(currentState);
default:
return {};
}
}
function applyNodeCompletionPayload(currentState = {}, payload = {}) {
return buildSessionStatePatch(currentState, payload);
}
function buildFreshKeepState(currentState = {}) {
const currentRuntimeState = ensureRuntimeState(currentState);
const nextRuntimeState = buildDefaultRuntimeState();
nextRuntimeState.upload.targetId = currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID;
return {
kiroRuntime: nextRuntimeState,
...(Object.prototype.hasOwnProperty.call(currentState, 'kiroTargetId')
? { kiroTargetId: normalizeString(currentState.kiroTargetId, DEFAULT_TARGET_ID).toLowerCase() }
: {}),
};
}
return {
DEFAULT_REGION,
DEFAULT_TARGET_ID,
FLAT_FIELD_DEFINITIONS,
FLAT_FIELD_KEYS,
applyNodeCompletionPayload,
buildDefaultRuntimeState,
buildDownstreamResetPatch,
buildFreshKeepState,
buildSessionStatePatch,
buildStateView,
ensureRuntimeState,
projectRuntimeFields,
};
});
+6
View File
@@ -143,6 +143,11 @@
return /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(message);
}
function isKiroProxyFailure(error) {
const message = getErrorMessage(error);
return /Kiro\s*(?:注册页|桌面授权页).*(?:CloudFront\s*拒绝请求|AWS\s*请求异常)|(?:当前代理\s*IP|出口区域异常).*(?:切换代理|更换代理)|AWS\s*风控.*(?:切换代理|更换代理)/i.test(message);
}
function isStep9RecoverableAuthError(error) {
const message = String(typeof error === 'string' ? error : error?.message || '');
return /STEP9_OAUTH_RETRY::/i.test(message)
@@ -236,6 +241,7 @@
getSourceLabel,
hasSavedNodeProgress,
hasSavedProgress,
isKiroProxyFailure,
isLegacyStep9RecoverableAuthError,
isRestartCurrentAttemptError,
isSignupUserAlreadyExistsFailure,
+262 -44
View File
@@ -31,11 +31,13 @@
doesNodeUseCompletionSignal,
ensureMail2925MailboxSession,
ensureManualInteractionAllowed,
assertNodeExecutionAllowedForState,
executeNode,
executeNodeViaCompletionSignal,
exportSettingsBundle,
fetchGeneratedEmail,
refreshGpcCardBalance,
testKiroRsConnection,
finalizePhoneActivationAfterSuccessfulFlow,
finalizeStep3Completion,
finalizeIcloudAliasAfterSuccessfulFlow,
@@ -65,7 +67,7 @@
}
return Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
&& !Boolean(state?.contributionMode);
&& !Boolean(state?.accountContributionEnabled);
},
resolveSignupMethod = (state = {}) => {
const method = normalizeSignupMethod(state?.signupMethod);
@@ -147,6 +149,7 @@
patchMail2925Account,
patchHotmailAccount,
pollContributionStatus,
submitFlowContribution,
registerTab,
requestStop,
probeIpProxyExit,
@@ -160,7 +163,7 @@
setCurrentPayPalAccount,
setCurrentMail2925Account,
setCurrentHotmailAccount,
setContributionMode,
setAccountContributionMode,
setEmailState,
setEmailStateSilently,
persistRegistrationEmailState,
@@ -177,7 +180,7 @@
setNodeStatus,
skipAutoRunCountdown,
skipNode,
startContributionFlow,
startFlowContribution,
startAutoRunLoop,
deleteMail2925Account,
deleteMail2925Accounts,
@@ -190,6 +193,55 @@
verifyHotmailAccount,
} = deps;
function normalizeMessageFlowId(value = '', fallback = 'openai') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (typeof rootScope.MultiPageFlowRegistry?.normalizeFlowId === 'function') {
return rootScope.MultiPageFlowRegistry.normalizeFlowId(value, fallback);
}
const fallbackFlowId = String(fallback || 'openai').trim().toLowerCase() || 'openai';
const normalized = String(value || '').trim().toLowerCase();
if (!normalized || normalized === 'codex') {
return fallbackFlowId;
}
return normalized;
}
function normalizeMessageTargetId(flowId, targetId = '', fallback = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (typeof rootScope.MultiPageFlowRegistry?.normalizeTargetId === 'function') {
return rootScope.MultiPageFlowRegistry.normalizeTargetId(flowId, targetId, fallback);
}
const fallbackSourceId = String(
fallback || (normalizeMessageFlowId(flowId) === 'kiro' ? 'kiro-rs' : 'cpa')
).trim().toLowerCase();
return String(targetId || fallbackSourceId).trim().toLowerCase() || fallbackSourceId;
}
function mapAutoRunTargetIdToPanelMode(targetId = '', fallback = 'cpa') {
return String(targetId || fallback || 'cpa').trim().toLowerCase() || 'cpa';
}
function buildAutoRunFlowStateUpdates(payload = {}) {
const hasActiveFlowId = Object.prototype.hasOwnProperty.call(payload, 'activeFlowId');
const hasTargetId = Object.prototype.hasOwnProperty.call(payload, 'targetId');
if (!hasActiveFlowId && !hasTargetId) {
return {};
}
const activeFlowId = normalizeMessageFlowId(payload.activeFlowId, 'openai');
const updates = {
activeFlowId,
flowId: activeFlowId,
};
if (hasTargetId) {
if (activeFlowId === 'kiro') {
updates.kiroTargetId = normalizeMessageTargetId('kiro', payload.targetId, 'kiro-rs');
} else {
updates.panelMode = mapAutoRunTargetIdToPanelMode(payload.targetId, 'cpa');
}
}
return updates;
}
function preserveKeyFromState(updates, currentState, key) {
if (!Object.prototype.hasOwnProperty.call(updates, key)) {
return;
@@ -541,6 +593,44 @@
return method === 'gopay' ? 'GoPay' : 'PayPal';
}
function normalizePlusAccountAccessStrategyForDisplay(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'sub2api_codex_session') {
return 'sub2api_codex_session';
}
if (normalized === 'cpa_codex_session') {
return 'cpa_codex_session';
}
return 'oauth';
}
function getPlusAccountAccessStrategyLabel(value = '') {
return normalizePlusAccountAccessStrategyForDisplay(value) === 'sub2api_codex_session'
? '导入当前 ChatGPT 会话到 SUB2API'
: 'OAuth';
}
function getPlusAccountAccessStrategyLabel(value = '', targetId = '') {
const strategy = normalizePlusAccountAccessStrategyForDisplay(value);
const normalizedTargetId = String(targetId || '').trim().toLowerCase();
if (strategy === 'sub2api_codex_session') {
return '导入当前 ChatGPT 会话到 SUB2API';
}
if (strategy === 'cpa_codex_session') {
return '导入当前 ChatGPT 会话到 CPA';
}
if (normalizedTargetId === 'cpa') {
return '通过 OAuth 回调创建 CPA 账号';
}
if (normalizedTargetId === 'sub2api') {
return '通过 OAuth 回调创建 SUB2API 账号';
}
if (normalizedTargetId === 'codex2api') {
return '通过 OAuth 回调创建 Codex2API 账号';
}
return 'OAuth';
}
async function handlePlatformVerifyStepData(payload) {
if (payload.localhostUrl) {
await closeLocalhostCallbackTabs(payload.localhostUrl);
@@ -1065,59 +1155,61 @@
return await setFreeReusablePhoneActivation(message.payload || {});
}
case 'SET_CONTRIBUTION_MODE': {
case 'SET_ACCOUNT_CONTRIBUTION_MODE': {
const enabled = Boolean(message.payload?.enabled);
const state = await ensureManualInteractionAllowed(enabled ? '进入贡献模式' : '退出贡献模式');
const state = await ensureManualInteractionAllowed(enabled ? '进入账号贡献' : '退出账号贡献');
if (Object.values(state.nodeStatuses || {}).some((status) => status === 'running')) {
throw new Error(enabled ? '当前有步骤正在执行,无法进入贡献模式。' : '当前有步骤正在执行,无法退出贡献模式。');
throw new Error(enabled ? '当前有步骤正在执行,无法进入账号贡献。' : '当前有步骤正在执行,无法退出账号贡献。');
}
if (typeof setContributionMode !== 'function') {
throw new Error('贡献模式切换能力未接入。');
if (typeof setAccountContributionMode !== 'function') {
throw new Error('账号贡献切换能力未接入。');
}
return {
ok: true,
state: await setContributionMode(enabled),
state: await setAccountContributionMode(enabled, {
adapterId: message.payload?.adapterId,
flowId: message.payload?.flowId || state?.activeFlowId || state?.flowId,
}),
};
}
case 'START_CONTRIBUTION_FLOW': {
case 'START_FLOW_CONTRIBUTION': {
const state = await ensureManualInteractionAllowed('开始贡献');
if (Object.values(state.nodeStatuses || {}).some((status) => status === 'running')) {
throw new Error('当前有步骤正在执行,无法开始贡献流程。');
}
if (typeof startContributionFlow !== 'function') {
if (!state?.accountContributionEnabled) {
throw new Error('请先进入账号贡献。');
}
if (typeof startFlowContribution !== 'function') {
throw new Error('贡献 OAuth 流程尚未接入。');
}
return {
ok: true,
state: await startContributionFlow({
state: await startFlowContribution({
nickname: message.payload?.nickname,
qq: message.payload?.qq,
}),
};
}
case 'SET_CONTRIBUTION_PROFILE': {
case 'SUBMIT_FLOW_CONTRIBUTION': {
const state = await getState();
if (!state?.contributionMode) {
throw new Error('请先进入贡献模式。');
if (!state?.accountContributionEnabled) {
throw new Error('请先进入账号贡献。');
}
const nickname = String(message.payload?.nickname || '').trim();
const qq = String(message.payload?.qq || '').trim();
if (qq && !/^\d{1,20}$/.test(qq)) {
throw new Error('QQ 只能填写数字,且长度不能超过 20 位。');
if (typeof submitFlowContribution !== 'function') {
throw new Error('贡献提交能力尚未接入。');
}
await setState({
contributionNickname: nickname,
contributionQq: qq,
});
return {
ok: true,
state: await getState(),
state: await submitFlowContribution(message.payload?.callbackUrl, {
reason: message.payload?.reason || 'sidepanel_submit',
}),
};
}
case 'POLL_CONTRIBUTION_STATUS': {
case 'POLL_FLOW_CONTRIBUTION_STATUS': {
if (typeof pollContributionStatus !== 'function') {
throw new Error('贡献状态轮询能力尚未接入。');
}
@@ -1166,6 +1258,9 @@
await lockAutomationWindowFromMessage(message, sender);
await ensureManualInteractionAllowed('手动执行节点');
}
if (typeof assertNodeExecutionAllowedForState === 'function') {
assertNodeExecutionAllowedForState(nodeId, requestState, '手动执行节点');
}
if (message.source === 'sidepanel') {
await ensureManualStepPrerequisites(resolvedStep);
}
@@ -1193,8 +1288,11 @@
if (message.source === 'sidepanel') {
await lockAutomationWindowFromMessage(message, sender);
}
if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
await setContributionMode(true);
if (Boolean(message.payload?.accountContributionEnabled) && typeof setAccountContributionMode === 'function') {
await setAccountContributionMode(true, {
adapterId: message.payload?.contributionAdapterId,
flowId: message.payload?.activeFlowId || message.payload?.flowId,
});
if (typeof setState === 'function') {
const contributionNickname = String(message.payload?.contributionNickname || '').trim();
const contributionQq = String(message.payload?.contributionQq || '').trim();
@@ -1204,8 +1302,16 @@
});
}
}
const autoRunFlowStateUpdates = buildAutoRunFlowStateUpdates(message.payload || {});
if (Object.keys(autoRunFlowStateUpdates).length > 0 && typeof setState === 'function') {
await setState(autoRunFlowStateUpdates);
}
const state = await getState();
const autoRunStartValidation = validateAutoRunStart(state, { state });
const autoRunStartValidation = validateAutoRunStart(state, {
activeFlowId: autoRunFlowStateUpdates.activeFlowId ?? state?.activeFlowId,
panelMode: autoRunFlowStateUpdates.panelMode ?? state?.panelMode,
state,
});
if (autoRunStartValidation?.ok === false) {
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
}
@@ -1225,8 +1331,11 @@
if (message.source === 'sidepanel') {
await lockAutomationWindowFromMessage(message, sender);
}
if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
await setContributionMode(true);
if (Boolean(message.payload?.accountContributionEnabled) && typeof setAccountContributionMode === 'function') {
await setAccountContributionMode(true, {
adapterId: message.payload?.contributionAdapterId,
flowId: message.payload?.activeFlowId || message.payload?.flowId,
});
if (typeof setState === 'function') {
const contributionNickname = String(message.payload?.contributionNickname || '').trim();
const contributionQq = String(message.payload?.contributionQq || '').trim();
@@ -1236,8 +1345,16 @@
});
}
}
const autoRunFlowStateUpdates = buildAutoRunFlowStateUpdates(message.payload || {});
if (Object.keys(autoRunFlowStateUpdates).length > 0 && typeof setState === 'function') {
await setState(autoRunFlowStateUpdates);
}
const state = await getState();
const autoRunStartValidation = validateAutoRunStart(state, { state });
const autoRunStartValidation = validateAutoRunStart(state, {
activeFlowId: autoRunFlowStateUpdates.activeFlowId ?? state?.activeFlowId,
panelMode: autoRunFlowStateUpdates.panelMode ?? state?.panelMode,
state,
});
if (autoRunStartValidation?.ok === false) {
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
}
@@ -1336,7 +1453,7 @@
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
|| Object.prototype.hasOwnProperty.call(updates, 'panelMode')
|| Object.prototype.hasOwnProperty.call(updates, 'activeFlowId')
|| Object.prototype.hasOwnProperty.call(updates, 'contributionMode')
|| Object.prototype.hasOwnProperty.call(updates, 'accountContributionEnabled')
) {
updates.signupMethod = resolveSignupMethod(nextSignupState);
}
@@ -1351,6 +1468,9 @@
const plusPaymentChanged = Object.prototype.hasOwnProperty.call(updates, 'plusPaymentMethod')
&& normalizePlusPaymentMethodForDisplay(currentState?.plusPaymentMethod || 'paypal')
!== normalizePlusPaymentMethodForDisplay(updates.plusPaymentMethod || 'paypal');
const plusAccountAccessStrategyChanged = Object.prototype.hasOwnProperty.call(updates, 'plusAccountAccessStrategy')
&& normalizePlusAccountAccessStrategyForDisplay(currentState?.plusAccountAccessStrategy || 'oauth')
!== normalizePlusAccountAccessStrategyForDisplay(updates.plusAccountAccessStrategy || 'oauth');
const phoneSignupReloginAfterBindEmailChanged = Object.prototype.hasOwnProperty.call(updates, 'phoneSignupReloginAfterBindEmailEnabled')
&& Boolean(currentState?.phoneSignupReloginAfterBindEmailEnabled) !== Boolean(updates.phoneSignupReloginAfterBindEmailEnabled);
const nextPlusModeEnabled = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
@@ -1358,29 +1478,68 @@
: Boolean(currentState?.plusModeEnabled);
const stepModeChanged = modeChanged
|| (nextPlusModeEnabled && plusPaymentChanged)
|| (nextPlusModeEnabled && plusAccountAccessStrategyChanged)
|| phoneSignupReloginAfterBindEmailChanged;
const oauthFlowTimeoutDisabled = Object.prototype.hasOwnProperty.call(updates, 'oauthFlowTimeoutEnabled')
&& updates.oauthFlowTimeoutEnabled === false;
await setPersistentSettings(updates);
const canonicalSettingsUpdates = await setPersistentSettings(updates);
const stateUpdates = {
...updates,
...canonicalSettingsUpdates,
...sessionUpdates,
...(oauthFlowTimeoutDisabled ? {
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
} : {}),
};
if (Object.prototype.hasOwnProperty.call(updates, 'icloudHostPreference')) {
const nextHostPreference = String(updates.icloudHostPreference || '').trim().toLowerCase();
if (Object.prototype.hasOwnProperty.call(canonicalSettingsUpdates, 'activeFlowId')
&& !Object.prototype.hasOwnProperty.call(stateUpdates, 'flowId')) {
stateUpdates.flowId = canonicalSettingsUpdates.activeFlowId;
}
if (Object.prototype.hasOwnProperty.call(canonicalSettingsUpdates, 'icloudHostPreference')) {
const nextHostPreference = String(canonicalSettingsUpdates.icloudHostPreference || '').trim().toLowerCase();
stateUpdates.preferredIcloudHost = nextHostPreference === 'icloud.com' || nextHostPreference === 'icloud.com.cn'
? nextHostPreference
: '';
}
if (stepModeChanged && typeof getStepIdsForState === 'function') {
const nextStateForSteps = { ...currentState, ...stateUpdates };
const nextNodeIds = typeof getNodeIdsForState === 'function'
? getNodeIdsForState(nextStateForSteps)
: getStepIdsForState(nextStateForSteps).map((stepId) => getStepKeyForState(stepId, nextStateForSteps)).filter(Boolean);
const currentNodeIds = typeof getNodeIdsForState === 'function'
? getNodeIdsForState(currentState)
: (typeof getStepIdsForState === 'function'
? getStepIdsForState(currentState).map((stepId) => getStepKeyForState(stepId, currentState)).filter(Boolean)
: []);
const nextStateForSteps = { ...currentState, ...stateUpdates };
const nextNodeIds = typeof getNodeIdsForState === 'function'
? getNodeIdsForState(nextStateForSteps)
: (typeof getStepIdsForState === 'function'
? getStepIdsForState(nextStateForSteps).map((stepId) => getStepKeyForState(stepId, nextStateForSteps)).filter(Boolean)
: []);
const nodeTopologyChanged = currentNodeIds.length !== nextNodeIds.length
|| currentNodeIds.some((nodeId, index) => nodeId !== nextNodeIds[index]);
const shouldRebuildNodeStatuses = stepModeChanged || nodeTopologyChanged;
if (shouldRebuildNodeStatuses && nextNodeIds.length > 0) {
Object.assign(stateUpdates, {
oauthUrl: null,
localhostUrl: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
cpaOAuthState: null,
cpaManagementOrigin: null,
sub2apiSessionId: null,
sub2apiOAuthState: null,
sub2apiGroupId: null,
sub2apiGroupIds: [],
sub2apiDraftName: null,
sub2apiProxyId: null,
codex2apiSessionId: null,
codex2apiOAuthState: null,
plusManualConfirmationPending: false,
plusManualConfirmationRequestId: '',
plusManualConfirmationStep: 0,
plusManualConfirmationMethod: '',
plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '',
});
}
if (shouldRebuildNodeStatuses && nextNodeIds.length > 0) {
stateUpdates.nodeStatuses = Object.fromEntries(nextNodeIds.map((nodeId) => [nodeId, 'pending']));
stateUpdates.currentNodeId = '';
}
@@ -1426,8 +1585,11 @@
error: error?.message || String(error || '代理应用失败'),
}));
}
if (Boolean(currentState?.contributionMode) && typeof setContributionMode === 'function') {
await setContributionMode(true);
if (Boolean(currentState?.accountContributionEnabled) && typeof setAccountContributionMode === 'function') {
await setAccountContributionMode(true, {
adapterId: currentState?.contributionAdapterId,
flowId: currentState?.activeFlowId || currentState?.flowId,
});
}
if (Object.keys(stateUpdates).length > 0 && typeof broadcastDataUpdate === 'function') {
broadcastDataUpdate(stateUpdates);
@@ -1436,9 +1598,17 @@
const selectedPlusPaymentMethod = getPlusPaymentMethodLabel(
stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal'
);
const selectedPlusAccountAccessStrategy = getPlusAccountAccessStrategyLabel(
stateUpdates.plusAccountAccessStrategy ?? currentState?.plusAccountAccessStrategy ?? 'oauth',
stateUpdates.panelMode
?? currentState?.panelMode
?? stateUpdates.openaiIntegrationTargetId
?? currentState?.openaiIntegrationTargetId
?? 'cpa'
);
await addLog(
Boolean(updates.plusModeEnabled)
? `Plus 模式已开启,已切换为 Plus Checkout 步骤,当前支付方式:${selectedPlusPaymentMethod}`
? `Plus 模式已开启,已切换为 Plus Checkout 步骤,当前支付方式:${selectedPlusPaymentMethod},账号接入策略:${selectedPlusAccountAccessStrategy}`
: 'Plus 模式已关闭,已恢复普通注册授权步骤。',
'info'
);
@@ -1447,6 +1617,16 @@
stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal'
);
await addLog(`Plus 支付方式已切换为 ${selectedPlusPaymentMethod},已更新对应的 Plus 步骤。`, 'info');
} else if (plusAccountAccessStrategyChanged && nextPlusModeEnabled) {
const selectedPlusAccountAccessStrategy = getPlusAccountAccessStrategyLabel(
stateUpdates.plusAccountAccessStrategy ?? currentState?.plusAccountAccessStrategy ?? 'oauth',
stateUpdates.panelMode
?? currentState?.panelMode
?? stateUpdates.openaiIntegrationTargetId
?? currentState?.openaiIntegrationTargetId
?? 'cpa'
);
await addLog(`Plus 账号接入策略已切换为 ${selectedPlusAccountAccessStrategy},已更新对应的 Plus 尾链。`, 'info');
}
return {
ok: true,
@@ -1470,6 +1650,44 @@
return { ok: true, ...result };
}
case 'CHECK_KIRO_RS_CONNECTION': {
if (typeof testKiroRsConnection !== 'function') {
throw new Error('kiro.rs 连接测试能力尚未接入。');
}
const currentState = await getState();
const activeFlowId = normalizeMessageFlowId(
message.payload?.activeFlowId || currentState?.activeFlowId || 'kiro',
'kiro'
);
const targetId = normalizeMessageTargetId(
activeFlowId,
message.payload?.targetId || currentState?.kiroTargetId || 'kiro-rs',
'kiro-rs'
);
const nestedTargetConfig = currentState?.settingsState?.flows?.kiro?.targets?.[targetId]
|| currentState?.flows?.kiro?.targets?.[targetId]
|| {};
const baseUrl = String(
message.payload?.baseUrl
?? nestedTargetConfig.baseUrl
?? currentState?.kiroRsUrl
?? ''
).trim();
const apiKey = String(
message.payload?.apiKey
?? nestedTargetConfig.apiKey
?? currentState?.kiroRsKey
?? ''
);
const result = await testKiroRsConnection(baseUrl, apiKey);
return {
ok: Boolean(result?.ok),
targetId,
status: Number(result?.status) || 0,
message: String(result?.message || '').trim(),
};
}
case 'RUN_IP_PROXY_AUTO_SYNC_NOW': {
if (typeof runIpProxyAutoSync !== 'function') {
throw new Error('IP 代理自动同步能力尚未接入。');
+162 -91
View File
@@ -120,6 +120,9 @@
'step8VerificationTargetEmail',
]),
});
const FLOW_FIELD_GROUPS = Object.freeze({
openai: OPENAI_FLOW_FIELD_GROUPS,
});
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
@@ -210,40 +213,151 @@
};
}
function flattenOpenAiFlowState(flowState = {}) {
const openaiState = normalizePlainObject(flowState.openai);
const next = {};
for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) {
const group = normalizePlainObject(openaiState[groupKey]);
for (const field of fields) {
if (Object.prototype.hasOwnProperty.call(group, field)) {
next[field] = cloneValue(group[field]);
function buildScopedFlowState(baseFlowState = {}, state = {}, flowId = 'openai') {
const fieldGroups = FLOW_FIELD_GROUPS[flowId] || {};
const baseScopedState = cloneValue(normalizePlainObject(baseFlowState[flowId]));
const scopedState = {
...baseScopedState,
};
for (const [groupKey, fields] of Object.entries(fieldGroups)) {
scopedState[groupKey] = {
...cloneValue(normalizePlainObject(baseScopedState[groupKey])),
...pickDefinedFields(state, fields),
};
}
return scopedState;
}
function buildFlowState(baseValue = {}, state = {}) {
const baseFlowState = cloneValue(normalizePlainObject(baseValue));
return {
...baseFlowState,
openai: buildScopedFlowState(baseFlowState, state, 'openai'),
};
}
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 buildOpenAiFlowState(baseValue = {}, state = {}) {
const baseFlowState = cloneValue(normalizePlainObject(baseValue));
const baseOpenAi = cloneValue(normalizePlainObject(baseFlowState.openai));
const openaiState = {
...baseOpenAi,
};
for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) {
openaiState[groupKey] = {
...cloneValue(normalizePlainObject(baseOpenAi[groupKey])),
...pickDefinedFields(state, fields),
};
}
function projectRuntimeViewFields(runtimeState = {}) {
const normalizedRuntimeState = normalizePlainObject(runtimeState);
return {
...baseFlowState,
openai: openaiState,
...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,
@@ -275,13 +389,13 @@
...cloneValue(normalizePlainObject(state.runtimeState)),
};
const activeFlowId = normalizeFlowId(
Object.prototype.hasOwnProperty.call(state, 'flowId')
? state.flowId
: Object.prototype.hasOwnProperty.call(state, 'activeFlowId')
Object.prototype.hasOwnProperty.call(state, 'activeFlowId')
? state.activeFlowId
: Object.prototype.hasOwnProperty.call(baseRuntimeState, 'flowId')
? baseRuntimeState.flowId
: baseRuntimeState.activeFlowId
: Object.prototype.hasOwnProperty.call(state, 'flowId')
? state.flowId
: Object.prototype.hasOwnProperty.call(baseRuntimeState, 'activeFlowId')
? baseRuntimeState.activeFlowId
: baseRuntimeState.flowId
);
const currentNodeId = String(
Object.prototype.hasOwnProperty.call(state, 'currentNodeId')
@@ -320,66 +434,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, flattenOpenAiFlowState(flowState));
if (Object.prototype.hasOwnProperty.call(runtimeState, 'flowState')) {
Object.assign(next, flattenOpenAiFlowState(normalizePlainObject(runtimeState.flowState)));
}
return next;
}
@@ -387,6 +453,7 @@
const runtimeState = ensureRuntimeState(state);
return {
...state,
...projectRuntimeViewFields(runtimeState),
flowId: runtimeState.flowId,
runId: runtimeState.runId,
activeFlowId: runtimeState.activeFlowId,
@@ -396,20 +463,23 @@
flowState: cloneValue(runtimeState.flowState),
sharedState: cloneValue(runtimeState.sharedState),
serviceState: cloneValue(runtimeState.serviceState),
flows: cloneValue(runtimeState.flowState),
shared: cloneValue(runtimeState.sharedState),
services: cloneValue(runtimeState.serviceState),
runtimeState,
};
}
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,
@@ -422,6 +492,7 @@
return {
DEFAULT_ACTIVE_FLOW_ID,
FLOW_FIELD_GROUPS,
OPENAI_FLOW_FIELD_GROUPS,
RUNTIME_PROXY_FIELDS,
RUNTIME_SHARED_FIELDS,
+276
View File
@@ -0,0 +1,276 @@
(function attachBackgroundCpaSessionImport(root, factory) {
root.MultiPageBackgroundCpaSessionImport = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundCpaSessionImportModule() {
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'];
function createCpaSessionImportExecutor(deps = {}) {
const {
addLog: rawAddLog = async () => {},
chrome,
completeNodeFromBackground,
ensureContentScriptReadyOnTabUntilStopped,
getTabId,
isTabAlive,
registerTab,
sendTabMessageUntilStopped,
sleepWithStop = async () => {},
throwIfStopped = () => {},
waitForTabCompleteUntilStopped = async () => {},
} = deps;
let cpaApi = null;
function addStepLog(step, message, level = 'info') {
return rawAddLog(message, level, {
step,
stepKey: 'cpa-session-import',
});
}
function getCpaApi() {
if (cpaApi) {
return cpaApi;
}
const factory = deps.createCpaApi
|| self.MultiPageBackgroundCpaApi?.createCpaApi;
if (typeof factory !== 'function') {
throw new Error('CPA 接口模块未加载,无法导入当前 ChatGPT 会话。');
}
cpaApi = factory({
addLog: rawAddLog,
});
return cpaApi;
}
function normalizeString(value = '') {
return String(value || '').trim();
}
function resolveVisibleStep(state = {}) {
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
return visibleStep > 0 ? visibleStep : 10;
}
function isSupportedChatGptSessionUrl(url = '') {
try {
const parsed = new URL(String(url || ''));
if (!/^https?:$/i.test(parsed.protocol)) {
return false;
}
const hostname = String(parsed.hostname || '').trim().toLowerCase();
return /(^|\.)chatgpt\.com$/.test(hostname)
|| hostname === 'chat.openai.com'
|| /(^|\.)openai\.com$/.test(hostname);
} catch {
return false;
}
}
function getSessionTabHostPriority(url = '') {
try {
const hostname = String(new URL(String(url || '')).hostname || '').trim().toLowerCase();
if (/(^|\.)chatgpt\.com$/.test(hostname)) {
return 0;
}
if (hostname === 'chat.openai.com') {
return 1;
}
if (/(^|\.)openai\.com$/.test(hostname)) {
return 2;
}
} catch {
return Number.POSITIVE_INFINITY;
}
return Number.POSITIVE_INFINITY;
}
function getSessionTabActivityPriority(tab = {}) {
if (tab?.active && tab?.currentWindow) {
return 0;
}
if (tab?.active) {
return 1;
}
return 2;
}
function pickPreferredSessionTab(tabs = []) {
const candidates = (Array.isArray(tabs) ? tabs : [])
.filter((tab) => Number.isInteger(tab?.id) && isSupportedChatGptSessionUrl(tab.url));
if (!candidates.length) {
return null;
}
return candidates.reduce((best, candidate) => {
if (!best) {
return candidate;
}
const candidateHostPriority = getSessionTabHostPriority(candidate.url);
const bestHostPriority = getSessionTabHostPriority(best.url);
if (candidateHostPriority !== bestHostPriority) {
return candidateHostPriority < bestHostPriority ? candidate : best;
}
const candidateActivityPriority = getSessionTabActivityPriority(candidate);
const bestActivityPriority = getSessionTabActivityPriority(best);
if (candidateActivityPriority !== bestActivityPriority) {
return candidateActivityPriority < bestActivityPriority ? candidate : best;
}
const candidateLastAccessed = Number(candidate?.lastAccessed) || 0;
const bestLastAccessed = Number(best?.lastAccessed) || 0;
if (candidateLastAccessed !== bestLastAccessed) {
return candidateLastAccessed > bestLastAccessed ? candidate : best;
}
return Number(candidate.id) < Number(best.id) ? candidate : best;
}, null);
}
async function readSupportedSessionTab(tabId) {
const numericTabId = Number(tabId) || 0;
if (!numericTabId || !chrome?.tabs?.get) {
return null;
}
const tab = await chrome.tabs.get(numericTabId).catch(() => null);
return tab?.id && isSupportedChatGptSessionUrl(tab.url)
? tab
: null;
}
async function findFallbackSessionTab() {
if (!chrome?.tabs?.query) {
return null;
}
const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true }).catch(() => []);
const activeMatch = pickPreferredSessionTab(activeTabs);
const allTabs = await chrome.tabs.query({}).catch(() => []);
const globalMatch = pickPreferredSessionTab(allTabs);
return pickPreferredSessionTab([activeMatch, globalMatch]);
}
async function resolveSessionTabId(state = {}) {
const registeredTabId = typeof getTabId === 'function'
? await getTabId(PLUS_CHECKOUT_SOURCE)
: null;
if (registeredTabId && typeof isTabAlive === 'function' && await isTabAlive(PLUS_CHECKOUT_SOURCE)) {
const registeredTab = await readSupportedSessionTab(registeredTabId);
if (registeredTab?.id) {
return registeredTab.id;
}
}
const storedTabId = Number(state?.plusCheckoutTabId) || 0;
const storedTab = await readSupportedSessionTab(storedTabId);
if (storedTab?.id) {
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, storedTab.id);
}
return storedTab.id;
}
const fallbackTab = await findFallbackSessionTab();
if (fallbackTab?.id) {
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, fallbackTab.id);
}
return fallbackTab.id;
}
throw new Error('未找到可读取 ChatGPT 会话的标签页,请先打开一个已登录的 ChatGPT / OpenAI 页面,或完成当前 Plus 支付链路。');
}
async function getResolvedSessionTab(tabId, visibleStep) {
const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
if (!tab?.id) {
throw new Error(`步骤 ${visibleStep}:ChatGPT 会话标签页不存在或已关闭,无法继续导入 CPA。`);
}
if (!isSupportedChatGptSessionUrl(tab.url)) {
throw new Error(`步骤 ${visibleStep}:当前标签页不在 ChatGPT / OpenAI 页面,无法读取当前登录会话。`);
}
return tab;
}
async function readCurrentChatGptSession(tabId, visibleStep) {
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
inject: PLUS_CHECKOUT_INJECT_FILES,
injectSource: PLUS_CHECKOUT_SOURCE,
logMessage: `步骤 ${visibleStep}:正在等待 ChatGPT 会话页完成加载,再继续读取当前登录会话...`,
});
const sessionResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
type: 'PLUS_CHECKOUT_GET_STATE',
source: 'background',
payload: {
includeSession: true,
includeAccessToken: true,
},
});
if (sessionResult?.error) {
throw new Error(sessionResult.error);
}
const session = sessionResult?.session && typeof sessionResult.session === 'object' && !Array.isArray(sessionResult.session)
? sessionResult.session
: null;
const accessToken = normalizeString(
sessionResult?.accessToken
|| session?.accessToken
);
if (!session && !accessToken) {
throw new Error(`步骤 ${visibleStep}:未读取到有效的 ChatGPT 会话或 accessToken,请确认当前标签页仍处于已登录状态。`);
}
return {
session,
accessToken,
};
}
async function executeCpaSessionImport(state = {}) {
throwIfStopped();
const visibleStep = resolveVisibleStep(state);
const api = getCpaApi();
await addStepLog(visibleStep, '正在定位当前 ChatGPT 会话页并准备导入 CPA...', 'info');
const tabId = await resolveSessionTabId(state);
const tab = await getResolvedSessionTab(tabId, visibleStep);
if (chrome?.tabs?.update) {
await chrome.tabs.update(tab.id, { active: true }).catch(() => {});
}
await addStepLog(visibleStep, '正在读取当前 ChatGPT 登录会话...', 'info');
const sessionState = await readCurrentChatGptSession(tab.id, visibleStep);
throwIfStopped();
const result = await api.importCurrentChatGptSession({
...state,
session: sessionState.session,
accessToken: sessionState.accessToken,
}, {
visibleStep,
logLabel: `步骤 ${visibleStep}`,
logOptions: { step: visibleStep, stepKey: 'cpa-session-import' },
timeoutMs: 120000,
importTimeoutMs: 120000,
});
await completeNodeFromBackground(state?.nodeId || 'cpa-session-import', result);
}
return {
executeCpaSessionImport,
isSupportedChatGptSessionUrl,
};
}
return {
createCpaSessionImportExecutor,
};
});
+4 -1
View File
@@ -212,7 +212,10 @@
{
type: 'SUBMIT_ADD_EMAIL',
source: 'background',
payload: { email: resolvedEmail },
payload: {
email: resolvedEmail,
nodeId: state?.nodeId || activeFetchLoginCodeStepKey || 'fetch-login-code',
},
},
{
timeoutMs,
+64 -5
View File
@@ -2,8 +2,61 @@
root.MultiPageBackgroundGoPayManualConfirm = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGoPayManualConfirmModule() {
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
const GOPAY_CONFIRM_NODE_ID = 'gopay-subscription-confirm';
const SUB2API_SESSION_IMPORT_NODE_ID = 'sub2api-session-import';
const CPA_SESSION_IMPORT_NODE_ID = 'cpa-session-import';
const DEFAULT_CONFIRM_TITLE = 'GoPay 订阅确认';
const DEFAULT_CONFIRM_MESSAGE = 'GoPay 订阅页已打开。请先手动完成订阅,完成后确认继续 OAuth 登录';
const OAUTH_CONTINUATION_LABEL = 'OAuth 登录';
const SUB2API_SESSION_CONTINUATION_LABEL = '导入当前 ChatGPT 会话到 SUB2API';
const CPA_SESSION_CONTINUATION_LABEL = '导入当前 ChatGPT 会话到 CPA';
function normalizeString(value = '') {
return String(value || '').trim();
}
function getContinuationActionLabelForNodeId(nodeId = '') {
const normalizedNodeId = normalizeString(nodeId);
if (normalizedNodeId === SUB2API_SESSION_IMPORT_NODE_ID) {
return SUB2API_SESSION_CONTINUATION_LABEL;
}
if (normalizedNodeId === CPA_SESSION_IMPORT_NODE_ID) {
return CPA_SESSION_CONTINUATION_LABEL;
}
return OAUTH_CONTINUATION_LABEL;
}
function getContinuationActionLabel(state = {}, options = {}) {
const { getNodeIdsForState = null } = options;
if (typeof getNodeIdsForState === 'function') {
const nodeIds = getNodeIdsForState(state)
.map((nodeId) => normalizeString(nodeId))
.filter(Boolean);
const currentNodeId = normalizeString(state?.nodeId || GOPAY_CONFIRM_NODE_ID) || GOPAY_CONFIRM_NODE_ID;
const currentNodeIndex = nodeIds.indexOf(currentNodeId);
const nextNodeId = currentNodeIndex >= 0
? normalizeString(nodeIds[currentNodeIndex + 1])
: '';
if (nextNodeId) {
return getContinuationActionLabelForNodeId(nextNodeId);
}
if (currentNodeIndex < 0) {
if (nodeIds.includes(SUB2API_SESSION_IMPORT_NODE_ID)) {
return SUB2API_SESSION_CONTINUATION_LABEL;
}
if (nodeIds.includes(CPA_SESSION_IMPORT_NODE_ID)) {
return CPA_SESSION_CONTINUATION_LABEL;
}
}
}
return OAUTH_CONTINUATION_LABEL;
}
function buildDefaultConfirmMessage(state = {}, options = {}) {
const continuationActionLabel = getContinuationActionLabel(state, options);
return `GoPay 订阅页已打开。请先手动完成订阅,完成后确认继续${continuationActionLabel}`;
}
function createGoPayManualConfirmExecutor(deps = {}) {
const {
@@ -12,6 +65,7 @@
chrome,
createAutomationTab = null,
getTabId,
getNodeIdsForState = null,
isTabAlive,
registerTab,
setState,
@@ -40,7 +94,7 @@
}
}
const checkoutUrl = String(state?.plusCheckoutUrl || '').trim();
const checkoutUrl = normalizeString(state?.plusCheckoutUrl);
if (!checkoutUrl) {
throw new Error('步骤 7:未检测到 GoPay 订阅页,请先执行步骤 6。');
}
@@ -63,19 +117,21 @@
}
async function executeGoPayManualConfirm(state = {}) {
const visibleStep = Number(state?.visibleStep) || 7;
const tabId = await resolveCheckoutTabId(state);
if (chrome?.tabs?.update && tabId) {
await chrome.tabs.update(tabId, { active: true }).catch(() => {});
}
const continuationActionLabel = getContinuationActionLabel(state, { getNodeIdsForState });
const payload = {
plusCheckoutTabId: tabId,
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: buildRequestId(),
plusManualConfirmationStep: 7,
plusManualConfirmationStep: visibleStep,
plusManualConfirmationMethod: 'gopay',
plusManualConfirmationTitle: DEFAULT_CONFIRM_TITLE,
plusManualConfirmationMessage: DEFAULT_CONFIRM_MESSAGE,
plusManualConfirmationMessage: buildDefaultConfirmMessage(state, { getNodeIdsForState }),
};
await setState(payload);
@@ -83,7 +139,10 @@
broadcastDataUpdate(payload);
}
await addLog('步骤 7:正在等待手动完成 GoPay 订阅,确认后继续 OAuth 登录。', 'info');
await addLog(
`步骤 ${visibleStep}:正在等待手动完成 GoPay 订阅,确认后继续${continuationActionLabel}`,
'info'
);
}
return {
+2 -2
View File
@@ -59,7 +59,7 @@
return normalizeStep7SignupMethod(state?.signupMethod) === 'phone'
&& Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
&& !Boolean(state?.contributionMode);
&& !Boolean(state?.accountContributionEnabled);
}
function hasStep7PhoneSignupIdentity(state = {}) {
@@ -307,7 +307,7 @@
'signup-page',
{
type: 'EXECUTE_NODE',
nodeId: 'oauth-login',
nodeId: state?.nodeId || 'oauth-login',
step: 7,
source: 'background',
payload: {
+36 -15
View File
@@ -10,6 +10,7 @@
ensureContentScriptReadyOnTab,
getPanelMode,
getTabId,
getStepIdByKeyForState,
isLocalhostOAuthCallbackUrl,
isTabAlive,
normalizeCodex2ApiUrl,
@@ -51,11 +52,31 @@
return visibleStep >= 10 ? visibleStep : 10;
}
function resolveConfirmOauthStep(platformVerifyStep = 10) {
return Number(platformVerifyStep) >= 13 ? 12 : 9;
function resolveStepIdByKey(state = {}, stepKey = '') {
if (typeof getStepIdByKeyForState !== 'function') {
return null;
}
const step = Number(getStepIdByKeyForState(stepKey, state));
return Number.isInteger(step) && step > 0 ? step : null;
}
function resolveAuthLoginStep(platformVerifyStep = 10) {
function resolveConfirmOauthStep(platformVerifyStep = 10, state = {}) {
const dynamicStep = resolveStepIdByKey(state, 'confirm-oauth');
if (dynamicStep && dynamicStep < Number(platformVerifyStep)) {
return dynamicStep;
}
return Math.max(1, Number(platformVerifyStep) - 1);
}
function resolveAuthLoginStep(platformVerifyStep = 10, state = {}) {
const reloginStep = resolveStepIdByKey(state, 'relogin-bound-email');
if (reloginStep && reloginStep < Number(platformVerifyStep)) {
return reloginStep;
}
const oauthLoginStep = resolveStepIdByKey(state, 'oauth-login');
if (oauthLoginStep && oauthLoginStep < Number(platformVerifyStep)) {
return oauthLoginStep;
}
return Number(platformVerifyStep) >= 13 ? 10 : 7;
}
@@ -63,8 +84,8 @@
return addLog(message, level, { step, stepKey: 'platform-verify' });
}
function parseLocalhostCallback(rawUrl, platformVerifyStep = 10) {
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep);
function parseLocalhostCallback(rawUrl, platformVerifyStep = 10, state = {}) {
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep, state);
let parsed;
try {
parsed = new URL(rawUrl);
@@ -73,15 +94,15 @@
}
const code = normalizeString(parsed.searchParams.get('code'));
const state = normalizeString(parsed.searchParams.get('state'));
if (!code || !state) {
const oauthState = normalizeString(parsed.searchParams.get('state'));
if (!code || !oauthState) {
throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmOauthStep}`);
}
return {
url: parsed.toString(),
code,
state,
state: oauthState,
};
}
@@ -239,8 +260,8 @@
async function executeCpaStep10(state) {
const platformVerifyStep = resolvePlatformVerifyStep(state);
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep);
const authLoginStep = resolveAuthLoginStep(platformVerifyStep);
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep, state);
const authLoginStep = resolveAuthLoginStep(platformVerifyStep, state);
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}`);
}
@@ -260,7 +281,7 @@
return;
}
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep);
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep, state);
const expectedState = normalizeString(state.cpaOAuthState);
if (expectedState && expectedState !== callback.state) {
throw new Error(`CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}`);
@@ -299,8 +320,8 @@
async function executeCodex2ApiStep10(state) {
const platformVerifyStep = resolvePlatformVerifyStep(state);
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep);
const authLoginStep = resolveAuthLoginStep(platformVerifyStep);
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep, state);
const authLoginStep = resolveAuthLoginStep(platformVerifyStep, state);
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}`);
}
@@ -314,7 +335,7 @@
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
}
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep);
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep, state);
const expectedState = normalizeString(state.codex2apiOAuthState);
if (expectedState && expectedState !== callback.state) {
throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}`);
@@ -345,7 +366,7 @@
async function executeSub2ApiStep10(state) {
const platformVerifyStep = resolvePlatformVerifyStep(state);
const visibleStep = platformVerifyStep;
const confirmOauthStep = resolveConfirmOauthStep(visibleStep);
const confirmOauthStep = resolveConfirmOauthStep(visibleStep, state);
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}`);
}
+7 -3
View File
@@ -4,13 +4,14 @@
function createNodeRegistry(definitions = []) {
const ordered = (Array.isArray(definitions) ? definitions : [])
.map((definition) => ({
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,
}))
.filter((definition) => definition.nodeId && typeof definition.execute === 'function')
.filter((definition) => definition.nodeId)
.sort((left, right) => {
const leftOrder = Number.isFinite(left.displayOrder) ? left.displayOrder : 0;
const rightOrder = Number.isFinite(right.displayOrder) ? right.displayOrder : 0;
@@ -31,7 +32,10 @@
function executeNode(nodeId, state) {
const definition = getNodeDefinition(nodeId);
if (!definition) {
throw new Error(`未知节点:${nodeId}`);
throw new Error(`Unknown node: ${nodeId}`);
}
if (typeof definition.execute !== 'function') {
throw new Error(`Missing node executor: ${definition.executeKey || definition.nodeId}`);
}
return definition.execute(state);
}
+280
View File
@@ -0,0 +1,280 @@
(function attachBackgroundSub2ApiSessionImport(root, factory) {
root.MultiPageBackgroundSub2ApiSessionImport = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundSub2ApiSessionImportModule() {
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'];
function createSub2ApiSessionImportExecutor(deps = {}) {
const {
addLog: rawAddLog = async () => {},
chrome,
completeNodeFromBackground,
ensureContentScriptReadyOnTabUntilStopped,
getTabId,
isTabAlive,
normalizeSub2ApiUrl = (value) => value,
registerTab,
sendTabMessageUntilStopped,
sleepWithStop = async () => {},
throwIfStopped = () => {},
waitForTabCompleteUntilStopped = async () => {},
DEFAULT_SUB2API_GROUP_NAME = 'codex',
} = deps;
let sub2ApiApi = null;
function addStepLog(step, message, level = 'info') {
return rawAddLog(message, level, {
step,
stepKey: 'sub2api-session-import',
});
}
function getSub2ApiApi() {
if (sub2ApiApi) {
return sub2ApiApi;
}
const factory = deps.createSub2ApiApi
|| self.MultiPageBackgroundSub2ApiApi?.createSub2ApiApi;
if (typeof factory !== 'function') {
throw new Error('SUB2API 接口模块未加载,无法导入当前 ChatGPT 会话。');
}
sub2ApiApi = factory({
addLog: rawAddLog,
normalizeSub2ApiUrl,
DEFAULT_SUB2API_GROUP_NAME,
});
return sub2ApiApi;
}
function normalizeString(value = '') {
return String(value || '').trim();
}
function resolveVisibleStep(state = {}) {
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
return visibleStep > 0 ? visibleStep : 10;
}
function isSupportedChatGptSessionUrl(url = '') {
try {
const parsed = new URL(String(url || ''));
if (!/^https?:$/i.test(parsed.protocol)) {
return false;
}
const hostname = String(parsed.hostname || '').trim().toLowerCase();
return /(^|\.)chatgpt\.com$/.test(hostname)
|| hostname === 'chat.openai.com'
|| /(^|\.)openai\.com$/.test(hostname);
} catch {
return false;
}
}
function getSessionTabHostPriority(url = '') {
try {
const hostname = String(new URL(String(url || '')).hostname || '').trim().toLowerCase();
if (/(^|\.)chatgpt\.com$/.test(hostname)) {
return 0;
}
if (hostname === 'chat.openai.com') {
return 1;
}
if (/(^|\.)openai\.com$/.test(hostname)) {
return 2;
}
} catch {
return Number.POSITIVE_INFINITY;
}
return Number.POSITIVE_INFINITY;
}
function getSessionTabActivityPriority(tab = {}) {
if (tab?.active && tab?.currentWindow) {
return 0;
}
if (tab?.active) {
return 1;
}
return 2;
}
function pickPreferredSessionTab(tabs = []) {
const candidates = (Array.isArray(tabs) ? tabs : [])
.filter((tab) => Number.isInteger(tab?.id) && isSupportedChatGptSessionUrl(tab.url));
if (!candidates.length) {
return null;
}
return candidates.reduce((best, candidate) => {
if (!best) {
return candidate;
}
const candidateHostPriority = getSessionTabHostPriority(candidate.url);
const bestHostPriority = getSessionTabHostPriority(best.url);
if (candidateHostPriority !== bestHostPriority) {
return candidateHostPriority < bestHostPriority ? candidate : best;
}
const candidateActivityPriority = getSessionTabActivityPriority(candidate);
const bestActivityPriority = getSessionTabActivityPriority(best);
if (candidateActivityPriority !== bestActivityPriority) {
return candidateActivityPriority < bestActivityPriority ? candidate : best;
}
const candidateLastAccessed = Number(candidate?.lastAccessed) || 0;
const bestLastAccessed = Number(best?.lastAccessed) || 0;
if (candidateLastAccessed !== bestLastAccessed) {
return candidateLastAccessed > bestLastAccessed ? candidate : best;
}
return Number(candidate.id) < Number(best.id) ? candidate : best;
}, null);
}
async function readSupportedSessionTab(tabId) {
const numericTabId = Number(tabId) || 0;
if (!numericTabId || !chrome?.tabs?.get) {
return null;
}
const tab = await chrome.tabs.get(numericTabId).catch(() => null);
return tab?.id && isSupportedChatGptSessionUrl(tab.url)
? tab
: null;
}
async function findFallbackSessionTab() {
if (!chrome?.tabs?.query) {
return null;
}
const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true }).catch(() => []);
const activeMatch = pickPreferredSessionTab(activeTabs);
const allTabs = await chrome.tabs.query({}).catch(() => []);
const globalMatch = pickPreferredSessionTab(allTabs);
return pickPreferredSessionTab([activeMatch, globalMatch]);
}
async function resolveSessionTabId(state = {}) {
const registeredTabId = typeof getTabId === 'function'
? await getTabId(PLUS_CHECKOUT_SOURCE)
: null;
if (registeredTabId && typeof isTabAlive === 'function' && await isTabAlive(PLUS_CHECKOUT_SOURCE)) {
const registeredTab = await readSupportedSessionTab(registeredTabId);
if (registeredTab?.id) {
return registeredTab.id;
}
}
const storedTabId = Number(state?.plusCheckoutTabId) || 0;
const storedTab = await readSupportedSessionTab(storedTabId);
if (storedTab?.id) {
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, storedTab.id);
}
return storedTab.id;
}
const fallbackTab = await findFallbackSessionTab();
if (fallbackTab?.id) {
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, fallbackTab.id);
}
return fallbackTab.id;
}
throw new Error('未找到可读取 ChatGPT 会话的标签页,请先打开一个已登录的 ChatGPT / OpenAI 页面,或完成当前 Plus 支付链路。');
}
async function getResolvedSessionTab(tabId, visibleStep) {
const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
if (!tab?.id) {
throw new Error(`步骤 ${visibleStep}:ChatGPT 会话标签页不存在或已关闭,无法继续导入 SUB2API。`);
}
if (!isSupportedChatGptSessionUrl(tab.url)) {
throw new Error(`步骤 ${visibleStep}:当前标签页不在 ChatGPT / OpenAI 页面,无法读取当前登录会话。`);
}
return tab;
}
async function readCurrentChatGptSession(tabId, visibleStep) {
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
inject: PLUS_CHECKOUT_INJECT_FILES,
injectSource: PLUS_CHECKOUT_SOURCE,
logMessage: `步骤 ${visibleStep}:正在等待 ChatGPT 会话页完成加载,再继续读取当前登录会话...`,
});
const sessionResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
type: 'PLUS_CHECKOUT_GET_STATE',
source: 'background',
payload: {
includeSession: true,
includeAccessToken: true,
},
});
if (sessionResult?.error) {
throw new Error(sessionResult.error);
}
const session = sessionResult?.session && typeof sessionResult.session === 'object' && !Array.isArray(sessionResult.session)
? sessionResult.session
: null;
const accessToken = normalizeString(
sessionResult?.accessToken
|| session?.accessToken
);
if (!session && !accessToken) {
throw new Error(`步骤 ${visibleStep}:未读取到有效的 ChatGPT 会话或 accessToken,请确认当前标签页仍处于已登录状态。`);
}
return {
session,
accessToken,
};
}
async function executeSub2ApiSessionImport(state = {}) {
throwIfStopped();
const visibleStep = resolveVisibleStep(state);
const api = getSub2ApiApi();
await addStepLog(visibleStep, '正在定位当前 ChatGPT 会话页并准备导入 SUB2API...', 'info');
const tabId = await resolveSessionTabId(state);
const tab = await getResolvedSessionTab(tabId, visibleStep);
if (chrome?.tabs?.update) {
await chrome.tabs.update(tab.id, { active: true }).catch(() => {});
}
await addStepLog(visibleStep, '正在读取当前 ChatGPT 登录会话...', 'info');
const sessionState = await readCurrentChatGptSession(tab.id, visibleStep);
throwIfStopped();
const result = await api.importCurrentChatGptSession({
...state,
session: sessionState.session,
accessToken: sessionState.accessToken,
}, {
visibleStep,
logLabel: `步骤 ${visibleStep}`,
logOptions: { step: visibleStep, stepKey: 'sub2api-session-import' },
timeoutMs: 120000,
importTimeoutMs: 120000,
});
await completeNodeFromBackground(state?.nodeId || 'sub2api-session-import', result);
}
return {
executeSub2ApiSessionImport,
isSupportedChatGptSessionUrl,
};
}
return {
createSub2ApiSessionImportExecutor,
};
});
+223 -3
View File
@@ -345,6 +345,145 @@
return `${prefix}-${stamp}-${random}`;
}
function normalizeCodexSessionObject(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
}
function normalizeEmailValue(value = '') {
const email = normalizeString(value);
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : '';
}
function decodeCodexBase64UrlSegment(segment = '') {
const normalized = normalizeString(segment)
.replace(/-/g, '+')
.replace(/_/g, '/');
if (!normalized) {
return '';
}
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
try {
if (typeof Buffer !== 'undefined') {
return Buffer.from(padded, 'base64').toString('utf8');
}
if (typeof atob === 'function') {
const binary = atob(padded);
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder().decode(bytes);
}
return binary;
}
} catch {
return '';
}
return '';
}
function parseCodexAccessTokenClaims(accessToken = '') {
const token = normalizeString(accessToken);
if (!token) {
return null;
}
const parts = token.split('.');
if (parts.length !== 3) {
return null;
}
try {
return JSON.parse(decodeCodexBase64UrlSegment(parts[1]));
} catch {
return null;
}
}
function resolveCodexSessionImportAccountName(state = {}, session = null, accessToken = '') {
const sessionObject = normalizeCodexSessionObject(session);
const claims = parseCodexAccessTokenClaims(accessToken || sessionObject?.accessToken);
const accountIdentifierType = normalizeString(state?.accountIdentifierType).toLowerCase();
const accountIdentifierEmail = accountIdentifierType === 'email'
? normalizeEmailValue(state?.accountIdentifier)
: '';
return normalizeEmailValue(sessionObject?.user?.email)
|| normalizeEmailValue(sessionObject?.email)
|| normalizeEmailValue(claims?.email)
|| normalizeEmailValue(state?.email)
|| accountIdentifierEmail;
}
function buildCodexSessionImportContent(session, accessToken = '') {
const normalizedAccessToken = normalizeString(accessToken);
const sessionObject = normalizeCodexSessionObject(session);
if (sessionObject) {
const contentObject = normalizedAccessToken
? {
...sessionObject,
accessToken: normalizedAccessToken,
}
: sessionObject;
return JSON.stringify(contentObject);
}
if (normalizedAccessToken) {
return normalizedAccessToken;
}
throw new Error('未读取到可导入的 ChatGPT 会话或 accessToken。');
}
function resolveCodexSessionImportExpiresAt(session) {
const sessionObject = normalizeCodexSessionObject(session);
const expiresValue = normalizeString(sessionObject?.expires);
if (!expiresValue) {
return null;
}
const expiresAtMs = Date.parse(expiresValue);
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= 0) {
return null;
}
return Math.floor(expiresAtMs / 1000);
}
function normalizeCodexSessionImportMessages(messages) {
return (Array.isArray(messages) ? messages : [])
.map((item, index) => ({
index: Number(item?.index) || index + 1,
name: normalizeString(item?.name),
message: normalizeString(item?.message),
}))
.filter((item) => item.message);
}
function normalizeCodexSessionImportResult(result) {
return {
total: Math.max(0, Number(result?.total) || 0),
created: Math.max(0, Number(result?.created) || 0),
updated: Math.max(0, Number(result?.updated) || 0),
skipped: Math.max(0, Number(result?.skipped) || 0),
failed: Math.max(0, Number(result?.failed) || 0),
items: Array.isArray(result?.items) ? result.items : [],
warnings: normalizeCodexSessionImportMessages(result?.warnings),
errors: normalizeCodexSessionImportMessages(result?.errors),
};
}
function buildCodexSessionImportSummary(result) {
const normalized = normalizeCodexSessionImportResult(result);
return `SUB2API 会话导入完成:新建 ${normalized.created},更新 ${normalized.updated},跳过 ${normalized.skipped},失败 ${normalized.failed}`;
}
function getCodexSessionImportFailureMessage(result) {
const normalized = normalizeCodexSessionImportResult(result);
const detail = normalized.errors.map((item) => item.message).find(Boolean)
|| normalized.warnings.map((item) => item.message).find(Boolean)
|| normalized.items
.map((item) => normalizeString(item?.message))
.find(Boolean)
|| buildCodexSessionImportSummary(normalized);
return detail || 'SUB2API 会话导入失败。';
}
function parseLocalhostCallback(rawUrl, visibleStep = 10) {
let parsed;
try {
@@ -364,15 +503,15 @@
}
const code = normalizeString(parsed.searchParams.get('code'));
const state = normalizeString(parsed.searchParams.get('state'));
if (!code || !state) {
const oauthState = normalizeString(parsed.searchParams.get('state'));
if (!code || !oauthState) {
throw new Error('回调 URL 中缺少 code 或 state。');
}
return {
url: parsed.toString(),
code,
state,
state: oauthState,
};
}
@@ -580,14 +719,95 @@
};
}
async function importCurrentChatGptSession(state = {}, options = {}) {
const logLabel = normalizeString(options.logLabel) || 'SUB2API 会话导入';
const session = normalizeCodexSessionObject(state?.session);
const accessToken = normalizeString(
state?.accessToken
|| session?.accessToken
);
const importContent = buildCodexSessionImportContent(session, accessToken);
const importExpiresAt = resolveCodexSessionImportExpiresAt(session);
const preferredAccountName = resolveCodexSessionImportAccountName(state, session, accessToken);
await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口登录并准备导入当前 ChatGPT 会话...`, 'info', options);
const { origin, token } = await loginSub2Api(state, options);
const groupNames = state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME;
const groups = await getGroupsByNames(origin, token, groupNames, options);
const groupLabel = groups.map((item) => `${item.name}${item.id}`).join('、');
const proxyPreference = resolveSub2ApiProxyPreference(state);
const proxy = proxyPreference ? await resolveSub2ApiProxy(origin, token, proxyPreference, options) : null;
const proxyId = normalizeProxyId(proxy?.id);
const accountPriority = resolveSub2ApiAccountPriority(state);
await logWithOptions(`${logLabel}:已登录 SUB2API,使用分组 ${groupLabel}`, 'info', options);
if (proxy) {
await logWithOptions(`${logLabel}:已选择 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}`, 'info', options);
} else {
await logWithOptions(`${logLabel}:未配置 SUB2API 默认代理,本次将不使用代理。`, 'info', options);
}
const importPayload = {
content: importContent,
group_ids: groups
.map((group) => Number(group?.id))
.filter((id) => Number.isFinite(id) && id > 0),
...(preferredAccountName ? { name: preferredAccountName } : {}),
priority: accountPriority,
auto_pause_on_expired: true,
update_existing: true,
};
if (!importPayload.group_ids.length) {
throw new Error('SUB2API 返回的目标分组 ID 无效。');
}
if (proxyId) {
importPayload.proxy_id = proxyId;
}
if (importExpiresAt) {
importPayload.expires_at = importExpiresAt;
}
await logWithOptions(`${logLabel}:正在导入当前 ChatGPT 会话到 SUB2API...`, 'info', options);
const importResult = normalizeCodexSessionImportResult(await requestJson(origin, '/api/v1/admin/accounts/import/codex-session', {
method: 'POST',
token,
timeoutMs: options.importTimeoutMs || options.timeoutMs,
body: importPayload,
}));
for (const warning of importResult.warnings) {
await logWithOptions(`${logLabel}${warning.message}`, 'warn', options);
}
if (importResult.failed > 0) {
throw new Error(getCodexSessionImportFailureMessage(importResult));
}
if (importResult.created <= 0 && importResult.updated <= 0) {
throw new Error(getCodexSessionImportFailureMessage(importResult));
}
const verifiedStatus = buildCodexSessionImportSummary(importResult);
await logWithOptions(verifiedStatus, 'ok', options);
return {
verifiedStatus,
sub2apiImportTotal: importResult.total,
sub2apiImportCreated: importResult.created,
sub2apiImportUpdated: importResult.updated,
sub2apiImportSkipped: importResult.skipped,
sub2apiImportFailed: importResult.failed,
};
}
return {
buildDraftAccountName,
buildCodexSessionImportContent,
buildOpenAiCredentials,
buildOpenAiExtra,
buildProxyDisplayName,
extractStateFromAuthUrl,
generateOpenAiAuthUrl,
getGroupsByNames,
importCurrentChatGptSession,
loginSub2Api,
normalizeProxyId,
normalizeRedirectUri,
+37 -4
View File
@@ -290,7 +290,7 @@
}
async function closeConflictingTabsForSource(source, currentUrl, options = {}) {
const { excludeTabIds = [], preserveActiveTab = true } = options;
const { excludeTabIds = [], preserveActiveTab = false } = options;
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
const state = await getState();
const lastUrl = getSourceMapValue(state.sourceLastUrls, source);
@@ -620,6 +620,16 @@
return Math.max(1, Math.min(requestedTimeoutMs, Math.floor(Number(remainingTimeoutMs))));
}
function buildRetryableTransportTimeoutError(source, error) {
const rawMessage = error?.message || String(error || '');
if (isRetryableContentScriptTransportError(error)) {
return new Error(
`${getSourceLabel(source)} 页面刚完成跳转或刷新,内容脚本还没有重新接回;扩展已自动重试,但仍未恢复。请重试当前步骤。`
);
}
return new Error(rawMessage || `${getSourceLabel(source)} 页面通信失败。`);
}
function getMessageDebugLabel(source, message, tabId = null) {
const parts = [source || 'unknown', message?.type || 'UNKNOWN'];
if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`);
@@ -722,8 +732,11 @@
async function reuseOrCreateTab(source, url, options = {}) {
if (options.forceNew) {
await closeConflictingTabsForSource(source, url);
const tab = await createAutomationTab({ url, active: true }, options);
await closeConflictingTabsForSource(source, url, {
excludeTabIds: [tab.id],
preserveActiveTab: false,
});
if (options.inject) {
await waitForTabUpdateComplete(tab.id);
@@ -749,7 +762,10 @@
const alive = await isTabAlive(source);
if (alive) {
const tabId = await getTabId(source);
await closeConflictingTabsForSource(source, url, { excludeTabIds: [tabId] });
await closeConflictingTabsForSource(source, url, {
excludeTabIds: [tabId],
preserveActiveTab: false,
});
const currentTab = await chrome.tabs.get(tabId);
const sameUrl = currentTab.url === url;
const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
@@ -830,8 +846,11 @@
return tabId;
}
await closeConflictingTabsForSource(source, url);
const tab = await createAutomationTab({ url, active: true }, options);
await closeConflictingTabsForSource(source, url, {
excludeTabIds: [tab.id],
preserveActiveTab: false,
});
if (options.inject) {
await waitForTabUpdateComplete(tab.id);
@@ -882,6 +901,7 @@
logMessage = '',
logStep = null,
logStepKey = '',
onRetryableError = null,
responseTimeoutMs,
} = options;
const start = Date.now();
@@ -920,10 +940,23 @@
logged = true;
}
if (typeof onRetryableError === 'function') {
await onRetryableError(err, {
attempt,
elapsedMs: Date.now() - start,
remainingTimeoutMs: Math.max(0, timeoutMs - (Date.now() - start)),
source,
message,
});
}
await sleepOrStop(retryDelayMs);
}
}
if (lastError && isRetryableContentScriptTransportError(lastError)) {
throw buildRetryableTransportTimeoutError(source, lastError);
}
throw lastError || new Error(`等待 ${getSourceLabel(source)} 重新就绪超时。`);
}
+3 -1
View File
@@ -1117,6 +1117,7 @@
}
await chrome.tabs.update(signupTabId, { active: true });
const completionNodeId = await getNodeIdForStep(completionStep);
const baseResponseTimeoutMs = await getResponseTimeoutMsForStep(
step,
step === 8
@@ -1134,6 +1135,7 @@
source: 'background',
payload: {
code,
...(completionNodeId ? { nodeId: completionNodeId } : {}),
...(step === 4 && options.signupProfile ? { signupProfile: options.signupProfile } : {}),
},
};
@@ -1268,6 +1270,7 @@
async function resolveVerificationStep(step, state, mail, options = {}) {
const completionStep = getCompletionStep(step, options);
activeVerificationLogStep = completionStep;
const completionNodeId = await getNodeIdForStep(completionStep);
const stateKey = getVerificationCodeStateKey(step);
const rejectedCodes = new Set();
const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER
@@ -1419,7 +1422,6 @@
[stateKey]: result.code,
});
const completionNodeId = await getNodeIdForStep(completionStep);
if (!completionNodeId) {
throw new Error(`步骤 ${completionStep} 未映射到验证码节点。`);
}