修复设置自动保存与标签清理边界
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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_token,access_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,
|
||||
};
|
||||
});
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -212,7 +212,10 @@
|
||||
{
|
||||
type: 'SUBMIT_ADD_EMAIL',
|
||||
source: 'background',
|
||||
payload: { email: resolvedEmail },
|
||||
payload: {
|
||||
email: resolvedEmail,
|
||||
nodeId: state?.nodeId || activeFetchLoginCodeStepKey || 'fetch-login-code',
|
||||
},
|
||||
},
|
||||
{
|
||||
timeoutMs,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}。`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)} 重新就绪超时。`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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} 未映射到验证码节点。`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user